From 1b66c396b6cfe83894e94d851b2f1fef18f38230 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sun, 11 Aug 2024 20:55:08 +1000 Subject: [PATCH] Remove and simplify for webassembly use case --- Cargo.toml | 94 +--- README.md | 12 +- examples/README.md | 40 -- examples/event-match-modifiers.rs | 68 --- examples/event-poll-read.rs | 61 --- examples/event-read-char-line.rs | 44 -- examples/event-read.rs | 112 ----- examples/event-stream-async-std.rs | 67 --- examples/event-stream-tokio.rs | 68 --- examples/interactive-demo/Cargo.toml | 13 - examples/interactive-demo/src/macros.rs | 29 -- examples/interactive-demo/src/main.rs | 104 ---- examples/interactive-demo/src/test.rs | 5 - .../interactive-demo/src/test/attribute.rs | 58 --- examples/interactive-demo/src/test/color.rs | 198 -------- examples/interactive-demo/src/test/cursor.rs | 222 --------- examples/interactive-demo/src/test/event.rs | 42 -- .../src/test/synchronized_output.rs | 41 -- examples/is_tty.rs | 18 - examples/key-display.rs | 49 -- examples/stderr.rs | 95 ---- src/cursor.rs | 5 - src/cursor/sys.rs | 20 - src/cursor/sys/unix.rs | 56 --- src/cursor/sys/windows.rs | 341 ------------- src/event.rs | 349 ++----------- src/event/filter.rs | 66 +-- src/event/read.rs | 430 ---------------- src/event/source.rs | 27 - src/event/source/unix.rs | 11 - src/event/source/unix/mio.rs | 229 --------- src/event/source/unix/tty.rs | 275 ---------- src/event/source/windows.rs | 100 ---- src/event/sys.rs | 8 - src/event/sys/unix.rs | 3 - src/event/sys/unix/parse.rs | 389 +++++++-------- src/event/sys/unix/waker.rs | 11 - src/event/sys/unix/waker/mio.rs | 34 -- src/event/sys/unix/waker/tty.rs | 28 -- src/event/sys/windows.rs | 48 -- src/event/sys/windows/parse.rs | 378 -------------- src/event/sys/windows/poll.rs | 86 ---- src/event/sys/windows/waker.rs | 40 -- src/event/timeout.rs | 92 ---- src/lib.rs | 15 - src/style.rs | 12 - src/style/types/colored.rs | 31 -- src/terminal.rs | 103 +--- src/terminal/sys.rs | 27 - src/terminal/sys/file_descriptor.rs | 154 ------ src/terminal/sys/unix.rs | 315 ------------ src/terminal/sys/windows.rs | 471 ------------------ src/tty.rs | 54 -- 53 files changed, 255 insertions(+), 5393 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/event-match-modifiers.rs delete mode 100644 examples/event-poll-read.rs delete mode 100644 examples/event-read-char-line.rs delete mode 100644 examples/event-read.rs delete mode 100644 examples/event-stream-async-std.rs delete mode 100644 examples/event-stream-tokio.rs delete mode 100644 examples/interactive-demo/Cargo.toml delete mode 100644 examples/interactive-demo/src/macros.rs delete mode 100644 examples/interactive-demo/src/main.rs delete mode 100644 examples/interactive-demo/src/test.rs delete mode 100644 examples/interactive-demo/src/test/attribute.rs delete mode 100644 examples/interactive-demo/src/test/color.rs delete mode 100644 examples/interactive-demo/src/test/cursor.rs delete mode 100644 examples/interactive-demo/src/test/event.rs delete mode 100644 examples/interactive-demo/src/test/synchronized_output.rs delete mode 100644 examples/is_tty.rs delete mode 100644 examples/key-display.rs delete mode 100644 examples/stderr.rs delete mode 100644 src/cursor/sys.rs delete mode 100644 src/cursor/sys/unix.rs delete mode 100644 src/cursor/sys/windows.rs delete mode 100644 src/event/read.rs delete mode 100644 src/event/source.rs delete mode 100644 src/event/source/unix.rs delete mode 100644 src/event/source/unix/mio.rs delete mode 100644 src/event/source/unix/tty.rs delete mode 100644 src/event/source/windows.rs delete mode 100644 src/event/sys/unix/waker.rs delete mode 100644 src/event/sys/unix/waker/mio.rs delete mode 100644 src/event/sys/unix/waker/tty.rs delete mode 100644 src/event/sys/windows.rs delete mode 100644 src/event/sys/windows/parse.rs delete mode 100644 src/event/sys/windows/poll.rs delete mode 100644 src/event/sys/windows/waker.rs delete mode 100644 src/event/timeout.rs delete mode 100644 src/terminal/sys.rs delete mode 100644 src/terminal/sys/file_descriptor.rs delete mode 100644 src/terminal/sys/unix.rs delete mode 100644 src/terminal/sys/windows.rs delete mode 100644 src/tty.rs diff --git a/Cargo.toml b/Cargo.toml index 27d6448..f297af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [package] -name = "crossterm" +name = "minicrossterm" version = "0.28.1" -authors = ["T. Post"] -description = "A crossplatform terminal library for manipulating terminals." -repository = "https://github.com/crossterm-rs/crossterm" -documentation = "https://docs.rs/crossterm/" +authors = ["Blasthavers", "T. Post"] +description = "A stripped back crossplatform terminal library for manipulating terminals asynchronously." +repository = "https://git.blastmud.org/blasthavers/crossterm" license = "MIT" keywords = ["event", "color", "cli", "input", "terminal"] exclude = ["target", "Cargo.lock"] @@ -27,22 +26,10 @@ all-features = true # Features # [features] -default = ["bracketed-paste", "windows", "events"] -windows = [ - "dep:winapi", - "dep:crossterm_winapi", -] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows). +default = ["bracketed-paste", "events"] bracketed-paste = [ ] # Enables triggering a `Event::Paste` when pasting text into the terminal. -event-stream = ["dep:futures-core", "events"] # Enables async events -use-dev-tty = [ - "filedescriptor", - "rustix/process", -] # Enables raw file descriptor polling / selecting instead of mio. events = [ - "dep:mio", - "dep:signal-hook", - "dep:signal-hook-mio", ] # Enables reading input/events from the system. serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types. @@ -51,85 +38,14 @@ serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types. # [dependencies] bitflags = { version = "2.3" } -parking_lot = "0.12" # optional deps only added when requested -futures-core = { version = "0.3", optional = true, default-features = false } serde = { version = "1.0", features = ["derive"], optional = true } -# -# Windows dependencies -# -[target.'cfg(windows)'.dependencies.winapi] -version = "0.3.9" -features = ["winuser", "winerror"] -optional = true - -[target.'cfg(windows)'.dependencies] -crossterm_winapi = { version = "0.9.1", optional = true } - -# -# UNIX dependencies -# -[target.'cfg(unix)'.dependencies] -# Default to using rustix for UNIX systems, but provide an option to use libc for backwards -# compatibility. -libc = { version = "0.2", default-features = false, optional = true } -rustix = { version = "0.38.34", default-features = false, features = [ - "std", - "stdio", - "termios", -] } -signal-hook = { version = "0.3.17", optional = true } -filedescriptor = { version = "0.8", optional = true } -mio = { version = "1.0", features = ["os-poll"], optional = true } -signal-hook-mio = { version = "0.2.4", features = [ - "support-v1_0", -], optional = true } - # # Dev dependencies (examples, ...) # [dev-dependencies] -tokio = { version = "1.25", features = ["full"] } -futures = "0.3" -futures-timer = "3.0" -async-std = "1.12" serde_json = "1.0" serial_test = "2.0.0" temp-env = "0.3.6" - -# -# Examples -# -[[example]] -name = "event-read" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-match-modifiers" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-poll-read" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-stream-async-std" -required-features = ["event-stream", "events"] - -[[example]] -name = "event-stream-tokio" -required-features = ["event-stream", "events"] - -[[example]] -name = "event-read-char-line" -required-features = ["events"] - -[[example]] -name = "stderr" -required-features = ["events"] - -[[example]] -name = "key-display" -required-features = ["events"] diff --git a/README.md b/README.md index 1fabfe0..3c3f5df 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -

+# Minicrossterm -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5] +Minicrossterm is a cut back fork of crossterm which doesn't depend on stdio, and is intended +to only provide non-blocking functionality for applications without relying on standard IO ( +for example, from webassembly). + +It was created for Blasthavers Worldwideportal, and not by the original authors of crossterm. + +The following information applies to crossterm - some might not be up to date for minicrossterm. # Cross-platform Terminal Manipulation Library Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested, see [Tested Terminals](#tested-terminals) for more info). +[![Donate to crossterm upstream](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2)] + ## Table of Contents - [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library) diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index c9884ec..0000000 --- a/examples/README.md +++ /dev/null @@ -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 diff --git a/examples/event-match-modifiers.rs b/examples/event-match-modifiers.rs deleted file mode 100644 index c3f75e9..0000000 --- a/examples/event-match-modifiers.rs +++ /dev/null @@ -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, - ))); -} diff --git a/examples/event-poll-read.rs b/examples/event-poll-read.rs deleted file mode 100644 index df960ab..0000000 --- a/examples/event-poll-read.rs +++ /dev/null @@ -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() -} diff --git a/examples/event-read-char-line.rs b/examples/event-read-char-line.rs deleted file mode 100644 index 5fa8349..0000000 --- a/examples/event-read-char-line.rs +++ /dev/null @@ -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 { - loop { - if let Event::Key(KeyEvent { - code: KeyCode::Char(c), - .. - }) = event::read()? - { - return Ok(c); - } - } -} - -pub fn read_line() -> io::Result { - 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()); -} diff --git a/examples/event-read.rs b/examples/event-read.rs deleted file mode 100644 index aaf74fe..0000000 --- a/examples/event-read.rs +++ /dev/null @@ -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() -} diff --git a/examples/event-stream-async-std.rs b/examples/event-stream-async-std.rs deleted file mode 100644 index abd5449..0000000 --- a/examples/event-stream-async-std.rs +++ /dev/null @@ -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() -} diff --git a/examples/event-stream-tokio.rs b/examples/event-stream-tokio.rs deleted file mode 100644 index 6cea39c..0000000 --- a/examples/event-stream-tokio.rs +++ /dev/null @@ -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() -} diff --git a/examples/interactive-demo/Cargo.toml b/examples/interactive-demo/Cargo.toml deleted file mode 100644 index f7a5134..0000000 --- a/examples/interactive-demo/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "interactive-demo" -version = "0.0.1" -authors = ["T. Post", "Robert Vojta "] -edition = "2018" -description = "Interactive demo for crossterm." -license = "MIT" -exclude = ["target", "Cargo.lock"] -readme = "README.md" -publish = false - -[dependencies] -crossterm = { path = "../../" } \ No newline at end of file diff --git a/examples/interactive-demo/src/macros.rs b/examples/interactive-demo/src/macros.rs deleted file mode 100644 index 02ce9e2..0000000 --- a/examples/interactive-demo/src/macros.rs +++ /dev/null @@ -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), - _ => { }, - }; - )* - } -} diff --git a/examples/interactive-demo/src/main.rs b/examples/interactive-demo/src/main.rs deleted file mode 100644 index 5eb1c73..0000000 --- a/examples/interactive-demo/src/main.rs +++ /dev/null @@ -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: &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 { - 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) -} diff --git a/examples/interactive-demo/src/test.rs b/examples/interactive-demo/src/test.rs deleted file mode 100644 index 1db95a2..0000000 --- a/examples/interactive-demo/src/test.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod attribute; -pub mod color; -pub mod cursor; -pub mod event; -pub mod synchronized_output; diff --git a/examples/interactive-demo/src/test/attribute.rs b/examples/interactive-demo/src/test/attribute.rs deleted file mode 100644 index 6c06752..0000000 --- a/examples/interactive-demo/src/test/attribute.rs +++ /dev/null @@ -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: &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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_set_display_attributes,); - Ok(()) -} diff --git a/examples/interactive-demo/src/test/color.rs b/examples/interactive-demo/src/test/color.rs deleted file mode 100644 index d34ac45..0000000 --- a/examples/interactive-demo/src/test/color.rs +++ /dev/null @@ -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: &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: &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: &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: &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: &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: &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: &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: &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(()) -} diff --git a/examples/interactive-demo/src/test/cursor.rs b/examples/interactive-demo/src/test/cursor.rs deleted file mode 100644 index 9ad10d5..0000000 --- a/examples/interactive-demo/src/test/cursor.rs +++ /dev/null @@ -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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2)) -} - -fn test_move_cursor_down(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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2)) -} - -fn test_move_cursor_right(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: &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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1)) -} - -fn test_move_cursor_to_column(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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, style::Print("HideCursor"), cursor::Hide) -} - -fn test_show_cursor(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, style::Print("ShowCursor"), cursor::Show) -} - -fn test_cursor_blinking_block(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: &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: &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: &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: &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: &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: &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(()) -} diff --git a/examples/interactive-demo/src/test/event.rs b/examples/interactive-demo/src/test/event.rs deleted file mode 100644 index 50a797e..0000000 --- a/examples/interactive-demo/src/test/event.rs +++ /dev/null @@ -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: &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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_event); - Ok(()) -} diff --git a/examples/interactive-demo/src/test/synchronized_output.rs b/examples/interactive-demo/src/test/synchronized_output.rs deleted file mode 100644 index 4fba674..0000000 --- a/examples/interactive-demo/src/test/synchronized_output.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io::Write; - -use crossterm::{cursor, execute, style::Print, SynchronizedUpdate}; - -fn render_slowly(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: &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: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_slow_rendering,); - Ok(()) -} diff --git a/examples/is_tty.rs b/examples/is_tty.rs deleted file mode 100644 index 85770e2..0000000 --- a/examples/is_tty.rs +++ /dev/null @@ -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"); - } -} diff --git a/examples/key-display.rs b/examples/key-display.rs deleted file mode 100644 index c5ea7b8..0000000 --- a/examples/key-display.rs +++ /dev/null @@ -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(()) -} diff --git a/examples/stderr.rs b/examples/stderr.rs deleted file mode 100644 index ce523c6..0000000 --- a/examples/stderr.rs +++ /dev/null @@ -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(write: &mut W) -> io::Result -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 { - 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), - } -} diff --git a/src/cursor.rs b/src/cursor.rs index 24796dc..d76c682 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -46,11 +46,6 @@ use std::fmt; use crate::{csi, impl_display, Command}; -pub(crate) mod sys; - -#[cfg(feature = "events")] -pub use sys::position; - /// A command that moves the terminal cursor to the given position (column, row). /// /// # Notes diff --git a/src/cursor/sys.rs b/src/cursor/sys.rs deleted file mode 100644 index 1623740..0000000 --- a/src/cursor/sys.rs +++ /dev/null @@ -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; diff --git a/src/cursor/sys/unix.rs b/src/cursor/sys/unix.rs deleted file mode 100644 index 4734212..0000000 --- a/src/cursor/sys/unix.rs +++ /dev/null @@ -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(_) => {} - } - } -} diff --git a/src/cursor/sys/windows.rs b/src/cursor/sys/windows.rs deleted file mode 100644 index 84a4cd0..0000000 --- a/src/cursor/sys/windows.rs +++ /dev/null @@ -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 { - 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 { - Ok(ScreenBufferCursor { - screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?), - }) - } - - fn position(&self) -> std::io::Result { - 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 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); - } -} diff --git a/src/event.rs b/src/event.rs index 4d28dc1..cbba317 100644 --- a/src/event.rs +++ b/src/event.rs @@ -119,165 +119,18 @@ //! them (`event-*`). pub(crate) mod filter; -pub(crate) mod read; -pub(crate) mod source; -#[cfg(feature = "event-stream")] -pub(crate) mod stream; pub(crate) mod sys; -pub(crate) mod timeout; -#[cfg(feature = "event-stream")] -pub use stream::EventStream; - -use crate::event::{ - filter::{EventFilter, Filter}, - read::InternalEventReader, - timeout::PollTimeout, -}; use crate::{csi, Command}; -use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; -use std::fmt::{self, Display}; -use std::time::Duration; +use std::{ + collections::VecDeque, + fmt::{self, Display}, +}; use bitflags::bitflags; use std::hash::{Hash, Hasher}; -/// Static instance of `InternalEventReader`. -/// This needs to be static because there can be one event reader. -static INTERNAL_EVENT_READER: Mutex> = 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> { - 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 { -/// // 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 { -/// // 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 { - 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 { -/// 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 { -/// 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 { - 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(timeout: Option, filter: &F) -> std::io::Result -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(filter: &F) -> std::io::Result -where - F: Filter, -{ - let mut reader = lock_internal_event_reader(); - reader.read(filter) -} +use self::sys::unix::parse::parse_event; bitflags! { /// Represents special flags that tell compatible terminals to add extra information to keyboard events. @@ -331,16 +184,6 @@ impl Command for EnableMouseCapture { csi!("?1006h"), )) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::enable_mouse_capture() - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } } /// A command that disables mouse event capturing. @@ -360,16 +203,6 @@ impl Command for DisableMouseCapture { csi!("?1000l"), )) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::disable_mouse_capture() - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } } /// A command that enables focus event emission. @@ -384,12 +217,6 @@ impl Command for EnableFocusChange { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?1004h")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // Focus events are always enabled on Windows - Ok(()) - } } /// A command that disables focus event emission. @@ -400,12 +227,6 @@ impl Command for DisableFocusChange { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?1004l")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // Focus events can't be disabled on Windows - Ok(()) - } } /// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste). @@ -423,14 +244,6 @@ impl Command for EnableBracketedPaste { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?2004h")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Bracketed paste not implemented in the legacy Windows API.", - )) - } } /// A command that disables bracketed paste mode. @@ -443,11 +256,6 @@ impl Command for DisableBracketedPaste { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?2004l")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } } /// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys. @@ -494,21 +302,6 @@ impl Command for PushKeyboardEnhancementFlags { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { write!(f, "{}{}u", csi!(">"), self.0.bits()) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - use std::io; - - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Keyboard progressive enhancement not implemented for the legacy Windows API.", - )) - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } } /// A command that disables extra kinds of keyboard events. @@ -523,21 +316,6 @@ impl Command for PopKeyboardEnhancementFlags { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("<1u")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - use std::io; - - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Keyboard progressive enhancement not implemented for the legacy Windows API.", - )) - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } } /// Represents an event. @@ -560,6 +338,12 @@ pub enum Event { /// An resize event with new dimensions after resize (columns, rows). /// **Note** that resize events can occur in batches. Resize(u16, u16), + /// A cursor position (`col`, `row`). + CursorPosition(u16, u16), + /// The progressive keyboard enhancement flags enabled by the terminal. + KeyboardEnhancementFlags(KeyboardEnhancementFlags), + /// Attributes and architectural class of the terminal. + PrimaryDeviceAttributes, } /// Represents a mouse event. @@ -677,19 +461,8 @@ impl Display for KeyModifiers { } match modifier { KeyModifiers::SHIFT => f.write_str("Shift")?, - #[cfg(unix)] KeyModifiers::CONTROL => f.write_str("Control")?, - #[cfg(windows)] - KeyModifiers::CONTROL => f.write_str("Ctrl")?, - #[cfg(target_os = "macos")] - KeyModifiers::ALT => f.write_str("Option")?, - #[cfg(not(target_os = "macos"))] KeyModifiers::ALT => f.write_str("Alt")?, - #[cfg(target_os = "macos")] - KeyModifiers::SUPER => f.write_str("Command")?, - #[cfg(target_os = "windows")] - KeyModifiers::SUPER => f.write_str("Windows")?, - #[cfg(not(any(target_os = "macos", target_os = "windows")))] KeyModifiers::SUPER => f.write_str("Super")?, KeyModifiers::HYPER => f.write_str("Hyper")?, KeyModifiers::META => f.write_str("Meta")?, @@ -1119,20 +892,9 @@ impl Display for KeyCode { /// displayed as "Del", and the Enter key is displayed as "Enter". fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - // On macOS, the Backspace key is called "Delete" and the Delete key is called "Fwd Del". - #[cfg(target_os = "macos")] - KeyCode::Backspace => write!(f, "Delete"), - #[cfg(target_os = "macos")] - KeyCode::Delete => write!(f, "Fwd Del"), - - #[cfg(not(target_os = "macos"))] KeyCode::Backspace => write!(f, "Backspace"), - #[cfg(not(target_os = "macos"))] KeyCode::Delete => write!(f, "Del"), - #[cfg(target_os = "macos")] - KeyCode::Enter => write!(f, "Return"), - #[cfg(not(target_os = "macos"))] KeyCode::Enter => write!(f, "Enter"), KeyCode::Left => write!(f, "Left"), KeyCode::Right => write!(f, "Right"), @@ -1166,25 +928,6 @@ impl Display for KeyCode { } } -/// An internal event. -/// -/// Encapsulates publicly available `Event` with additional internal -/// events that shouldn't be publicly available to the crate users. -#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Eq)] -pub(crate) enum InternalEvent { - /// An event. - Event(Event), - /// A cursor position (`col`, `row`). - #[cfg(unix)] - CursorPosition(u16, u16), - /// The progressive keyboard enhancement flags enabled by the terminal. - #[cfg(unix)] - KeyboardEnhancementFlags(KeyboardEnhancementFlags), - /// Attributes and architectural class of the terminal. - #[cfg(unix)] - PrimaryDeviceAttributes, -} - #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; @@ -1227,18 +970,9 @@ mod tests { #[test] fn keycode_display() { - #[cfg(target_os = "macos")] - { - assert_eq!(format!("{}", Backspace), "Delete"); - assert_eq!(format!("{}", Delete), "Fwd Del"); - assert_eq!(format!("{}", Enter), "Return"); - } - #[cfg(not(target_os = "macos"))] - { - assert_eq!(format!("{}", Backspace), "Backspace"); - assert_eq!(format!("{}", Delete), "Del"); - assert_eq!(format!("{}", Enter), "Enter"); - } + assert_eq!(format!("{}", Backspace), "Backspace"); + assert_eq!(format!("{}", Delete), "Del"); + assert_eq!(format!("{}", Enter), "Enter"); assert_eq!(format!("{}", Left), "Left"); assert_eq!(format!("{}", Right), "Right"); assert_eq!(format!("{}", Up), "Up"); @@ -1292,29 +1026,6 @@ mod tests { assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift"); } - #[cfg(target_os = "macos")] - #[test] - fn modifier_keycode_display_macos() { - assert_eq!(format!("{}", Modifier(LeftControl)), "Left Control"); - assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Option"); - assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Command"); - assert_eq!(format!("{}", Modifier(RightControl)), "Right Control"); - assert_eq!(format!("{}", Modifier(RightAlt)), "Right Option"); - assert_eq!(format!("{}", Modifier(RightSuper)), "Right Command"); - } - - #[cfg(target_os = "windows")] - #[test] - fn modifier_keycode_display_windows() { - assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl"); - assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Alt"); - assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Windows"); - assert_eq!(format!("{}", Modifier(RightControl)), "Right Ctrl"); - assert_eq!(format!("{}", Modifier(RightAlt)), "Right Alt"); - assert_eq!(format!("{}", Modifier(RightSuper)), "Right Windows"); - } - - #[cfg(not(any(target_os = "macos", target_os = "windows")))] #[test] fn modifier_keycode_display_other() { assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl"); @@ -1325,3 +1036,35 @@ mod tests { assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super"); } } + +pub struct TerminalState { + char_buffer: VecDeque, +} + +impl TerminalState { + pub fn new() -> Self { + Self { + char_buffer: VecDeque::::new(), + } + } + + pub fn events_for_input(&mut self, input: &[u8]) -> std::io::Result> { + let mut result: Vec = Vec::new(); + + self.char_buffer.extend(input); + let mut buf: Vec = 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), + } + } + } + } + } +} diff --git a/src/event/filter.rs b/src/event/filter.rs index f78730d..1f825cd 100644 --- a/src/event/filter.rs +++ b/src/event/filter.rs @@ -1,48 +1,42 @@ -use crate::event::InternalEvent; +use crate::event::Event; /// Interface for filtering an `InternalEvent`. pub(crate) trait Filter: Send + Sync + 'static { /// Returns whether the given event fulfills the filter. - fn eval(&self, event: &InternalEvent) -> bool; + fn eval(&self, event: &Event) -> bool; } -#[cfg(unix)] #[derive(Debug, Clone)] pub(crate) struct CursorPositionFilter; -#[cfg(unix)] impl Filter for CursorPositionFilter { - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::CursorPosition(_, _)) + fn eval(&self, event: &Event) -> bool { + matches!(*event, Event::CursorPosition(_, _)) } } -#[cfg(unix)] #[derive(Debug, Clone)] pub(crate) struct KeyboardEnhancementFlagsFilter; -#[cfg(unix)] impl Filter for KeyboardEnhancementFlagsFilter { - fn eval(&self, event: &InternalEvent) -> bool { + fn eval(&self, event: &Event) -> bool { // This filter checks for either a KeyboardEnhancementFlags response or // a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes // response but not KeyboardEnhancementFlags, the terminal does not support // progressive keyboard enhancement. matches!( *event, - InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes + Event::KeyboardEnhancementFlags(_) | Event::PrimaryDeviceAttributes ) } } -#[cfg(unix)] #[derive(Debug, Clone)] pub(crate) struct PrimaryDeviceAttributesFilter; -#[cfg(unix)] impl Filter for PrimaryDeviceAttributesFilter { - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::PrimaryDeviceAttributes) + fn eval(&self, event: &Event) -> bool { + matches!(*event, Event::PrimaryDeviceAttributes) } } @@ -50,66 +44,50 @@ impl Filter for PrimaryDeviceAttributesFilter { pub(crate) struct EventFilter; impl Filter for EventFilter { - #[cfg(unix)] - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::Event(_)) - } - - #[cfg(windows)] - fn eval(&self, _: &InternalEvent) -> bool { - true - } -} - -#[derive(Debug, Clone)] -pub(crate) struct InternalEventFilter; - -impl Filter for InternalEventFilter { - fn eval(&self, _: &InternalEvent) -> bool { + fn eval(&self, _: &Event) -> bool { true } } #[cfg(test)] -#[cfg(unix)] mod tests { use super::{ - super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, - InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, + super::Event, CursorPositionFilter, Event, EventFilter, Filter, InternalEventFilter, + KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, }; #[test] fn test_cursor_position_filter_filters_cursor_position() { - assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); + assert!(!CursorPositionFilter.eval(&Event::Event(Event::Resize(10, 10)))); + assert!(CursorPositionFilter.eval(&Event::CursorPosition(0, 0))); } #[test] fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { - assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!(!KeyboardEnhancementFlagsFilter.eval(&Event::Event(Event::Resize(10, 10)))); assert!( - KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( + KeyboardEnhancementFlagsFilter.eval(&Event::KeyboardEnhancementFlags( crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES )) ); - assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); + assert!(KeyboardEnhancementFlagsFilter.eval(&Event::PrimaryDeviceAttributes)); } #[test] fn test_primary_device_attributes_filter_filters_primary_device_attributes() { - assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); + assert!(!PrimaryDeviceAttributesFilter.eval(&Event::Event(Event::Resize(10, 10)))); + assert!(PrimaryDeviceAttributesFilter.eval(&Event::PrimaryDeviceAttributes)); } #[test] fn test_event_filter_filters_events() { - assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + assert!(EventFilter.eval(&Event::Event(Event::Resize(10, 10)))); + assert!(!EventFilter.eval(&Event::CursorPosition(0, 0))); } #[test] fn test_event_filter_filters_internal_events() { - assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + assert!(InternalEventFilter.eval(&Event::Event(Event::Resize(10, 10)))); + assert!(InternalEventFilter.eval(&Event::CursorPosition(0, 0))); } } diff --git a/src/event/read.rs b/src/event/read.rs deleted file mode 100644 index 6ddbace..0000000 --- a/src/event/read.rs +++ /dev/null @@ -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, - source: Option>, - skipped_events: Vec, -} - -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); - - 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(&mut self, timeout: Option, filter: &F) -> io::Result - 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(&mut self, filter: &F) -> io::Result - 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, - error: Option, - } - - 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) -> io::Result> { - // 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!(); - } - } -} diff --git a/src/event/source.rs b/src/event/source.rs deleted file mode 100644 index ae0f05a..0000000 --- a/src/event/source.rs +++ /dev/null @@ -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) -> io::Result>; - - /// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`. - #[cfg(feature = "event-stream")] - fn waker(&self) -> Waker; -} diff --git a/src/event/source/unix.rs b/src/event/source/unix.rs deleted file mode 100644 index 810bad3..0000000 --- a/src/event/source/unix.rs +++ /dev/null @@ -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; diff --git a/src/event/source/unix/mio.rs b/src/event/source/unix/mio.rs deleted file mode 100644 index 6d19e0c..0000000 --- a/src/event/source/unix/mio.rs +++ /dev/null @@ -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 { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) - } - - pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { - 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) -> io::Result> { - 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, - internal_events: VecDeque, -} - -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.internal_events.pop_front() - } -} diff --git a/src/event/source/unix/tty.rs b/src/event/source/unix/tty.rs deleted file mode 100644 index 03d76b4..0000000 --- a/src/event/source/unix/tty.rs +++ /dev/null @@ -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 { - 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 { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) - } - - pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { - 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 { - 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) -> io::Result> { - let timeout = PollTimeout::new(timeout); - - fn make_pollfd(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, - internal_events: VecDeque, -} - -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.internal_events.pop_front() - } -} diff --git a/src/event/source/windows.rs b/src/event/source/windows.rs deleted file mode 100644 index 33ecffe..0000000 --- a/src/event/source/windows.rs +++ /dev/null @@ -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, - mouse_buttons_pressed: MouseButtonsPressed, -} - -impl WindowsEventSource { - pub(crate) fn new() -> std::io::Result { - 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) -> std::io::Result> { - 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() - } -} diff --git a/src/event/sys.rs b/src/event/sys.rs index bd79307..e3aa103 100644 --- a/src/event/sys.rs +++ b/src/event/sys.rs @@ -1,9 +1 @@ -#[cfg(all(unix, feature = "event-stream"))] -pub(crate) use unix::waker::Waker; -#[cfg(all(windows, feature = "event-stream"))] -pub(crate) use windows::waker::Waker; - -#[cfg(unix)] pub(crate) mod unix; -#[cfg(windows)] -pub(crate) mod windows; diff --git a/src/event/sys/unix.rs b/src/event/sys/unix.rs index 2106ca0..35aa212 100644 --- a/src/event/sys/unix.rs +++ b/src/event/sys/unix.rs @@ -1,5 +1,2 @@ -#[cfg(feature = "event-stream")] -pub(crate) mod waker; - #[cfg(feature = "events")] pub(crate) mod parse; diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 2019b5f..c16f1f5 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -5,8 +5,6 @@ use crate::event::{ MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind, }; -use super::super::super::InternalEvent; - // Event parsing // // This code (& previous one) are kind of ugly. We have to think about this, @@ -23,10 +21,7 @@ fn could_not_parse_event_error() -> io::Error { io::Error::new(io::ErrorKind::Other, "Could not parse an event.") } -pub(crate) fn parse_event( - buffer: &[u8], - input_available: bool, -) -> io::Result> { +pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result> { if buffer.is_empty() { return Ok(None); } @@ -38,7 +33,7 @@ pub(crate) fn parse_event( // Possible Esc sequence Ok(None) } else { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) + Ok(Some(Event::Key(KeyCode::Esc.into()))) } } else { match buffer[1] { @@ -47,40 +42,28 @@ pub(crate) fn parse_event( Ok(None) } else { match buffer[2] { - b'D' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) - } - b'C' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Right.into(), - )))), - b'A' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into())))) - } - b'B' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into())))) - } - b'H' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into())))) - } - b'F' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into())))) - } + b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))), + b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))), + b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))), + b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))), + b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))), + b'F' => Ok(Some(Event::Key(KeyCode::End.into()))), // F1-F4 - val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::F(1 + val - b'P').into(), - )))), + val @ b'P'..=b'S' => { + Ok(Some(Event::Key(KeyCode::F(1 + val - b'P').into()))) + } _ => Err(could_not_parse_event_error()), } } } b'[' => parse_csi(buffer), - b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), + b'\x1B' => Ok(Some(Event::Key(KeyCode::Esc.into()))), _ => parse_event(&buffer[1..], input_available).map(|event_option| { event_option.map(|event| { - if let InternalEvent::Event(Event::Key(key_event)) = event { + if let Event::Key(key_event) = event { let mut alt_key_event = key_event; alt_key_event.modifiers |= KeyModifiers::ALT; - InternalEvent::Event(Event::Key(alt_key_event)) + Event::Key(alt_key_event) } else { event } @@ -89,38 +72,26 @@ pub(crate) fn parse_event( } } } - b'\r' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Enter.into(), - )))), - // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get - // newlines as input is because the terminal converts \r into \n for us. When we - // enter raw mode, we disable that, so \n no longer has any meaning - it's better to - // use Ctrl+J. Waiting to handle it here means it gets picked up later - b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event( - Event::Key(KeyCode::Enter.into()), - ))), - b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))), - b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Backspace.into(), - )))), - c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( + b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))), + b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))), + b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))), + c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new( KeyCode::Char((c - 0x1 + b'a') as char), KeyModifiers::CONTROL, - ))))), - c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( + )))), + c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new( KeyCode::Char((c - 0x1C + b'4') as char), KeyModifiers::CONTROL, - ))))), - b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( + )))), + b'\0' => Ok(Some(Event::Key(KeyEvent::new( KeyCode::Char(' '), KeyModifiers::CONTROL, - ))))), + )))), _ => parse_utf8_char(buffer).map(|maybe_char| { maybe_char .map(KeyCode::Char) .map(char_code_to_event) .map(Event::Key) - .map(InternalEvent::Event) }), } } @@ -134,7 +105,7 @@ fn char_code_to_event(code: KeyCode) -> KeyEvent { KeyEvent::new(code, modifiers) } -pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ if buffer.len() == 2 { @@ -210,7 +181,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { _ => return Err(could_not_parse_event_error()), }; - Ok(input_event.map(InternalEvent::Event)) + Ok(input_event) } pub(crate) fn next_parsed(iter: &mut dyn Iterator) -> io::Result @@ -238,7 +209,7 @@ fn modifier_and_kind_parsed(iter: &mut dyn Iterator) -> io::Result< } } -pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result> { // ESC [ Cy ; Cx R // Cy - cursor row number (starting from 1) // Cx - cursor column number (starting from 1) @@ -253,10 +224,10 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result(&mut split)? - 1; let x = next_parsed::(&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> { +fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result> { // ESC [ ? flags u assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? assert!(buffer.ends_with(&[b'u'])); @@ -285,10 +256,10 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result io::Result> { +fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result> { // ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); assert!(buffer.ends_with(&[b'c'])); @@ -297,7 +268,7 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result - Ok(Some(InternalEvent::PrimaryDeviceAttributes)) + Ok(Some(Event::PrimaryDeviceAttributes)) } fn parse_modifiers(mask: u8) -> KeyModifiers { @@ -345,7 +316,7 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind { } } -pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ // let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) @@ -390,7 +361,7 @@ pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result Option<(KeyCode, KeyEventState)> { @@ -494,7 +465,7 @@ fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventSta None } -pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.ends_with(&[b'u'])); @@ -545,11 +516,6 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result KeyCode::Esc, '\r' => KeyCode::Enter, - // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get - // newlines as input is because the terminal converts \r into \n for us. When we - // enter raw mode, we disable that, so \n no longer has any meaning - it's better to - // use Ctrl+J. Waiting to handle it here means it gets picked up later - '\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter, '\t' => { if modifiers.contains(KeyModifiers::SHIFT) { KeyCode::BackTab @@ -613,10 +579,10 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result io::Result> { +pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ assert!(buffer.ends_with(&[b'~'])); @@ -657,10 +623,10 @@ pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result io::Result> { +pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result> { // rxvt mouse encoding: // ESC [ Cb ; Cx ; Cy ; M @@ -679,15 +645,15 @@ pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result(&mut split)? - 1; let cy = next_parsed::(&mut split)? - 1; - Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { + Ok(Some(Event::Mouse(MouseEvent { kind, column: cx, row: cy, modifiers, - })))) + }))) } -pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result> { // Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only). assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M @@ -707,15 +673,15 @@ pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result io::Result> { +pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result> { // ESC [ < Cb ; Cx ; Cy (;) (M or m) assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ < @@ -752,12 +718,12 @@ pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result io::Result<(MouseEventKind, KeyModifiers)> { } #[cfg(feature = "bracketed-paste")] -pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result> { +pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result> { // ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~ assert!(buffer.starts_with(b"\x1B[200~")); @@ -818,7 +784,7 @@ pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result>, -} - -impl Waker { - /// Create a new `Waker`. - pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result { - 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(()) - } -} diff --git a/src/event/sys/unix/waker/tty.rs b/src/event/sys/unix/waker/tty.rs deleted file mode 100644 index 249d406..0000000 --- a/src/event/sys/unix/waker/tty.rs +++ /dev/null @@ -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>, -} - -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(()) - } -} diff --git a/src/event/sys/windows.rs b/src/event/sys/windows.rs deleted file mode 100644 index d405ae8..0000000 --- a/src/event/sys/windows.rs +++ /dev/null @@ -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::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(()) -} diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs deleted file mode 100644 index 97677ec..0000000 --- a/src/event/sys/windows/parse.rs +++ /dev/null @@ -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 { - 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, -) -> Option { - 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, new_surrogate: u16) -> Option { - 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 { - 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 { - 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 { - 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> { - 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, - })) -} diff --git a/src/event/sys/windows/poll.rs b/src/event/sys/windows/poll.rs deleted file mode 100644 index 84d9bf7..0000000 --- a/src/event/sys/windows/poll.rs +++ /dev/null @@ -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 { - Ok(WinApiPoll { - waker: Waker::new()?, - }) - } -} - -impl WinApiPoll { - pub fn poll(&mut self, timeout: Option) -> std::io::Result> { - 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() - } -} diff --git a/src/event/sys/windows/waker.rs b/src/event/sys/windows/waker.rs deleted file mode 100644 index 9ec582b..0000000 --- a/src/event/sys/windows/waker.rs +++ /dev/null @@ -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>, -} - -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 { - 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() - } -} diff --git a/src/event/timeout.rs b/src/event/timeout.rs deleted file mode 100644 index f266d28..0000000 --- a/src/event/timeout.rs +++ /dev/null @@ -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, - start: Instant, -} - -impl PollTimeout { - /// Constructs a new `PollTimeout` with the given optional `Duration`. - pub fn new(timeout: Option) -> 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 { - 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)); - } -} diff --git a/src/lib.rs b/src/lib.rs index 0217bef..2501f52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,20 +241,5 @@ pub mod style; /// A module to work with the terminal. pub mod terminal; -/// A module to query if the current instance is a tty. -pub mod tty; - -#[cfg(windows)] -/// A module that exposes one function to check if the current terminal supports ANSI sequences. -pub mod ansi_support; mod command; pub(crate) mod macros; - -#[cfg(all(windows, not(feature = "windows")))] -compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows."); - -#[cfg(all(winapi, not(feature = "winapi")))] -compile_error!("Compiling on Windows with \"winapi\" feature disabled. Feature \"winapi\" should only be disabled when project will never be compiled on Windows."); - -#[cfg(all(crossterm_winapi, not(feature = "crossterm_winapi")))] -compile_error!("Compiling on Windows with \"crossterm_winapi\" feature disabled. Feature \"crossterm_winapi\" should only be disabled when project will never be compiled on Windows."); diff --git a/src/style.rs b/src/style.rs index b67a421..82af2f8 100644 --- a/src/style.rs +++ b/src/style.rs @@ -180,18 +180,6 @@ pub fn available_color_count() -> u16 { }) } -/// Forces colored output on or off globally, overriding NO_COLOR. -/// -/// # Notes -/// -/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output. -/// -/// This API allows applications to override that behavior and force colorized output -/// even if NO_COLOR is set. -pub fn force_color_output(enabled: bool) { - Colored::set_ansi_color_disabled(!enabled) -} - /// A command that sets the the foreground color. /// /// See [`Color`](enum.Color.html) for more info. diff --git a/src/style/types/colored.rs b/src/style/types/colored.rs index c9feecd..9cbb109 100644 --- a/src/style/types/colored.rs +++ b/src/style/types/colored.rs @@ -1,6 +1,4 @@ -use parking_lot::Once; use std::fmt::{self, Formatter}; -use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -23,9 +21,6 @@ pub enum Colored { UnderlineColor(Color), } -static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false); -static INITIALIZER: Once = Once::new(); - impl Colored { /// Parse an ANSI foreground or background color. /// This is the string that would appear within an `ESC [ m` escape sequence, as found in @@ -69,38 +64,12 @@ impl Colored { Some(output) } - - /// Checks whether ansi color sequences are disabled by setting of NO_COLOR - /// in environment as per https://no-color.org/ - pub fn ansi_color_disabled() -> bool { - !std::env::var("NO_COLOR") - .unwrap_or("".to_string()) - .is_empty() - } - - pub fn ansi_color_disabled_memoized() -> bool { - INITIALIZER.call_once(|| { - ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst); - }); - - ANSI_COLOR_DISABLED.load(Ordering::SeqCst) - } - - pub fn set_ansi_color_disabled(val: bool) { - // Force the one-time initializer to run. - _ = Self::ansi_color_disabled_memoized(); - ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst); - } } impl fmt::Display for Colored { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let color; - if Self::ansi_color_disabled_memoized() { - return Ok(()); - } - match *self { Colored::ForegroundColor(new_color) => { if new_color == Color::Reset { diff --git a/src/terminal.rs b/src/terminal.rs index e7406be..635e946 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -83,60 +83,15 @@ //! //! For manual execution control check out [crossterm::queue](../macro.queue.html). -use std::{fmt, io}; +use std::fmt; -#[cfg(windows)] -use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -#[cfg(windows)] -use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT; #[doc(no_inline)] use crate::Command; use crate::{csi, impl_display}; -pub(crate) mod sys; - -#[cfg(feature = "events")] -pub use sys::supports_keyboard_enhancement; - -/// Tells whether the raw mode is enabled. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn is_raw_mode_enabled() -> io::Result { - #[cfg(unix)] - { - Ok(sys::is_raw_mode_enabled()) - } - - #[cfg(windows)] - { - sys::is_raw_mode_enabled() - } -} - -/// Enables raw mode. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn enable_raw_mode() -> io::Result<()> { - sys::enable_raw_mode() -} - -/// Disables raw mode. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn disable_raw_mode() -> io::Result<()> { - sys::disable_raw_mode() -} - -/// Returns the terminal size `(columns, rows)`. -/// -/// The top left cell is represented `(1, 1)`. -pub fn size() -> io::Result<(u16, u16)> { - sys::size() -} - #[derive(Debug)] pub struct WindowSize { pub rows: u16, @@ -145,15 +100,6 @@ pub struct WindowSize { pub height: u16, } -/// Returns the terminal size `[WindowSize]`. -/// -/// The width and height in pixels may not be reliably implemented or default to 0. -/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused". -/// For windows it is not implemented. -pub fn window_size() -> io::Result { - sys::window_size() -} - /// Disables line wrapping. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DisableLineWrap; @@ -301,11 +247,6 @@ impl Command for ScrollUp { } Ok(()) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::scroll_up(self.0) - } } /// A command that scrolls the terminal screen a given number of rows down. @@ -323,11 +264,6 @@ impl Command for ScrollDown { } Ok(()) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::scroll_down(self.0) - } } /// A command that clears the terminal screen buffer. @@ -351,11 +287,6 @@ impl Command for Clear { ClearType::UntilNewLine => csi!("K"), }) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::clear(self.0) - } } /// A command that sets the terminal buffer size `(columns, rows)`. @@ -370,11 +301,6 @@ impl Command for SetSize { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { write!(f, csi!("8;{};{}t"), self.1, self.0) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::set_size(self.0, self.1) - } } /// A command that sets the terminal title @@ -389,11 +315,6 @@ impl Command for SetTitle { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { write!(f, "\x1B]0;{}\x07", &self.0) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::set_window_title(&self.0) - } } /// A command that instructs the terminal emulator to begin a synchronized frame. @@ -436,17 +357,6 @@ impl Command for BeginSynchronizedUpdate { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?2026h")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - Ok(()) - } - - #[cfg(windows)] - #[inline] - fn is_ansi_code_supported(&self) -> bool { - true - } } /// A command that instructs the terminal to end a synchronized frame. @@ -489,17 +399,6 @@ impl Command for EndSynchronizedUpdate { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(csi!("?2026l")) } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - Ok(()) - } - - #[cfg(windows)] - #[inline] - fn is_ansi_code_supported(&self) -> bool { - true - } } impl_display!(for ScrollUp); diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs deleted file mode 100644 index 9dde47d..0000000 --- a/src/terminal/sys.rs +++ /dev/null @@ -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; diff --git a/src/terminal/sys/file_descriptor.rs b/src/terminal/sys/file_descriptor.rs deleted file mode 100644 index baff266..0000000 --- a/src/terminal/sys/file_descriptor.rs +++ /dev/null @@ -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 { - 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 { - 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> { - 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> { - 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) -} diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs deleted file mode 100644 index 7129730..0000000 --- a/src/terminal/sys/unix.rs +++ /dev/null @@ -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> = 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 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 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 { - // 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 { - 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 { - 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 { - 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 { - 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 { - 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 - - // 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 { - 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 { - 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(()) - } -} diff --git a/src/terminal/sys/windows.rs b/src/terminal/sys/windows.rs deleted file mode 100644 index cd71fab..0000000 --- a/src/terminal/sys/windows.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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); - 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 { - 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[..]); - } -} diff --git a/src/tty.rs b/src/tty.rs deleted file mode 100644 index 5a710b4..0000000 --- a/src/tty.rs +++ /dev/null @@ -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 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 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 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 - } -}