Remove and simplify for webassembly use case

This commit is contained in:
Condorra 2024-08-11 20:55:08 +10:00
parent b056370038
commit 1b66c396b6
53 changed files with 255 additions and 5393 deletions

View File

@ -1,10 +1,9 @@
[package] [package]
name = "crossterm" name = "minicrossterm"
version = "0.28.1" version = "0.28.1"
authors = ["T. Post"] authors = ["Blasthavers", "T. Post"]
description = "A crossplatform terminal library for manipulating terminals." description = "A stripped back crossplatform terminal library for manipulating terminals asynchronously."
repository = "https://github.com/crossterm-rs/crossterm" repository = "https://git.blastmud.org/blasthavers/crossterm"
documentation = "https://docs.rs/crossterm/"
license = "MIT" license = "MIT"
keywords = ["event", "color", "cli", "input", "terminal"] keywords = ["event", "color", "cli", "input", "terminal"]
exclude = ["target", "Cargo.lock"] exclude = ["target", "Cargo.lock"]
@ -27,22 +26,10 @@ all-features = true
# Features # Features
# #
[features] [features]
default = ["bracketed-paste", "windows", "events"] default = ["bracketed-paste", "events"]
windows = [
"dep:winapi",
"dep:crossterm_winapi",
] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
bracketed-paste = [ bracketed-paste = [
] # Enables triggering a `Event::Paste` when pasting text into the terminal. ] # Enables triggering a `Event::Paste` when pasting text into the terminal.
event-stream = ["dep:futures-core", "events"] # Enables async events
use-dev-tty = [
"filedescriptor",
"rustix/process",
] # Enables raw file descriptor polling / selecting instead of mio.
events = [ events = [
"dep:mio",
"dep:signal-hook",
"dep:signal-hook-mio",
] # Enables reading input/events from the system. ] # Enables reading input/events from the system.
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types. serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
@ -51,85 +38,14 @@ serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
# #
[dependencies] [dependencies]
bitflags = { version = "2.3" } bitflags = { version = "2.3" }
parking_lot = "0.12"
# optional deps only added when requested # optional deps only added when requested
futures-core = { version = "0.3", optional = true, default-features = false }
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
#
# Windows dependencies
#
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["winuser", "winerror"]
optional = true
[target.'cfg(windows)'.dependencies]
crossterm_winapi = { version = "0.9.1", optional = true }
#
# UNIX dependencies
#
[target.'cfg(unix)'.dependencies]
# Default to using rustix for UNIX systems, but provide an option to use libc for backwards
# compatibility.
libc = { version = "0.2", default-features = false, optional = true }
rustix = { version = "0.38.34", default-features = false, features = [
"std",
"stdio",
"termios",
] }
signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true }
mio = { version = "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 (examples, ...)
# #
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.25", features = ["full"] }
futures = "0.3"
futures-timer = "3.0"
async-std = "1.12"
serde_json = "1.0" serde_json = "1.0"
serial_test = "2.0.0" serial_test = "2.0.0"
temp-env = "0.3.6" 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"]

View File

@ -1,12 +1,20 @@
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1> # Minicrossterm
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5] Minicrossterm is a cut back fork of crossterm which doesn't depend on stdio, and is intended
to only provide non-blocking functionality for applications without relying on standard IO (
for example, from webassembly).
It was created for Blasthavers Worldwideportal, and not by the original authors of crossterm.
The following information applies to crossterm - some might not be up to date for minicrossterm.
# Cross-platform Terminal Manipulation Library # Cross-platform Terminal Manipulation Library
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested, Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
see [Tested Terminals](#tested-terminals) for more info). see [Tested Terminals](#tested-terminals) for more info).
[![Donate to crossterm upstream](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2)]
## Table of Contents ## Table of Contents
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library) - [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)

View File

@ -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

View File

@ -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,
)));
}

View File

@ -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()
}

View File

@ -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());
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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 = "../../" }

View File

@ -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),
_ => { },
};
)*
}
}

View File

@ -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)
}

View File

@ -1,5 +0,0 @@
pub mod attribute;
pub mod color;
pub mod cursor;
pub mod event;
pub mod synchronized_output;

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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");
}
}

View File

@ -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(())
}

View File

@ -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),
}
}

View File

@ -46,11 +46,6 @@ use std::fmt;
use crate::{csi, impl_display, Command}; use crate::{csi, impl_display, Command};
pub(crate) mod sys;
#[cfg(feature = "events")]
pub use sys::position;
/// A command that moves the terminal cursor to the given position (column, row). /// A command that moves the terminal cursor to the given position (column, row).
/// ///
/// # Notes /// # Notes

View File

@ -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;

View File

@ -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(_) => {}
}
}
}

View File

@ -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);
}
}

View File

@ -119,165 +119,18 @@
//! them (`event-*`). //! them (`event-*`).
pub(crate) mod filter; pub(crate) mod filter;
pub(crate) mod read;
pub(crate) mod source;
#[cfg(feature = "event-stream")]
pub(crate) mod stream;
pub(crate) mod sys; pub(crate) mod sys;
pub(crate) mod timeout;
#[cfg(feature = "event-stream")]
pub use stream::EventStream;
use crate::event::{
filter::{EventFilter, Filter},
read::InternalEventReader,
timeout::PollTimeout,
};
use crate::{csi, Command}; use crate::{csi, Command};
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use std::{
use std::fmt::{self, Display}; collections::VecDeque,
use std::time::Duration; fmt::{self, Display},
};
use bitflags::bitflags; use bitflags::bitflags;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
/// Static instance of `InternalEventReader`. use self::sys::unix::parse::parse_event;
/// This needs to be static because there can be one event reader.
static INTERNAL_EVENT_READER: Mutex<Option<InternalEventReader>> = parking_lot::const_mutex(None);
pub(crate) fn lock_internal_event_reader() -> MappedMutexGuard<'static, InternalEventReader> {
MutexGuard::map(INTERNAL_EVENT_READER.lock(), |reader| {
reader.get_or_insert_with(InternalEventReader::default)
})
}
fn try_lock_internal_event_reader_for(
duration: Duration,
) -> Option<MappedMutexGuard<'static, InternalEventReader>> {
Some(MutexGuard::map(
INTERNAL_EVENT_READER.try_lock_for(duration)?,
|reader| reader.get_or_insert_with(InternalEventReader::default),
))
}
/// Checks if there is an [`Event`](enum.Event.html) available.
///
/// Returns `Ok(true)` if an [`Event`](enum.Event.html) is available otherwise it returns `Ok(false)`.
///
/// `Ok(true)` guarantees that subsequent call to the [`read`](fn.read.html) function
/// won't block.
///
/// # Arguments
///
/// * `timeout` - maximum waiting time for event availability
///
/// # Examples
///
/// Return immediately:
///
/// ```no_run
/// use std::{time::Duration, io};
/// use crossterm::{event::poll};
///
/// fn is_event_available() -> io::Result<bool> {
/// // Zero duration says that the `poll` function must return immediately
/// // with an `Event` availability information
/// poll(Duration::from_secs(0))
/// }
/// ```
///
/// Wait up to 100ms:
///
/// ```no_run
/// use std::{time::Duration, io};
///
/// use crossterm::event::poll;
///
/// fn is_event_available() -> io::Result<bool> {
/// // Wait for an `Event` availability for 100ms. It returns immediately
/// // if an `Event` is/becomes available.
/// poll(Duration::from_millis(100))
/// }
/// ```
pub fn poll(timeout: Duration) -> std::io::Result<bool> {
poll_internal(Some(timeout), &EventFilter)
}
/// Reads a single [`Event`](enum.Event.html).
///
/// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the
/// [`poll`](fn.poll.html) function to get non-blocking reads.
///
/// # Examples
///
/// Blocking read:
///
/// ```no_run
/// use crossterm::event::read;
/// use std::io;
///
/// fn print_events() -> io::Result<bool> {
/// loop {
/// // Blocks until an `Event` is available
/// println!("{:?}", read()?);
/// }
/// }
/// ```
///
/// Non-blocking read:
///
/// ```no_run
/// use std::time::Duration;
/// use std::io;
///
/// use crossterm::event::{read, poll};
///
/// fn print_events() -> io::Result<bool> {
/// loop {
/// if poll(Duration::from_millis(100))? {
/// // It's guaranteed that `read` won't block, because `poll` returned
/// // `Ok(true)`.
/// println!("{:?}", read()?);
/// } else {
/// // Timeout expired, no `Event` is available
/// }
/// }
/// }
/// ```
pub fn read() -> std::io::Result<Event> {
match read_internal(&EventFilter)? {
InternalEvent::Event(event) => Ok(event),
#[cfg(unix)]
_ => unreachable!(),
}
}
/// Polls to check if there are any `InternalEvent`s that can be read within the given duration.
pub(crate) fn poll_internal<F>(timeout: Option<Duration>, filter: &F) -> std::io::Result<bool>
where
F: Filter,
{
let (mut reader, timeout) = if let Some(timeout) = timeout {
let poll_timeout = PollTimeout::new(Some(timeout));
if let Some(reader) = try_lock_internal_event_reader_for(timeout) {
(reader, poll_timeout.leftover())
} else {
return Ok(false);
}
} else {
(lock_internal_event_reader(), None)
};
reader.poll(timeout, filter)
}
/// Reads a single `InternalEvent`.
pub(crate) fn read_internal<F>(filter: &F) -> std::io::Result<InternalEvent>
where
F: Filter,
{
let mut reader = lock_internal_event_reader();
reader.read(filter)
}
bitflags! { bitflags! {
/// Represents special flags that tell compatible terminals to add extra information to keyboard events. /// Represents special flags that tell compatible terminals to add extra information to keyboard events.
@ -331,16 +184,6 @@ impl Command for EnableMouseCapture {
csi!("?1006h"), csi!("?1006h"),
)) ))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
sys::windows::enable_mouse_capture()
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
} }
/// A command that disables mouse event capturing. /// A command that disables mouse event capturing.
@ -360,16 +203,6 @@ impl Command for DisableMouseCapture {
csi!("?1000l"), csi!("?1000l"),
)) ))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
sys::windows::disable_mouse_capture()
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
} }
/// A command that enables focus event emission. /// A command that enables focus event emission.
@ -384,12 +217,6 @@ impl Command for EnableFocusChange {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?1004h")) f.write_str(csi!("?1004h"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
// Focus events are always enabled on Windows
Ok(())
}
} }
/// A command that disables focus event emission. /// A command that disables focus event emission.
@ -400,12 +227,6 @@ impl Command for DisableFocusChange {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?1004l")) f.write_str(csi!("?1004l"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
// Focus events can't be disabled on Windows
Ok(())
}
} }
/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste). /// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste).
@ -423,14 +244,6 @@ impl Command for EnableBracketedPaste {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?2004h")) f.write_str(csi!("?2004h"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Bracketed paste not implemented in the legacy Windows API.",
))
}
} }
/// A command that disables bracketed paste mode. /// A command that disables bracketed paste mode.
@ -443,11 +256,6 @@ impl Command for DisableBracketedPaste {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?2004l")) f.write_str(csi!("?2004l"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
Ok(())
}
} }
/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys. /// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys.
@ -494,21 +302,6 @@ impl Command for PushKeyboardEnhancementFlags {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "{}{}u", csi!(">"), self.0.bits()) write!(f, "{}{}u", csi!(">"), self.0.bits())
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
use std::io;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
} }
/// A command that disables extra kinds of keyboard events. /// A command that disables extra kinds of keyboard events.
@ -523,21 +316,6 @@ impl Command for PopKeyboardEnhancementFlags {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("<1u")) f.write_str(csi!("<1u"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
use std::io;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
} }
/// Represents an event. /// Represents an event.
@ -560,6 +338,12 @@ pub enum Event {
/// An resize event with new dimensions after resize (columns, rows). /// An resize event with new dimensions after resize (columns, rows).
/// **Note** that resize events can occur in batches. /// **Note** that resize events can occur in batches.
Resize(u16, u16), Resize(u16, u16),
/// A cursor position (`col`, `row`).
CursorPosition(u16, u16),
/// The progressive keyboard enhancement flags enabled by the terminal.
KeyboardEnhancementFlags(KeyboardEnhancementFlags),
/// Attributes and architectural class of the terminal.
PrimaryDeviceAttributes,
} }
/// Represents a mouse event. /// Represents a mouse event.
@ -677,19 +461,8 @@ impl Display for KeyModifiers {
} }
match modifier { match modifier {
KeyModifiers::SHIFT => f.write_str("Shift")?, KeyModifiers::SHIFT => f.write_str("Shift")?,
#[cfg(unix)]
KeyModifiers::CONTROL => f.write_str("Control")?, KeyModifiers::CONTROL => f.write_str("Control")?,
#[cfg(windows)]
KeyModifiers::CONTROL => f.write_str("Ctrl")?,
#[cfg(target_os = "macos")]
KeyModifiers::ALT => f.write_str("Option")?,
#[cfg(not(target_os = "macos"))]
KeyModifiers::ALT => f.write_str("Alt")?, KeyModifiers::ALT => f.write_str("Alt")?,
#[cfg(target_os = "macos")]
KeyModifiers::SUPER => f.write_str("Command")?,
#[cfg(target_os = "windows")]
KeyModifiers::SUPER => f.write_str("Windows")?,
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
KeyModifiers::SUPER => f.write_str("Super")?, KeyModifiers::SUPER => f.write_str("Super")?,
KeyModifiers::HYPER => f.write_str("Hyper")?, KeyModifiers::HYPER => f.write_str("Hyper")?,
KeyModifiers::META => f.write_str("Meta")?, KeyModifiers::META => f.write_str("Meta")?,
@ -1119,20 +892,9 @@ impl Display for KeyCode {
/// displayed as "Del", and the Enter key is displayed as "Enter". /// displayed as "Del", and the Enter key is displayed as "Enter".
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
// On macOS, the Backspace key is called "Delete" and the Delete key is called "Fwd Del".
#[cfg(target_os = "macos")]
KeyCode::Backspace => write!(f, "Delete"),
#[cfg(target_os = "macos")]
KeyCode::Delete => write!(f, "Fwd Del"),
#[cfg(not(target_os = "macos"))]
KeyCode::Backspace => write!(f, "Backspace"), KeyCode::Backspace => write!(f, "Backspace"),
#[cfg(not(target_os = "macos"))]
KeyCode::Delete => write!(f, "Del"), KeyCode::Delete => write!(f, "Del"),
#[cfg(target_os = "macos")]
KeyCode::Enter => write!(f, "Return"),
#[cfg(not(target_os = "macos"))]
KeyCode::Enter => write!(f, "Enter"), KeyCode::Enter => write!(f, "Enter"),
KeyCode::Left => write!(f, "Left"), KeyCode::Left => write!(f, "Left"),
KeyCode::Right => write!(f, "Right"), KeyCode::Right => write!(f, "Right"),
@ -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)] #[cfg(test)]
mod tests { mod tests {
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
@ -1227,18 +970,9 @@ mod tests {
#[test] #[test]
fn keycode_display() { fn keycode_display() {
#[cfg(target_os = "macos")]
{
assert_eq!(format!("{}", Backspace), "Delete");
assert_eq!(format!("{}", Delete), "Fwd Del");
assert_eq!(format!("{}", Enter), "Return");
}
#[cfg(not(target_os = "macos"))]
{
assert_eq!(format!("{}", Backspace), "Backspace"); assert_eq!(format!("{}", Backspace), "Backspace");
assert_eq!(format!("{}", Delete), "Del"); assert_eq!(format!("{}", Delete), "Del");
assert_eq!(format!("{}", Enter), "Enter"); assert_eq!(format!("{}", Enter), "Enter");
}
assert_eq!(format!("{}", Left), "Left"); assert_eq!(format!("{}", Left), "Left");
assert_eq!(format!("{}", Right), "Right"); assert_eq!(format!("{}", Right), "Right");
assert_eq!(format!("{}", Up), "Up"); assert_eq!(format!("{}", Up), "Up");
@ -1292,29 +1026,6 @@ mod tests {
assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift"); assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift");
} }
#[cfg(target_os = "macos")]
#[test]
fn modifier_keycode_display_macos() {
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Control");
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Option");
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Command");
assert_eq!(format!("{}", Modifier(RightControl)), "Right Control");
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Option");
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Command");
}
#[cfg(target_os = "windows")]
#[test]
fn modifier_keycode_display_windows() {
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Alt");
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Windows");
assert_eq!(format!("{}", Modifier(RightControl)), "Right Ctrl");
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Alt");
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Windows");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test] #[test]
fn modifier_keycode_display_other() { fn modifier_keycode_display_other() {
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl"); assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
@ -1325,3 +1036,35 @@ mod tests {
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super"); assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super");
} }
} }
pub struct TerminalState {
char_buffer: VecDeque<u8>,
}
impl TerminalState {
pub fn new() -> Self {
Self {
char_buffer: VecDeque::<u8>::new(),
}
}
pub fn events_for_input(&mut self, input: &[u8]) -> std::io::Result<Vec<Event>> {
let mut result: Vec<Event> = Vec::new();
self.char_buffer.extend(input);
let mut buf: Vec<u8> = Vec::new();
loop {
match self.char_buffer.pop_front() {
None => return Ok(result),
Some(c) => {
buf.push(c);
match parse_event(&buf, true)? {
None => continue,
Some(e) => result.push(e),
}
}
}
}
}
}

View File

@ -1,48 +1,42 @@
use crate::event::InternalEvent; use crate::event::Event;
/// Interface for filtering an `InternalEvent`. /// Interface for filtering an `InternalEvent`.
pub(crate) trait Filter: Send + Sync + 'static { pub(crate) trait Filter: Send + Sync + 'static {
/// Returns whether the given event fulfills the filter. /// Returns whether the given event fulfills the filter.
fn eval(&self, event: &InternalEvent) -> bool; fn eval(&self, event: &Event) -> bool;
} }
#[cfg(unix)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct CursorPositionFilter; pub(crate) struct CursorPositionFilter;
#[cfg(unix)]
impl Filter for CursorPositionFilter { impl Filter for CursorPositionFilter {
fn eval(&self, event: &InternalEvent) -> bool { fn eval(&self, event: &Event) -> bool {
matches!(*event, InternalEvent::CursorPosition(_, _)) matches!(*event, Event::CursorPosition(_, _))
} }
} }
#[cfg(unix)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct KeyboardEnhancementFlagsFilter; pub(crate) struct KeyboardEnhancementFlagsFilter;
#[cfg(unix)]
impl Filter for KeyboardEnhancementFlagsFilter { impl Filter for KeyboardEnhancementFlagsFilter {
fn eval(&self, event: &InternalEvent) -> bool { fn eval(&self, event: &Event) -> bool {
// This filter checks for either a KeyboardEnhancementFlags response or // This filter checks for either a KeyboardEnhancementFlags response or
// a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes // a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes
// response but not KeyboardEnhancementFlags, the terminal does not support // response but not KeyboardEnhancementFlags, the terminal does not support
// progressive keyboard enhancement. // progressive keyboard enhancement.
matches!( matches!(
*event, *event,
InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes Event::KeyboardEnhancementFlags(_) | Event::PrimaryDeviceAttributes
) )
} }
} }
#[cfg(unix)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct PrimaryDeviceAttributesFilter; pub(crate) struct PrimaryDeviceAttributesFilter;
#[cfg(unix)]
impl Filter for PrimaryDeviceAttributesFilter { impl Filter for PrimaryDeviceAttributesFilter {
fn eval(&self, event: &InternalEvent) -> bool { fn eval(&self, event: &Event) -> bool {
matches!(*event, InternalEvent::PrimaryDeviceAttributes) matches!(*event, Event::PrimaryDeviceAttributes)
} }
} }
@ -50,66 +44,50 @@ impl Filter for PrimaryDeviceAttributesFilter {
pub(crate) struct EventFilter; pub(crate) struct EventFilter;
impl Filter for EventFilter { impl Filter for EventFilter {
#[cfg(unix)] fn eval(&self, _: &Event) -> bool {
fn eval(&self, event: &InternalEvent) -> bool {
matches!(*event, InternalEvent::Event(_))
}
#[cfg(windows)]
fn eval(&self, _: &InternalEvent) -> bool {
true
}
}
#[derive(Debug, Clone)]
pub(crate) struct InternalEventFilter;
impl Filter for InternalEventFilter {
fn eval(&self, _: &InternalEvent) -> bool {
true true
} }
} }
#[cfg(test)] #[cfg(test)]
#[cfg(unix)]
mod tests { mod tests {
use super::{ use super::{
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, super::Event, CursorPositionFilter, Event, EventFilter, Filter, InternalEventFilter,
InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
}; };
#[test] #[test]
fn test_cursor_position_filter_filters_cursor_position() { fn test_cursor_position_filter_filters_cursor_position() {
assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(!CursorPositionFilter.eval(&Event::Event(Event::Resize(10, 10))));
assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); assert!(CursorPositionFilter.eval(&Event::CursorPosition(0, 0)));
} }
#[test] #[test]
fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() {
assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(!KeyboardEnhancementFlagsFilter.eval(&Event::Event(Event::Resize(10, 10))));
assert!( assert!(
KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( KeyboardEnhancementFlagsFilter.eval(&Event::KeyboardEnhancementFlags(
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)) ))
); );
assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); assert!(KeyboardEnhancementFlagsFilter.eval(&Event::PrimaryDeviceAttributes));
} }
#[test] #[test]
fn test_primary_device_attributes_filter_filters_primary_device_attributes() { fn test_primary_device_attributes_filter_filters_primary_device_attributes() {
assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(!PrimaryDeviceAttributesFilter.eval(&Event::Event(Event::Resize(10, 10))));
assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); assert!(PrimaryDeviceAttributesFilter.eval(&Event::PrimaryDeviceAttributes));
} }
#[test] #[test]
fn test_event_filter_filters_events() { fn test_event_filter_filters_events() {
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(EventFilter.eval(&Event::Event(Event::Resize(10, 10))));
assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); assert!(!EventFilter.eval(&Event::CursorPosition(0, 0)));
} }
#[test] #[test]
fn test_event_filter_filters_internal_events() { fn test_event_filter_filters_internal_events() {
assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(InternalEventFilter.eval(&Event::Event(Event::Resize(10, 10))));
assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); assert!(InternalEventFilter.eval(&Event::CursorPosition(0, 0)));
} }
} }

View File

@ -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!();
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -1,9 +1 @@
#[cfg(all(unix, feature = "event-stream"))]
pub(crate) use unix::waker::Waker;
#[cfg(all(windows, feature = "event-stream"))]
pub(crate) use windows::waker::Waker;
#[cfg(unix)]
pub(crate) mod unix; pub(crate) mod unix;
#[cfg(windows)]
pub(crate) mod windows;

View File

@ -1,5 +1,2 @@
#[cfg(feature = "event-stream")]
pub(crate) mod waker;
#[cfg(feature = "events")] #[cfg(feature = "events")]
pub(crate) mod parse; pub(crate) mod parse;

View File

@ -5,8 +5,6 @@ use crate::event::{
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind, MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
}; };
use super::super::super::InternalEvent;
// Event parsing // Event parsing
// //
// This code (& previous one) are kind of ugly. We have to think about this, // This code (& previous one) are kind of ugly. We have to think about this,
@ -23,10 +21,7 @@ fn could_not_parse_event_error() -> io::Error {
io::Error::new(io::ErrorKind::Other, "Could not parse an event.") io::Error::new(io::ErrorKind::Other, "Could not parse an event.")
} }
pub(crate) fn parse_event( pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Option<Event>> {
buffer: &[u8],
input_available: bool,
) -> io::Result<Option<InternalEvent>> {
if buffer.is_empty() { if buffer.is_empty() {
return Ok(None); return Ok(None);
} }
@ -38,7 +33,7 @@ pub(crate) fn parse_event(
// Possible Esc sequence // Possible Esc sequence
Ok(None) Ok(None)
} else { } else {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) Ok(Some(Event::Key(KeyCode::Esc.into())))
} }
} else { } else {
match buffer[1] { match buffer[1] {
@ -47,40 +42,28 @@ pub(crate) fn parse_event(
Ok(None) Ok(None)
} else { } else {
match buffer[2] { match buffer[2] {
b'D' => { b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))),
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))),
} b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))),
b'C' => Ok(Some(InternalEvent::Event(Event::Key( b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))),
KeyCode::Right.into(), b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))),
)))), b'F' => Ok(Some(Event::Key(KeyCode::End.into()))),
b'A' => {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into()))))
}
b'B' => {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into()))))
}
b'H' => {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into()))))
}
b'F' => {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into()))))
}
// F1-F4 // F1-F4
val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( val @ b'P'..=b'S' => {
KeyCode::F(1 + val - b'P').into(), Ok(Some(Event::Key(KeyCode::F(1 + val - b'P').into())))
)))), }
_ => Err(could_not_parse_event_error()), _ => Err(could_not_parse_event_error()),
} }
} }
} }
b'[' => parse_csi(buffer), b'[' => parse_csi(buffer),
b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), b'\x1B' => Ok(Some(Event::Key(KeyCode::Esc.into()))),
_ => parse_event(&buffer[1..], input_available).map(|event_option| { _ => parse_event(&buffer[1..], input_available).map(|event_option| {
event_option.map(|event| { event_option.map(|event| {
if let InternalEvent::Event(Event::Key(key_event)) = event { if let Event::Key(key_event) = event {
let mut alt_key_event = key_event; let mut alt_key_event = key_event;
alt_key_event.modifiers |= KeyModifiers::ALT; alt_key_event.modifiers |= KeyModifiers::ALT;
InternalEvent::Event(Event::Key(alt_key_event)) Event::Key(alt_key_event)
} else { } else {
event event
} }
@ -89,38 +72,26 @@ pub(crate) fn parse_event(
} }
} }
} }
b'\r' => Ok(Some(InternalEvent::Event(Event::Key( b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))),
KeyCode::Enter.into(), b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))),
)))), b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))),
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new(
// newlines as input is because the terminal converts \r into \n for us. When we
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
// use Ctrl+J. Waiting to handle it here means it gets picked up later
b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event(
Event::Key(KeyCode::Enter.into()),
))),
b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))),
b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key(
KeyCode::Backspace.into(),
)))),
c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char((c - 0x1 + b'a') as char), KeyCode::Char((c - 0x1 + b'a') as char),
KeyModifiers::CONTROL, KeyModifiers::CONTROL,
))))), )))),
c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char((c - 0x1C + b'4') as char), KeyCode::Char((c - 0x1C + b'4') as char),
KeyModifiers::CONTROL, KeyModifiers::CONTROL,
))))), )))),
b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( b'\0' => Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char(' '), KeyCode::Char(' '),
KeyModifiers::CONTROL, KeyModifiers::CONTROL,
))))), )))),
_ => parse_utf8_char(buffer).map(|maybe_char| { _ => parse_utf8_char(buffer).map(|maybe_char| {
maybe_char maybe_char
.map(KeyCode::Char) .map(KeyCode::Char)
.map(char_code_to_event) .map(char_code_to_event)
.map(Event::Key) .map(Event::Key)
.map(InternalEvent::Event)
}), }),
} }
} }
@ -134,7 +105,7 @@ fn char_code_to_event(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, modifiers) KeyEvent::new(code, modifiers)
} }
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<Event>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
if buffer.len() == 2 { if buffer.len() == 2 {
@ -210,7 +181,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
_ => return Err(could_not_parse_event_error()), _ => return Err(could_not_parse_event_error()),
}; };
Ok(input_event.map(InternalEvent::Event)) Ok(input_event)
} }
pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> io::Result<T> pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> io::Result<T>
@ -238,7 +209,7 @@ fn modifier_and_kind_parsed(iter: &mut dyn Iterator<Item = &str>) -> io::Result<
} }
} }
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Event>> {
// ESC [ Cy ; Cx R // ESC [ Cy ; Cx R
// Cy - cursor row number (starting from 1) // Cy - cursor row number (starting from 1)
// Cx - cursor column number (starting from 1) // Cx - cursor column number (starting from 1)
@ -253,10 +224,10 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Inte
let y = next_parsed::<u16>(&mut split)? - 1; let y = next_parsed::<u16>(&mut split)? - 1;
let x = next_parsed::<u16>(&mut split)? - 1; let x = next_parsed::<u16>(&mut split)? - 1;
Ok(Some(InternalEvent::CursorPosition(x, y))) Ok(Some(Event::CursorPosition(x, y)))
} }
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Event>> {
// ESC [ ? flags u // ESC [ ? flags u
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ?
assert!(buffer.ends_with(&[b'u'])); assert!(buffer.ends_with(&[b'u']));
@ -285,10 +256,10 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Inte
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
// } // }
Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags))) Ok(Some(Event::KeyboardEnhancementFlags(flags)))
} }
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Event>> {
// ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c // ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); assert!(buffer.starts_with(&[b'\x1B', b'[', b'?']));
assert!(buffer.ends_with(&[b'c'])); assert!(buffer.ends_with(&[b'c']));
@ -297,7 +268,7 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Inter
// exposed in the crossterm API so we don't need to parse the individual attributes yet. // exposed in the crossterm API so we don't need to parse the individual attributes yet.
// See <https://vt100.net/docs/vt510-rm/DA1.html> // See <https://vt100.net/docs/vt510-rm/DA1.html>
Ok(Some(InternalEvent::PrimaryDeviceAttributes)) Ok(Some(Event::PrimaryDeviceAttributes))
} }
fn parse_modifiers(mask: u8) -> KeyModifiers { fn parse_modifiers(mask: u8) -> KeyModifiers {
@ -345,7 +316,7 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind {
} }
} }
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
// //
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
@ -390,7 +361,7 @@ pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<In
let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind)); let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind));
Ok(Some(InternalEvent::Event(input_event))) Ok(Some(input_event))
} }
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> { fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
@ -494,7 +465,7 @@ fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventSta
None None
} }
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'u'])); assert!(buffer.ends_with(&[b'u']));
@ -545,11 +516,6 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
match c { match c {
'\x1B' => KeyCode::Esc, '\x1B' => KeyCode::Esc,
'\r' => KeyCode::Enter, '\r' => KeyCode::Enter,
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
// newlines as input is because the terminal converts \r into \n for us. When we
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
// use Ctrl+J. Waiting to handle it here means it gets picked up later
'\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter,
'\t' => { '\t' => {
if modifiers.contains(KeyModifiers::SHIFT) { if modifiers.contains(KeyModifiers::SHIFT) {
KeyCode::BackTab KeyCode::BackTab
@ -613,10 +579,10 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
state_from_keycode | state_from_modifiers, state_from_keycode | state_from_modifiers,
)); ));
Ok(Some(InternalEvent::Event(input_event))) Ok(Some(input_event))
} }
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'~'])); assert!(buffer.ends_with(&[b'~']));
@ -657,10 +623,10 @@ pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Int
keycode, modifiers, kind, state, keycode, modifiers, kind, state,
)); ));
Ok(Some(InternalEvent::Event(input_event))) Ok(Some(input_event))
} }
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
// rxvt mouse encoding: // rxvt mouse encoding:
// ESC [ Cb ; Cx ; Cy ; M // ESC [ Cb ; Cx ; Cy ; M
@ -679,15 +645,15 @@ pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalE
let cx = next_parsed::<u16>(&mut split)? - 1; let cx = next_parsed::<u16>(&mut split)? - 1;
let cy = next_parsed::<u16>(&mut split)? - 1; let cy = next_parsed::<u16>(&mut split)? - 1;
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { Ok(Some(Event::Mouse(MouseEvent {
kind, kind,
column: cx, column: cx,
row: cy, row: cy,
modifiers, modifiers,
})))) })))
} }
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
// Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only). // Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only).
assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M
@ -707,15 +673,15 @@ pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Interna
let cx = u16::from(buffer[4].saturating_sub(32)) - 1; let cx = u16::from(buffer[4].saturating_sub(32)) - 1;
let cy = u16::from(buffer[5].saturating_sub(32)) - 1; let cy = u16::from(buffer[5].saturating_sub(32)) - 1;
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { Ok(Some(Event::Mouse(MouseEvent {
kind, kind,
column: cx, column: cx,
row: cy, row: cy,
modifiers, modifiers,
})))) })))
} }
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
// ESC [ < Cb ; Cx ; Cy (;) (M or m) // ESC [ < Cb ; Cx ; Cy (;) (M or m)
assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ < assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ <
@ -752,12 +718,12 @@ pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEv
kind kind
}; };
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { Ok(Some(Event::Mouse(MouseEvent {
kind, kind,
column: cx, column: cx,
row: cy, row: cy,
modifiers, modifiers,
})))) })))
} }
/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being /// Cb is the byte of a mouse input that contains the button being used, the key modifiers being
@ -810,7 +776,7 @@ fn parse_cb(cb: u8) -> io::Result<(MouseEventKind, KeyModifiers)> {
} }
#[cfg(feature = "bracketed-paste")] #[cfg(feature = "bracketed-paste")]
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<InternalEvent>> { pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Event>> {
// ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~ // ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~
assert!(buffer.starts_with(b"\x1B[200~")); assert!(buffer.starts_with(b"\x1B[200~"));
@ -818,7 +784,7 @@ pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Inte
Ok(None) Ok(None)
} else { } else {
let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string(); let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string();
Ok(Some(InternalEvent::Event(Event::Paste(paste)))) Ok(Some(Event::Paste(paste)))
} }
} }
@ -871,7 +837,7 @@ mod tests {
fn test_esc_key() { fn test_esc_key() {
assert_eq!( assert_eq!(
parse_event(b"\x1B", false).unwrap(), parse_event(b"\x1B", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))), Some(Event::Key(KeyCode::Esc.into())),
); );
} }
@ -884,10 +850,10 @@ mod tests {
fn test_alt_key() { fn test_alt_key() {
assert_eq!( assert_eq!(
parse_event(b"\x1Bc", false).unwrap(), parse_event(b"\x1Bc", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('c'), KeyCode::Char('c'),
KeyModifiers::ALT KeyModifiers::ALT
)))), ))),
); );
} }
@ -895,10 +861,10 @@ mod tests {
fn test_alt_shift() { fn test_alt_shift() {
assert_eq!( assert_eq!(
parse_event(b"\x1BH", false).unwrap(), parse_event(b"\x1BH", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('H'), KeyCode::Char('H'),
KeyModifiers::ALT | KeyModifiers::SHIFT KeyModifiers::ALT | KeyModifiers::SHIFT
)))), ))),
); );
} }
@ -906,10 +872,10 @@ mod tests {
fn test_alt_ctrl() { fn test_alt_ctrl() {
assert_eq!( assert_eq!(
parse_event(b"\x1B\x14", false).unwrap(), parse_event(b"\x1B\x14", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('t'), KeyCode::Char('t'),
KeyModifiers::ALT | KeyModifiers::CONTROL KeyModifiers::ALT | KeyModifiers::CONTROL
)))), ))),
); );
} }
@ -921,79 +887,77 @@ mod tests {
// parse_csi_cursor_position // parse_csi_cursor_position
assert_eq!( assert_eq!(
parse_event(b"\x1B[20;10R", false).unwrap(), parse_event(b"\x1B[20;10R", false).unwrap(),
Some(InternalEvent::CursorPosition(9, 19)) Some(Event::CursorPosition(9, 19))
); );
// parse_csi // parse_csi
assert_eq!( assert_eq!(
parse_event(b"\x1B[D", false).unwrap(), parse_event(b"\x1B[D", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), Some(Event::Key(KeyCode::Left.into())),
); );
// parse_csi_modifier_key_code // parse_csi_modifier_key_code
assert_eq!( assert_eq!(
parse_event(b"\x1B[2D", false).unwrap(), parse_event(b"\x1B[2D", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Left, KeyCode::Left,
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))) )))
); );
// parse_csi_special_key_code // parse_csi_special_key_code
assert_eq!( assert_eq!(
parse_event(b"\x1B[3~", false).unwrap(), parse_event(b"\x1B[3~", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), Some(Event::Key(KeyCode::Delete.into())),
); );
// parse_csi_bracketed_paste // parse_csi_bracketed_paste
#[cfg(feature = "bracketed-paste")] #[cfg(feature = "bracketed-paste")]
assert_eq!( assert_eq!(
parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(), parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(),
Some(InternalEvent::Event(Event::Paste( Some(Event::Paste("on and on and on".to_string())),
"on and on and on".to_string()
))),
); );
// parse_csi_rxvt_mouse // parse_csi_rxvt_mouse
assert_eq!( assert_eq!(
parse_event(b"\x1B[32;30;40;M", false).unwrap(), parse_event(b"\x1B[32;30;40;M", false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 29, column: 29,
row: 39, row: 39,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
// parse_csi_normal_mouse // parse_csi_normal_mouse
assert_eq!( assert_eq!(
parse_event(b"\x1B[M0\x60\x70", false).unwrap(), parse_event(b"\x1B[M0\x60\x70", false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 63, column: 63,
row: 79, row: 79,
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}))) }))
); );
// parse_csi_sgr_mouse // parse_csi_sgr_mouse
assert_eq!( assert_eq!(
parse_event(b"\x1B[<0;20;10;M", false).unwrap(), parse_event(b"\x1B[<0;20;10;M", false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 19, column: 19,
row: 9, row: 9,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
// parse_utf8_char // parse_utf8_char
assert_eq!( assert_eq!(
parse_event("Ž".as_bytes(), false).unwrap(), parse_event("Ž".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('Ž'), KeyCode::Char('Ž'),
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))), ))),
); );
} }
@ -1001,7 +965,7 @@ mod tests {
fn test_parse_event() { fn test_parse_event() {
assert_eq!( assert_eq!(
parse_event(b"\t", false).unwrap(), parse_event(b"\t", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))), Some(Event::Key(KeyCode::Tab.into())),
); );
} }
@ -1009,7 +973,7 @@ mod tests {
fn test_parse_csi_cursor_position() { fn test_parse_csi_cursor_position() {
assert_eq!( assert_eq!(
parse_csi_cursor_position(b"\x1B[20;10R").unwrap(), parse_csi_cursor_position(b"\x1B[20;10R").unwrap(),
Some(InternalEvent::CursorPosition(9, 19)) Some(Event::CursorPosition(9, 19))
); );
} }
@ -1017,7 +981,7 @@ mod tests {
fn test_parse_csi() { fn test_parse_csi() {
assert_eq!( assert_eq!(
parse_csi(b"\x1B[D").unwrap(), parse_csi(b"\x1B[D").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), Some(Event::Key(KeyCode::Left.into())),
); );
} }
@ -1025,10 +989,10 @@ mod tests {
fn test_parse_csi_modifier_key_code() { fn test_parse_csi_modifier_key_code() {
assert_eq!( assert_eq!(
parse_csi_modifier_key_code(b"\x1B[2D").unwrap(), parse_csi_modifier_key_code(b"\x1B[2D").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Left, KeyCode::Left,
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))), ))),
); );
} }
@ -1036,7 +1000,7 @@ mod tests {
fn test_parse_csi_special_key_code() { fn test_parse_csi_special_key_code() {
assert_eq!( assert_eq!(
parse_csi_special_key_code(b"\x1B[3~").unwrap(), parse_csi_special_key_code(b"\x1B[3~").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), Some(Event::Key(KeyCode::Delete.into())),
); );
} }
@ -1044,10 +1008,10 @@ mod tests {
fn test_parse_csi_special_key_code_multiple_values_not_supported() { fn test_parse_csi_special_key_code_multiple_values_not_supported() {
assert_eq!( assert_eq!(
parse_csi_special_key_code(b"\x1B[3;2~").unwrap(), parse_csi_special_key_code(b"\x1B[3;2~").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Delete, KeyCode::Delete,
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))), ))),
); );
} }
@ -1067,28 +1031,25 @@ mod tests {
); );
assert_eq!( assert_eq!(
parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(), parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(),
Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string()))) Some(Event::Paste("o\x1B[2D".to_string()))
); );
} }
#[test] #[test]
fn test_parse_csi_focus() { fn test_parse_csi_focus() {
assert_eq!( assert_eq!(parse_csi(b"\x1B[O").unwrap(), Some(Event::FocusLost));
parse_csi(b"\x1B[O").unwrap(),
Some(InternalEvent::Event(Event::FocusLost))
);
} }
#[test] #[test]
fn test_parse_csi_rxvt_mouse() { fn test_parse_csi_rxvt_mouse() {
assert_eq!( assert_eq!(
parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(), parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 29, column: 29,
row: 39, row: 39,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
} }
@ -1096,12 +1057,12 @@ mod tests {
fn test_parse_csi_normal_mouse() { fn test_parse_csi_normal_mouse() {
assert_eq!( assert_eq!(
parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(), parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 63, column: 63,
row: 79, row: 79,
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}))) }))
); );
} }
@ -1109,39 +1070,39 @@ mod tests {
fn test_parse_csi_sgr_mouse() { fn test_parse_csi_sgr_mouse() {
assert_eq!( assert_eq!(
parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(), parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 19, column: 19,
row: 9, row: 9,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
assert_eq!( assert_eq!(
parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(), parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
column: 19, column: 19,
row: 9, row: 9,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
assert_eq!( assert_eq!(
parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(), parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left), kind: MouseEventKind::Up(MouseButton::Left),
column: 19, column: 19,
row: 9, row: 9,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
assert_eq!( assert_eq!(
parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(), parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent { Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left), kind: MouseEventKind::Up(MouseButton::Left),
column: 19, column: 19,
row: 9, row: 9,
modifiers: KeyModifiers::empty(), modifiers: KeyModifiers::empty(),
}))) }))
); );
} }
@ -1193,10 +1154,10 @@ mod tests {
fn test_parse_char_event_lowercase() { fn test_parse_char_event_lowercase() {
assert_eq!( assert_eq!(
parse_event(b"c", false).unwrap(), parse_event(b"c", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('c'), KeyCode::Char('c'),
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
} }
@ -1204,10 +1165,10 @@ mod tests {
fn test_parse_char_event_uppercase() { fn test_parse_char_event_uppercase() {
assert_eq!( assert_eq!(
parse_event(b"C", false).unwrap(), parse_event(b"C", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('C'), KeyCode::Char('C'),
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))), ))),
); );
} }
@ -1215,24 +1176,24 @@ mod tests {
fn test_parse_basic_csi_u_encoded_key_code() { fn test_parse_basic_csi_u_encoded_key_code() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('A'), KeyCode::Char('A'),
KeyModifiers::SHIFT KeyModifiers::SHIFT
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::ALT | KeyModifiers::CONTROL KeyModifiers::ALT | KeyModifiers::CONTROL
)))), ))),
); );
} }
@ -1240,45 +1201,45 @@ mod tests {
fn test_parse_basic_csi_u_encoded_key_code_special_keys() { fn test_parse_basic_csi_u_encoded_key_code_special_keys() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Enter, KeyCode::Enter,
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Esc, KeyCode::Esc,
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::CapsLock, KeyCode::CapsLock,
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::F(13), KeyCode::F(13),
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Media(MediaKeyCode::Play), KeyCode::Media(MediaKeyCode::Play),
KeyModifiers::empty() KeyModifiers::empty()
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Modifier(ModifierKeyCode::LeftShift), KeyCode::Modifier(ModifierKeyCode::LeftShift),
KeyModifiers::SHIFT, KeyModifiers::SHIFT,
)))), ))),
); );
} }
@ -1286,24 +1247,20 @@ mod tests {
fn test_parse_csi_u_encoded_keypad_code() { fn test_parse_csi_u_encoded_keypad_code() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(),
Some(InternalEvent::Event(Event::Key( Some(Event::Key(KeyEvent::new_with_kind_and_state(
KeyEvent::new_with_kind_and_state(
KeyCode::Char('0'), KeyCode::Char('0'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
KeyEventState::KEYPAD, KeyEventState::KEYPAD,
)
))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(),
Some(InternalEvent::Event(Event::Key( Some(Event::Key(KeyEvent::new_with_kind_and_state(
KeyEvent::new_with_kind_and_state(
KeyCode::Up, KeyCode::Up,
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
KeyEventState::KEYPAD, KeyEventState::KEYPAD,
)
))), ))),
); );
} }
@ -1312,43 +1269,43 @@ mod tests {
fn test_parse_csi_u_encoded_key_code_with_types() { fn test_parse_csi_u_encoded_key_code_with_types() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::CONTROL, KeyModifiers::CONTROL,
KeyEventKind::Press, KeyEventKind::Press,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Repeat, KeyEventKind::Repeat,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
} }
@ -1356,40 +1313,40 @@ mod tests {
fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() { fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Modifier(ModifierKeyCode::RightAlt), KeyCode::Modifier(ModifierKeyCode::RightAlt),
KeyModifiers::ALT, KeyModifiers::ALT,
KeyEventKind::Press, KeyEventKind::Press,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Modifier(ModifierKeyCode::RightAlt), KeyCode::Modifier(ModifierKeyCode::RightAlt),
KeyModifiers::ALT, KeyModifiers::ALT,
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Modifier(ModifierKeyCode::RightSuper), KeyCode::Modifier(ModifierKeyCode::RightSuper),
KeyModifiers::SUPER, KeyModifiers::SUPER,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Modifier(ModifierKeyCode::RightHyper), KeyCode::Modifier(ModifierKeyCode::RightHyper),
KeyModifiers::HYPER, KeyModifiers::HYPER,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Modifier(ModifierKeyCode::RightMeta), KeyCode::Modifier(ModifierKeyCode::RightMeta),
KeyModifiers::META, KeyModifiers::META,
)))), ))),
); );
} }
@ -1397,24 +1354,24 @@ mod tests {
fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() { fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::SUPER KeyModifiers::SUPER
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::HYPER, KeyModifiers::HYPER,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::META, KeyModifiers::META,
)))), ))),
); );
} }
@ -1422,24 +1379,20 @@ mod tests {
fn test_parse_csi_u_encoded_key_code_with_extra_state() { fn test_parse_csi_u_encoded_key_code_with_extra_state() {
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(),
Some(InternalEvent::Event(Event::Key( Some(Event::Key(KeyEvent::new_with_kind_and_state(
KeyEvent::new_with_kind_and_state(
KeyCode::Char('a'), KeyCode::Char('a'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
KeyEventState::CAPS_LOCK, KeyEventState::CAPS_LOCK,
)
))), ))),
); );
assert_eq!( assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(), parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(),
Some(InternalEvent::Event(Event::Key( Some(Event::Key(KeyEvent::new_with_kind_and_state(
KeyEvent::new_with_kind_and_state(
KeyCode::Char('1'), KeyCode::Char('1'),
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Press, KeyEventKind::Press,
KeyEventState::NUM_LOCK, KeyEventState::NUM_LOCK,
)
))), ))),
); );
} }
@ -1449,18 +1402,18 @@ mod tests {
assert_eq!( assert_eq!(
// A-S-9 is equivalent to A-( // A-S-9 is equivalent to A-(
parse_event(b"\x1B[57:40;4u", false).unwrap(), parse_event(b"\x1B[57:40;4u", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('('), KeyCode::Char('('),
KeyModifiers::ALT, KeyModifiers::ALT,
)))), ))),
); );
assert_eq!( assert_eq!(
// A-S-minus is equivalent to A-_ // A-S-minus is equivalent to A-_
parse_event(b"\x1B[45:95;4u", false).unwrap(), parse_event(b"\x1B[45:95;4u", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new( Some(Event::Key(KeyEvent::new(
KeyCode::Char('_'), KeyCode::Char('_'),
KeyModifiers::ALT, KeyModifiers::ALT,
)))), ))),
); );
} }
@ -1468,19 +1421,19 @@ mod tests {
fn test_parse_csi_special_key_code_with_types() { fn test_parse_csi_special_key_code_with_types() {
assert_eq!( assert_eq!(
parse_event(b"\x1B[;1:3B", false).unwrap(), parse_event(b"\x1B[;1:3B", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Down, KeyCode::Down,
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_event(b"\x1B[1;1:3B", false).unwrap(), parse_event(b"\x1B[1;1:3B", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::Down, KeyCode::Down,
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
} }
@ -1488,19 +1441,19 @@ mod tests {
fn test_parse_csi_numbered_escape_code_with_types() { fn test_parse_csi_numbered_escape_code_with_types() {
assert_eq!( assert_eq!(
parse_event(b"\x1B[5;1:3~", false).unwrap(), parse_event(b"\x1B[5;1:3~", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::PageUp, KeyCode::PageUp,
KeyModifiers::empty(), KeyModifiers::empty(),
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
assert_eq!( assert_eq!(
parse_event(b"\x1B[6;5:3~", false).unwrap(), parse_event(b"\x1B[6;5:3~", false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( Some(Event::Key(KeyEvent::new_with_kind(
KeyCode::PageDown, KeyCode::PageDown,
KeyModifiers::CONTROL, KeyModifiers::CONTROL,
KeyEventKind::Release, KeyEventKind::Release,
)))), ))),
); );
} }
} }

View File

@ -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;

View File

@ -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(())
}
}

View File

@ -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(())
}
}

View File

@ -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(())
}

View File

@ -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,
}))
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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));
}
}

View File

@ -241,20 +241,5 @@ pub mod style;
/// A module to work with the terminal. /// A module to work with the terminal.
pub mod terminal; pub mod terminal;
/// A module to query if the current instance is a tty.
pub mod tty;
#[cfg(windows)]
/// A module that exposes one function to check if the current terminal supports ANSI sequences.
pub mod ansi_support;
mod command; mod command;
pub(crate) mod macros; pub(crate) mod macros;
#[cfg(all(windows, not(feature = "windows")))]
compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows.");
#[cfg(all(winapi, not(feature = "winapi")))]
compile_error!("Compiling on Windows with \"winapi\" feature disabled. Feature \"winapi\" should only be disabled when project will never be compiled on Windows.");
#[cfg(all(crossterm_winapi, not(feature = "crossterm_winapi")))]
compile_error!("Compiling on Windows with \"crossterm_winapi\" feature disabled. Feature \"crossterm_winapi\" should only be disabled when project will never be compiled on Windows.");

View File

@ -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. /// A command that sets the the foreground color.
/// ///
/// See [`Color`](enum.Color.html) for more info. /// See [`Color`](enum.Color.html) for more info.

View File

@ -1,6 +1,4 @@
use parking_lot::Once;
use std::fmt::{self, Formatter}; use std::fmt::{self, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,9 +21,6 @@ pub enum Colored {
UnderlineColor(Color), UnderlineColor(Color),
} }
static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false);
static INITIALIZER: Once = Once::new();
impl Colored { impl Colored {
/// Parse an ANSI foreground or background color. /// Parse an ANSI foreground or background color.
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in /// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
@ -69,38 +64,12 @@ impl Colored {
Some(output) Some(output)
} }
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
/// in environment as per https://no-color.org/
pub fn ansi_color_disabled() -> bool {
!std::env::var("NO_COLOR")
.unwrap_or("".to_string())
.is_empty()
}
pub fn ansi_color_disabled_memoized() -> bool {
INITIALIZER.call_once(|| {
ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst);
});
ANSI_COLOR_DISABLED.load(Ordering::SeqCst)
}
pub fn set_ansi_color_disabled(val: bool) {
// Force the one-time initializer to run.
_ = Self::ansi_color_disabled_memoized();
ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst);
}
} }
impl fmt::Display for Colored { impl fmt::Display for Colored {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let color; let color;
if Self::ansi_color_disabled_memoized() {
return Ok(());
}
match *self { match *self {
Colored::ForegroundColor(new_color) => { Colored::ForegroundColor(new_color) => {
if new_color == Color::Reset { if new_color == Color::Reset {

View File

@ -83,60 +83,15 @@
//! //!
//! For manual execution control check out [crossterm::queue](../macro.queue.html). //! For manual execution control check out [crossterm::queue](../macro.queue.html).
use std::{fmt, io}; use std::fmt;
#[cfg(windows)]
use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(windows)]
use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT;
#[doc(no_inline)] #[doc(no_inline)]
use crate::Command; use crate::Command;
use crate::{csi, impl_display}; use crate::{csi, impl_display};
pub(crate) mod sys;
#[cfg(feature = "events")]
pub use sys::supports_keyboard_enhancement;
/// Tells whether the raw mode is enabled.
///
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
pub fn is_raw_mode_enabled() -> io::Result<bool> {
#[cfg(unix)]
{
Ok(sys::is_raw_mode_enabled())
}
#[cfg(windows)]
{
sys::is_raw_mode_enabled()
}
}
/// Enables raw mode.
///
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
pub fn enable_raw_mode() -> io::Result<()> {
sys::enable_raw_mode()
}
/// Disables raw mode.
///
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
pub fn disable_raw_mode() -> io::Result<()> {
sys::disable_raw_mode()
}
/// Returns the terminal size `(columns, rows)`.
///
/// The top left cell is represented `(1, 1)`.
pub fn size() -> io::Result<(u16, u16)> {
sys::size()
}
#[derive(Debug)] #[derive(Debug)]
pub struct WindowSize { pub struct WindowSize {
pub rows: u16, pub rows: u16,
@ -145,15 +100,6 @@ pub struct WindowSize {
pub height: u16, pub height: u16,
} }
/// Returns the terminal size `[WindowSize]`.
///
/// The width and height in pixels may not be reliably implemented or default to 0.
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
/// For windows it is not implemented.
pub fn window_size() -> io::Result<WindowSize> {
sys::window_size()
}
/// Disables line wrapping. /// Disables line wrapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisableLineWrap; pub struct DisableLineWrap;
@ -301,11 +247,6 @@ impl Command for ScrollUp {
} }
Ok(()) Ok(())
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
sys::scroll_up(self.0)
}
} }
/// A command that scrolls the terminal screen a given number of rows down. /// A command that scrolls the terminal screen a given number of rows down.
@ -323,11 +264,6 @@ impl Command for ScrollDown {
} }
Ok(()) Ok(())
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
sys::scroll_down(self.0)
}
} }
/// A command that clears the terminal screen buffer. /// A command that clears the terminal screen buffer.
@ -351,11 +287,6 @@ impl Command for Clear {
ClearType::UntilNewLine => csi!("K"), ClearType::UntilNewLine => csi!("K"),
}) })
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
sys::clear(self.0)
}
} }
/// A command that sets the terminal buffer size `(columns, rows)`. /// A command that sets the terminal buffer size `(columns, rows)`.
@ -370,11 +301,6 @@ impl Command for SetSize {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, csi!("8;{};{}t"), self.1, self.0) write!(f, csi!("8;{};{}t"), self.1, self.0)
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
sys::set_size(self.0, self.1)
}
} }
/// A command that sets the terminal title /// A command that sets the terminal title
@ -389,11 +315,6 @@ impl<T: fmt::Display> Command for SetTitle<T> {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1B]0;{}\x07", &self.0) write!(f, "\x1B]0;{}\x07", &self.0)
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
sys::set_window_title(&self.0)
}
} }
/// A command that instructs the terminal emulator to begin a synchronized frame. /// A command that instructs the terminal emulator to begin a synchronized frame.
@ -436,17 +357,6 @@ impl Command for BeginSynchronizedUpdate {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?2026h")) f.write_str(csi!("?2026h"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Ok(())
}
#[cfg(windows)]
#[inline]
fn is_ansi_code_supported(&self) -> bool {
true
}
} }
/// A command that instructs the terminal to end a synchronized frame. /// A command that instructs the terminal to end a synchronized frame.
@ -489,17 +399,6 @@ impl Command for EndSynchronizedUpdate {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("?2026l")) f.write_str(csi!("?2026l"))
} }
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Ok(())
}
#[cfg(windows)]
#[inline]
fn is_ansi_code_supported(&self) -> bool {
true
}
} }
impl_display!(for ScrollUp); impl_display!(for ScrollUp);

View File

@ -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;

View File

@ -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)
}

View File

@ -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(())
}
}

View File

@ -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[..]);
}
}

View File

@ -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
}
}