diff --git a/keyfork-crossterm/.travis.yml b/keyfork-crossterm/.travis.yml new file mode 100644 index 0000000..c18a687 --- /dev/null +++ b/keyfork-crossterm/.travis.yml @@ -0,0 +1,42 @@ +# Build only pushed (merged) master or any pull request. This avoids the +# pull request to be build twice. +branches: + only: + - master + +language: rust + +rust: + - stable + - nightly + +os: + - linux + - windows + - osx + +git: + depth: 1 + quiet: true + +matrix: + allow_failures: + - rust: nightly + +before_script: + - export PATH=$PATH:/home/travis/.cargo/bin + - rustup component add rustfmt + - rustup component add clippy + +script: + - cargo fmt --version + - rustup --version + - rustc --version + - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi + - cargo clippy -- -D clippy::all + - cargo build + - cargo test --lib -- --nocapture --test-threads 1 + - cargo test --lib --features serde -- --nocapture --test-threads 1 + - cargo test --lib --features event-stream -- --nocapture --test-threads 1 + - cargo test --all-features -- --nocapture --test-threads 1 + - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi diff --git a/keyfork-crossterm/CHANGELOG.md b/keyfork-crossterm/CHANGELOG.md new file mode 100644 index 0000000..8c7f661 --- /dev/null +++ b/keyfork-crossterm/CHANGELOG.md @@ -0,0 +1,744 @@ +# Version 0.27.1 + +## Added ⭐ +- Add support for (de)serializing `Reset` `Color` + +# Version 0.27 + +## Added ⭐ + +- Add `NO_COLOR` support (https://no-color.org/) +- Add option to force overwrite `NO_COLOR` (#802) +- Add support for scroll left/right events on windows and unix systems (#788). +- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals. +- Add support for deserializing hex color strings to `Color` e.g #fffff. + +## Changes + +- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776) + +## Breaking ⚠️ + +- Set minimum rustc version to 1.58 (#798) +- Change all error types to `std::io::Result` (#765) + +# Version 0.26.1 + +## Added ⭐ + +- Add synchronized output/update control (#756) +- Add kitty report alternate keys functionality (#754) +- Updates dev dependencies. + +## Fixed 🐛 +- Fix icorrect return in kitty keyboard enhancement check (#751) +- Fix panic when using `use-dev-tty` feature flag (#762) + +# Version 0.26.0 +## Added ⭐ + +- Add `SetCursorStyle` to set the cursor apearance and visibility. (#742) +- Add a function to check if kitty keyboard enhancement protocol is available. (#732) +- Add filedescriptors poll in order to move away from mio in the future (can be used via `use-dev-tty`). (#735) + +## Fixed 🐛 +- Improved F1-F4 handling for kitty keyboard protocol. (#736) +- Improved parsing of event types/modifiers with certain keys for kitty protocol. (#716) + +## Breaking ⚠️ +- Remove `SetCursorShape` in favour of `SetCursorStyle`. (#742) +- Make Windows resize event match `terminal::size` (#714) +- Rust 1.58 or later is now required. +- Add key release event for windows. (#745) + +# Version 0.25.0 +BREAKING: `Copy` trait is removed from `Event`, you can keep it by removing the "bracked-paste" feature flag. However this flag might be standardized in the future. +We removed the `Copy` from `Event` because the new `Paste` event, which contains a pasted string into the terminal, which is a non-copy string. + +- Add ability to paste a string in into the terminal and fetch the pasted string via events (see `Event::Paste` and `EnableBracketedPaste `). +- Add support for functional key codes from kitty keyboard protocol. Try out by `PushKeyboardEnhancementFlags`. This protocol allows for: + - See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers + - Press, Repeat, Release event kinds. + - SUPER, HYPER, META modifiers. + - Media keycodes + - Right/left SHIFT, Control, Alt, Super, Hyper, Meta + - IsoLevel3Shift, IsoLevel5Shift + - Capslock, scroll lock, numlock + - Printscreen, pauze, menue, keyboard begin. +- Create `SetStyle` command to allow setting various styling in one command. +- Terminal Focus events (see `Event::FocusGained` and `Event::FocusLost`) + +# Version 0.24.0 +- Add DoubleUnderlined, Undercurled, Underdots the text, Underdotted, Underdashes, Underdashed attributes and allow coloring their foreground / background color. +- Fix windows unicode character parsing, this fixed various key combinations and support typing unicode characters. +- Consistency and better documentation on mouse cursor operations (BREAKING CHANGE). + - MoveTo, MoveToColumn, MoveToRow are 0-based. (left top most cell is 0,0). Moving like this is absolute + - MoveToNextLine, MoveToPreviousLine, MoveUp, MoveDown, MoveRight, MoveLeft are 1-based,. Moving like this is relative. Moving 1 left means moving 1 left. Moving 0 to the left is not possible, wikipedia states that most terminals will just default to 1. +- terminal::size returns error when previously it returned (0,0). +- Remove println from serialisation code. +- Fix mouse up for middle and right buttons. +- Fix escape codes on Git-Bash + Windows Terminal / Alacritty / WezTerm. +- Add support for cursor keys in application mode. +# Version 0.23.2 +- Update signal-hook and mio to version 0.8. + +# Version 0.23.1 +- Fix control key parsing problem. + +# Version 0.23 +- Update dependencies. +- Add 0 check for all cursor functions to prevent undefined behaviour. +- Add CSIu key parsing for unix. +- Improve control character window key parsing supporting (e.g. CTRL [ and ]) +- Update library to 2021 edition. + +# Version 0.22.1 +- Update yanked version crossterm-winapi and move to crossterm-winapi 0.9.0. +- Changed panic to error when calling disable-mouse capture without setting it first. +- Update bitflags dependency. + +# Version 0.22 +- Fix serde Color serialisation/deserialization inconsistency. +- Update crossterm-winapi 0.8.1 to fix panic for certain mouse events + +# Version 0.21 +- Expose `is_raw` function. +- Add 'purge' option on unix system, this clears the entire screen buffer. +- Improve serialisation for color enum values. + +# Version 0.20 +- Update from signal-hook with 'mio-feature flag' to signal-hook-mio 0.2.1. +- Manually implements Eq, PartialEq and Hash for KeyEvent improving equality checks and hash calculation. +- `crossterm::ErrorKind` to `io::Error`. +- Added Cursor Shape Support. +- Add support for function keys F13...F20. +- Support taking any Display in `SetTitle` command. +- Remove lazy_static dependency. +- Remove extra Clone bounds in the style module. + - Add `MoveToRow` command. + - Remove writer parameter from execute_winapi + +# Version 0.19 +- Use single thread for async event reader. +- Patch timeout handling for event polling this was not working correctly. +- Add unix support for more key combinations mainly complex ones with ALT/SHIFT/CTRL. +- Derive `PartialEq` and `Eq` for ContentStyle +- Fix windows resize event size, this used to be the buffer size but is screen size now. +- Change `Command::ansi_code` to `Command::write_ansi`, this way the ansi code will be written to given formatter. + +# Version 0.18.2 +- Fix panic when only setting bold and redirecting stdout. +- Use `tty_fd` for set/get terminal attributes + +# Version 0.18.1 +- Fix enabling ANSI support when stdout is redirected +- Update crossterm-winapi to 0.6.2 + +# Version 0.18.0 +- Fix get position bug +- Fix windows 8 or lower write to user-given stdout instead of stdout. +- Make MoveCursor(Left/Right/Up/Dow) command with input 0 not move. +- Switch to futures-core to reduce dependencies. +- Command API restricts to only accept `std::io::Write` +- Make `supports_ansi` public +- Implement ALT + numbers windows systems. + +# Version 0.17.7 +- Fix cursor position retrieval bug linux. + +# Version 0.17.6 +- Add functionality to retrieve color based on passed ansi code. +- Switch from 'futures' to 'futures-util' crate to reduce dependency count +- Mio 0.7 update +- signal-hook update +- Make windows raw_mode act on CONIN$ +- Added From<(u8, u8, u8)> Trait to Color::Rgb Enum +- Implement Color::try_from() +- Implement styler traits for `&'a str` + +# Version 0.17.5 +- Improved support of keymodifier for linux, arrow keys, function keys, home keys etc. +- Add `SetTitle` command to change the terminal title. +- Mio 0.7 update + +# Version 0.17.4 +- Add macros for `Colorize` and `Styler` impls, add an impl for `String` +- Add shift modifier to uppercase char events on unix + +# Version 0.17.3 +- Fix get terminal size mac os, this did not report the correct size. + +# Version 0.17.2 +- Windows unicode support + +# Version 0.17.1 +- Reverted bug in 0.17.0: "Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.". +- Support for querying whether the current instance is a TTY. + +# Version 0.17 +- Impl Display for MoveToColumn, MoveToNextLine, MoveToPreviousLine +- Make unix event reader always use `/dev/tty`. +- Direct write command ansi_codes into formatter instead of double allocation. +- Add NONE flag to KeyModifiers +- Add support for converting chars to StylizedContent +- Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing. + +# Version 0.16.0 +- Change attribute vector in `ContentStyle` to bitmask. +- Add `SetAttributes` command. +- Add `Attributes` type, which is a bitfield of enabled attributes. +- Remove `exit()`, was useless. + +# Version 0.15.0 +- Fix CTRL + J key combination. This used to return an ENTER event. +- Add a generic implementation `Command` for `&T: Command`. This allows commands to be queued by reference, as well as by value. +- Remove unnecessary `Clone` trait bounds from `StyledContent`. +- Add `StyledContent::style_mut`. +- Handle error correctly for `execute!` and `queue!`. +- Fix minor syntax bug in `execute!` and `queue!`. +- Change `ContentStyle::apply` to take self by value instead of reference, to prevent an unnecessary extra clone. +- Added basic trait implementations (`Debug`, `Clone`, `Copy`, etc) to all of the command structs +- `ResetColor` uses `&'static str` instead of `String` + +# Version 0.14.2 +- Fix TIOCGWINSZ for FreeBSD + +# Version 0.14.1 +- Made windows cursor position relative to the window instead absolute to the screen buffer windows. +- Fix windows bug with `queue` macro were it consumed a type and required an type to be `Copy`. + +# Version 0.14 + +- Replace the `input` module with brand new `event` module + - Terminal Resize Events + - Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and + - futures Stream (feature 'event-stream') + - Poll/read API + - It's **highly recommended** to read the + [Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14) + documentation +- Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths) + documentation +- Add `MoveToColumn`, `MoveToPreviousLine`, `MoveToNextLine` commands +- Merge `screen` module into `terminal` + - Remove `screen::AlternateScreen` + - Remove `screen::Rawscreen` + * Move and rename `Rawscreen::into_raw_mode` and `Rawscreen::disable_raw_mode` to `terminal::enable_raw_mode` and `terminal::disable_raw_mode` + - Move `screen::EnterAlternateScreen` and `screen::LeaveAlternateScreen` to `terminal::EnterAlternateScreen` and `terminal::LeaveAlternateScreen` + - Replace `utils::Output` command with `style::Print` command +- Fix enable/disable mouse capture commands on Windows +- Allow trailing comma `queue!` & `execute!` macros + +# Version 0.13.3 + +- Remove thread from AsyncReader on Windows. +- Improve HANDLE management windows. + +# Version 0.13.2 + +- New `input::stop_reading_thread()` function + - Temporary workaround for the UNIX platform to stop the background + reading thread and close the file descriptor + - This function will be removed in the next version + +# Version 0.13.1 + +- Async Reader fix, join background thread and avoid looping forever on windows. + +# Version 0.13.0 + +**Major API-change, removed old-api** + +- Remove `Crossterm` type +- Remove `TerminalCursor`, `TerminalColor`, `Terminal` +- Remove `cursor()`, `color()` , `terminal()` +- Remove re-exports at root, accessible via `module::types` (`cursor::MoveTo`) +- `input` module + - Derive 'Copy' for 'KeyEvent' + - Add the `EnableMouseCapture` and `EnableMouseCapture` commands +- `cursor` module + - Introduce static function `crossterm::cursor::position` in place of `TerminalCursor::pos` + - Rename `Goto` to `MoveTo` + - Rename `Up` to `MoveLeft` + - Rename `Right` to `MoveRight` + - Rename `Down` to `MoveDown` + - Rename `BlinkOn` to `EnableBlinking` + - Rename `BlinkOff` to `DisableBlinking` + - Rename `ResetPos` to `ResetPosition` + - Rename `SavePos` to `SavePosition` +- `terminal` + - Introduce static function `crossterm::terminal::size` in place of `Terminal::size` + - Introduce static function `crossterm::terminal::exit` in place of `Terminal::exit` +- `style module` + - Rename `ObjectStyle` to `ContentStyle`. Now full names are used for methods + - Rename `StyledObject` to `StyledContent` and made members private + - Rename `PrintStyledFont` to `PrintStyledContent` + - Rename `attr` method to `attribute`. + - Rename `Attribute::NoInverse` to `NoReverse` + - Update documentation + - Made `Colored` private, user should use commands instead + - Rename `SetFg` -> `SetForegroundColor` + - Rename `SetBg` -> `SetBackgroundColor` + - Rename `SetAttr` -> `SetAttribute` + - Rename `ContentStyle::fg_color` -> `ContentStyle::foreground_color` + - Rename `ContentStyle::bg_color` -> `ContentStyle::background_color` + - Rename `ContentStyle::attrs` -> `ContentStyle::attributes` +- Improve documentation +- Unix terminal size calculation with TPUT + +# Version 0.12.1 + +- Move all the `crossterm_` crates code was moved to the `crossterm` crate + - `crossterm_cursor` is in the `cursor` module, etc. + - All these modules are public +- No public API breaking changes + +# Version 0.12.0 + +- Following crates are deprecated and no longer maintained + - `crossterm_cursor` + - `crossterm_input` + - `crossterm_screen` + - `crossterm_style` + - `crossterm_terminal` + - `crossterm_utils` + +## `crossterm_cursor` 0.4.0 + +- Fix examples link ([PR #6](https://github.com/crossterm-rs/crossterm-cursor/pull/6)) +- Sync documentation style ([PR #7](https://github.com/crossterm-rs/crossterm-cursor/pull/7)) +- Remove all references to the crossterm book ([PR #8](https://github.com/crossterm-rs/crossterm-cursor/pull/8)) +- Replace `RAW_MODE_ENABLED` with `is_raw_mode_enabled` ([PR #9](https://github.com/crossterm-rs/crossterm-cursor/pull/9)) +- Use `SyncReader` & `InputEvent::CursorPosition` for `pos_raw()` ([PR #10](https://github.com/crossterm-rs/crossterm-cursor/pull/10)) + +## `crossterm_input` 0.5.0 + +- Sync documentation style ([PR #4](https://github.com/crossterm-rs/crossterm-input/pull/4)) +- Sync `SyncReader::next()` Windows and UNIX behavior ([PR #5](https://github.com/crossterm-rs/crossterm-input/pull/5)) +- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-input/pull/6)) +- Mouse coordinates synchronized with the cursor ([PR #7](https://github.com/crossterm-rs/crossterm-input/pull/7)) + - Upper/left reported as `(0, 0)` +- Fix bug that read sync didn't block (Windows) ([PR #8](https://github.com/crossterm-rs/crossterm-input/pull/8)) +- Refactor UNIX readers ([PR #9](https://github.com/crossterm-rs/crossterm-input/pull/9)) + - AsyncReader produces mouse events + - One reading thread per application, not per `AsyncReader` + - Cursor position no longer consumed by another `AsyncReader` + - Implement sync reader for read_char (requires raw mode) + - Fix `SIGTTIN` when executed under the LLDB + - Add mio for reading from FD and more efficient polling (UNIX only) +- Sync UNIX and Windows vertical mouse position ([PR #11](https://github.com/crossterm-rs/crossterm-input/pull/11)) + - Top is always reported as `0` + +## `crossterm_screen` 0.3.2 + +- `to_alternate` switch back to main screen if it fails to switch into raw mode ([PR #4](https://github.com/crossterm-rs/crossterm-screen/pull/4)) +- Improve the documentation ([PR #5](https://github.com/crossterm-rs/crossterm-screen/pull/5)) + - Public API + - Include the book content in the documentation +- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-screen/pull/6)) +- New commands introduced ([PR #7](https://github.com/crossterm-rs/crossterm-screen/pull/7)) + - `EnterAlternateScreen` + - `LeaveAlternateScreen` +- Sync Windows and UNIX raw mode behavior ([PR #8](https://github.com/crossterm-rs/crossterm-screen/pull/8)) + +## `crossterm_style` 0.5.2 + +- Refactor ([PR #2](https://github.com/crossterm-rs/crossterm-style/pull/2)) + - Added unit tests + - Improved documentation and added book page to `lib.rs` + - Fixed bug with `SetBg` command, WinApi logic + - Fixed bug with `StyledObject`, used stdout for resetting terminal color + - Introduced `ResetColor` command +- Sync documentation style ([PR #3](https://github.com/crossterm-rs/crossterm-style/pull/3)) +- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-style/pull/4)) +- Windows 7 grey/white foreground/intensity swapped ([PR #5](https://github.com/crossterm-rs/crossterm-style/pull/5)) + +## `crossterm_terminal` 0.3.2 + +- Removed `crossterm_cursor::sys` dependency ([PR #2](https://github.com/crossterm-rs/crossterm-terminal/pull/2)) +- Internal refactoring & documentation ([PR #3](https://github.com/crossterm-rs/crossterm-terminal/pull/3)) +- Removed all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-terminal/pull/4)) + +## `crossterm_utils` 0.4.0 + +- Add deprecation note ([PR #3](https://github.com/crossterm-rs/crossterm-utils/pull/3)) +- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-utils/pull/4)) +- Remove unsafe static mut ([PR #5](https://github.com/crossterm-rs/crossterm-utils/pull/5)) + - `sys::unix::RAW_MODE_ENABLED` replaced with `sys::unix::is_raw_mode_enabled()` (breaking) + - New `lazy_static` dependency + +## `crossterm_winapi` 0.3.0 + +- Make read sync block for windows systems ([PR #2](https://github.com/crossterm-rs/crossterm-winapi/pull/2)) + +# Version 0.11.1 + +- Maintenance release +- All sub-crates were moved to their own repositories in the `crossterm-rs` organization + +# Version 0.11.0 + +As a preparation for crossterm 0.1.0 we have moved crossterm to an organisation called 'crossterm-rs'. + +### Code Quality + +- Code Cleanup: [warning-cleanup], [crossterm_style-cleanup], [crossterm_screen-cleanup], [crossterm_terminal-cleanup], [crossterm_utils-cleanup], [2018-cleanup], [api-cleanup-1], [api-cleanup-2], [api-cleanup-3] +- Examples: [example-cleanup_1], [example-cleanup_2], [example-fix], [commandbar-fix], [snake-game-improved] +- Fixed all broken tests and added tests + +### Important Changes + +- Return written bytes: [return-written-bytes] +- Added derives: `Debug` for `ObjectStyle` [debug-derive], Serialize/Deserialize for key events [serde] +- Improved error handling: + - Return `crossterm::Result` from all api's: [return_crossterm_result] + * `TerminalCursor::pos()` returns `Result<(u16, u16)>` + * `Terminal::size()` returns `Result<(u16, u16)>` + * `TerminalCursor::move_*` returns `crossterm::Result` + * `ExecutableCommand::queue` returns `crossterm::Result` + * `QueueableCommand::queue` returns `crossterm::Result` + * `get_available_color_count` returns no result + * `RawScreen::into_raw_mode` returns `crossterm::Result` instead of `io::Result` + * `RawScreen::disable_raw_mode` returns `crossterm::Result` instead of `io::Result` + * `AlternateScreen::to_alternate` returns `crossterm::Result` instead of `io::Result` + * `TerminalInput::read_line` returns `crossterm::Result` instead of `io::Result` + * `TerminalInput::read_char` returns `crossterm::Result` instead of `io::Result` + * Maybe I forgot something, a lot of functions have changed + - Removed all unwraps/expects from library +- Add KeyEvent::Enter and KeyEvent::Tab: [added-key-event-enter], [added-key-event-tab] +- Sync set/get terminal size behaviour: [fixed-get-set-terminal-size] +- Method renames: + * `AsyncReader::stop_reading()` to `stop()` + * `RawScreen::disable_raw_mode_on_drop` to `keep_raw_mode_on_drop` + * `TerminalCursor::reset_position()` to `restore_position()` + * `Command::get_anis_code()` to `ansi_code()` + * `available_color_count` to `available_color_count()` + * `Terminal::terminal_size` to `Terminal::size` + * `Console::get_handle` to `Console::handle` +- All `i16` values for indexing: set size, set cursor pos, scrolling synced to `u16` values +- Command API takes mutable self instead of self + +[serde]: https://github.com/crossterm-rs/crossterm/pull/190 + +[debug-derive]: https://github.com/crossterm-rs/crossterm/pull/192 +[example-fix]: https://github.com/crossterm-rs/crossterm/pull/193 +[commandbar-fix]: https://github.com/crossterm-rs/crossterm/pull/204 + +[warning-cleanup]: https://github.com/crossterm-rs/crossterm/pull/198 +[example-cleanup_1]: https://github.com/crossterm-rs/crossterm/pull/196 +[example-cleanup_2]: https://github.com/crossterm-rs/crossterm/pull/225 +[snake-game-improved]: https://github.com/crossterm-rs/crossterm/pull/231 +[crossterm_style-cleanup]: https://github.com/crossterm-rs/crossterm/pull/208 +[crossterm_screen-cleanup]: https://github.com/crossterm-rs/crossterm/pull/209 +[crossterm_terminal-cleanup]: https://github.com/crossterm-rs/crossterm/pull/210 +[crossterm_utils-cleanup]: https://github.com/crossterm-rs/crossterm/pull/211 +[2018-cleanup]: https://github.com/crossterm-rs/crossterm/pull/222 +[wild-card-cleanup]: https://github.com/crossterm-rs/crossterm/pull/224 + +[api-cleanup-1]: https://github.com/crossterm-rs/crossterm/pull/235 +[api-cleanup-2]: https://github.com/crossterm-rs/crossterm/pull/238 +[api-cleanup-3]: https://github.com/crossterm-rs/crossterm/pull/240 + +[return-written-bytes]: https://github.com/crossterm-rs/crossterm/pull/212 + +[return_crossterm_result]: https://github.com/crossterm-rs/crossterm/pull/232 +[added-key-event-tab]: https://github.com/crossterm-rs/crossterm/pull/239 +[added-key-event-enter]: https://github.com/crossterm-rs/crossterm/pull/236 +[fixed-get-set-terminal-size]: https://github.com/crossterm-rs/crossterm/pull/242 + +# Version 0.10.1 + +# Version 0.10.0 ~ yanked +- Implement command API, to have better performance and more control over how and when commands are executed. [PR](https://github.com/crossterm-rs/crossterm/commit/1a60924abd462ab169b6706aab68f4cca31d7bc2), [issue](https://github.com/crossterm-rs/crossterm/issues/171) +- Fix showing, hiding cursor windows implementation +- Remove some of the parsing logic from windows keys to ansi codes to key events [PR](https://github.com/crossterm-rs/crossterm/commit/762c3a9b8e3d1fba87acde237f8ed09e74cd9ecd) +- Made terminal size 1-based [PR](https://github.com/crossterm-rs/crossterm/commit/d689d7e8ed46a335474b8262bd76f21feaaf0c50) +- Add some derives + +# Version 0.9.6 + +- Copy for KeyEvent +- CTRL + Left, Down, Up, Right key support +- SHIFT + Left, Down, Up, Right key support +- Fixed UNIX cursor position bug [issue](https://github.com/crossterm-rs/crossterm/issues/140), [PR](https://github.com/crossterm-rs/crossterm/pull/152) + +# Version 0.9.5 + +- Prefetch buffer size for more efficient windows input reads. [PR](https://github.com/crossterm-rs/crossterm/pull/144) + +# Version 0.9.4 + +- Reset foreground and background color individually. [PR](https://github.com/crossterm-rs/crossterm/pull/138) +- Backtap input support. [PR](https://github.com/crossterm-rs/crossterm/pull/129) +- Corrected white/grey and added dark grey. +- Fixed getting cursor position with raw screen enabled. [PR](https://github.com/crossterm-rs/crossterm/pull/134) +- Removed one redundant stdout lock + +# Version 0.9.3 + +- Removed println from `SyncReader` + +## Version 0.9.2 + +- Terminal size linux was not 0-based +- Windows mouse input event position was 0-based and should be 1-based +- Result, ErrorKind are made re-exported +- Fixed some special key combination detections for UNIX systems +- Made FreeBSD compile + +## Version 0.9.1 + +- Fixed libc compile error + +## Version 0.9.0 (yanked) + +This release is all about moving to a stabilized API for 1.0. + +- Major refactor and cleanup. +- Improved performance; + - No locking when writing to stdout. + - UNIX doesn't have any dynamic dispatch anymore. + - Windows has improved the way to check if ANSI modes are enabled. + - Removed lot's of complex API calls: `from_screen`, `from_output` + - Removed `Arc` from all internal Api's. +- Removed termios dependency for UNIX systems. +- Upgraded deps. +- Removed about 1000 lines of code + - `TerminalOutput` + - `Screen` + - unsafe code + - Some duplicated code introduced by a previous refactor. +- Raw modes UNIX systems improved +- Added `NoItalic` attribute + +## Version 0.8.2 + +- Bug fix for sync reader UNIX. + +## Version 0.8.1 + +- Added public re-exports for input. + +# Version 0.8.0 + +- Introduced KeyEvents +- Introduced MouseEvents +- Upgraded crossterm_winapi 0.2 + +# Version 0.7.0 + +- Introduced more `Attributes` +- Introduced easier ways to style text [issue 87](https://github.com/crossterm-rs/crossterm/issues/87). +- Removed `ColorType` since it was unnecessary. + +# Version 0.6.0 + +- Introduced feature flags; input, cursor, style, terminal, screen. +- All modules are moved to their own crate. +- Introduced crossterm workspace +- Less dependencies. +- Improved namespaces. + +[PR 84](https://github.com/crossterm-rs/crossterm/pull/84) + +# Version 0.5.5 + +- Error module is made public [PR 78](https://github.com/crossterm-rs/crossterm/pull/78). + +# Version 0.5.4 + +- WinApi rewrite and correctly error handled [PR 67](https://github.com/crossterm-rs/crossterm/pull/67) +- Windows attribute support [PR 62](https://github.com/crossterm-rs/crossterm/pull/62) +- Readline bug fix windows systems [PR 62](https://github.com/crossterm-rs/crossterm/pull/62) +- Error handling improvement. +- General refactoring, all warnings removed. +- Documentation improvement. + +# Version 0.5.1 + +- Documentation refactor. +- Fixed broken API documentation [PR 53](https://github.com/crossterm-rs/crossterm/pull/53). + +# Version 0.5.0 + +- Added ability to pause the terminal [issue](https://github.com/crossterm-rs/crossterm/issues/39) +- RGB support for Windows 10 systems +- ANSI color value (255) color support +- More convenient API, no need to care about `Screen` unless working with when working with alternate or raw screen [PR](https://github.com/crossterm-rs/crossterm/pull/44) +- Implemented Display for styled object + +# Version 0.4.3 + +- Fixed bug [issue 41](https://github.com/crossterm-rs/crossterm/issues/41) + +# Version 0.4.2 + +- Added functionality to make a styled object writable to screen [issue 33](https://github.com/crossterm-rs/crossterm/issues/33) +- Added unit tests. +- Bugfix with getting terminal size unix. +- Bugfix with returning written bytes [pull request 31](https://github.com/crossterm-rs/crossterm/pull/31) +- removed methods calls: `as_any()` and `as_any_mut()` from `TerminalOutput` + +# Version 0.4.1 + +- Fixed resizing of ansi terminal with and height where in the wrong order. + +# Version 0.4.0 + +- Input support (read_line, read_char, read_async, read_until_async) +- Styling module improved +- Everything is multithreaded (`Send`, `Sync`) +- Performance enhancements: removed mutexes, removed state manager, removed context type removed unnecessarily RC types. +- Bug fix resetting console color. +- Bug fix whit undoing raw modes. +- More correct error handling. +- Overall command improvement. +- Overall refactor of code. + +# Version 0.3.0 + +This version has some braking changes check [upgrade manual](UPGRADE%20Manual.md) for more information about what is changed. +I think you should not switch to version `0.3.0` if you aren't going to use the AlternateScreen feature. +Because you will have some work to get to the new version of crossterm depending on your situation. + +Some Features crossterm 0.3.0 +- Alternate Screen for windows and unix systems. +- Raw screen for unix and windows systems [Issue 5](https://github.com/crossterm-rs/crossterm/issues/5).. +- Hiding an showing the cursor. +- Control over blinking of the terminal cursor (only some terminals are supporting this). +- The terminal state will be set to its original state when process ends [issue7](https://github.com/crossterm-rs/crossterm/issues/7). +- exit the current process. + +## Alternate screen + +This create supports alternate screen for both windows and unix systems. You can use + +*Nix style applications often utilize an alternate screen buffer, so that they can modify the entire contents of the buffer, without affecting the application that started them. +The alternate buffer is exactly the dimensions of the window, without any scrollback region. +For an example of this behavior, consider when vim is launched from bash. +Vim uses the entirety of the screen to edit the file, then returning to bash leaves the original buffer unchanged. + +I Highly recommend you to check the `examples/program_examples/first_depth_search` for seeing this in action. + +## Raw screen + +This crate now supports raw screen for both windows and unix systems. +What exactly is raw state: +- No line buffering. + Normally the terminals uses line buffering. This means that the input will be send to the terminal line by line. + With raw mode the input will be send one byte at a time. +- Input + All input has to be written manually by the programmer. +- Characters + The characters are not processed by the terminal driver, but are sent straight through. + Special character have no meaning, like backspace will not be interpret as backspace but instead will be directly send to the terminal. +With these modes you can easier design the terminal screen. + +## Some functionalities added + +- Hiding and showing terminal cursor +- Enable or disabling blinking of the cursor for unix systems (this is not widely supported) +- Restoring the terminal to original modes. +- Added a [wrapper](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) for managing all the functionalities of crossterm `Crossterm`. +- Exit the current running process + +## Examples +Added [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) for each version of the crossterm version. +Also added a folder with some [real life examples](https://github.com/crossterm-rs/crossterm/tree/master/examples/program_examples). + +## Context + +What is the `Context` all about? This `Context` has several reasons why it is introduced into `crossterm version 0.3.0`. +These points are related to the features like `Alternatescreen` and managing the terminal state. + +- At first `Terminal state`: + + Because this is a terminal manipulating library there will be made changes to terminal when running an process. + If you stop the process you want the terminal back in its original state. + Therefore, I need to track the changes made to the terminal. + +- At second `Handle to the console` + + In Rust we can use `stdout()` to get an handle to the current default console handle. + For example when in unix systems you want to print something to the main screen you can use the following code: + + write!(std::io::stdout(), "{}", "some text"). + + But things change when we are in alternate screen modes. + We can not simply use `stdout()` to get a handle to the alternate screen, since this call returns the current default console handle (handle to mainscreen). + + Because of that we need to store an handle to the current screen. + This handle could be used to put into alternate screen modes and back into main screen modes. + Through this stored handle Crossterm can execute its command and write on and to the current screen whether it be alternate screen or main screen. + + For unix systems we store the handle gotten from `stdout()` for windows systems that are not supporting ANSI escape codes we store WinApi `HANDLE` struct witch will provide access to the current screen. + +So to recap this `Context` struct is a wrapper for a type that manges terminal state changes. +When this `Context` goes out of scope all changes made will be undone. +Also is this `Context` is a wrapper for access to the current console screen. + +Because Crossterm needs access to the above to types quite often I have chosen to add those two in one struct called `Context` so that this type could be shared throughout library. +Check this link for more info: [cleanup of rust code](https://stackoverflow.com/questions/48732387/how-can-i-run-clean-up-code-in-a-rust-library). +More info over writing to alternate screen buffer on windows and unix see this [link](https://github.com/crossterm-rs/crossterm/issues/17) + +__Now the user has to pass an context type to the modules of Crossterm like this:__ + + let context = Context::new(); + + let cursor = cursor(&context); + let terminal = terminal(&context); + let color = color(&context); + +Because this looks a little odd I will provide a type widths will manage the `Context` for you. You can call the different modules like the following: + + let crossterm = Crossterm::new(); + let color = crossterm.color(); + let cursor = crossterm.cursor(); + let terminal = crossterm.terminal(); + + +### Alternate screen +When you want to switch to alternate screen there are a couple of things to keep in mind for it to work correctly. +First off some code of how to switch to Alternate screen, for more info check the [alternate screen example](https://github.com/crossterm-rs/crossterm/blob/master/examples/alternate_screen.rs). + +_Create alternate screen from `Context`_ + + // create context. + let context = crossterm::Context::new(); + // create instance of Alternatescreen by the given context, this will also switch to it. + let mut screen = crossterm::AlternateScreen::from(context.clone()); + // write to the alternate screen. + write!(screen, "test"); + +_Create alternate screen from `Crossterm`:_ + + // create context. + let crossterm = ::crossterm::Crossterm::new(); + // create instance of Alternatescreen by the given reference to crossterm, this will also switch to it. + let mut screen = crossterm::AlternateScreen::from(&crossterm); + // write to the alternate screen. + write!(screen, "test"); + +like demonstrated above, to get the functionalities of `cursor(), color(), terminal()` also working on alternate screen. +You need to pass it the same `Context` as you have passed to the previous three called functions, +If you don't use the same `Context` in `cursor(), color(), terminal()` than these modules will be using the main screen and you will not see anything at the alternate screen. If you use the [Crossterm](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) type you can get the `Context` from it by calling the crossterm.get_context() whereafter you can create the AlternateScreen from it. + +# Version 0.2.2 + +- Bug see [issue 15](https://github.com/crossterm-rs/crossterm/issues/15) + +# Version 0.2.1 + +- Default ANSI escape codes for windows machines, if windows does not support ANSI switch back to WinApi. +- method grammar mistake fixed [Issue 3](https://github.com/crossterm-rs/crossterm/issues/3) +- Some Refactorings in method names see [issue 4](https://github.com/crossterm-rs/crossterm/issues/4) +- Removed bin reference from crate [Issue 6](https://github.com/crossterm-rs/crossterm/issues/6) +- Get position unix fixed [issue 8](https://github.com/crossterm-rs/crossterm/issues/8) + +# Version 0.2 + +- 256 color support. +- Text Attributes like: bold, italic, underscore and crossed word etc. +- Custom ANSI color code input to set fore- and background color for unix. +- Storing the current cursor position and resetting to that stored cursor position later. +- Resizing the terminal. diff --git a/keyfork-crossterm/Cargo.toml b/keyfork-crossterm/Cargo.toml new file mode 100644 index 0000000..32006e5 --- /dev/null +++ b/keyfork-crossterm/Cargo.toml @@ -0,0 +1,110 @@ +[package] +name = "crossterm" +version = "0.27.0" +authors = ["T. Post"] +description = "A crossplatform terminal library for manipulating terminals." +repository = "https://github.com/crossterm-rs/crossterm" +documentation = "https://docs.rs/crossterm/" +license = "MIT" +keywords = ["event", "color", "cli", "input", "terminal"] +exclude = ["target", "Cargo.lock"] +readme = "README.md" +edition = "2021" +rust-version = "1.58.0" +categories = ["command-line-interface", "command-line-utilities"] + +[lib] +name = "crossterm" +path = "src/lib.rs" + +# +# Build documentation with all features -> EventStream is available +# +[package.metadata.docs.rs] +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). +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"] # 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. + +# +# Shared dependencies +# +[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] +libc = "0.2" +signal-hook = { version = "0.3.17", optional = true } +filedescriptor = { version = "0.8", optional = true } +mio = { version = "0.8", features = ["os-poll"], optional = true } +signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true } + +# +# Dev dependencies (examples, ...) +# +[dev-dependencies] +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" + +# +# 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"] diff --git a/keyfork-crossterm/LICENSE b/keyfork-crossterm/LICENSE new file mode 100644 index 0000000..8b02a7f --- /dev/null +++ b/keyfork-crossterm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Timon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/keyfork-crossterm/README.md b/keyfork-crossterm/README.md new file mode 100644 index 0000000..b9f83f7 --- /dev/null +++ b/keyfork-crossterm/README.md @@ -0,0 +1,213 @@ +

+ +[![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] + +# 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). + +## Table of Contents + +- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library) + - [Table of Contents](#table-of-contents) + - [Features](#features) + - [Tested Terminals](#tested-terminals) + - [Getting Started](#getting-started) + - [Feature Flags](#feature-flags) + - [Dependency Justification](#dependency-justification) + - [Other Resources](#other-resources) + - [Used By](#used-by) + - [Contributing](#contributing) + - [Authors](#authors) + - [License](#license) + +## Features + +- Cross-platform +- Multi-threaded (send, sync) +- Detailed documentation +- Few dependencies +- Full control over writing and flushing output buffer +- Is tty +- Cursor + - Move the cursor N times (up, down, left, right) + - Move to previous / next line + - Move to column + - Set/get the cursor position + - Store the cursor position and restore to it later + - Hide/show the cursor + - Enable/disable cursor blinking (not all terminals do support this feature) +- Styled output + - Foreground color (16 base colors) + - Background color (16 base colors) + - 256 (ANSI) color support (Windows 10 and UNIX only) + - RGB color support (Windows 10 and UNIX only) + - Text attributes like bold, italic, underscore, crossed, etc +- Terminal + - Clear (all lines, current line, from cursor down and up, until new line) + - Scroll up, down + - Set/get the terminal size + - Exit current process + - Alternate screen + - Raw screen + - Set terminal title + - Enable/disable line wrapping +- Event + - Input Events + - Mouse Events (press, release, position, button, drag) + - Terminal Resize Events + - Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and + - futures Stream (feature 'event-stream') + - Poll/read API + + + +### Tested Terminals + +- Console Host + - Windows 10 (Pro) + - Windows 8.1 (N) +- Ubuntu Desktop Terminal + - Ubuntu 17.10 + - Pop!_OS ( Ubuntu ) 20.04 +- (Arch, Manjaro) KDE Konsole +- (Arch, NixOS) Kitty +- Linux Mint +- (OpenSuse) Alacritty +- (Chrome OS) Crostini +- Apple + - macOS Monterey 12.7.1 (Intel-Chip) + +This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the +terminals have been tested. If you have used this library for a terminal other than the above list without +issues, then feel free to add it to the above list - I really would appreciate it! + +## Getting Started +_see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._ + +
+ +Click to show Cargo.toml. + + +```toml +[dependencies] +crossterm = "0.27" +``` + +
+

+ +```rust +use std::io::{stdout, Write}; + +use crossterm::{ + execute, + style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}, + ExecutableCommand, Result, + event, +}; + +fn main() -> std::io::Result<()> { + // using the macro + execute!( + stdout(), + SetForegroundColor(Color::Blue), + SetBackgroundColor(Color::Red), + Print("Styled text here."), + ResetColor + )?; + + // or using functions + stdout() + .execute(SetForegroundColor(Color::Blue))? + .execute(SetBackgroundColor(Color::Red))? + .execute(Print("Styled text here."))? + .execute(ResetColor)?; + + Ok(()) +} +``` + +Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands. + +### Feature Flags + +```toml +[dependencies.crossterm] +version = "0.27" +features = ["event-stream"] +``` + +| Feature | Description | +|:---------------|:---------------------------------------------| +| `event-stream` | `futures::Stream` producing `Result`. | +| `serde` | (De)serializing of events. | +| `events` | Reading input/system events (enabled by default) | +| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency | + + +To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature. +This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies. + +### Dependency Justification + +| Dependency | Used for | Included | +|:---------------|:---------------------------------------------------------------------------------|:--------------------------------------| +| `bitflags` | `KeyModifiers`, those are differ based on input. | always | +| `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always | +| `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only | +| `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only | +| `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only | +| `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only | +| `futures-core` | For async stream of events | only with `event-stream` feature flag | +| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag | + +### Other Resources + +- [API documentation](https://docs.rs/crossterm/) +- [Deprecated examples repository](https://github.com/crossterm-rs/examples) + +## Used By + +- [Broot](https://dystroy.org/broot/) +- [Cursive](https://github.com/gyscos/Cursive) +- [TUI](https://github.com/fdehau/tui-rs) +- [Rust-sloth](https://github.com/ecumene/rust-sloth) +- [Rusty-rain](https://github.com/cowboy8625/rusty-rain) + +## Contributing + +We highly appreciate when anyone contributes to this crate. Before you do, please, +read the [Contributing](docs/CONTRIBUTING.md) guidelines. + +## Authors + +* **Timon Post** - *Project Owner & creator* + +## License + +This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`, +`crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT +License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details. + +[s1]: https://img.shields.io/crates/v/crossterm.svg +[l1]: https://crates.io/crates/crossterm + +[s2]: https://img.shields.io/badge/license-MIT-blue.svg +[l2]: ./LICENSE + +[s3]: https://docs.rs/crossterm/badge.svg +[l3]: https://docs.rs/crossterm/ + +[s3]: https://docs.rs/crossterm/badge.svg +[l3]: https://docs.rs/crossterm/ + +[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord +[l5]: https://discord.gg/K4nyTDB + +[s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code +[s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master diff --git a/keyfork-crossterm/docs/.gitignore b/keyfork-crossterm/docs/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/keyfork-crossterm/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/keyfork-crossterm/docs/CONTRIBUTING.md b/keyfork-crossterm/docs/CONTRIBUTING.md new file mode 100644 index 0000000..d99d4e2 --- /dev/null +++ b/keyfork-crossterm/docs/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing + +I would appreciate any contributions to this crate. However, some things are handy to know. + +## Code Style + +### Import Order + +All imports are semantically grouped and ordered. The order is: + +- standard library (`use std::...`) +- external crates (`use rand::...`) +- current crate (`use crate::...`) +- parent module (`use super::..`) +- current module (`use self::...`) +- module declaration (`mod ...`) + +There must be an empty line between groups. An example: + +```rust +use crossterm_utils::{csi, write_cout, Result}; + +use crate::sys::{get_cursor_position, show_cursor}; + +use super::Cursor; +``` + +#### CLion Tips + +The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts +imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation +to get proper grouping & sorting: + +* _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports +* `cargo fmt` - fix ordering within the group + +Second step can be automated via _CLion_ -> _Preferences_ -> +_Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_. + +### Max Line Length + +| Type | Max line length | +|:---------------------|----------------:| +| Code | 100 | +| Comments in the code | 120 | +| Documentation | 120 | + +100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width) +default value. + +120 is because of the GitHub. The editor & viewer width there is +- 123 characters. + +### Warnings + +The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings. +If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required. + +Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs. + +### Forbidden Warnings + +Search for `#![deny(...)]` in the code: + +* `unused_must_use` +* `unused_imports` diff --git a/keyfork-crossterm/docs/crossterm_c.png b/keyfork-crossterm/docs/crossterm_c.png new file mode 100644 index 0000000..b4a40b1 Binary files /dev/null and b/keyfork-crossterm/docs/crossterm_c.png differ diff --git a/keyfork-crossterm/docs/crossterm_full.png b/keyfork-crossterm/docs/crossterm_full.png new file mode 100644 index 0000000..1b1c448 Binary files /dev/null and b/keyfork-crossterm/docs/crossterm_full.png differ diff --git a/keyfork-crossterm/docs/crossterm_full.svg b/keyfork-crossterm/docs/crossterm_full.svg new file mode 100644 index 0000000..e8f9007 --- /dev/null +++ b/keyfork-crossterm/docs/crossterm_full.svg @@ -0,0 +1,103 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + diff --git a/keyfork-crossterm/docs/know-problems.md b/keyfork-crossterm/docs/know-problems.md new file mode 100644 index 0000000..826aaee --- /dev/null +++ b/keyfork-crossterm/docs/know-problems.md @@ -0,0 +1,14 @@ +# Known Problems + +There are some problems I discovered during development. +And I don't think it has to do anything with crossterm but it has to do whit how terminals handle ANSI or WinApi. + +## WinAPI + +- Power shell does not interpreter 'DarkYellow' and is instead using gray instead, cmd is working perfectly fine. +- Power shell inserts an '\n' (enter) when the program starts, this enter is the one you pressed when running the command. +- After the program ran, power shell will reset the background and foreground colors. + +## UNIX-terminals + +The Arc and Manjaro KDE Konsole's are not seeming to resize the terminal instead they are resizing the buffer. diff --git a/keyfork-crossterm/examples/README.md b/keyfork-crossterm/examples/README.md new file mode 100644 index 0000000..c9884ec --- /dev/null +++ b/keyfork-crossterm/examples/README.md @@ -0,0 +1,40 @@ +![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/keyfork-crossterm/examples/event-match-modifiers.rs b/keyfork-crossterm/examples/event-match-modifiers.rs new file mode 100644 index 0000000..c3f75e9 --- /dev/null +++ b/keyfork-crossterm/examples/event-match-modifiers.rs @@ -0,0 +1,68 @@ +//! 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/keyfork-crossterm/examples/event-poll-read.rs b/keyfork-crossterm/examples/event-poll-read.rs new file mode 100644 index 0000000..df960ab --- /dev/null +++ b/keyfork-crossterm/examples/event-poll-read.rs @@ -0,0 +1,61 @@ +//! 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/keyfork-crossterm/examples/event-read-char-line.rs b/keyfork-crossterm/examples/event-read-char-line.rs new file mode 100644 index 0000000..5fa8349 --- /dev/null +++ b/keyfork-crossterm/examples/event-read-char-line.rs @@ -0,0 +1,44 @@ +//! 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/keyfork-crossterm/examples/event-read.rs b/keyfork-crossterm/examples/event-read.rs new file mode 100644 index 0000000..633d158 --- /dev/null +++ b/keyfork-crossterm/examples/event-read.rs @@ -0,0 +1,113 @@ +//! 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, + PopKeyboardEnhancementFlags, + DisableFocusChange, + DisableMouseCapture + )?; + + disable_raw_mode() +} diff --git a/keyfork-crossterm/examples/event-stream-async-std.rs b/keyfork-crossterm/examples/event-stream-async-std.rs new file mode 100644 index 0000000..abd5449 --- /dev/null +++ b/keyfork-crossterm/examples/event-stream-async-std.rs @@ -0,0 +1,67 @@ +//! 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/keyfork-crossterm/examples/event-stream-tokio.rs b/keyfork-crossterm/examples/event-stream-tokio.rs new file mode 100644 index 0000000..6cea39c --- /dev/null +++ b/keyfork-crossterm/examples/event-stream-tokio.rs @@ -0,0 +1,68 @@ +//! 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/keyfork-crossterm/examples/interactive-demo/Cargo.toml b/keyfork-crossterm/examples/interactive-demo/Cargo.toml new file mode 100644 index 0000000..f7a5134 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/Cargo.toml @@ -0,0 +1,13 @@ +[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/keyfork-crossterm/examples/interactive-demo/src/macros.rs b/keyfork-crossterm/examples/interactive-demo/src/macros.rs new file mode 100644 index 0000000..02ce9e2 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/macros.rs @@ -0,0 +1,29 @@ +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/keyfork-crossterm/examples/interactive-demo/src/main.rs b/keyfork-crossterm/examples/interactive-demo/src/main.rs new file mode 100644 index 0000000..5eb1c73 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/main.rs @@ -0,0 +1,104 @@ +#![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/keyfork-crossterm/examples/interactive-demo/src/test.rs b/keyfork-crossterm/examples/interactive-demo/src/test.rs new file mode 100644 index 0000000..1db95a2 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test.rs @@ -0,0 +1,5 @@ +pub mod attribute; +pub mod color; +pub mod cursor; +pub mod event; +pub mod synchronized_output; diff --git a/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs b/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs new file mode 100644 index 0000000..6c06752 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs @@ -0,0 +1,58 @@ +#![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/keyfork-crossterm/examples/interactive-demo/src/test/color.rs b/keyfork-crossterm/examples/interactive-demo/src/test/color.rs new file mode 100644 index 0000000..d34ac45 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test/color.rs @@ -0,0 +1,198 @@ +#![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/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs b/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs new file mode 100644 index 0000000..9ad10d5 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs @@ -0,0 +1,222 @@ +#![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/keyfork-crossterm/examples/interactive-demo/src/test/event.rs b/keyfork-crossterm/examples/interactive-demo/src/test/event.rs new file mode 100644 index 0000000..50a797e --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test/event.rs @@ -0,0 +1,42 @@ +#![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/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs b/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs new file mode 100644 index 0000000..4fba674 --- /dev/null +++ b/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs @@ -0,0 +1,41 @@ +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/keyfork-crossterm/examples/is_tty.rs b/keyfork-crossterm/examples/is_tty.rs new file mode 100644 index 0000000..85770e2 --- /dev/null +++ b/keyfork-crossterm/examples/is_tty.rs @@ -0,0 +1,18 @@ +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/keyfork-crossterm/examples/stderr.rs b/keyfork-crossterm/examples/stderr.rs new file mode 100644 index 0000000..ce523c6 --- /dev/null +++ b/keyfork-crossterm/examples/stderr.rs @@ -0,0 +1,95 @@ +//! 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/keyfork-crossterm/src/ansi_support.rs b/keyfork-crossterm/src/ansi_support.rs new file mode 100644 index 0000000..fc2f6f1 --- /dev/null +++ b/keyfork-crossterm/src/ansi_support.rs @@ -0,0 +1,46 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use crossterm_winapi::{ConsoleMode, Handle}; +use parking_lot::Once; +use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + +/// Enable virtual terminal processing. +/// +/// This method attempts to enable virtual terminal processing for this +/// console. If there was a problem enabling it, then an error returned. +/// On success, the caller may assume that enabling it was successful. +/// +/// When virtual terminal processing is enabled, characters emitted to the +/// console are parsed for VT100 and similar control character sequences +/// that control color and other similar operations. +fn enable_vt_processing() -> std::io::Result<()> { + let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + let console_mode = ConsoleMode::from(Handle::current_out_handle()?); + let old_mode = console_mode.mode()?; + + if old_mode & mask == 0 { + console_mode.set_mode(old_mode | mask)?; + } + + Ok(()) +} + +static SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false); +static INITIALIZER: Once = Once::new(); + +/// Checks if the current terminal supports ANSI escape sequences +pub fn supports_ansi() -> bool { + INITIALIZER.call_once(|| { + // Some terminals on Windows like GitBash can't use WinAPI calls directly + // so when we try to enable the ANSI-flag for Windows this won't work. + // Because of that we should check first if the TERM-variable is set + // and see if the current terminal is a terminal who does support ANSI. + let supported = enable_vt_processing().is_ok() + || std::env::var("TERM").map_or(false, |term| term != "dumb"); + + SUPPORTS_ANSI_ESCAPE_CODES.store(supported, Ordering::SeqCst); + }); + + SUPPORTS_ANSI_ESCAPE_CODES.load(Ordering::SeqCst) +} diff --git a/keyfork-crossterm/src/command.rs b/keyfork-crossterm/src/command.rs new file mode 100644 index 0000000..11f83f7 --- /dev/null +++ b/keyfork-crossterm/src/command.rs @@ -0,0 +1,295 @@ +use std::fmt; +use std::io::{self, Write}; + +use crate::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}; + +/// An interface for a command that performs an action on the terminal. +/// +/// Crossterm provides a set of commands, +/// and there is no immediate reason to implement a command yourself. +/// In order to understand how to use and execute commands, +/// it is recommended that you take a look at [Command API](./index.html#command-api) chapter. +pub trait Command { + /// Write an ANSI representation of this command to the given writer. + /// An ANSI code can manipulate the terminal by writing it to the terminal buffer. + /// However, only Windows 10 and UNIX systems support this. + /// + /// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api) + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result; + + /// Execute this command. + /// + /// Windows versions lower than windows 10 do not support ANSI escape codes, + /// therefore a direct WinAPI call is made. + /// + /// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api) + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()>; + + /// Returns whether the ANSI code representation of this command is supported by windows. + /// + /// A list of supported ANSI escape codes + /// can be found [here](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + super::ansi_support::supports_ansi() + } +} + +impl Command for &T { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + (**self).write_ansi(f) + } + + #[inline] + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + T::execute_winapi(self) + } + + #[cfg(windows)] + #[inline] + fn is_ansi_code_supported(&self) -> bool { + T::is_ansi_code_supported(self) + } +} + +/// An interface for types that can queue commands for further execution. +pub trait QueueableCommand { + /// Queues the given command for further execution. + fn queue(&mut self, command: impl Command) -> io::Result<&mut Self>; +} + +/// An interface for types that can directly execute commands. +pub trait ExecutableCommand { + /// Executes the given command directly. + fn execute(&mut self, command: impl Command) -> io::Result<&mut Self>; +} + +impl QueueableCommand for T { + /// Queues the given command for further execution. + /// + /// Queued commands will be executed in the following cases: + /// + /// * When `flush` is called manually on the given type implementing `io::Write`. + /// * The terminal will `flush` automatically if the buffer is full. + /// * Each line is flushed in case of `stdout`, because it is line buffered. + /// + /// # Arguments + /// + /// - [Command](./trait.Command.html) + /// + /// The command that you want to queue for later execution. + /// + /// # Examples + /// + /// ```rust + /// use std::io::{self, Write}; + /// use crossterm::{QueueableCommand, style::Print}; + /// + /// fn main() -> io::Result<()> { + /// let mut stdout = io::stdout(); + /// + /// // `Print` will executed executed when `flush` is called. + /// stdout + /// .queue(Print("foo 1\n".to_string()))? + /// .queue(Print("foo 2".to_string()))?; + /// + /// // some other code (no execution happening here) ... + /// + /// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed. + /// stdout.flush()?; + /// + /// Ok(()) + /// + /// // ==== Output ==== + /// // foo 1 + /// // foo 2 + /// } + /// ``` + /// + /// Have a look over at the [Command API](./index.html#command-api) for more details. + /// + /// # Notes + /// + /// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. + /// * In case of Windows versions lower than 10, a direct WinAPI call will be made. + /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, + /// and can therefore not be written to the given `writer`. + /// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html) + /// and [queue](./trait.QueueableCommand.html) for those old Windows versions. + fn queue(&mut self, command: impl Command) -> io::Result<&mut Self> { + #[cfg(windows)] + if !command.is_ansi_code_supported() { + // There may be queued commands in this writer, but `execute_winapi` will execute the + // command immediately. To prevent commands being executed out of order we flush the + // writer now. + self.flush()?; + command.execute_winapi()?; + return Ok(self); + } + + write_command_ansi(self, command)?; + Ok(self) + } +} + +impl ExecutableCommand for T { + /// Executes the given command directly. + /// + /// The given command its ANSI escape code will be written and flushed onto `Self`. + /// + /// # Arguments + /// + /// - [Command](./trait.Command.html) + /// + /// The command that you want to execute directly. + /// + /// # Example + /// + /// ```rust + /// use std::io; + /// use crossterm::{ExecutableCommand, style::Print}; + /// + /// fn main() -> io::Result<()> { + /// // will be executed directly + /// io::stdout() + /// .execute(Print("sum:\n".to_string()))? + /// .execute(Print(format!("1 + 1= {} ", 1 + 1)))?; + /// + /// Ok(()) + /// + /// // ==== Output ==== + /// // sum: + /// // 1 + 1 = 2 + /// } + /// ``` + /// + /// Have a look over at the [Command API](./index.html#command-api) for more details. + /// + /// # Notes + /// + /// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. + /// * In case of Windows versions lower than 10, a direct WinAPI call will be made. + /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, + /// and can therefore not be written to the given `writer`. + /// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html) + /// and [queue](./trait.QueueableCommand.html) for those old Windows versions. + fn execute(&mut self, command: impl Command) -> io::Result<&mut Self> { + self.queue(command)?; + self.flush()?; + Ok(self) + } +} + +/// An interface for types that support synchronized updates. +pub trait SynchronizedUpdate { + /// Performs a set of actions against the given type. + fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result; +} + +impl SynchronizedUpdate for W { + /// Performs a set of actions within a synchronous update. + /// + /// Updates will be suspended in the terminal, the function will be executed against self, + /// updates will be resumed, and a flush will be performed. + /// + /// # Arguments + /// + /// - Function + /// + /// A function that performs the operations that must execute in a synchronized update. + /// + /// # Examples + /// + /// ```rust + /// use std::io; + /// use crossterm::{ExecutableCommand, SynchronizedUpdate, style::Print}; + /// + /// fn main() -> io::Result<()> { + /// let mut stdout = io::stdout(); + /// + /// stdout.sync_update(|stdout| { + /// stdout.execute(Print("foo 1\n".to_string()))?; + /// stdout.execute(Print("foo 2".to_string()))?; + /// // The effects of the print command will not be present in the terminal + /// // buffer, but not visible in the terminal. + /// std::io::Result::Ok(()) + /// })?; + /// + /// // The effects of the commands will be visible. + /// + /// Ok(()) + /// + /// // ==== Output ==== + /// // foo 1 + /// // foo 2 + /// } + /// ``` + /// + /// # Notes + /// + /// This command is performed only using ANSI codes, and will do nothing on terminals that do not support ANSI + /// codes, or this specific extension. + /// + /// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and + /// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. + /// + /// This mode attempts to mitigate that. + /// + /// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. + /// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled + /// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect + /// by unintentionally rendering in the middle a of an application screen update. + /// + fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result { + self.queue(BeginSynchronizedUpdate)?; + let result = operations(self); + self.execute(EndSynchronizedUpdate)?; + Ok(result) + } +} +/// Writes the ANSI representation of a command to the given writer. +fn write_command_ansi( + io: &mut (impl io::Write + ?Sized), + command: C, +) -> io::Result<()> { + struct Adapter { + inner: T, + res: io::Result<()>, + } + + impl fmt::Write for Adapter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.inner.write_all(s.as_bytes()).map_err(|e| { + self.res = Err(e); + fmt::Error + }) + } + } + + let mut adapter = Adapter { + inner: io, + res: Ok(()), + }; + + command + .write_ansi(&mut adapter) + .map_err(|fmt::Error| match adapter.res { + Ok(()) => panic!( + "<{}>::write_ansi incorrectly errored", + std::any::type_name::() + ), + Err(e) => e, + }) +} + +/// Executes the ANSI representation of a command, using the given `fmt::Write`. +pub(crate) fn execute_fmt(f: &mut impl fmt::Write, command: impl Command) -> fmt::Result { + #[cfg(windows)] + if !command.is_ansi_code_supported() { + return command.execute_winapi().map_err(|_| fmt::Error); + } + + command.write_ansi(f) +} diff --git a/keyfork-crossterm/src/cursor.rs b/keyfork-crossterm/src/cursor.rs new file mode 100644 index 0000000..24c4fdd --- /dev/null +++ b/keyfork-crossterm/src/cursor.rs @@ -0,0 +1,504 @@ +//! # Cursor +//! +//! The `cursor` module provides functionality to work with the terminal cursor. +//! +//! This documentation does not contain a lot of examples. The reason is that it's fairly +//! obvious how to use this crate. Although, we do provide +//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository +//! to demonstrate the capabilities. +//! +//! ## Examples +//! +//! Cursor actions can be performed with commands. +//! Please have a look at [command documentation](../index.html#command-api) for a more detailed documentation. +//! +//! ```no_run +//! use std::io::{self, Write}; +//! +//! use crossterm::{ +//! ExecutableCommand, execute, +//! cursor::{DisableBlinking, EnableBlinking, MoveTo, RestorePosition, SavePosition} +//! }; +//! +//! fn main() -> io::Result<()> { +//! // with macro +//! execute!( +//! io::stdout(), +//! SavePosition, +//! MoveTo(10, 10), +//! EnableBlinking, +//! DisableBlinking, +//! RestorePosition +//! ); +//! +//! // with function +//! io::stdout() +//! .execute(MoveTo(11,11))? +//! .execute(RestorePosition); +//! +//! Ok(()) +//! } +//! ``` +//! +//! For manual execution control check out [crossterm::queue](../macro.queue.html). + +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 +/// * Top left cell is represented as `0,0`. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveTo(pub u16, pub u16); + +impl Command for MoveTo { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{};{}H"), self.1 + 1, self.0 + 1) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_to(self.0, self.1) + } +} + +/// A command that moves the terminal cursor down the given number of lines, +/// and moves it to the first column. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveToNextLine(1)` moves to the next line. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveToNextLine(pub u16); + +impl Command for MoveToNextLine { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}E"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + if self.0 != 0 { + sys::move_to_next_line(self.0)?; + } + Ok(()) + } +} + +/// A command that moves the terminal cursor up the given number of lines, +/// and moves it to the first column. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveToPreviousLine(1)` moves to the previous line. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveToPreviousLine(pub u16); + +impl Command for MoveToPreviousLine { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}F"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + if self.0 != 0 { + sys::move_to_previous_line(self.0)?; + } + Ok(()) + } +} + +/// A command that moves the terminal cursor to the given column on the current row. +/// +/// # Notes +/// * This command is 0 based, meaning 0 is the leftmost column. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveToColumn(pub u16); + +impl Command for MoveToColumn { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}G"), self.0 + 1)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_to_column(self.0) + } +} + +/// A command that moves the terminal cursor to the given row on the current column. +/// +/// # Notes +/// * This command is 0 based, meaning 0 is the topmost row. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveToRow(pub u16); + +impl Command for MoveToRow { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}d"), self.0 + 1)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_to_row(self.0) + } +} + +/// A command that moves the terminal cursor a given number of rows up. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveUp(1)` moves the cursor up one cell. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveUp(pub u16); + +impl Command for MoveUp { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}A"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_up(self.0) + } +} + +/// A command that moves the terminal cursor a given number of columns to the right. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveRight(1)` moves the cursor right one cell. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveRight(pub u16); + +impl Command for MoveRight { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}C"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_right(self.0) + } +} + +/// A command that moves the terminal cursor a given number of rows down. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveDown(1)` moves the cursor down one cell. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveDown(pub u16); + +impl Command for MoveDown { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}B"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_down(self.0) + } +} + +/// A command that moves the terminal cursor a given number of columns to the left. +/// +/// # Notes +/// * This command is 1 based, meaning `MoveLeft(1)` moves the cursor left one cell. +/// * Most terminals default 0 argument to 1. +/// * Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoveLeft(pub u16); + +impl Command for MoveLeft { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}D"), self.0)?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::move_left(self.0) + } +} + +/// A command that saves the current terminal cursor position. +/// +/// See the [RestorePosition](./struct.RestorePosition.html) command. +/// +/// # Notes +/// +/// - The cursor position is stored globally. +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SavePosition; + +impl Command for SavePosition { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str("\x1B7") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::save_position() + } +} + +/// A command that restores the saved terminal cursor position. +/// +/// See the [SavePosition](./struct.SavePosition.html) command. +/// +/// # Notes +/// +/// - The cursor position is stored globally. +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RestorePosition; + +impl Command for RestorePosition { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str("\x1B8") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::restore_position() + } +} + +/// A command that hides the terminal cursor. +/// +/// # Notes +/// +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Hide; + +impl Command for Hide { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?25l")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::show_cursor(false) + } +} + +/// A command that shows the terminal cursor. +/// +/// # Notes +/// +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Show; + +impl Command for Show { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?25h")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::show_cursor(true) + } +} + +/// A command that enables blinking of the terminal cursor. +/// +/// # Notes +/// +/// - Some Unix terminals (ex: GNOME and Konsole) as well as Windows versions lower than Windows 10 do not support this functionality. +/// Use `SetCursorStyle` for better cross-compatibility. +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableBlinking; +impl Command for EnableBlinking { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?12h")) + } + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Ok(()) + } +} + +/// A command that disables blinking of the terminal cursor. +/// +/// # Notes +/// +/// - Some Unix terminals (ex: GNOME and Konsole) as well as Windows versions lower than Windows 10 do not support this functionality. +/// Use `SetCursorStyle` for better cross-compatibility. +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableBlinking; +impl Command for DisableBlinking { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?12l")) + } + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Ok(()) + } +} + +/// A command that sets the style of the cursor. +/// It uses two types of escape codes, one to control blinking, and the other the shape. +/// +/// # Note +/// +/// - Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Clone, Copy)] +pub enum SetCursorStyle { + /// Default cursor shape configured by the user. + DefaultUserShape, + /// A blinking block cursor shape (■). + BlinkingBlock, + /// A non blinking block cursor shape (inverse of `BlinkingBlock`). + SteadyBlock, + /// A blinking underscore cursor shape(_). + BlinkingUnderScore, + /// A non blinking underscore cursor shape (inverse of `BlinkingUnderScore`). + SteadyUnderScore, + /// A blinking cursor bar shape (|) + BlinkingBar, + /// A steady cursor bar shape (inverse of `BlinkingBar`). + SteadyBar, +} + +impl Command for SetCursorStyle { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + match self { + SetCursorStyle::DefaultUserShape => f.write_str("\x1b[0 q"), + SetCursorStyle::BlinkingBlock => f.write_str("\x1b[1 q"), + SetCursorStyle::SteadyBlock => f.write_str("\x1b[2 q"), + SetCursorStyle::BlinkingUnderScore => f.write_str("\x1b[3 q"), + SetCursorStyle::SteadyUnderScore => f.write_str("\x1b[4 q"), + SetCursorStyle::BlinkingBar => f.write_str("\x1b[5 q"), + SetCursorStyle::SteadyBar => f.write_str("\x1b[6 q"), + } + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Ok(()) + } +} + +impl_display!(for MoveTo); +impl_display!(for MoveToColumn); +impl_display!(for MoveToRow); +impl_display!(for MoveToNextLine); +impl_display!(for MoveToPreviousLine); +impl_display!(for MoveUp); +impl_display!(for MoveDown); +impl_display!(for MoveLeft); +impl_display!(for MoveRight); +impl_display!(for SavePosition); +impl_display!(for RestorePosition); +impl_display!(for Hide); +impl_display!(for Show); +impl_display!(for EnableBlinking); +impl_display!(for DisableBlinking); +impl_display!(for SetCursorStyle); + +#[cfg(test)] +#[cfg(feature = "events")] +mod tests { + use std::io::{self, stdout}; + + use crate::execute; + + use super::{ + sys::position, MoveDown, MoveLeft, MoveRight, MoveTo, MoveUp, RestorePosition, SavePosition, + }; + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_move_to() { + let (saved_x, saved_y) = position().unwrap(); + + execute!(stdout(), MoveTo(saved_x + 1, saved_y + 1)).unwrap(); + assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1)); + + execute!(stdout(), MoveTo(saved_x, saved_y)).unwrap(); + assert_eq!(position().unwrap(), (saved_x, saved_y)); + } + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_move_right() { + let (saved_x, saved_y) = position().unwrap(); + execute!(io::stdout(), MoveRight(1)).unwrap(); + assert_eq!(position().unwrap(), (saved_x + 1, saved_y)); + } + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_move_left() { + execute!(stdout(), MoveTo(2, 0), MoveLeft(2)).unwrap(); + assert_eq!(position().unwrap(), (0, 0)); + } + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_move_up() { + execute!(stdout(), MoveTo(0, 2), MoveUp(2)).unwrap(); + assert_eq!(position().unwrap(), (0, 0)); + } + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_move_down() { + execute!(stdout(), MoveTo(0, 0), MoveDown(2)).unwrap(); + + assert_eq!(position().unwrap(), (0, 2)); + } + + // Test is disabled, because it's failing on Travis + #[test] + #[ignore] + fn test_save_restore_position() { + let (saved_x, saved_y) = position().unwrap(); + + execute!( + stdout(), + SavePosition, + MoveTo(saved_x + 1, saved_y + 1), + RestorePosition + ) + .unwrap(); + + let (x, y) = position().unwrap(); + + assert_eq!(x, saved_x); + assert_eq!(y, saved_y); + } +} diff --git a/keyfork-crossterm/src/cursor/sys.rs b/keyfork-crossterm/src/cursor/sys.rs new file mode 100644 index 0000000..f85a95c --- /dev/null +++ b/keyfork-crossterm/src/cursor/sys.rs @@ -0,0 +1,19 @@ +//! This module provides platform related functions. + +#[cfg(unix)] +#[cfg(feature = "events")] +pub use self::unix::position; +#[cfg(windows)] +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/keyfork-crossterm/src/cursor/sys/unix.rs b/keyfork-crossterm/src/cursor/sys/unix.rs new file mode 100644 index 0000000..4734212 --- /dev/null +++ b/keyfork-crossterm/src/cursor/sys/unix.rs @@ -0,0 +1,56 @@ +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/keyfork-crossterm/src/cursor/sys/windows.rs b/keyfork-crossterm/src/cursor/sys/windows.rs new file mode 100644 index 0000000..84a4cd0 --- /dev/null +++ b/keyfork-crossterm/src/cursor/sys/windows.rs @@ -0,0 +1,341 @@ +//! 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/keyfork-crossterm/src/event.rs b/keyfork-crossterm/src/event.rs new file mode 100644 index 0000000..3464467 --- /dev/null +++ b/keyfork-crossterm/src/event.rs @@ -0,0 +1,994 @@ +//! # Event +//! +//! The `event` module provides the functionality to read keyboard, mouse and terminal resize events. +//! +//! * The [`read`](fn.read.html) function returns an [`Event`](enum.Event.html) immediately +//! (if available) or blocks until an [`Event`](enum.Event.html) is available. +//! +//! * The [`poll`](fn.poll.html) function allows you to check if there is or isn't an [`Event`](enum.Event.html) available +//! within the given period of time. In other words - if subsequent call to the [`read`](fn.read.html) +//! function will block or not. +//! +//! It's **not allowed** to call these functions from different threads or combine them with the +//! [`EventStream`](struct.EventStream.html). You're allowed to either: +//! +//! * use the [`read`](fn.read.html) & [`poll`](fn.poll.html) functions on any, but same, thread +//! * or the [`EventStream`](struct.EventStream.html). +//! +//! **Make sure to enable [raw mode](../terminal/index.html#raw-mode) in order for keyboard events to work properly** +//! +//! ## Mouse Events +//! +//! Mouse events are not enabled by default. You have to enable them with the +//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) command. See [Command API](../index.html#command-api) +//! for more information. +//! +//! ## Examples +//! +//! Blocking read: +//! +//! ```no_run +//! use crossterm::event::{read, Event}; +//! +//! fn print_events() -> std::io::Result<()> { +//! loop { +//! // `read()` blocks until an `Event` is available +//! match read()? { +//! Event::FocusGained => println!("FocusGained"), +//! Event::FocusLost => println!("FocusLost"), +//! Event::Key(event) => println!("{:?}", event), +//! Event::Mouse(event) => println!("{:?}", event), +//! #[cfg(feature = "bracketed-paste")] +//! Event::Paste(data) => println!("{:?}", data), +//! Event::Resize(width, height) => println!("New size {}x{}", width, height), +//! } +//! } +//! Ok(()) +//! } +//! ``` +//! +//! Non-blocking read: +//! +//! ```no_run +//! use std::{time::Duration, io}; +//! +//! use crossterm::event::{poll, read, Event}; +//! +//! fn print_events() -> io::Result<()> { +//! loop { +//! // `poll()` waits for an `Event` for a given time period +//! if poll(Duration::from_millis(500))? { +//! // It's guaranteed that the `read()` won't block when the `poll()` +//! // function returns `true` +//! match read()? { +//! Event::FocusGained => println!("FocusGained"), +//! Event::FocusLost => println!("FocusLost"), +//! Event::Key(event) => println!("{:?}", event), +//! Event::Mouse(event) => println!("{:?}", event), +//! #[cfg(feature = "bracketed-paste")] +//! Event::Paste(data) => println!("Pasted {:?}", data), +//! Event::Resize(width, height) => println!("New size {}x{}", width, height), +//! } +//! } else { +//! // Timeout expired and no `Event` is available +//! } +//! } +//! Ok(()) +//! } +//! ``` +//! +//! Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder for more of +//! them (`event-*`). + +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; +use std::time::Duration; + +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) +} + +bitflags! { + /// Represents special flags that tell compatible terminals to add extra information to keyboard events. + /// + /// See for more information. + /// + /// Alternate keys and Unicode codepoints are not yet supported by crossterm. + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] + #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] + pub struct KeyboardEnhancementFlags: u8 { + /// Represent Escape and modified keys using CSI-u sequences, so they can be unambiguously + /// read. + const DISAMBIGUATE_ESCAPE_CODES = 0b0000_0001; + /// Add extra events with [`KeyEvent.kind`] set to [`KeyEventKind::Repeat`] or + /// [`KeyEventKind::Release`] when keys are autorepeated or released. + const REPORT_EVENT_TYPES = 0b0000_0010; + // Send [alternate keycodes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-codes) + // in addition to the base keycode. The alternate keycode overrides the base keycode in + // resulting `KeyEvent`s. + const REPORT_ALTERNATE_KEYS = 0b0000_0100; + /// Represent all keyboard events as CSI-u sequences. This is required to get repeat/release + /// events for plain-text keys. + const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b0000_1000; + // Send the Unicode codepoint as well as the keycode. + // + // *Note*: this is not yet supported by crossterm. + // const REPORT_ASSOCIATED_TEXT = 0b0001_0000; + } +} + +/// A command that enables mouse event capturing. +/// +/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). +#[cfg(feature = "events")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableMouseCapture; + +#[cfg(feature = "events")] +impl Command for EnableMouseCapture { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(concat!( + // Normal tracking: Send mouse X & Y on button press and release + csi!("?1000h"), + // Button-event tracking: Report button motion events (dragging) + csi!("?1002h"), + // Any-event tracking: Report all motion events + csi!("?1003h"), + // RXVT mouse mode: Allows mouse coordinates of >223 + csi!("?1015h"), + // SGR mouse mode: Allows mouse coordinates of >223, preferred over RXVT mode + 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. +/// +/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableMouseCapture; + +impl Command for DisableMouseCapture { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(concat!( + // The inverse commands of EnableMouseCapture, in reverse order. + csi!("?1006l"), + csi!("?1015l"), + csi!("?1003l"), + csi!("?1002l"), + 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. +/// +/// It should be paired with [`DisableFocusChange`] at the end of execution. +/// +/// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableFocusChange; + +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. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableFocusChange; + +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). +/// +/// It should be paired with [`DisableBracketedPaste`] at the end of execution. +/// +/// This is not supported in older Windows terminals without +/// [virtual terminal sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). +#[cfg(feature = "bracketed-paste")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableBracketedPaste; + +#[cfg(feature = "bracketed-paste")] +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. +#[cfg(feature = "bracketed-paste")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableBracketedPaste; + +#[cfg(feature = "bracketed-paste")] +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. +/// +/// It should be paired with [`PopKeyboardEnhancementFlags`] at the end of execution. +/// +/// Example usage: +/// ```no_run +/// use std::io::{Write, stdout}; +/// use crossterm::execute; +/// use crossterm::event::{ +/// KeyboardEnhancementFlags, +/// PushKeyboardEnhancementFlags, +/// PopKeyboardEnhancementFlags +/// }; +/// +/// let mut stdout = stdout(); +/// +/// execute!( +/// stdout, +/// PushKeyboardEnhancementFlags( +/// KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES +/// ) +/// ); +/// +/// // ... +/// +/// execute!(stdout, PopKeyboardEnhancementFlags); +/// ``` +/// +/// Note that, currently, only the following support this protocol: +/// * [kitty terminal](https://sw.kovidgoyal.net/kitty/) +/// * [foot terminal](https://codeberg.org/dnkl/foot/issues/319) +/// * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html) +/// * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131) +/// * [neovim text editor](https://github.com/neovim/neovim/pull/18181) +/// * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103) +/// * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PushKeyboardEnhancementFlags(pub KeyboardEnhancementFlags); + +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. +/// +/// Specifically, it pops one level of keyboard enhancement flags. +/// +/// See [`PushKeyboardEnhancementFlags`] and for more information. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PopKeyboardEnhancementFlags; + +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. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(not(feature = "bracketed-paste"), derive(Copy))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] +pub enum Event { + /// The terminal gained focus + FocusGained, + /// The terminal lost focus + FocusLost, + /// A single key event with additional pressed modifiers. + Key(KeyEvent), + /// A single mouse event with additional pressed modifiers. + Mouse(MouseEvent), + /// A string that was pasted into the terminal. Only emitted if bracketed paste has been + /// enabled. + #[cfg(feature = "bracketed-paste")] + Paste(String), + /// An resize event with new dimensions after resize (columns, rows). + /// **Note** that resize events can occur in batches. + Resize(u16, u16), +} + +/// Represents a mouse event. +/// +/// # Platform-specific Notes +/// +/// ## Mouse Buttons +/// +/// Some platforms/terminals do not report mouse button for the +/// `MouseEventKind::Up` and `MouseEventKind::Drag` events. `MouseButton::Left` +/// is returned if we don't know which button was used. +/// +/// ## Key Modifiers +/// +/// Some platforms/terminals does not report all key modifiers +/// combinations for all mouse event types. For example - macOS reports +/// `Ctrl` + left mouse button click as a right mouse button click. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +pub struct MouseEvent { + /// The kind of mouse event that was caused. + pub kind: MouseEventKind, + /// The column that the event occurred on. + pub column: u16, + /// The row that the event occurred on. + pub row: u16, + /// The key modifiers active when the event occurred. + pub modifiers: KeyModifiers, +} + +/// A mouse event kind. +/// +/// # Platform-specific Notes +/// +/// ## Mouse Buttons +/// +/// Some platforms/terminals do not report mouse button for the +/// `MouseEventKind::Up` and `MouseEventKind::Drag` events. `MouseButton::Left` +/// is returned if we don't know which button was used. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +pub enum MouseEventKind { + /// Pressed mouse button. Contains the button that was pressed. + Down(MouseButton), + /// Released mouse button. Contains the button that was released. + Up(MouseButton), + /// Moved the mouse cursor while pressing the contained mouse button. + Drag(MouseButton), + /// Moved the mouse cursor while not pressing a mouse button. + Moved, + /// Scrolled mouse wheel downwards (towards the user). + ScrollDown, + /// Scrolled mouse wheel upwards (away from the user). + ScrollUp, + /// Scrolled mouse wheel left (mostly on a laptop touchpad). + ScrollLeft, + /// Scrolled mouse wheel right (mostly on a laptop touchpad). + ScrollRight, +} + +/// Represents a mouse button. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +pub enum MouseButton { + /// Left mouse button. + Left, + /// Right mouse button. + Right, + /// Middle mouse button. + Middle, +} + +bitflags! { + /// Represents key modifiers (shift, control, alt, etc.). + /// + /// **Note:** `SUPER`, `HYPER`, and `META` can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] + #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] + pub struct KeyModifiers: u8 { + const SHIFT = 0b0000_0001; + const CONTROL = 0b0000_0010; + const ALT = 0b0000_0100; + const SUPER = 0b0000_1000; + const HYPER = 0b0001_0000; + const META = 0b0010_0000; + const NONE = 0b0000_0000; + } +} + +/// Represents a keyboard event kind. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +pub enum KeyEventKind { + Press, + Repeat, + Release, +} + +bitflags! { + /// Represents extra state about the key event. + /// + /// **Note:** This state can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] + pub struct KeyEventState: u8 { + /// The key event origins from the keypad. + const KEYPAD = 0b0000_0001; + /// Caps Lock was enabled for this key event. + /// + /// **Note:** this is set for the initial press of Caps Lock itself. + const CAPS_LOCK = 0b0000_1000; + /// Num Lock was enabled for this key event. + /// + /// **Note:** this is set for the initial press of Num Lock itself. + const NUM_LOCK = 0b0000_1000; + const NONE = 0b0000_0000; + } +} + +/// Represents a key event. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, PartialOrd, Clone, Copy)] +pub struct KeyEvent { + /// The key itself. + pub code: KeyCode, + /// Additional key modifiers. + pub modifiers: KeyModifiers, + /// Kind of event. + /// + /// Only set if: + /// - Unix: [`KeyboardEnhancementFlags::REPORT_EVENT_TYPES`] has been enabled with [`PushKeyboardEnhancementFlags`]. + /// - Windows: always + pub kind: KeyEventKind, + /// Keyboard state. + /// + /// Only set if [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + pub state: KeyEventState, +} + +impl KeyEvent { + pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { + KeyEvent { + code, + modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + } + } + + pub const fn new_with_kind( + code: KeyCode, + modifiers: KeyModifiers, + kind: KeyEventKind, + ) -> KeyEvent { + KeyEvent { + code, + modifiers, + kind, + state: KeyEventState::empty(), + } + } + + pub const fn new_with_kind_and_state( + code: KeyCode, + modifiers: KeyModifiers, + kind: KeyEventKind, + state: KeyEventState, + ) -> KeyEvent { + KeyEvent { + code, + modifiers, + kind, + state, + } + } + + // modifies the KeyEvent, + // so that KeyModifiers::SHIFT is present iff + // an uppercase char is present. + fn normalize_case(mut self) -> KeyEvent { + let c = match self.code { + KeyCode::Char(c) => c, + _ => return self, + }; + + if c.is_ascii_uppercase() { + self.modifiers.insert(KeyModifiers::SHIFT); + } else if self.modifiers.contains(KeyModifiers::SHIFT) { + self.code = KeyCode::Char(c.to_ascii_uppercase()) + } + self + } +} + +impl From for KeyEvent { + fn from(code: KeyCode) -> Self { + KeyEvent { + code, + modifiers: KeyModifiers::empty(), + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + } + } +} + +impl PartialEq for KeyEvent { + fn eq(&self, other: &KeyEvent) -> bool { + let KeyEvent { + code: lhs_code, + modifiers: lhs_modifiers, + kind: lhs_kind, + state: lhs_state, + } = self.normalize_case(); + let KeyEvent { + code: rhs_code, + modifiers: rhs_modifiers, + kind: rhs_kind, + state: rhs_state, + } = other.normalize_case(); + (lhs_code == rhs_code) + && (lhs_modifiers == rhs_modifiers) + && (lhs_kind == rhs_kind) + && (lhs_state == rhs_state) + } +} + +impl Eq for KeyEvent {} + +impl Hash for KeyEvent { + fn hash(&self, hash_state: &mut H) { + let KeyEvent { + code, + modifiers, + kind, + state, + } = self.normalize_case(); + code.hash(hash_state); + modifiers.hash(hash_state); + kind.hash(hash_state); + state.hash(hash_state); + } +} + +/// Represents a media key (as part of [`KeyCode::Media`]). +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum MediaKeyCode { + /// Play media key. + Play, + /// Pause media key. + Pause, + /// Play/Pause media key. + PlayPause, + /// Reverse media key. + Reverse, + /// Stop media key. + Stop, + /// Fast-forward media key. + FastForward, + /// Rewind media key. + Rewind, + /// Next-track media key. + TrackNext, + /// Previous-track media key. + TrackPrevious, + /// Record media key. + Record, + /// Lower-volume media key. + LowerVolume, + /// Raise-volume media key. + RaiseVolume, + /// Mute media key. + MuteVolume, +} + +/// Represents a modifier key (as part of [`KeyCode::Modifier`]). +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ModifierKeyCode { + /// Left Shift key. + LeftShift, + /// Left Control key. + LeftControl, + /// Left Alt key. + LeftAlt, + /// Left Super key. + LeftSuper, + /// Left Hyper key. + LeftHyper, + /// Left Meta key. + LeftMeta, + /// Right Shift key. + RightShift, + /// Right Control key. + RightControl, + /// Right Alt key. + RightAlt, + /// Right Super key. + RightSuper, + /// Right Hyper key. + RightHyper, + /// Right Meta key. + RightMeta, + /// Iso Level3 Shift key. + IsoLevel3Shift, + /// Iso Level5 Shift key. + IsoLevel5Shift, +} + +/// Represents a key. +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum KeyCode { + /// Backspace key. + Backspace, + /// Enter key. + Enter, + /// Left arrow key. + Left, + /// Right arrow key. + Right, + /// Up arrow key. + Up, + /// Down arrow key. + Down, + /// Home key. + Home, + /// End key. + End, + /// Page up key. + PageUp, + /// Page down key. + PageDown, + /// Tab key. + Tab, + /// Shift + Tab key. + BackTab, + /// Delete key. + Delete, + /// Insert key. + Insert, + /// F key. + /// + /// `KeyCode::F(1)` represents F1 key, etc. + F(u8), + /// A character. + /// + /// `KeyCode::Char('c')` represents `c` character, etc. + Char(char), + /// Null. + Null, + /// Escape key. + Esc, + /// Caps Lock key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + CapsLock, + /// Scroll Lock key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + ScrollLock, + /// Num Lock key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + NumLock, + /// Print Screen key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + PrintScreen, + /// Pause key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + Pause, + /// Menu key. + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + Menu, + /// The "Begin" key (often mapped to the 5 key when Num Lock is turned on). + /// + /// **Note:** this key can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + KeypadBegin, + /// A media key. + /// + /// **Note:** these keys can only be read if + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with + /// [`PushKeyboardEnhancementFlags`]. + Media(MediaKeyCode), + /// A modifier key. + /// + /// **Note:** these keys can only be read if **both** + /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] and + /// [`KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES`] have been enabled with + /// [`PushKeyboardEnhancementFlags`]. + Modifier(ModifierKeyCode), +} + +/// 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; + use std::hash::{Hash, Hasher}; + + use super::{KeyCode, KeyEvent, KeyModifiers}; + + #[test] + fn test_equality() { + let lowercase_d_with_shift = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::SHIFT); + let uppercase_d_with_shift = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT); + let uppercase_d = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE); + assert_eq!(lowercase_d_with_shift, uppercase_d_with_shift); + assert_eq!(uppercase_d, uppercase_d_with_shift); + } + + #[test] + fn test_hash() { + let lowercase_d_with_shift_hash = { + let mut hasher = DefaultHasher::new(); + KeyEvent::new(KeyCode::Char('d'), KeyModifiers::SHIFT).hash(&mut hasher); + hasher.finish() + }; + let uppercase_d_with_shift_hash = { + let mut hasher = DefaultHasher::new(); + KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT).hash(&mut hasher); + hasher.finish() + }; + let uppercase_d_hash = { + let mut hasher = DefaultHasher::new(); + KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE).hash(&mut hasher); + hasher.finish() + }; + assert_eq!(lowercase_d_with_shift_hash, uppercase_d_with_shift_hash); + assert_eq!(uppercase_d_hash, uppercase_d_with_shift_hash); + } +} diff --git a/keyfork-crossterm/src/event/filter.rs b/keyfork-crossterm/src/event/filter.rs new file mode 100644 index 0000000..f78730d --- /dev/null +++ b/keyfork-crossterm/src/event/filter.rs @@ -0,0 +1,115 @@ +use crate::event::InternalEvent; + +/// Interface for filtering an `InternalEvent`. +pub(crate) trait Filter: Send + Sync + 'static { + /// Returns whether the given event fulfills the filter. + fn eval(&self, event: &InternalEvent) -> bool; +} + +#[cfg(unix)] +#[derive(Debug, Clone)] +pub(crate) struct CursorPositionFilter; + +#[cfg(unix)] +impl Filter for CursorPositionFilter { + fn eval(&self, event: &InternalEvent) -> bool { + matches!(*event, InternalEvent::CursorPosition(_, _)) + } +} + +#[cfg(unix)] +#[derive(Debug, Clone)] +pub(crate) struct KeyboardEnhancementFlagsFilter; + +#[cfg(unix)] +impl Filter for KeyboardEnhancementFlagsFilter { + fn eval(&self, event: &InternalEvent) -> 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 + ) + } +} + +#[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) + } +} + +#[derive(Debug, Clone)] +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 { + true + } +} + +#[cfg(test)] +#[cfg(unix)] +mod tests { + use super::{ + super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, + 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))); + } + + #[test] + fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { + assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!( + KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( + crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + )) + ); + assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::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)); + } + + #[test] + fn test_event_filter_filters_events() { + assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + } + + #[test] + fn test_event_filter_filters_internal_events() { + assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + } +} diff --git a/keyfork-crossterm/src/event/read.rs b/keyfork-crossterm/src/event/read.rs new file mode 100644 index 0000000..6ddbace --- /dev/null +++ b/keyfork-crossterm/src/event/read.rs @@ -0,0 +1,430 @@ +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/keyfork-crossterm/src/event/source.rs b/keyfork-crossterm/src/event/source.rs new file mode 100644 index 0000000..ae0f05a --- /dev/null +++ b/keyfork-crossterm/src/event/source.rs @@ -0,0 +1,27 @@ +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/keyfork-crossterm/src/event/source/unix.rs b/keyfork-crossterm/src/event/source/unix.rs new file mode 100644 index 0000000..810bad3 --- /dev/null +++ b/keyfork-crossterm/src/event/source/unix.rs @@ -0,0 +1,11 @@ +#[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/keyfork-crossterm/src/event/source/unix/mio.rs b/keyfork-crossterm/src/event/source/unix/mio.rs new file mode 100644 index 0000000..c252f12 --- /dev/null +++ b/keyfork-crossterm/src/event/source/unix/mio.rs @@ -0,0 +1,234 @@ +use std::{collections::VecDeque, io, time::Duration}; + +use mio::{unix::SourceFd, Events, Interest, Poll, Token}; +use signal_hook_mio::v0_8::Signals; + +#[cfg(feature = "event-stream")] +use crate::event::sys::Waker; +use crate::event::{ + source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent, +}; +use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; + +// Tokens to identify file descriptor +const TTY_TOKEN: Token = Token(0); +const SIGNAL_TOKEN: Token = Token(1); +#[cfg(feature = "event-stream")] +const WAKE_TOKEN: Token = Token(2); + +// I (@zrzka) wasn't able to read more than 1_022 bytes when testing +// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes +// is enough. +const TTY_BUFFER_SIZE: usize = 1_024; + +pub(crate) struct UnixInternalEventSource { + poll: Poll, + events: Events, + parser: Parser, + tty_buffer: [u8; TTY_BUFFER_SIZE], + tty_fd: FileDesc, + 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) -> 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, TTY_BUFFER_SIZE) { + 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 => { + for signal in self.signals.pending() { + match signal { + 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, + )))); + } + _ => unreachable!("Synchronize signal registration & handling"), + }; + } + } + #[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/keyfork-crossterm/src/event/source/unix/tty.rs b/keyfork-crossterm/src/event/source/unix/tty.rs new file mode 100644 index 0000000..320a12c --- /dev/null +++ b/keyfork-crossterm/src/event/source/unix/tty.rs @@ -0,0 +1,265 @@ +use std::os::unix::prelude::AsRawFd; +use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; + +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, + 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) -> 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 + pipe::register(libc::SIGWINCH, 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, buf.len()) { + 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 { + let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); + // 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/keyfork-crossterm/src/event/source/windows.rs b/keyfork-crossterm/src/event/source/windows.rs new file mode 100644 index 0000000..33ecffe --- /dev/null +++ b/keyfork-crossterm/src/event/source/windows.rs @@ -0,0 +1,100 @@ +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/keyfork-crossterm/src/event/stream.rs b/keyfork-crossterm/src/event/stream.rs new file mode 100644 index 0000000..e74dcea --- /dev/null +++ b/keyfork-crossterm/src/event/stream.rs @@ -0,0 +1,146 @@ +use std::{ + io, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, SyncSender}, + Arc, + }, + task::{Context, Poll}, + thread, + time::Duration, +}; + +use futures_core::stream::Stream; + +use crate::event::{ + filter::EventFilter, lock_internal_event_reader, poll_internal, read_internal, sys::Waker, + Event, InternalEvent, +}; + +/// A stream of `Result`. +/// +/// **This type is not available by default. You have to use the `event-stream` feature flag +/// to make it available.** +/// +/// It implements the [Stream](futures_core::stream::Stream) +/// trait and allows you to receive [`Event`]s with [`async-std`](https://crates.io/crates/async-std) +/// or [`tokio`](https://crates.io/crates/tokio) crates. +/// +/// Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder to see how to use +/// it (`event-stream-*`). +#[derive(Debug)] +pub struct EventStream { + poll_internal_waker: Waker, + stream_wake_task_executed: Arc, + stream_wake_task_should_shutdown: Arc, + task_sender: SyncSender, +} + +impl Default for EventStream { + fn default() -> Self { + let (task_sender, receiver) = mpsc::sync_channel::(1); + + thread::spawn(move || { + while let Ok(task) = receiver.recv() { + loop { + if let Ok(true) = poll_internal(None, &EventFilter) { + break; + } + + if task.stream_wake_task_should_shutdown.load(Ordering::SeqCst) { + break; + } + } + task.stream_wake_task_executed + .store(false, Ordering::SeqCst); + task.stream_waker.wake(); + } + }); + + EventStream { + poll_internal_waker: lock_internal_event_reader().waker(), + stream_wake_task_executed: Arc::new(AtomicBool::new(false)), + stream_wake_task_should_shutdown: Arc::new(AtomicBool::new(false)), + task_sender, + } + } +} + +impl EventStream { + /// Constructs a new instance of `EventStream`. + pub fn new() -> EventStream { + EventStream::default() + } +} + +struct Task { + stream_waker: std::task::Waker, + stream_wake_task_executed: Arc, + stream_wake_task_should_shutdown: Arc, +} + +// Note to future me +// +// We need two wakers in order to implement EventStream correctly. +// +// 1. futures::Stream waker +// +// Stream::poll_next can return Poll::Pending which means that there's no +// event available. We are going to spawn a thread with the +// poll_internal(None, &EventFilter) call. This call blocks until an +// event is available and then we have to wake up the executor with notification +// that the task can be resumed. +// +// 2. poll_internal waker +// +// There's no event available, Poll::Pending was returned, stream waker thread +// is up and sitting in the poll_internal. User wants to drop the EventStream. +// We have to wake up the poll_internal (force it to return Ok(false)) and quit +// the thread before we drop. +impl Stream for EventStream { + type Item = io::Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let result = match poll_internal(Some(Duration::from_secs(0)), &EventFilter) { + Ok(true) => match read_internal(&EventFilter) { + Ok(InternalEvent::Event(event)) => Poll::Ready(Some(Ok(event))), + Err(e) => Poll::Ready(Some(Err(e))), + #[cfg(unix)] + _ => unreachable!(), + }, + Ok(false) => { + if !self + .stream_wake_task_executed + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + // https://github.com/rust-lang/rust/issues/80486#issuecomment-752244166 + .unwrap_or_else(|x| x) + { + let stream_waker = cx.waker().clone(); + let stream_wake_task_executed = self.stream_wake_task_executed.clone(); + let stream_wake_task_should_shutdown = + self.stream_wake_task_should_shutdown.clone(); + + stream_wake_task_should_shutdown.store(false, Ordering::SeqCst); + + let _ = self.task_sender.send(Task { + stream_waker, + stream_wake_task_executed, + stream_wake_task_should_shutdown, + }); + } + Poll::Pending + } + Err(e) => Poll::Ready(Some(Err(e))), + }; + result + } +} + +impl Drop for EventStream { + fn drop(&mut self) { + self.stream_wake_task_should_shutdown + .store(true, Ordering::SeqCst); + let _ = self.poll_internal_waker.wake(); + } +} diff --git a/keyfork-crossterm/src/event/sys.rs b/keyfork-crossterm/src/event/sys.rs new file mode 100644 index 0000000..bd79307 --- /dev/null +++ b/keyfork-crossterm/src/event/sys.rs @@ -0,0 +1,9 @@ +#[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/keyfork-crossterm/src/event/sys/unix.rs b/keyfork-crossterm/src/event/sys/unix.rs new file mode 100644 index 0000000..2106ca0 --- /dev/null +++ b/keyfork-crossterm/src/event/sys/unix.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "event-stream")] +pub(crate) mod waker; + +#[cfg(feature = "events")] +pub(crate) mod parse; diff --git a/keyfork-crossterm/src/event/sys/unix/parse.rs b/keyfork-crossterm/src/event/sys/unix/parse.rs new file mode 100644 index 0000000..2019b5f --- /dev/null +++ b/keyfork-crossterm/src/event/sys/unix/parse.rs @@ -0,0 +1,1506 @@ +use std::io; + +use crate::event::{ + Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyboardEnhancementFlags, + 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, +// because it's really not maintainable, no tests, etc. +// +// Every fn returns Result> +// +// Ok(None) -> wait for more bytes +// Err(_) -> failed to parse event, clear the buffer +// Ok(Some(event)) -> we have event, clear the buffer +// + +fn could_not_parse_event_error() -> 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> { + if buffer.is_empty() { + return Ok(None); + } + + match buffer[0] { + b'\x1B' => { + if buffer.len() == 1 { + if input_available { + // Possible Esc sequence + Ok(None) + } else { + Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) + } + } else { + match buffer[1] { + b'O' => { + if buffer.len() == 2 { + Ok(None) + } else { + match buffer[2] { + 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())))) + } + // F1-F4 + val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( + KeyCode::F(1 + val - b'P').into(), + )))), + _ => Err(could_not_parse_event_error()), + } + } + } + b'[' => parse_csi(buffer), + b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), + _ => parse_event(&buffer[1..], input_available).map(|event_option| { + event_option.map(|event| { + if let InternalEvent::Event(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)) + } else { + 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( + KeyCode::Char((c - 0x1 + b'a') as char), + KeyModifiers::CONTROL, + ))))), + c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char((c - 0x1C + b'4') as char), + KeyModifiers::CONTROL, + ))))), + b'\0' => Ok(Some(InternalEvent::Event(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) + }), + } +} + +// converts KeyCode to KeyEvent (adds shift modifier in case of uppercase characters) +fn char_code_to_event(code: KeyCode) -> KeyEvent { + let modifiers = match code { + KeyCode::Char(c) if c.is_uppercase() => KeyModifiers::SHIFT, + _ => KeyModifiers::empty(), + }; + KeyEvent::new(code, modifiers) +} + +pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { + assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ + + if buffer.len() == 2 { + return Ok(None); + } + + let input_event = match buffer[2] { + b'[' => { + if buffer.len() == 3 { + None + } else { + match buffer[3] { + // NOTE (@imdaveho): cannot find when this occurs; + // having another '[' after ESC[ not a likely scenario + val @ b'A'..=b'E' => Some(Event::Key(KeyCode::F(1 + val - b'A').into())), + _ => return Err(could_not_parse_event_error()), + } + } + } + b'D' => Some(Event::Key(KeyCode::Left.into())), + b'C' => Some(Event::Key(KeyCode::Right.into())), + b'A' => Some(Event::Key(KeyCode::Up.into())), + b'B' => Some(Event::Key(KeyCode::Down.into())), + b'H' => Some(Event::Key(KeyCode::Home.into())), + b'F' => Some(Event::Key(KeyCode::End.into())), + b'Z' => Some(Event::Key(KeyEvent::new_with_kind( + KeyCode::BackTab, + KeyModifiers::SHIFT, + KeyEventKind::Press, + ))), + b'M' => return parse_csi_normal_mouse(buffer), + b'<' => return parse_csi_sgr_mouse(buffer), + b'I' => Some(Event::FocusGained), + b'O' => Some(Event::FocusLost), + b';' => return parse_csi_modifier_key_code(buffer), + // P, Q, and S for compatibility with Kitty keyboard protocol, + // as the 1 in 'CSI 1 P' etc. must be omitted if there are no + // modifiers pressed: + // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys + b'P' => Some(Event::Key(KeyCode::F(1).into())), + b'Q' => Some(Event::Key(KeyCode::F(2).into())), + b'S' => Some(Event::Key(KeyCode::F(4).into())), + b'?' => match buffer[buffer.len() - 1] { + b'u' => return parse_csi_keyboard_enhancement_flags(buffer), + b'c' => return parse_csi_primary_device_attributes(buffer), + _ => None, + }, + b'0'..=b'9' => { + // Numbered escape code. + if buffer.len() == 3 { + None + } else { + // The final byte of a CSI sequence can be in the range 64-126, so + // let's keep reading anything else. + let last_byte = buffer[buffer.len() - 1]; + if !(64..=126).contains(&last_byte) { + None + } else { + #[cfg(feature = "bracketed-paste")] + if buffer.starts_with(b"\x1B[200~") { + return parse_csi_bracketed_paste(buffer); + } + match last_byte { + b'M' => return parse_csi_rxvt_mouse(buffer), + b'~' => return parse_csi_special_key_code(buffer), + b'u' => return parse_csi_u_encoded_key_code(buffer), + b'R' => return parse_csi_cursor_position(buffer), + _ => return parse_csi_modifier_key_code(buffer), + } + } + } + } + _ => return Err(could_not_parse_event_error()), + }; + + Ok(input_event.map(InternalEvent::Event)) +} + +pub(crate) fn next_parsed(iter: &mut dyn Iterator) -> io::Result +where + T: std::str::FromStr, +{ + iter.next() + .ok_or_else(could_not_parse_event_error)? + .parse::() + .map_err(|_| could_not_parse_event_error()) +} + +fn modifier_and_kind_parsed(iter: &mut dyn Iterator) -> io::Result<(u8, u8)> { + let mut sub_split = iter + .next() + .ok_or_else(could_not_parse_event_error)? + .split(':'); + + let modifier_mask = next_parsed::(&mut sub_split)?; + + if let Ok(kind_code) = next_parsed::(&mut sub_split) { + Ok((modifier_mask, kind_code)) + } else { + Ok((modifier_mask, 1)) + } +} + +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) + assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ + assert!(buffer.ends_with(&[b'R'])); + + let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + + let mut split = s.split(';'); + + let y = next_parsed::(&mut split)? - 1; + let x = next_parsed::(&mut split)? - 1; + + Ok(Some(InternalEvent::CursorPosition(x, y))) +} + +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'])); + + if buffer.len() < 5 { + return Ok(None); + } + + let bits = buffer[3]; + let mut flags = KeyboardEnhancementFlags::empty(); + + if bits & 1 != 0 { + flags |= KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES; + } + if bits & 2 != 0 { + flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES; + } + if bits & 4 != 0 { + flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS; + } + if bits & 8 != 0 { + flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; + } + // *Note*: this is not yet supported by crossterm. + // if bits & 16 != 0 { + // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; + // } + + Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags))) +} + +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'])); + + // This is a stub for parsing the primary device attributes. This response is not + // exposed in the crossterm API so we don't need to parse the individual attributes yet. + // See + + Ok(Some(InternalEvent::PrimaryDeviceAttributes)) +} + +fn parse_modifiers(mask: u8) -> KeyModifiers { + let modifier_mask = mask.saturating_sub(1); + let mut modifiers = KeyModifiers::empty(); + if modifier_mask & 1 != 0 { + modifiers |= KeyModifiers::SHIFT; + } + if modifier_mask & 2 != 0 { + modifiers |= KeyModifiers::ALT; + } + if modifier_mask & 4 != 0 { + modifiers |= KeyModifiers::CONTROL; + } + if modifier_mask & 8 != 0 { + modifiers |= KeyModifiers::SUPER; + } + if modifier_mask & 16 != 0 { + modifiers |= KeyModifiers::HYPER; + } + if modifier_mask & 32 != 0 { + modifiers |= KeyModifiers::META; + } + modifiers +} + +fn parse_modifiers_to_state(mask: u8) -> KeyEventState { + let modifier_mask = mask.saturating_sub(1); + let mut state = KeyEventState::empty(); + if modifier_mask & 64 != 0 { + state |= KeyEventState::CAPS_LOCK; + } + if modifier_mask & 128 != 0 { + state |= KeyEventState::NUM_LOCK; + } + state +} + +fn parse_key_event_kind(kind: u8) -> KeyEventKind { + match kind { + 1 => KeyEventKind::Press, + 2 => KeyEventKind::Repeat, + 3 => KeyEventKind::Release, + _ => KeyEventKind::Press, + } +} + +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]) + .map_err(|_| could_not_parse_event_error())?; + let mut split = s.split(';'); + + split.next(); + + let (modifiers, kind) = + if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { + ( + parse_modifiers(modifier_mask), + parse_key_event_kind(kind_code), + ) + } else if buffer.len() > 3 { + ( + parse_modifiers( + (buffer[buffer.len() - 2] as char) + .to_digit(10) + .ok_or_else(could_not_parse_event_error)? as u8, + ), + KeyEventKind::Press, + ) + } else { + (KeyModifiers::NONE, KeyEventKind::Press) + }; + let key = buffer[buffer.len() - 1]; + + let keycode = match key { + b'A' => KeyCode::Up, + b'B' => KeyCode::Down, + b'C' => KeyCode::Right, + b'D' => KeyCode::Left, + b'F' => KeyCode::End, + b'H' => KeyCode::Home, + b'P' => KeyCode::F(1), + b'Q' => KeyCode::F(2), + b'R' => KeyCode::F(3), + b'S' => KeyCode::F(4), + _ => return Err(could_not_parse_event_error()), + }; + + let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind)); + + Ok(Some(InternalEvent::Event(input_event))) +} + +fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> { + if let Some(keycode) = match codepoint { + 57399 => Some(KeyCode::Char('0')), + 57400 => Some(KeyCode::Char('1')), + 57401 => Some(KeyCode::Char('2')), + 57402 => Some(KeyCode::Char('3')), + 57403 => Some(KeyCode::Char('4')), + 57404 => Some(KeyCode::Char('5')), + 57405 => Some(KeyCode::Char('6')), + 57406 => Some(KeyCode::Char('7')), + 57407 => Some(KeyCode::Char('8')), + 57408 => Some(KeyCode::Char('9')), + 57409 => Some(KeyCode::Char('.')), + 57410 => Some(KeyCode::Char('/')), + 57411 => Some(KeyCode::Char('*')), + 57412 => Some(KeyCode::Char('-')), + 57413 => Some(KeyCode::Char('+')), + 57414 => Some(KeyCode::Enter), + 57415 => Some(KeyCode::Char('=')), + 57416 => Some(KeyCode::Char(',')), + 57417 => Some(KeyCode::Left), + 57418 => Some(KeyCode::Right), + 57419 => Some(KeyCode::Up), + 57420 => Some(KeyCode::Down), + 57421 => Some(KeyCode::PageUp), + 57422 => Some(KeyCode::PageDown), + 57423 => Some(KeyCode::Home), + 57424 => Some(KeyCode::End), + 57425 => Some(KeyCode::Insert), + 57426 => Some(KeyCode::Delete), + 57427 => Some(KeyCode::KeypadBegin), + _ => None, + } { + return Some((keycode, KeyEventState::KEYPAD)); + } + + if let Some(keycode) = match codepoint { + 57358 => Some(KeyCode::CapsLock), + 57359 => Some(KeyCode::ScrollLock), + 57360 => Some(KeyCode::NumLock), + 57361 => Some(KeyCode::PrintScreen), + 57362 => Some(KeyCode::Pause), + 57363 => Some(KeyCode::Menu), + 57376 => Some(KeyCode::F(13)), + 57377 => Some(KeyCode::F(14)), + 57378 => Some(KeyCode::F(15)), + 57379 => Some(KeyCode::F(16)), + 57380 => Some(KeyCode::F(17)), + 57381 => Some(KeyCode::F(18)), + 57382 => Some(KeyCode::F(19)), + 57383 => Some(KeyCode::F(20)), + 57384 => Some(KeyCode::F(21)), + 57385 => Some(KeyCode::F(22)), + 57386 => Some(KeyCode::F(23)), + 57387 => Some(KeyCode::F(24)), + 57388 => Some(KeyCode::F(25)), + 57389 => Some(KeyCode::F(26)), + 57390 => Some(KeyCode::F(27)), + 57391 => Some(KeyCode::F(28)), + 57392 => Some(KeyCode::F(29)), + 57393 => Some(KeyCode::F(30)), + 57394 => Some(KeyCode::F(31)), + 57395 => Some(KeyCode::F(32)), + 57396 => Some(KeyCode::F(33)), + 57397 => Some(KeyCode::F(34)), + 57398 => Some(KeyCode::F(35)), + 57428 => Some(KeyCode::Media(MediaKeyCode::Play)), + 57429 => Some(KeyCode::Media(MediaKeyCode::Pause)), + 57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)), + 57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)), + 57432 => Some(KeyCode::Media(MediaKeyCode::Stop)), + 57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)), + 57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)), + 57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)), + 57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)), + 57437 => Some(KeyCode::Media(MediaKeyCode::Record)), + 57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)), + 57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)), + 57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)), + 57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)), + 57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)), + 57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)), + 57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)), + 57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)), + 57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)), + 57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)), + 57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)), + 57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)), + 57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)), + 57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)), + 57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)), + 57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)), + 57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)), + _ => None, + } { + return Some((keycode, KeyEventState::empty())); + } + + None +} + +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'])); + + // This function parses `CSI … u` sequences. These are sequences defined in either + // the `CSI u` (a.k.a. "Fix Keyboard Input on Terminals - Please", https://www.leonerd.org.uk/hacks/fixterms/) + // or Kitty Keyboard Protocol (https://sw.kovidgoyal.net/kitty/keyboard-protocol/) specifications. + // This CSI sequence is a tuple of semicolon-separated numbers. + let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + let mut split = s.split(';'); + + // In `CSI u`, this is parsed as: + // + // CSI codepoint ; modifiers u + // codepoint: ASCII Dec value + // + // The Kitty Keyboard Protocol extends this with optional components that can be + // enabled progressively. The full sequence is parsed as: + // + // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u + let mut codepoints = split + .next() + .ok_or_else(could_not_parse_event_error)? + .split(':'); + + let codepoint = codepoints + .next() + .ok_or_else(could_not_parse_event_error)? + .parse::() + .map_err(|_| could_not_parse_event_error())?; + + let (mut modifiers, kind, state_from_modifiers) = + if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { + ( + parse_modifiers(modifier_mask), + parse_key_event_kind(kind_code), + parse_modifiers_to_state(modifier_mask), + ) + } else { + (KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) + }; + + let (mut keycode, state_from_keycode) = { + if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) { + (special_key_code, state) + } else if let Some(c) = char::from_u32(codepoint) { + ( + match c { + '\x1B' => KeyCode::Esc, + '\r' => KeyCode::Enter, + // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get + // newlines as input is because the terminal converts \r into \n for us. When we + // enter raw mode, we disable that, so \n no longer has any meaning - it's better to + // use Ctrl+J. Waiting to handle it here means it gets picked up later + '\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter, + '\t' => { + if modifiers.contains(KeyModifiers::SHIFT) { + KeyCode::BackTab + } else { + KeyCode::Tab + } + } + '\x7F' => KeyCode::Backspace, + _ => KeyCode::Char(c), + }, + KeyEventState::empty(), + ) + } else { + return Err(could_not_parse_event_error()); + } + }; + + if let KeyCode::Modifier(modifier_keycode) = keycode { + match modifier_keycode { + ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => { + modifiers.set(KeyModifiers::ALT, true) + } + ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => { + modifiers.set(KeyModifiers::CONTROL, true) + } + ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => { + modifiers.set(KeyModifiers::SHIFT, true) + } + ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => { + modifiers.set(KeyModifiers::SUPER, true) + } + ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => { + modifiers.set(KeyModifiers::HYPER, true) + } + ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => { + modifiers.set(KeyModifiers::META, true) + } + _ => {} + } + } + + // When the "report alternate keys" flag is enabled in the Kitty Keyboard Protocol + // and the terminal sends a keyboard event containing shift, the sequence will + // contain an additional codepoint separated by a ':' character which contains + // the shifted character according to the keyboard layout. + if modifiers.contains(KeyModifiers::SHIFT) { + if let Some(shifted_c) = codepoints + .next() + .and_then(|codepoint| codepoint.parse::().ok()) + .and_then(char::from_u32) + { + keycode = KeyCode::Char(shifted_c); + modifiers.set(KeyModifiers::SHIFT, false); + } + } + + let input_event = Event::Key(KeyEvent::new_with_kind_and_state( + keycode, + modifiers, + kind, + state_from_keycode | state_from_modifiers, + )); + + Ok(Some(InternalEvent::Event(input_event))) +} + +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'~'])); + + let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + let mut split = s.split(';'); + + // This CSI sequence can be a list of semicolon-separated numbers. + let first = next_parsed::(&mut split)?; + + let (modifiers, kind, state) = + if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { + ( + parse_modifiers(modifier_mask), + parse_key_event_kind(kind_code), + parse_modifiers_to_state(modifier_mask), + ) + } else { + (KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) + }; + + let keycode = match first { + 1 | 7 => KeyCode::Home, + 2 => KeyCode::Insert, + 3 => KeyCode::Delete, + 4 | 8 => KeyCode::End, + 5 => KeyCode::PageUp, + 6 => KeyCode::PageDown, + v @ 11..=15 => KeyCode::F(v - 10), + v @ 17..=21 => KeyCode::F(v - 11), + v @ 23..=26 => KeyCode::F(v - 12), + v @ 28..=29 => KeyCode::F(v - 15), + v @ 31..=34 => KeyCode::F(v - 17), + _ => return Err(could_not_parse_event_error()), + }; + + let input_event = Event::Key(KeyEvent::new_with_kind_and_state( + keycode, modifiers, kind, state, + )); + + Ok(Some(InternalEvent::Event(input_event))) +} + +pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result> { + // rxvt mouse encoding: + // ESC [ Cb ; Cx ; Cy ; M + + assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ + assert!(buffer.ends_with(&[b'M'])); + + let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + let mut split = s.split(';'); + + let cb = next_parsed::(&mut split)? + .checked_sub(32) + .ok_or_else(could_not_parse_event_error)?; + let (kind, modifiers) = parse_cb(cb)?; + + let cx = next_parsed::(&mut split)? - 1; + let cy = next_parsed::(&mut split)? - 1; + + Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind, + column: cx, + row: cy, + modifiers, + })))) +} + +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 + + if buffer.len() < 6 { + return Ok(None); + } + + let cb = buffer[3] + .checked_sub(32) + .ok_or_else(could_not_parse_event_error)?; + let (kind, modifiers) = parse_cb(cb)?; + + // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking + // The upper left character position on the terminal is denoted as 1,1. + // Subtract 1 to keep it synced with cursor + let cx = u16::from(buffer[4].saturating_sub(32)) - 1; + let cy = u16::from(buffer[5].saturating_sub(32)) - 1; + + Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind, + column: cx, + row: cy, + modifiers, + })))) +} + +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 [ < + + if !buffer.ends_with(&[b'm']) && !buffer.ends_with(&[b'M']) { + return Ok(None); + } + + let s = std::str::from_utf8(&buffer[3..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + let mut split = s.split(';'); + + let cb = next_parsed::(&mut split)?; + let (kind, modifiers) = parse_cb(cb)?; + + // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking + // The upper left character position on the terminal is denoted as 1,1. + // Subtract 1 to keep it synced with cursor + let cx = next_parsed::(&mut split)? - 1; + let cy = next_parsed::(&mut split)? - 1; + + // When button 3 in Cb is used to represent mouse release, you can't tell which button was + // released. SGR mode solves this by having the sequence end with a lowercase m if it's a + // button release and an uppercase M if it's a button press. + // + // We've already checked that the last character is a lowercase or uppercase M at the start of + // this function, so we just need one if. + let kind = if buffer.last() == Some(&b'm') { + match kind { + MouseEventKind::Down(button) => MouseEventKind::Up(button), + other => other, + } + } else { + kind + }; + + Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind, + column: cx, + row: cy, + modifiers, + })))) +} + +/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being +/// held and whether the mouse is dragging or not. +/// +/// Bit layout of cb, from low to high: +/// +/// - button number +/// - button number +/// - shift +/// - meta (alt) +/// - control +/// - mouse is dragging +/// - button number +/// - button number +fn parse_cb(cb: u8) -> io::Result<(MouseEventKind, KeyModifiers)> { + let button_number = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4); + let dragging = cb & 0b0010_0000 == 0b0010_0000; + + let kind = match (button_number, dragging) { + (0, false) => MouseEventKind::Down(MouseButton::Left), + (1, false) => MouseEventKind::Down(MouseButton::Middle), + (2, false) => MouseEventKind::Down(MouseButton::Right), + (0, true) => MouseEventKind::Drag(MouseButton::Left), + (1, true) => MouseEventKind::Drag(MouseButton::Middle), + (2, true) => MouseEventKind::Drag(MouseButton::Right), + (3, false) => MouseEventKind::Up(MouseButton::Left), + (3, true) | (4, true) | (5, true) => MouseEventKind::Moved, + (4, false) => MouseEventKind::ScrollUp, + (5, false) => MouseEventKind::ScrollDown, + (6, false) => MouseEventKind::ScrollLeft, + (7, false) => MouseEventKind::ScrollRight, + // We do not support other buttons. + _ => return Err(could_not_parse_event_error()), + }; + + let mut modifiers = KeyModifiers::empty(); + + if cb & 0b0000_0100 == 0b0000_0100 { + modifiers |= KeyModifiers::SHIFT; + } + if cb & 0b0000_1000 == 0b0000_1000 { + modifiers |= KeyModifiers::ALT; + } + if cb & 0b0001_0000 == 0b0001_0000 { + modifiers |= KeyModifiers::CONTROL; + } + + Ok((kind, modifiers)) +} + +#[cfg(feature = "bracketed-paste")] +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~")); + + if !buffer.ends_with(b"\x1b[201~") { + Ok(None) + } else { + let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string(); + Ok(Some(InternalEvent::Event(Event::Paste(paste)))) + } +} + +pub(crate) fn parse_utf8_char(buffer: &[u8]) -> io::Result> { + match std::str::from_utf8(buffer) { + Ok(s) => { + let ch = s.chars().next().ok_or_else(could_not_parse_event_error)?; + + Ok(Some(ch)) + } + Err(_) => { + // from_utf8 failed, but we have to check if we need more bytes for code point + // and if all the bytes we have no are valid + + let required_bytes = match buffer[0] { + // https://en.wikipedia.org/wiki/UTF-8#Description + (0x00..=0x7F) => 1, // 0xxxxxxx + (0xC0..=0xDF) => 2, // 110xxxxx 10xxxxxx + (0xE0..=0xEF) => 3, // 1110xxxx 10xxxxxx 10xxxxxx + (0xF0..=0xF7) => 4, // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + (0x80..=0xBF) | (0xF8..=0xFF) => return Err(could_not_parse_event_error()), + }; + + // More than 1 byte, check them for 10xxxxxx pattern + if required_bytes > 1 && buffer.len() > 1 { + for byte in &buffer[1..] { + if byte & !0b0011_1111 != 0b1000_0000 { + return Err(could_not_parse_event_error()); + } + } + } + + if buffer.len() < required_bytes { + // All bytes looks good so far, but we need more of them + Ok(None) + } else { + Err(could_not_parse_event_error()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::event::{KeyEventState, KeyModifiers, MouseButton, MouseEvent}; + + use super::*; + + #[test] + fn test_esc_key() { + assert_eq!( + parse_event(b"\x1B", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))), + ); + } + + #[test] + fn test_possible_esc_sequence() { + assert_eq!(parse_event(b"\x1B", true).unwrap(), None,); + } + + #[test] + fn test_alt_key() { + assert_eq!( + parse_event(b"\x1Bc", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('c'), + KeyModifiers::ALT + )))), + ); + } + + #[test] + fn test_alt_shift() { + assert_eq!( + parse_event(b"\x1BH", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('H'), + KeyModifiers::ALT | KeyModifiers::SHIFT + )))), + ); + } + + #[test] + fn test_alt_ctrl() { + assert_eq!( + parse_event(b"\x1B\x14", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('t'), + KeyModifiers::ALT | KeyModifiers::CONTROL + )))), + ); + } + + #[test] + fn test_parse_event_subsequent_calls() { + // The main purpose of this test is to check if we're passing + // correct slice to other parse_ functions. + + // parse_csi_cursor_position + assert_eq!( + parse_event(b"\x1B[20;10R", false).unwrap(), + Some(InternalEvent::CursorPosition(9, 19)) + ); + + // parse_csi + assert_eq!( + parse_event(b"\x1B[D", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), + ); + + // parse_csi_modifier_key_code + assert_eq!( + parse_event(b"\x1B[2D", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Left, + KeyModifiers::SHIFT + )))) + ); + + // parse_csi_special_key_code + assert_eq!( + parse_event(b"\x1B[3~", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), + ); + + // parse_csi_bracketed_paste + #[cfg(feature = "bracketed-paste")] + assert_eq!( + parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(), + Some(InternalEvent::Event(Event::Paste( + "on and on and on".to_string() + ))), + ); + + // parse_csi_rxvt_mouse + assert_eq!( + parse_event(b"\x1B[32;30;40;M", false).unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 29, + row: 39, + modifiers: KeyModifiers::empty(), + }))) + ); + + // parse_csi_normal_mouse + assert_eq!( + parse_event(b"\x1B[M0\x60\x70", false).unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 63, + row: 79, + modifiers: KeyModifiers::CONTROL, + }))) + ); + + // parse_csi_sgr_mouse + assert_eq!( + parse_event(b"\x1B[<0;20;10;M", false).unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 19, + row: 9, + modifiers: KeyModifiers::empty(), + }))) + ); + + // parse_utf8_char + assert_eq!( + parse_event("Ž".as_bytes(), false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('Ž'), + KeyModifiers::SHIFT + )))), + ); + } + + #[test] + fn test_parse_event() { + assert_eq!( + parse_event(b"\t", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))), + ); + } + + #[test] + fn test_parse_csi_cursor_position() { + assert_eq!( + parse_csi_cursor_position(b"\x1B[20;10R").unwrap(), + Some(InternalEvent::CursorPosition(9, 19)) + ); + } + + #[test] + fn test_parse_csi() { + assert_eq!( + parse_csi(b"\x1B[D").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), + ); + } + + #[test] + fn test_parse_csi_modifier_key_code() { + assert_eq!( + parse_csi_modifier_key_code(b"\x1B[2D").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Left, + KeyModifiers::SHIFT + )))), + ); + } + + #[test] + fn test_parse_csi_special_key_code() { + assert_eq!( + parse_csi_special_key_code(b"\x1B[3~").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), + ); + } + + #[test] + fn test_parse_csi_special_key_code_multiple_values_not_supported() { + assert_eq!( + parse_csi_special_key_code(b"\x1B[3;2~").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Delete, + KeyModifiers::SHIFT + )))), + ); + } + + #[cfg(feature = "bracketed-paste")] + #[test] + fn test_parse_csi_bracketed_paste() { + // + assert_eq!( + parse_event(b"\x1B[200~o", false).unwrap(), + None, + "A partial bracketed paste isn't parsed" + ); + assert_eq!( + parse_event(b"\x1B[200~o\x1B[2D", false).unwrap(), + None, + "A partial bracketed paste containing another escape code isn't parsed" + ); + assert_eq!( + parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(), + Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string()))) + ); + } + + #[test] + fn test_parse_csi_focus() { + assert_eq!( + parse_csi(b"\x1B[O").unwrap(), + Some(InternalEvent::Event(Event::FocusLost)) + ); + } + + #[test] + fn test_parse_csi_rxvt_mouse() { + assert_eq!( + parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 29, + row: 39, + modifiers: KeyModifiers::empty(), + }))) + ); + } + + #[test] + fn test_parse_csi_normal_mouse() { + assert_eq!( + parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 63, + row: 79, + modifiers: KeyModifiers::CONTROL, + }))) + ); + } + + #[test] + fn test_parse_csi_sgr_mouse() { + assert_eq!( + parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 19, + row: 9, + modifiers: KeyModifiers::empty(), + }))) + ); + assert_eq!( + parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 19, + row: 9, + modifiers: KeyModifiers::empty(), + }))) + ); + assert_eq!( + parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + column: 19, + row: 9, + modifiers: KeyModifiers::empty(), + }))) + ); + assert_eq!( + parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(), + Some(InternalEvent::Event(Event::Mouse(MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + column: 19, + row: 9, + modifiers: KeyModifiers::empty(), + }))) + ); + } + + #[test] + fn test_utf8() { + // https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php#54805 + + // 'Valid ASCII' => "a", + assert_eq!(parse_utf8_char(b"a").unwrap(), Some('a'),); + + // 'Valid 2 Octet Sequence' => "\xc3\xb1", + assert_eq!(parse_utf8_char(&[0xC3, 0xB1]).unwrap(), Some('ñ'),); + + // 'Invalid 2 Octet Sequence' => "\xc3\x28", + assert!(parse_utf8_char(&[0xC3, 0x28]).is_err()); + + // 'Invalid Sequence Identifier' => "\xa0\xa1", + assert!(parse_utf8_char(&[0xA0, 0xA1]).is_err()); + + // 'Valid 3 Octet Sequence' => "\xe2\x82\xa1", + assert_eq!( + parse_utf8_char(&[0xE2, 0x81, 0xA1]).unwrap(), + Some('\u{2061}'), + ); + + // 'Invalid 3 Octet Sequence (in 2nd Octet)' => "\xe2\x28\xa1", + assert!(parse_utf8_char(&[0xE2, 0x28, 0xA1]).is_err()); + + // 'Invalid 3 Octet Sequence (in 3rd Octet)' => "\xe2\x82\x28", + assert!(parse_utf8_char(&[0xE2, 0x82, 0x28]).is_err()); + + // 'Valid 4 Octet Sequence' => "\xf0\x90\x8c\xbc", + assert_eq!( + parse_utf8_char(&[0xF0, 0x90, 0x8C, 0xBC]).unwrap(), + Some('𐌼'), + ); + + // 'Invalid 4 Octet Sequence (in 2nd Octet)' => "\xf0\x28\x8c\xbc", + assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0xBC]).is_err()); + + // 'Invalid 4 Octet Sequence (in 3rd Octet)' => "\xf0\x90\x28\xbc", + assert!(parse_utf8_char(&[0xF0, 0x90, 0x28, 0xBC]).is_err()); + + // 'Invalid 4 Octet Sequence (in 4th Octet)' => "\xf0\x28\x8c\x28", + assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0x28]).is_err()); + } + + #[test] + fn test_parse_char_event_lowercase() { + assert_eq!( + parse_event(b"c", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('c'), + KeyModifiers::empty() + )))), + ); + } + + #[test] + fn test_parse_char_event_uppercase() { + assert_eq!( + parse_event(b"C", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('C'), + KeyModifiers::SHIFT + )))), + ); + } + + #[test] + fn test_parse_basic_csi_u_encoded_key_code() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('A'), + KeyModifiers::SHIFT + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::ALT | KeyModifiers::CONTROL + )))), + ); + } + + #[test] + fn test_parse_basic_csi_u_encoded_key_code_special_keys() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Enter, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Esc, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::CapsLock, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::F(13), + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Media(MediaKeyCode::Play), + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Modifier(ModifierKeyCode::LeftShift), + KeyModifiers::SHIFT, + )))), + ); + } + + #[test] + fn test_parse_csi_u_encoded_keypad_code() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(), + Some(InternalEvent::Event(Event::Key( + KeyEvent::new_with_kind_and_state( + KeyCode::Char('0'), + KeyModifiers::empty(), + KeyEventKind::Press, + KeyEventState::KEYPAD, + ) + ))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(), + Some(InternalEvent::Event(Event::Key( + KeyEvent::new_with_kind_and_state( + KeyCode::Up, + KeyModifiers::empty(), + KeyEventKind::Press, + KeyEventState::KEYPAD, + ) + ))), + ); + } + + #[test] + fn test_parse_csi_u_encoded_key_code_with_types() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::empty(), + KeyEventKind::Press, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::empty(), + KeyEventKind::Press, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::CONTROL, + KeyEventKind::Press, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::empty(), + KeyEventKind::Repeat, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Char('a'), + KeyModifiers::empty(), + KeyEventKind::Release, + )))), + ); + } + + #[test] + fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Modifier(ModifierKeyCode::RightAlt), + KeyModifiers::ALT, + KeyEventKind::Press, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Modifier(ModifierKeyCode::RightAlt), + KeyModifiers::ALT, + KeyEventKind::Release, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Modifier(ModifierKeyCode::RightSuper), + KeyModifiers::SUPER, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Modifier(ModifierKeyCode::RightHyper), + KeyModifiers::HYPER, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Modifier(ModifierKeyCode::RightMeta), + KeyModifiers::META, + )))), + ); + } + + #[test] + fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::SUPER + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::HYPER, + )))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::META, + )))), + ); + } + + #[test] + fn test_parse_csi_u_encoded_key_code_with_extra_state() { + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(), + Some(InternalEvent::Event(Event::Key( + KeyEvent::new_with_kind_and_state( + KeyCode::Char('a'), + KeyModifiers::empty(), + KeyEventKind::Press, + KeyEventState::CAPS_LOCK, + ) + ))), + ); + assert_eq!( + parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(), + Some(InternalEvent::Event(Event::Key( + KeyEvent::new_with_kind_and_state( + KeyCode::Char('1'), + KeyModifiers::empty(), + KeyEventKind::Press, + KeyEventState::NUM_LOCK, + ) + ))), + ); + } + + #[test] + fn test_parse_csi_u_with_shifted_keycode() { + assert_eq!( + // A-S-9 is equivalent to A-( + parse_event(b"\x1B[57:40;4u", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('('), + KeyModifiers::ALT, + )))), + ); + assert_eq!( + // A-S-minus is equivalent to A-_ + parse_event(b"\x1B[45:95;4u", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Char('_'), + KeyModifiers::ALT, + )))), + ); + } + + #[test] + fn test_parse_csi_special_key_code_with_types() { + assert_eq!( + parse_event(b"\x1B[;1:3B", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Down, + KeyModifiers::empty(), + KeyEventKind::Release, + )))), + ); + assert_eq!( + parse_event(b"\x1B[1;1:3B", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::Down, + KeyModifiers::empty(), + KeyEventKind::Release, + )))), + ); + } + + #[test] + fn test_parse_csi_numbered_escape_code_with_types() { + assert_eq!( + parse_event(b"\x1B[5;1:3~", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::PageUp, + KeyModifiers::empty(), + KeyEventKind::Release, + )))), + ); + assert_eq!( + parse_event(b"\x1B[6;5:3~", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( + KeyCode::PageDown, + KeyModifiers::CONTROL, + KeyEventKind::Release, + )))), + ); + } +} diff --git a/keyfork-crossterm/src/event/sys/unix/waker.rs b/keyfork-crossterm/src/event/sys/unix/waker.rs new file mode 100644 index 0000000..1c55f3b --- /dev/null +++ b/keyfork-crossterm/src/event/sys/unix/waker.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "use-dev-tty")] +pub(crate) mod tty; + +#[cfg(not(feature = "use-dev-tty"))] +pub(crate) mod mio; + +#[cfg(feature = "use-dev-tty")] +pub(crate) use self::tty::Waker; + +#[cfg(not(feature = "use-dev-tty"))] +pub(crate) use self::mio::Waker; diff --git a/keyfork-crossterm/src/event/sys/unix/waker/mio.rs b/keyfork-crossterm/src/event/sys/unix/waker/mio.rs new file mode 100644 index 0000000..025db73 --- /dev/null +++ b/keyfork-crossterm/src/event/sys/unix/waker/mio.rs @@ -0,0 +1,34 @@ +use std::sync::{Arc, Mutex}; + +use ::mio::{Registry, Token}; + +/// Allows to wake up the `mio::Poll::poll()` method. +/// This type wraps `mio::Waker`, for more information see its documentation. +#[derive(Clone, Debug)] +pub(crate) struct Waker { + inner: Arc>, +} + +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/keyfork-crossterm/src/event/sys/unix/waker/tty.rs b/keyfork-crossterm/src/event/sys/unix/waker/tty.rs new file mode 100644 index 0000000..249d406 --- /dev/null +++ b/keyfork-crossterm/src/event/sys/unix/waker/tty.rs @@ -0,0 +1,28 @@ +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/keyfork-crossterm/src/event/sys/windows.rs b/keyfork-crossterm/src/event/sys/windows.rs new file mode 100644 index 0000000..d405ae8 --- /dev/null +++ b/keyfork-crossterm/src/event/sys/windows.rs @@ -0,0 +1,48 @@ +//! 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/keyfork-crossterm/src/event/sys/windows/parse.rs b/keyfork-crossterm/src/event/sys/windows/parse.rs new file mode 100644 index 0000000..97677ec --- /dev/null +++ b/keyfork-crossterm/src/event/sys/windows/parse.rs @@ -0,0 +1,378 @@ +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/keyfork-crossterm/src/event/sys/windows/poll.rs b/keyfork-crossterm/src/event/sys/windows/poll.rs new file mode 100644 index 0000000..84d9bf7 --- /dev/null +++ b/keyfork-crossterm/src/event/sys/windows/poll.rs @@ -0,0 +1,86 @@ +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/keyfork-crossterm/src/event/sys/windows/waker.rs b/keyfork-crossterm/src/event/sys/windows/waker.rs new file mode 100644 index 0000000..9ec582b --- /dev/null +++ b/keyfork-crossterm/src/event/sys/windows/waker.rs @@ -0,0 +1,40 @@ +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/keyfork-crossterm/src/event/timeout.rs b/keyfork-crossterm/src/event/timeout.rs new file mode 100644 index 0000000..f266d28 --- /dev/null +++ b/keyfork-crossterm/src/event/timeout.rs @@ -0,0 +1,92 @@ +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/keyfork-crossterm/src/lib.rs b/keyfork-crossterm/src/lib.rs new file mode 100644 index 0000000..0217bef --- /dev/null +++ b/keyfork-crossterm/src/lib.rs @@ -0,0 +1,260 @@ +#![deny(unused_imports, unused_must_use)] + +//! # Cross-platform Terminal Manipulation Library +//! +//! Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces. +//! +//! This crate supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested +//! see [Tested Terminals](https://github.com/crossterm-rs/crossterm#tested-terminals) +//! for more info). +//! +//! ## Command API +//! +//! The command API makes the use of `crossterm` much easier and offers more control over when and how a +//! command is executed. A command is just an action you can perform on the terminal e.g. cursor movement. +//! +//! The command API offers: +//! +//! * Better Performance. +//! * Complete control over when to flush. +//! * Complete control over where the ANSI escape commands are executed to. +//! * Way easier and nicer API. +//! +//! There are two ways to use the API command: +//! +//! * Functions can execute commands on types that implement Write. Functions are easier to use and debug. +//! There is a disadvantage, and that is that there is a boilerplate code involved. +//! * Macros are generally seen as more difficult and aren't always well supported by editors but offer an API with less boilerplate code. If you are +//! not afraid of macros, this is a recommendation. +//! +//! Linux and Windows 10 systems support ANSI escape codes. Those ANSI escape codes are strings or rather a +//! byte sequence. When we `write` and `flush` those to the terminal we can perform some action. +//! For older windows systems a WinAPI call is made. +//! +//! ### Supported Commands +//! +//! - Module [`cursor`](cursor/index.html) +//! - Visibility - [`Show`](cursor/struct.Show.html), [`Hide`](cursor/struct.Hide.html) +//! - Appearance - [`EnableBlinking`](cursor/struct.EnableBlinking.html), +//! [`DisableBlinking`](cursor/struct.DisableBlinking.html), +//! [`SetCursorStyle`](cursor/enum.SetCursorStyle.html) +//! - Position - +//! [`SavePosition`](cursor/struct.SavePosition.html), [`RestorePosition`](cursor/struct.RestorePosition.html), +//! [`MoveUp`](cursor/struct.MoveUp.html), [`MoveDown`](cursor/struct.MoveDown.html), +//! [`MoveLeft`](cursor/struct.MoveLeft.html), [`MoveRight`](cursor/struct.MoveRight.html), +//! [`MoveTo`](cursor/struct.MoveTo.html), [`MoveToColumn`](cursor/struct.MoveToColumn.html),[`MoveToRow`](cursor/struct.MoveToRow.html), +//! [`MoveToNextLine`](cursor/struct.MoveToNextLine.html), [`MoveToPreviousLine`](cursor/struct.MoveToPreviousLine.html) +//! - Module [`event`](event/index.html) +//! - Keyboard events - +//! [`PushKeyboardEnhancementFlags`](event/struct.PushKeyboardEnhancementFlags.html), +//! [`PopKeyboardEnhancementFlags`](event/struct.PopKeyboardEnhancementFlags.html) +//! - Mouse events - [`EnableMouseCapture`](event/struct.EnableMouseCapture.html), +//! [`DisableMouseCapture`](event/struct.DisableMouseCapture.html) +//! - Module [`style`](style/index.html) +//! - Colors - [`SetForegroundColor`](style/struct.SetForegroundColor.html), +//! [`SetBackgroundColor`](style/struct.SetBackgroundColor.html), +//! [`ResetColor`](style/struct.ResetColor.html), [`SetColors`](style/struct.SetColors.html) +//! - Attributes - [`SetAttribute`](style/struct.SetAttribute.html), [`SetAttributes`](style/struct.SetAttributes.html), +//! [`PrintStyledContent`](style/struct.PrintStyledContent.html) +//! - Module [`terminal`](terminal/index.html) +//! - Scrolling - [`ScrollUp`](terminal/struct.ScrollUp.html), +//! [`ScrollDown`](terminal/struct.ScrollDown.html) +//! - Miscellaneous - [`Clear`](terminal/struct.Clear.html), +//! [`SetSize`](terminal/struct.SetSize.html), +//! [`SetTitle`](terminal/struct.SetTitle.html), +//! [`DisableLineWrap`](terminal/struct.DisableLineWrap.html), +//! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html) +//! - Alternate screen - [`EnterAlternateScreen`](terminal/struct.EnterAlternateScreen.html), +//! [`LeaveAlternateScreen`](terminal/struct.LeaveAlternateScreen.html) +//! +//! ### Command Execution +//! +//! There are two different ways to execute commands: +//! +//! * [Lazy Execution](#lazy-execution) +//! * [Direct Execution](#direct-execution) +//! +//! #### Lazy Execution +//! +//! Flushing bytes to the terminal buffer is a heavy system call. If we perform a lot of actions with the terminal, +//! we want to do this periodically - like with a TUI editor - so that we can flush more data to the terminal buffer +//! at the same time. +//! +//! Crossterm offers the possibility to do this with `queue`. +//! With `queue` you can queue commands, and when you call [Write::flush][flush] these commands will be executed. +//! +//! You can pass a custom buffer implementing [std::io::Write][write] to this `queue` operation. +//! The commands will be executed on that buffer. +//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. +//! +//! ##### Examples +//! +//! A simple demonstration that shows the command API in action with cursor commands. +//! +//! Functions: +//! +//! ```no_run +//! use std::io::{Write, stdout}; +//! use crossterm::{QueueableCommand, cursor}; +//! +//! let mut stdout = stdout(); +//! stdout.queue(cursor::MoveTo(5,5)); +//! +//! // some other code ... +//! +//! stdout.flush(); +//! ``` +//! +//! The [queue](./trait.QueueableCommand.html) function returns itself, therefore you can use this to queue another +//! command. Like `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`. +//! +//! Macros: +//! +//! ```no_run +//! use std::io::{Write, stdout}; +//! use crossterm::{queue, QueueableCommand, cursor}; +//! +//! let mut stdout = stdout(); +//! queue!(stdout, cursor::MoveTo(5, 5)); +//! +//! // some other code ... +//! +//! // move operation is performed only if we flush the buffer. +//! stdout.flush(); +//! ``` +//! +//! You can pass more than one command into the [queue](./macro.queue.html) macro like +//! `queue!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and +//! they will be executed in the given order from left to right. +//! +//! #### Direct Execution +//! +//! For many applications it is not at all important to be efficient with 'flush' operations. +//! For this use case there is the `execute` operation. +//! This operation executes the command immediately, and calls the `flush` under water. +//! +//! You can pass a custom buffer implementing [std::io::Write][write] to this `execute` operation. +//! The commands will be executed on that buffer. +//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. +//! +//! ##### Examples +//! +//! Functions: +//! +//! ```no_run +//! use std::io::{Write, stdout}; +//! use crossterm::{ExecutableCommand, cursor}; +//! +//! let mut stdout = stdout(); +//! stdout.execute(cursor::MoveTo(5,5)); +//! ``` +//! The [execute](./trait.ExecutableCommand.html) function returns itself, therefore you can use this to queue +//! another command. Like `stdout.execute(Goto(5,5))?.execute(Clear(ClearType::All))`. +//! +//! Macros: +//! +//! ```no_run +//! use std::io::{stdout, Write}; +//! use crossterm::{execute, ExecutableCommand, cursor}; +//! +//! let mut stdout = stdout(); +//! execute!(stdout, cursor::MoveTo(5, 5)); +//! ``` +//! You can pass more than one command into the [execute](./macro.execute.html) macro like +//! `execute!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and they will be executed in the given order from +//! left to right. +//! +//! ## Examples +//! +//! Print a rectangle colored with magenta and use both direct execution and lazy execution. +//! +//! Functions: +//! +//! ```no_run +//! use std::io::{self, Write}; +//! use crossterm::{ +//! ExecutableCommand, QueueableCommand, +//! terminal, cursor, style::{self, Stylize} +//! }; +//! +//! fn main() -> io::Result<()> { +//! let mut stdout = io::stdout(); +//! +//! stdout.execute(terminal::Clear(terminal::ClearType::All))?; +//! +//! for y in 0..40 { +//! for x in 0..150 { +//! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { +//! // in this loop we are more efficient by not flushing the buffer. +//! stdout +//! .queue(cursor::MoveTo(x,y))? +//! .queue(style::PrintStyledContent( "█".magenta()))?; +//! } +//! } +//! } +//! stdout.flush()?; +//! Ok(()) +//! } +//! ``` +//! +//! Macros: +//! +//! ```no_run +//! use std::io::{self, Write}; +//! use crossterm::{ +//! execute, queue, +//! style::{self, Stylize}, cursor, terminal +//! }; +//! +//! fn main() -> io::Result<()> { +//! let mut stdout = io::stdout(); +//! +//! execute!(stdout, terminal::Clear(terminal::ClearType::All))?; +//! +//! for y in 0..40 { +//! for x in 0..150 { +//! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { +//! // in this loop we are more efficient by not flushing the buffer. +//! queue!(stdout, cursor::MoveTo(x,y), style::PrintStyledContent( "█".magenta()))?; +//! } +//! } +//! } +//! stdout.flush()?; +//! Ok(()) +//! } +//!``` +//! +//! [write]: https://doc.rust-lang.org/std/io/trait.Write.html +//! [stdout]: https://doc.rust-lang.org/std/io/fn.stdout.html +//! [stderr]: https://doc.rust-lang.org/std/io/fn.stderr.html +//! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush + +pub use crate::command::{Command, ExecutableCommand, QueueableCommand, SynchronizedUpdate}; + +/// A module to work with the terminal cursor +pub mod cursor; +/// A module to read events. +#[cfg(feature = "events")] +pub mod event; +/// A module to apply attributes and colors on your text. +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/keyfork-crossterm/src/macros.rs b/keyfork-crossterm/src/macros.rs new file mode 100644 index 0000000..9261c42 --- /dev/null +++ b/keyfork-crossterm/src/macros.rs @@ -0,0 +1,370 @@ +/// Append a the first few characters of an ANSI escape code to the given string. +#[macro_export] +#[doc(hidden)] +macro_rules! csi { + ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) }; +} + +/// Queues one or more command(s) for further execution. +/// +/// Queued commands must be flushed to the underlying device to be executed. +/// This generally happens in the following cases: +/// +/// * When `flush` is called manually on the given type implementing `io::Write`. +/// * The terminal will `flush` automatically if the buffer is full. +/// * Each line is flushed in case of `stdout`, because it is line buffered. +/// +/// # Arguments +/// +/// - [std::io::Writer](std::io::Write) +/// +/// ANSI escape codes are written on the given 'writer', after which they are flushed. +/// +/// - [Command](./trait.Command.html) +/// +/// One or more commands +/// +/// # Examples +/// +/// ```rust +/// use std::io::{Write, stdout}; +/// use crossterm::{queue, style::Print}; +/// +/// let mut stdout = stdout(); +/// +/// // `Print` will executed executed when `flush` is called. +/// queue!(stdout, Print("foo".to_string())); +/// +/// // some other code (no execution happening here) ... +/// +/// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed. +/// stdout.flush(); +/// +/// // ==== Output ==== +/// // foo +/// ``` +/// +/// Have a look over at the [Command API](./index.html#command-api) for more details. +/// +/// # Notes +/// +/// In case of Windows versions lower than 10, a direct WinAPI call will be made. +/// The reason for this is that Windows versions lower than 10 do not support ANSI codes, +/// and can therefore not be written to the given `writer`. +/// Therefore, there is no difference between [execute](macro.execute.html) +/// and [queue](macro.queue.html) for those old Windows versions. +/// +#[macro_export] +macro_rules! queue { + ($writer:expr $(, $command:expr)* $(,)?) => {{ + use ::std::io::Write; + + // This allows the macro to take both mut impl Write and &mut impl Write. + Ok($writer.by_ref()) + $(.and_then(|writer| $crate::QueueableCommand::queue(writer, $command)))* + .map(|_| ()) + }} +} + +/// Executes one or more command(s). +/// +/// # Arguments +/// +/// - [std::io::Writer](std::io::Write) +/// +/// ANSI escape codes are written on the given 'writer', after which they are flushed. +/// +/// - [Command](./trait.Command.html) +/// +/// One or more commands +/// +/// # Examples +/// +/// ```rust +/// use std::io::{Write, stdout}; +/// use crossterm::{execute, style::Print}; +/// +/// // will be executed directly +/// execute!(stdout(), Print("sum:\n".to_string())); +/// +/// // will be executed directly +/// execute!(stdout(), Print("1 + 1 = ".to_string()), Print((1+1).to_string())); +/// +/// // ==== Output ==== +/// // sum: +/// // 1 + 1 = 2 +/// ``` +/// +/// Have a look over at the [Command API](./index.html#command-api) for more details. +/// +/// # Notes +/// +/// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. +/// * In case of Windows versions lower than 10, a direct WinAPI call will be made. +/// The reason for this is that Windows versions lower than 10 do not support ANSI codes, +/// and can therefore not be written to the given `writer`. +/// Therefore, there is no difference between [execute](macro.execute.html) +/// and [queue](macro.queue.html) for those old Windows versions. +#[macro_export] +macro_rules! execute { + ($writer:expr $(, $command:expr)* $(,)? ) => {{ + use ::std::io::Write; + + // Queue each command, then flush + $crate::queue!($writer $(, $command)*) + .and_then(|()| { + ::std::io::Write::flush($writer.by_ref()) + }) + }} +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_display { + (for $($t:ty),+) => { + $(impl ::std::fmt::Display for $t { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + $crate::command::execute_fmt(f, self) + } + })* + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_from { + ($from:path, $to:expr) => { + impl From<$from> for ErrorKind { + fn from(e: $from) -> Self { + $to(e) + } + } + }; +} + +#[cfg(test)] +mod tests { + use std::io; + use std::str; + + // Helper for execute tests to confirm flush + #[derive(Default, Debug, Clone)] + pub(self) struct FakeWrite { + buffer: String, + flushed: bool, + } + + impl io::Write for FakeWrite { + fn write(&mut self, content: &[u8]) -> io::Result { + let content = str::from_utf8(content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + self.buffer.push_str(content); + self.flushed = false; + Ok(content.len()) + } + + fn flush(&mut self) -> io::Result<()> { + self.flushed = true; + Ok(()) + } + } + + #[cfg(not(windows))] + mod unix { + use std::fmt; + + use super::FakeWrite; + use crate::command::Command; + + pub struct FakeCommand; + + impl Command for FakeCommand { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str("cmd") + } + } + + #[test] + fn test_queue_one() { + let mut result = FakeWrite::default(); + queue!(&mut result, FakeCommand).unwrap(); + assert_eq!(&result.buffer, "cmd"); + assert!(!result.flushed); + } + + #[test] + fn test_queue_many() { + let mut result = FakeWrite::default(); + queue!(&mut result, FakeCommand, FakeCommand).unwrap(); + assert_eq!(&result.buffer, "cmdcmd"); + assert!(!result.flushed); + } + + #[test] + fn test_queue_trailing_comma() { + let mut result = FakeWrite::default(); + queue!(&mut result, FakeCommand, FakeCommand,).unwrap(); + assert_eq!(&result.buffer, "cmdcmd"); + assert!(!result.flushed); + } + + #[test] + fn test_execute_one() { + let mut result = FakeWrite::default(); + execute!(&mut result, FakeCommand).unwrap(); + assert_eq!(&result.buffer, "cmd"); + assert!(result.flushed); + } + + #[test] + fn test_execute_many() { + let mut result = FakeWrite::default(); + execute!(&mut result, FakeCommand, FakeCommand).unwrap(); + assert_eq!(&result.buffer, "cmdcmd"); + assert!(result.flushed); + } + + #[test] + fn test_execute_trailing_comma() { + let mut result = FakeWrite::default(); + execute!(&mut result, FakeCommand, FakeCommand,).unwrap(); + assert_eq!(&result.buffer, "cmdcmd"); + assert!(result.flushed); + } + } + + #[cfg(windows)] + mod windows { + use std::fmt; + + use std::cell::RefCell; + + use super::FakeWrite; + use crate::command::Command; + + // We need to test two different APIs: WinAPI and the write api. We + // don't know until runtime which we're supporting (via + // Command::is_ansi_code_supported), so we have to test them both. The + // CI environment hopefully includes both versions of windows. + + // WindowsEventStream is a place for execute_winapi to push strings, + // when called. + type WindowsEventStream = Vec<&'static str>; + + struct FakeCommand<'a> { + // Need to use a refcell because we want execute_winapi to be able + // push to the vector, but execute_winapi take &self. + stream: RefCell<&'a mut WindowsEventStream>, + value: &'static str, + } + + impl<'a> FakeCommand<'a> { + fn new(stream: &'a mut WindowsEventStream, value: &'static str) -> Self { + Self { + value, + stream: RefCell::new(stream), + } + } + } + + impl<'a> Command for FakeCommand<'a> { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(self.value) + } + + fn execute_winapi(&self) -> std::io::Result<()> { + self.stream.borrow_mut().push(self.value); + Ok(()) + } + } + + // Helper function for running tests against either WinAPI or an + // io::Write. + // + // This function will execute the `test` function, which should + // queue some commands against the given FakeWrite and + // WindowsEventStream. It will then test that the correct data sink + // was populated. It does not currently check is_ansi_code_supported; + // for now it simply checks that one of the two streams was correctly + // populated. + // + // If the stream was populated, it tests that the two arrays are equal. + // If the writer was populated, it tests that the contents of the + // write buffer are equal to the concatenation of `stream_result`. + fn test_harness( + stream_result: &[&'static str], + test: impl FnOnce(&mut FakeWrite, &mut WindowsEventStream) -> std::io::Result<()>, + ) { + let mut stream = WindowsEventStream::default(); + let mut writer = FakeWrite::default(); + + if let Err(err) = test(&mut writer, &mut stream) { + panic!("Error returned from test function: {:?}", err); + } + + // We need this for type inference, for whatever reason. + const EMPTY_RESULT: [&str; 0] = []; + + // TODO: confirm that the correct sink was used, based on + // is_ansi_code_supported + match (writer.buffer.is_empty(), stream.is_empty()) { + (true, true) if stream_result == EMPTY_RESULT => {} + (true, true) => panic!( + "Neither the event stream nor the writer were populated. Expected {:?}", + stream_result + ), + + // writer is populated + (false, true) => { + // Concat the stream result to find the string result + let result: String = stream_result.iter().copied().collect(); + assert_eq!(result, writer.buffer); + assert_eq!(&stream, &EMPTY_RESULT); + } + + // stream is populated + (true, false) => { + assert_eq!(stream, stream_result); + assert_eq!(writer.buffer, ""); + } + + // Both are populated + (false, false) => panic!( + "Both the writer and the event stream were written to.\n\ + Only one should be used, based on is_ansi_code_supported.\n\ + stream: {stream:?}\n\ + writer: {writer:?}", + stream = stream, + writer = writer, + ), + } + } + + #[test] + fn test_queue_one() { + test_harness(&["cmd1"], |writer, stream| { + queue!(writer, FakeCommand::new(stream, "cmd1")) + }) + } + + #[test] + fn test_queue_some() { + test_harness(&["cmd1", "cmd2"], |writer, stream| { + queue!( + writer, + FakeCommand::new(stream, "cmd1"), + FakeCommand::new(stream, "cmd2"), + ) + }) + } + + #[test] + fn test_many_queues() { + test_harness(&["cmd1", "cmd2", "cmd3"], |writer, stream| { + queue!(writer, FakeCommand::new(stream, "cmd1"))?; + queue!(writer, FakeCommand::new(stream, "cmd2"))?; + queue!(writer, FakeCommand::new(stream, "cmd3")) + }) + } + } +} diff --git a/keyfork-crossterm/src/style.rs b/keyfork-crossterm/src/style.rs new file mode 100644 index 0000000..72c2281 --- /dev/null +++ b/keyfork-crossterm/src/style.rs @@ -0,0 +1,510 @@ +//! # Style +//! +//! The `style` module provides a functionality to apply attributes and colors on your text. +//! +//! This documentation does not contain a lot of examples. The reason is that it's fairly +//! obvious how to use this crate. Although, we do provide +//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository +//! to demonstrate the capabilities. +//! +//! ## Platform-specific Notes +//! +//! Not all features are supported on all terminals/platforms. You should always consult +//! platform-specific notes of the following types: +//! +//! * [Color](enum.Color.html#platform-specific-notes) +//! * [Attribute](enum.Attribute.html#platform-specific-notes) +//! +//! ## Examples +//! +//! A few examples of how to use the style module. +//! +//! ### Colors +//! +//! How to change the terminal text color. +//! +//! Command API: +//! +//! Using the Command API to color text. +//! +//! ```no_run +//! use std::io::{self, Write}; +//! use crossterm::execute; +//! use crossterm::style::{Print, SetForegroundColor, SetBackgroundColor, ResetColor, Color, Attribute}; +//! +//! fn main() -> io::Result<()> { +//! execute!( +//! io::stdout(), +//! // Blue foreground +//! SetForegroundColor(Color::Blue), +//! // Red background +//! SetBackgroundColor(Color::Red), +//! // Print text +//! Print("Blue text on Red.".to_string()), +//! // Reset to default colors +//! ResetColor +//! ) +//! } +//! ``` +//! +//! Functions: +//! +//! Using functions from [`Stylize`](crate::style::Stylize) on a `String` or `&'static str` to color +//! it. +//! +//! ```no_run +//! use crossterm::style::Stylize; +//! +//! println!("{}", "Red foreground color & blue background.".red().on_blue()); +//! ``` +//! +//! ### Attributes +//! +//! How to apply terminal attributes to text. +//! +//! Command API: +//! +//! Using the Command API to set attributes. +//! +//! ```no_run +//! use std::io::{self, Write}; +//! +//! use crossterm::execute; +//! use crossterm::style::{Attribute, Print, SetAttribute}; +//! +//! fn main() -> io::Result<()> { +//! execute!( +//! io::stdout(), +//! // Set to bold +//! SetAttribute(Attribute::Bold), +//! Print("Bold text here.".to_string()), +//! // Reset all attributes +//! SetAttribute(Attribute::Reset) +//! ) +//! } +//! ``` +//! +//! Functions: +//! +//! Using [`Stylize`](crate::style::Stylize) functions on a `String` or `&'static str` to set +//! attributes to it. +//! +//! ```no_run +//! use crossterm::style::Stylize; +//! +//! println!("{}", "Bold".bold()); +//! println!("{}", "Underlined".underlined()); +//! println!("{}", "Negative".negative()); +//! ``` +//! +//! Displayable: +//! +//! [`Attribute`](enum.Attribute.html) implements [Display](https://doc.rust-lang.org/beta/std/fmt/trait.Display.html) and therefore it can be formatted like: +//! +//! ```no_run +//! use crossterm::style::Attribute; +//! +//! println!( +//! "{} Underlined {} No Underline", +//! Attribute::Underlined, +//! Attribute::NoUnderline +//! ); +//! ``` + +use std::{ + env, + fmt::{self, Display}, +}; + +use crate::command::execute_fmt; +use crate::{csi, impl_display, Command}; + +pub use self::{ + attributes::Attributes, + content_style::ContentStyle, + styled_content::StyledContent, + stylize::Stylize, + types::{Attribute, Color, Colored, Colors}, +}; + +mod attributes; +mod content_style; +mod styled_content; +mod stylize; +mod sys; +mod types; + +/// Creates a `StyledContent`. +/// +/// This could be used to style any type that implements `Display` with colors and text attributes. +/// +/// See [`StyledContent`](struct.StyledContent.html) for more info. +/// +/// # Examples +/// +/// ```no_run +/// use crossterm::style::{style, Stylize, Color}; +/// +/// let styled_content = style("Blue colored text on yellow background") +/// .with(Color::Blue) +/// .on(Color::Yellow); +/// +/// println!("{}", styled_content); +/// ``` +pub fn style(val: D) -> StyledContent { + ContentStyle::new().apply(val) +} + +/// Returns available color count. +/// +/// # Notes +/// +/// This does not always provide a good result. +pub fn available_color_count() -> u16 { + env::var("TERM") + .map(|x| if x.contains("256color") { 256 } else { 8 }) + .unwrap_or(8) +} + +/// 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. +/// +/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background +/// color in one command. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetForegroundColor(pub Color); + +impl Command for SetForegroundColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}m"), Colored::ForegroundColor(self.0)) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::windows::set_foreground_color(self.0) + } +} + +/// A command that sets the the background color. +/// +/// See [`Color`](enum.Color.html) for more info. +/// +/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background +/// color with one command. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetBackgroundColor(pub Color); + +impl Command for SetBackgroundColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}m"), Colored::BackgroundColor(self.0)) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::windows::set_background_color(self.0) + } +} + +/// A command that sets the the underline color. +/// +/// See [`Color`](enum.Color.html) for more info. +/// +/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background +/// color with one command. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetUnderlineColor(pub Color); + +impl Command for SetUnderlineColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}m"), Colored::UnderlineColor(self.0)) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "SetUnderlineColor not supported by winapi.", + )) + } +} + +/// A command that optionally sets the foreground and/or background color. +/// +/// For example: +/// ```no_run +/// use std::io::{stdout, Write}; +/// +/// use crossterm::execute; +/// use crossterm::style::{Color::{Green, Black}, Colors, Print, SetColors}; +/// +/// execute!( +/// stdout(), +/// SetColors(Colors::new(Green, Black)), +/// Print("Hello, world!".to_string()), +/// ).unwrap(); +/// ``` +/// +/// See [`Colors`](struct.Colors.html) for more info. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetColors(pub Colors); + +impl Command for SetColors { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + if let Some(color) = self.0.foreground { + SetForegroundColor(color).write_ansi(f)?; + } + if let Some(color) = self.0.background { + SetBackgroundColor(color).write_ansi(f)?; + } + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + if let Some(color) = self.0.foreground { + sys::windows::set_foreground_color(color)?; + } + if let Some(color) = self.0.background { + sys::windows::set_background_color(color)?; + } + Ok(()) + } +} + +/// A command that sets an attribute. +/// +/// See [`Attribute`](enum.Attribute.html) for more info. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetAttribute(pub Attribute); + +impl Command for SetAttribute { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, csi!("{}m"), self.0.sgr()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + // attributes are not supported by WinAPI. + Ok(()) + } +} + +/// A command that sets several attributes. +/// +/// See [`Attributes`](struct.Attributes.html) for more info. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetAttributes(pub Attributes); + +impl Command for SetAttributes { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + for attr in Attribute::iterator() { + if self.0.has(attr) { + SetAttribute(attr).write_ansi(f)?; + } + } + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + // attributes are not supported by WinAPI. + Ok(()) + } +} + +/// A command that sets a style (colors and attributes). +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetStyle(pub ContentStyle); + +impl Command for SetStyle { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + if let Some(bg) = self.0.background_color { + execute_fmt(f, SetBackgroundColor(bg)).map_err(|_| fmt::Error)?; + } + if let Some(fg) = self.0.foreground_color { + execute_fmt(f, SetForegroundColor(fg)).map_err(|_| fmt::Error)?; + } + if let Some(ul) = self.0.underline_color { + execute_fmt(f, SetUnderlineColor(ul)).map_err(|_| fmt::Error)?; + } + if !self.0.attributes.is_empty() { + execute_fmt(f, SetAttributes(self.0.attributes)).map_err(|_| fmt::Error)?; + } + + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + panic!("tried to execute SetStyle command using WinAPI, use ANSI instead"); + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + +/// A command that prints styled content. +/// +/// See [`StyledContent`](struct.StyledContent.html) for more info. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Copy, Clone)] +pub struct PrintStyledContent(pub StyledContent); + +impl Command for PrintStyledContent { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + let style = self.0.style(); + + let mut reset_background = false; + let mut reset_foreground = false; + let mut reset = false; + + if let Some(bg) = style.background_color { + execute_fmt(f, SetBackgroundColor(bg)).map_err(|_| fmt::Error)?; + reset_background = true; + } + if let Some(fg) = style.foreground_color { + execute_fmt(f, SetForegroundColor(fg)).map_err(|_| fmt::Error)?; + reset_foreground = true; + } + if let Some(ul) = style.underline_color { + execute_fmt(f, SetUnderlineColor(ul)).map_err(|_| fmt::Error)?; + reset_foreground = true; + } + + if !style.attributes.is_empty() { + execute_fmt(f, SetAttributes(style.attributes)).map_err(|_| fmt::Error)?; + reset = true; + } + + write!(f, "{}", self.0.content())?; + + if reset { + // NOTE: This will reset colors even though self has no colors, hence produce unexpected + // resets. + // TODO: reset the set attributes only. + execute_fmt(f, ResetColor).map_err(|_| fmt::Error)?; + } else { + // NOTE: Since the above bug, we do not need to reset colors when we reset attributes. + if reset_background { + execute_fmt(f, SetBackgroundColor(Color::Reset)).map_err(|_| fmt::Error)?; + } + if reset_foreground { + execute_fmt(f, SetForegroundColor(Color::Reset)).map_err(|_| fmt::Error)?; + } + } + + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Ok(()) + } +} + +/// A command that resets the colors back to default. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResetColor; + +impl Command for ResetColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("0m")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::windows::reset() + } +} + +/// A command that prints the given displayable type. +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Print(pub T); + +impl Command for Print { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, "{}", self.0) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + panic!("tried to execute Print command using WinAPI, use ANSI instead"); + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + +impl Display for Print { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl_display!(for SetForegroundColor); +impl_display!(for SetBackgroundColor); +impl_display!(for SetColors); +impl_display!(for SetAttribute); +impl_display!(for PrintStyledContent); +impl_display!(for PrintStyledContent<&'static str>); +impl_display!(for ResetColor); + +/// Utility function for ANSI parsing in Color and Colored. +/// Gets the next element of `iter` and tries to parse it as a `u8`. +fn parse_next_u8<'a>(iter: &mut impl Iterator) -> Option { + iter.next().and_then(|s| s.parse().ok()) +} diff --git a/keyfork-crossterm/src/style/attributes.rs b/keyfork-crossterm/src/style/attributes.rs new file mode 100644 index 0000000..aa481c3 --- /dev/null +++ b/keyfork-crossterm/src/style/attributes.rs @@ -0,0 +1,147 @@ +use std::ops::{BitAnd, BitOr, BitXor}; + +use crate::style::Attribute; + +/// a bitset for all possible attributes +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Attributes(u32); + +impl From for Attributes { + fn from(attribute: Attribute) -> Self { + Self(attribute.bytes()) + } +} + +impl From<&[Attribute]> for Attributes { + fn from(arr: &[Attribute]) -> Self { + let mut attributes = Attributes::default(); + for &attr in arr { + attributes.set(attr); + } + attributes + } +} + +impl BitAnd for Attributes { + type Output = Self; + fn bitand(self, rhs: Attribute) -> Self { + Self(self.0 & rhs.bytes()) + } +} +impl BitAnd for Attributes { + type Output = Self; + fn bitand(self, rhs: Self) -> Self { + Self(self.0 & rhs.0) + } +} + +impl BitOr for Attributes { + type Output = Self; + fn bitor(self, rhs: Attribute) -> Self { + Self(self.0 | rhs.bytes()) + } +} +impl BitOr for Attributes { + type Output = Self; + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } +} + +impl BitXor for Attributes { + type Output = Self; + fn bitxor(self, rhs: Attribute) -> Self { + Self(self.0 ^ rhs.bytes()) + } +} +impl BitXor for Attributes { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self { + Self(self.0 ^ rhs.0) + } +} + +impl Attributes { + /// Returns the empty bitset. + #[inline(always)] + pub const fn none() -> Self { + Self(0) + } + + /// Returns a copy of the bitset with the given attribute set. + /// If it's already set, this returns the bitset unmodified. + #[inline(always)] + pub const fn with(self, attribute: Attribute) -> Self { + Self(self.0 | attribute.bytes()) + } + + /// Returns a copy of the bitset with the given attribute unset. + /// If it's not set, this returns the bitset unmodified. + #[inline(always)] + pub const fn without(self, attribute: Attribute) -> Self { + Self(self.0 & !attribute.bytes()) + } + + /// Sets the attribute. + /// If it's already set, this does nothing. + #[inline(always)] + pub fn set(&mut self, attribute: Attribute) { + self.0 |= attribute.bytes(); + } + + /// Unsets the attribute. + /// If it's not set, this changes nothing. + #[inline(always)] + pub fn unset(&mut self, attribute: Attribute) { + self.0 &= !attribute.bytes(); + } + + /// Sets the attribute if it's unset, unset it + /// if it is set. + #[inline(always)] + pub fn toggle(&mut self, attribute: Attribute) { + self.0 ^= attribute.bytes(); + } + + /// Returns whether the attribute is set. + #[inline(always)] + pub const fn has(self, attribute: Attribute) -> bool { + self.0 & attribute.bytes() != 0 + } + + /// Sets all the passed attributes. Removes none. + #[inline(always)] + pub fn extend(&mut self, attributes: Attributes) { + self.0 |= attributes.0; + } + + /// Returns whether there is no attribute set. + #[inline(always)] + pub const fn is_empty(self) -> bool { + self.0 == 0 + } +} + +#[cfg(test)] +mod tests { + use super::{Attribute, Attributes}; + + #[test] + fn test_attributes() { + let mut attributes: Attributes = Attribute::Bold.into(); + assert!(attributes.has(Attribute::Bold)); + attributes.set(Attribute::Italic); + assert!(attributes.has(Attribute::Italic)); + attributes.unset(Attribute::Italic); + assert!(!attributes.has(Attribute::Italic)); + attributes.toggle(Attribute::Bold); + assert!(attributes.is_empty()); + } + + #[test] + fn test_attributes_const() { + const ATTRIBUTES: Attributes = Attributes::none().with(Attribute::Bold).with(Attribute::Italic).without(Attribute::Bold); + assert!(!ATTRIBUTES.has(Attribute::Bold)); + assert!(ATTRIBUTES.has(Attribute::Italic)); + } +} diff --git a/keyfork-crossterm/src/style/content_style.rs b/keyfork-crossterm/src/style/content_style.rs new file mode 100644 index 0000000..6e99bb6 --- /dev/null +++ b/keyfork-crossterm/src/style/content_style.rs @@ -0,0 +1,43 @@ +//! This module contains the `content style` that can be applied to an `styled content`. + +use std::fmt::Display; + +use crate::style::{Attributes, Color, StyledContent}; + +/// The style that can be put on content. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct ContentStyle { + /// The foreground color. + pub foreground_color: Option, + /// The background color. + pub background_color: Option, + /// The underline color. + pub underline_color: Option, + /// List of attributes. + pub attributes: Attributes, +} + +impl ContentStyle { + /// Creates a `StyledContent` by applying the style to the given `val`. + #[inline] + pub fn apply(self, val: D) -> StyledContent { + StyledContent::new(self, val) + } + + /// Creates a new `ContentStyle`. + #[inline] + pub fn new() -> ContentStyle { + ContentStyle::default() + } +} + +impl AsRef for ContentStyle { + fn as_ref(&self) -> &Self { + self + } +} +impl AsMut for ContentStyle { + fn as_mut(&mut self) -> &mut Self { + self + } +} diff --git a/keyfork-crossterm/src/style/styled_content.rs b/keyfork-crossterm/src/style/styled_content.rs new file mode 100644 index 0000000..39ebe0d --- /dev/null +++ b/keyfork-crossterm/src/style/styled_content.rs @@ -0,0 +1,77 @@ +//! This module contains the logic to style some content. + +use std::fmt::{self, Display, Formatter}; + +use super::{ContentStyle, PrintStyledContent}; + +/// The style with the content to be styled. +/// +/// # Examples +/// +/// ```rust +/// use crossterm::style::{style, Color, Attribute, Stylize}; +/// +/// let styled = "Hello there" +/// .with(Color::Yellow) +/// .on(Color::Blue) +/// .attribute(Attribute::Bold); +/// +/// println!("{}", styled); +/// ``` +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct StyledContent { + /// The style (colors, content attributes). + style: ContentStyle, + /// A content to apply the style on. + content: D, +} + +impl StyledContent { + /// Creates a new `StyledContent`. + #[inline] + pub fn new(style: ContentStyle, content: D) -> StyledContent { + StyledContent { style, content } + } + + /// Returns the content. + #[inline] + pub fn content(&self) -> &D { + &self.content + } + + /// Returns the style. + #[inline] + pub fn style(&self) -> &ContentStyle { + &self.style + } + + /// Returns a mutable reference to the style, so that it can be further + /// manipulated + #[inline] + pub fn style_mut(&mut self) -> &mut ContentStyle { + &mut self.style + } +} + +impl AsRef for StyledContent { + fn as_ref(&self) -> &ContentStyle { + &self.style + } +} +impl AsMut for StyledContent { + fn as_mut(&mut self) -> &mut ContentStyle { + &mut self.style + } +} + +impl Display for StyledContent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + crate::command::execute_fmt( + f, + PrintStyledContent(StyledContent { + style: self.style, + content: &self.content, + }), + ) + } +} diff --git a/keyfork-crossterm/src/style/stylize.rs b/keyfork-crossterm/src/style/stylize.rs new file mode 100644 index 0000000..fcb12da --- /dev/null +++ b/keyfork-crossterm/src/style/stylize.rs @@ -0,0 +1,200 @@ +use std::fmt::Display; + +use super::{style, Attribute, Color, ContentStyle, StyledContent}; + +macro_rules! stylize_method { + ($method_name:ident Attribute::$attribute:ident) => { + calculated_docs! { + #[doc = concat!( + "Applies the [`", + stringify!($attribute), + "`](Attribute::", + stringify!($attribute), + ") attribute to the text.", + )] + fn $method_name(self) -> Self::Styled { + self.attribute(Attribute::$attribute) + } + } + }; + ($method_name_fg:ident, $method_name_bg:ident, $method_name_ul:ident Color::$color:ident) => { + calculated_docs! { + #[doc = concat!( + "Sets the foreground color to [`", + stringify!($color), + "`](Color::", + stringify!($color), + ")." + )] + fn $method_name_fg(self) -> Self::Styled { + self.with(Color::$color) + } + + #[doc = concat!( + "Sets the background color to [`", + stringify!($color), + "`](Color::", + stringify!($color), + ")." + )] + fn $method_name_bg(self) -> Self::Styled { + self.on(Color::$color) + } + + #[doc = concat!( + "Sets the underline color to [`", + stringify!($color), + "`](Color::", + stringify!($color), + ")." + )] + fn $method_name_ul(self) -> Self::Styled { + self.underline(Color::$color) + } + } + }; +} + +/// Provides a set of methods to set attributes and colors. +/// +/// # Examples +/// +/// ```no_run +/// use crossterm::style::Stylize; +/// +/// println!("{}", "Bold text".bold()); +/// println!("{}", "Underlined text".underlined()); +/// println!("{}", "Negative text".negative()); +/// println!("{}", "Red on blue".red().on_blue()); +/// ``` +pub trait Stylize: Sized { + /// This type with styles applied. + type Styled: AsRef + AsMut; + + /// Styles this type. + fn stylize(self) -> Self::Styled; + + /// Sets the foreground color. + fn with(self, color: Color) -> Self::Styled { + let mut styled = self.stylize(); + styled.as_mut().foreground_color = Some(color); + styled + } + + /// Sets the background color. + fn on(self, color: Color) -> Self::Styled { + let mut styled = self.stylize(); + styled.as_mut().background_color = Some(color); + styled + } + + /// Sets the underline color. + fn underline(self, color: Color) -> Self::Styled { + let mut styled = self.stylize(); + styled.as_mut().underline_color = Some(color); + styled + } + + /// Styles the content with the attribute. + fn attribute(self, attr: Attribute) -> Self::Styled { + let mut styled = self.stylize(); + styled.as_mut().attributes.set(attr); + styled + } + + stylize_method!(reset Attribute::Reset); + stylize_method!(bold Attribute::Bold); + stylize_method!(underlined Attribute::Underlined); + stylize_method!(reverse Attribute::Reverse); + stylize_method!(dim Attribute::Dim); + stylize_method!(italic Attribute::Italic); + stylize_method!(negative Attribute::Reverse); + stylize_method!(slow_blink Attribute::SlowBlink); + stylize_method!(rapid_blink Attribute::RapidBlink); + stylize_method!(hidden Attribute::Hidden); + stylize_method!(crossed_out Attribute::CrossedOut); + + stylize_method!(black, on_black, underline_black Color::Black); + stylize_method!(dark_grey, on_dark_grey, underline_dark_grey Color::DarkGrey); + stylize_method!(red, on_red, underline_red Color::Red); + stylize_method!(dark_red, on_dark_red, underline_dark_red Color::DarkRed); + stylize_method!(green, on_green, underline_green Color::Green); + stylize_method!(dark_green, on_dark_green, underline_dark_green Color::DarkGreen); + stylize_method!(yellow, on_yellow, underline_yellow Color::Yellow); + stylize_method!(dark_yellow, on_dark_yellow, underline_dark_yellow Color::DarkYellow); + stylize_method!(blue, on_blue, underline_blue Color::Blue); + stylize_method!(dark_blue, on_dark_blue, underline_dark_blue Color::DarkBlue); + stylize_method!(magenta, on_magenta, underline_magenta Color::Magenta); + stylize_method!(dark_magenta, on_dark_magenta, underline_dark_magenta Color::DarkMagenta); + stylize_method!(cyan, on_cyan, underline_cyan Color::Cyan); + stylize_method!(dark_cyan, on_dark_cyan, underline_dark_cyan Color::DarkCyan); + stylize_method!(white, on_white, underline_white Color::White); + stylize_method!(grey, on_grey, underline_grey Color::Grey); +} + +macro_rules! impl_stylize_for_display { + ($($t:ty),*) => { $( + impl Stylize for $t { + type Styled = StyledContent; + #[inline] + fn stylize(self) -> Self::Styled { + style(self) + } + } + )* } +} +impl_stylize_for_display!(String, char, &str); + +impl Stylize for ContentStyle { + type Styled = Self; + #[inline] + fn stylize(self) -> Self::Styled { + self + } +} +impl Stylize for StyledContent { + type Styled = StyledContent; + fn stylize(self) -> Self::Styled { + self + } +} + +// Workaround for https://github.com/rust-lang/rust/issues/78835 +macro_rules! calculated_docs { + ($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* }; +} +// Remove once https://github.com/rust-lang/rust-clippy/issues/7106 stabilizes. +#[allow(clippy::single_component_path_imports)] +#[allow(clippy::useless_attribute)] +use calculated_docs; + +#[cfg(test)] +mod tests { + use super::super::{Attribute, Color, ContentStyle, Stylize}; + + #[test] + fn set_fg_bg_add_attr() { + let style = ContentStyle::new() + .with(Color::Blue) + .on(Color::Red) + .attribute(Attribute::Bold); + + assert_eq!(style.foreground_color, Some(Color::Blue)); + assert_eq!(style.background_color, Some(Color::Red)); + assert!(style.attributes.has(Attribute::Bold)); + + let mut styled_content = style.apply("test"); + + styled_content = styled_content + .with(Color::Green) + .on(Color::Magenta) + .attribute(Attribute::NoItalic); + + let style = styled_content.style(); + + assert_eq!(style.foreground_color, Some(Color::Green)); + assert_eq!(style.background_color, Some(Color::Magenta)); + assert!(style.attributes.has(Attribute::Bold)); + assert!(style.attributes.has(Attribute::NoItalic)); + } +} diff --git a/keyfork-crossterm/src/style/sys.rs b/keyfork-crossterm/src/style/sys.rs new file mode 100644 index 0000000..5a54276 --- /dev/null +++ b/keyfork-crossterm/src/style/sys.rs @@ -0,0 +1,2 @@ +#[cfg(windows)] +pub(crate) mod windows; diff --git a/keyfork-crossterm/src/style/sys/windows.rs b/keyfork-crossterm/src/style/sys/windows.rs new file mode 100644 index 0000000..f5e48b7 --- /dev/null +++ b/keyfork-crossterm/src/style/sys/windows.rs @@ -0,0 +1,204 @@ +use std::convert::TryFrom; +use std::sync::atomic::{AtomicU32, Ordering}; + +use crossterm_winapi::{Console, Handle, HandleType, ScreenBuffer}; +use winapi::um::wincon; + +use super::super::{Color, Colored}; + +const FG_GREEN: u16 = wincon::FOREGROUND_GREEN; +const FG_RED: u16 = wincon::FOREGROUND_RED; +const FG_BLUE: u16 = wincon::FOREGROUND_BLUE; +const FG_INTENSITY: u16 = wincon::FOREGROUND_INTENSITY; + +const BG_GREEN: u16 = wincon::BACKGROUND_GREEN; +const BG_RED: u16 = wincon::BACKGROUND_RED; +const BG_BLUE: u16 = wincon::BACKGROUND_BLUE; +const BG_INTENSITY: u16 = wincon::BACKGROUND_INTENSITY; + +pub(crate) fn set_foreground_color(fg_color: Color) -> std::io::Result<()> { + init_console_color()?; + + let color_value: u16 = Colored::ForegroundColor(fg_color).into(); + + let screen_buffer = ScreenBuffer::current()?; + let csbi = screen_buffer.info()?; + + // Notice that the color values are stored in wAttribute. + // So we need to use bitwise operators to check if the values exists or to get current console colors. + let attrs = csbi.attributes(); + let bg_color = attrs & 0x0070; + let mut color = color_value | bg_color; + + // background intensity is a separate value in attrs, + // we need to check if this was applied to the current bg color. + if (attrs & wincon::BACKGROUND_INTENSITY) != 0 { + color |= wincon::BACKGROUND_INTENSITY; + } + + Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; + Ok(()) +} + +pub(crate) fn set_background_color(bg_color: Color) -> std::io::Result<()> { + init_console_color()?; + + let color_value: u16 = Colored::BackgroundColor(bg_color).into(); + + let screen_buffer = ScreenBuffer::current()?; + let csbi = screen_buffer.info()?; + + // Notice that the color values are stored in wAttribute. + // So we need to use bitwise operators to check if the values exists or to get current console colors. + let attrs = csbi.attributes(); + let fg_color = attrs & 0x0007; + let mut color = fg_color | color_value; + + // Foreground intensity is a separate value in attrs, + // So we need to check if this was applied to the current fg color. + if (attrs & wincon::FOREGROUND_INTENSITY) != 0 { + color |= wincon::FOREGROUND_INTENSITY; + } + + Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; + Ok(()) +} + +pub(crate) fn reset() -> std::io::Result<()> { + if let Ok(original_color) = u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) { + Console::from(Handle::new(HandleType::CurrentOutputHandle)?) + .set_text_attribute(original_color)?; + } + + Ok(()) +} + +/// Initializes the default console color. It will will be skipped if it has already been initialized. +pub(crate) fn init_console_color() -> std::io::Result<()> { + if ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed) == u32::MAX { + let screen_buffer = ScreenBuffer::current()?; + let attr = screen_buffer.info()?.attributes(); + ORIGINAL_CONSOLE_COLOR.store(u32::from(attr), Ordering::Relaxed); + } + + Ok(()) +} + +/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic. +pub(crate) fn original_console_color() -> u16 { + u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) + // safe unwrap, initial console color was set with `init_console_color` in `WinApiColor::new()` + .expect("Initial console color not set") +} + +// This is either a valid u16 in which case it stores the original console color or it is u32::MAX +// in which case it is uninitialized. +static ORIGINAL_CONSOLE_COLOR: AtomicU32 = AtomicU32::new(u32::MAX); + +impl From for u16 { + /// Returns the WinAPI color value (u16) from the `Colored` struct. + fn from(colored: Colored) -> Self { + match colored { + Colored::ForegroundColor(color) => { + match color { + Color::Black => 0, + Color::DarkGrey => FG_INTENSITY, + Color::Red => FG_INTENSITY | FG_RED, + Color::DarkRed => FG_RED, + Color::Green => FG_INTENSITY | FG_GREEN, + Color::DarkGreen => FG_GREEN, + Color::Yellow => FG_INTENSITY | FG_GREEN | FG_RED, + Color::DarkYellow => FG_GREEN | FG_RED, + Color::Blue => FG_INTENSITY | FG_BLUE, + Color::DarkBlue => FG_BLUE, + Color::Magenta => FG_INTENSITY | FG_RED | FG_BLUE, + Color::DarkMagenta => FG_RED | FG_BLUE, + Color::Cyan => FG_INTENSITY | FG_GREEN | FG_BLUE, + Color::DarkCyan => FG_GREEN | FG_BLUE, + Color::White => FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE, + Color::Grey => FG_RED | FG_GREEN | FG_BLUE, + + Color::Reset => { + // safe unwrap, initial console color was set with `init_console_color`. + let original_color = original_console_color(); + + const REMOVE_BG_MASK: u16 = BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE; + // remove all background values from the original color, we don't want to reset those. + + original_color & !REMOVE_BG_MASK + } + + /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ + Color::Rgb { .. } => 0, + Color::AnsiValue(_val) => 0, + } + } + Colored::BackgroundColor(color) => { + match color { + Color::Black => 0, + Color::DarkGrey => BG_INTENSITY, + Color::Red => BG_INTENSITY | BG_RED, + Color::DarkRed => BG_RED, + Color::Green => BG_INTENSITY | BG_GREEN, + Color::DarkGreen => BG_GREEN, + Color::Yellow => BG_INTENSITY | BG_GREEN | BG_RED, + Color::DarkYellow => BG_GREEN | BG_RED, + Color::Blue => BG_INTENSITY | BG_BLUE, + Color::DarkBlue => BG_BLUE, + Color::Magenta => BG_INTENSITY | BG_RED | BG_BLUE, + Color::DarkMagenta => BG_RED | BG_BLUE, + Color::Cyan => BG_INTENSITY | BG_GREEN | BG_BLUE, + Color::DarkCyan => BG_GREEN | BG_BLUE, + Color::White => BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE, + Color::Grey => BG_RED | BG_GREEN | BG_BLUE, + + Color::Reset => { + let original_color = original_console_color(); + + const REMOVE_FG_MASK: u16 = FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE; + // remove all foreground values from the original color, we don't want to reset those. + + original_color & !REMOVE_FG_MASK + } + /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ + Color::Rgb { .. } => 0, + Color::AnsiValue(_val) => 0, + } + } + Colored::UnderlineColor(_) => 0, + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::Ordering; + + use crate::style::sys::windows::set_foreground_color; + + use super::{ + Color, Colored, BG_INTENSITY, BG_RED, FG_INTENSITY, FG_RED, ORIGINAL_CONSOLE_COLOR, + }; + + #[test] + fn test_parse_fg_color() { + let colored = Colored::ForegroundColor(Color::Red); + assert_eq!(Into::::into(colored), FG_INTENSITY | FG_RED); + } + + #[test] + fn test_parse_bg_color() { + let colored = Colored::BackgroundColor(Color::Red); + assert_eq!(Into::::into(colored), BG_INTENSITY | BG_RED); + } + + #[test] + fn test_original_console_color_is_set() { + assert_eq!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); + + // will call `init_console_color` + set_foreground_color(Color::Blue).unwrap(); + + assert_ne!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); + } +} diff --git a/keyfork-crossterm/src/style/types.rs b/keyfork-crossterm/src/style/types.rs new file mode 100644 index 0000000..7cd7d6e --- /dev/null +++ b/keyfork-crossterm/src/style/types.rs @@ -0,0 +1,6 @@ +pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors}; + +mod attribute; +mod color; +mod colored; +mod colors; diff --git a/keyfork-crossterm/src/style/types/attribute.rs b/keyfork-crossterm/src/style/types/attribute.rs new file mode 100644 index 0000000..8b7d52c --- /dev/null +++ b/keyfork-crossterm/src/style/types/attribute.rs @@ -0,0 +1,183 @@ +use std::fmt::Display; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::super::SetAttribute; + +// This macro generates the Attribute enum, its iterator +// function, and the static array containing the sgr code +// of each attribute +macro_rules! Attribute { + ( + $( + $(#[$inner:ident $($args:tt)*])* + $name:ident = $sgr:expr, + )* + ) => { + /// Represents an attribute. + /// + /// # Platform-specific Notes + /// + /// * Only UNIX and Windows 10 terminals do support text attributes. + /// * Keep in mind that not all terminals support all attributes. + /// * Crossterm implements almost all attributes listed in the + /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). + /// + /// | Attribute | Windows | UNIX | Notes | + /// | :-- | :--: | :--: | :-- | + /// | `Reset` | ✓ | ✓ | | + /// | `Bold` | ✓ | ✓ | | + /// | `Dim` | ✓ | ✓ | | + /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | + /// | `Underlined` | ✓ | ✓ | | + /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | + /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | + /// | `Reverse` | ✓ | ✓ | | + /// | `Hidden` | ✓ | ✓ | Also known as Conceal. | + /// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | + /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | + /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | + /// | `Framed` | ? | ? | Not widely supported. | + /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | + /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use crossterm::style::Attribute; + /// + /// println!( + /// "{} Underlined {} No Underline", + /// Attribute::Underlined, + /// Attribute::NoUnderline + /// ); + /// ``` + /// + /// Style existing text: + /// + /// ```no_run + /// use crossterm::style::Stylize; + /// + /// println!("{}", "Bold text".bold()); + /// println!("{}", "Underlined text".underlined()); + /// println!("{}", "Negative text".negative()); + /// ``` + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] + #[non_exhaustive] + pub enum Attribute { + $( + $(#[$inner $($args)*])* + $name, + )* + } + + pub static SGR: &'static[i16] = &[ + $($sgr,)* + ]; + + impl Attribute { + /// Iterates over all the variants of the Attribute enum. + pub fn iterator() -> impl Iterator { + use self::Attribute::*; + [ $($name,)* ].iter().copied() + } + } + } +} + +Attribute! { + /// Resets all the attributes. + Reset = 0, + /// Increases the text intensity. + Bold = 1, + /// Decreases the text intensity. + Dim = 2, + /// Emphasises the text. + Italic = 3, + /// Underlines the text. + Underlined = 4, + + // Other types of underlining + /// Double underlines the text. + DoubleUnderlined = 2, + /// Undercurls the text. + Undercurled = 3, + /// Underdots the text. + Underdotted = 4, + /// Underdashes the text. + Underdashed = 5, + + /// Makes the text blinking (< 150 per minute). + SlowBlink = 5, + /// Makes the text blinking (>= 150 per minute). + RapidBlink = 6, + /// Swaps foreground and background colors. + Reverse = 7, + /// Hides the text (also known as Conceal). + Hidden = 8, + /// Crosses the text. + CrossedOut = 9, + /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. + /// + /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). + Fraktur = 20, + /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity + NoBold = 21, + /// Switches the text back to normal intensity (no bold, italic). + NormalIntensity = 22, + /// Turns off the `Italic` attribute. + NoItalic = 23, + /// Turns off the `Underlined` attribute. + NoUnderline = 24, + /// Turns off the text blinking (`SlowBlink` or `RapidBlink`). + NoBlink = 25, + /// Turns off the `Reverse` attribute. + NoReverse = 27, + /// Turns off the `Hidden` attribute. + NoHidden = 28, + /// Turns off the `CrossedOut` attribute. + NotCrossedOut = 29, + /// Makes the text framed. + Framed = 51, + /// Makes the text encircled. + Encircled = 52, + /// Draws a line at the top of the text. + OverLined = 53, + /// Turns off the `Frame` and `Encircled` attributes. + NotFramedOrEncircled = 54, + /// Turns off the `OverLined` attribute. + NotOverLined = 55, +} + +impl Display for Attribute { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", SetAttribute(*self))?; + Ok(()) + } +} + +impl Attribute { + /// Returns a u32 with one bit set, which is the + /// signature of this attribute in the Attributes + /// bitset. + /// + /// The +1 enables storing Reset (whose index is 0) + /// in the bitset Attributes. + #[inline(always)] + pub const fn bytes(self) -> u32 { + 1 << ((self as u32) + 1) + } + /// Returns the SGR attribute value. + /// + /// See + pub fn sgr(self) -> String { + if (self as usize) > 4 && (self as usize) < 9 { + return "4:".to_string() + SGR[self as usize].to_string().as_str(); + } + SGR[self as usize].to_string() + } +} diff --git a/keyfork-crossterm/src/style/types/color.rs b/keyfork-crossterm/src/style/types/color.rs new file mode 100644 index 0000000..b73c837 --- /dev/null +++ b/keyfork-crossterm/src/style/types/color.rs @@ -0,0 +1,524 @@ +use std::{ + convert::{AsRef, TryFrom}, + str::FromStr, +}; + +#[cfg(feature = "serde")] +use std::fmt; + +use crate::style::parse_next_u8; + +/// Represents a color. +/// +/// # Platform-specific Notes +/// +/// The following list of 16 base colors are available for almost all terminals (Windows 7 and 8 included). +/// +/// | Light | Dark | +/// | :--------- | :------------ | +/// | `DarkGrey` | `Black` | +/// | `Red` | `DarkRed` | +/// | `Green` | `DarkGreen` | +/// | `Yellow` | `DarkYellow` | +/// | `Blue` | `DarkBlue` | +/// | `Magenta` | `DarkMagenta` | +/// | `Cyan` | `DarkCyan` | +/// | `White` | `Grey` | +/// +/// Most UNIX terminals and Windows 10 consoles support additional colors. +/// See [`Color::Rgb`] or [`Color::AnsiValue`] for more info. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum Color { + /// Resets the terminal color. + Reset, + + /// Black color. + Black, + + /// Dark grey color. + DarkGrey, + + /// Light red color. + Red, + + /// Dark red color. + DarkRed, + + /// Light green color. + Green, + + /// Dark green color. + DarkGreen, + + /// Light yellow color. + Yellow, + + /// Dark yellow color. + DarkYellow, + + /// Light blue color. + Blue, + + /// Dark blue color. + DarkBlue, + + /// Light magenta color. + Magenta, + + /// Dark magenta color. + DarkMagenta, + + /// Light cyan color. + Cyan, + + /// Dark cyan color. + DarkCyan, + + /// White color. + White, + + /// Grey color. + Grey, + + /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info. + /// + /// Most UNIX terminals and Windows 10 supported only. + /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. + Rgb { r: u8, g: u8, b: u8 }, + + /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info. + /// + /// Most UNIX terminals and Windows 10 supported only. + /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. + AnsiValue(u8), +} + +impl Color { + /// Parses an ANSI color sequence. + /// + /// # Examples + /// + /// ``` + /// use crossterm::style::Color; + /// + /// assert_eq!(Color::parse_ansi("5;0"), Some(Color::Black)); + /// assert_eq!(Color::parse_ansi("5;26"), Some(Color::AnsiValue(26))); + /// assert_eq!(Color::parse_ansi("2;50;60;70"), Some(Color::Rgb { r: 50, g: 60, b: 70 })); + /// assert_eq!(Color::parse_ansi("invalid color"), None); + /// ``` + /// + /// Currently, 3/4 bit color values aren't supported so return `None`. + /// + /// See also: [`Colored::parse_ansi`](crate::style::Colored::parse_ansi). + pub fn parse_ansi(ansi: &str) -> Option { + Self::parse_ansi_iter(&mut ansi.split(';')) + } + + /// The logic for parse_ansi, takes an iterator of the sequences terms (the numbers between the + /// ';'). It's a separate function so it can be used by both Color::parse_ansi and + /// colored::parse_ansi. + /// Tested in Colored tests. + pub(crate) fn parse_ansi_iter<'a>(values: &mut impl Iterator) -> Option { + let color = match parse_next_u8(values)? { + // 8 bit colors: `5;` + 5 => { + let n = parse_next_u8(values)?; + + use Color::*; + [ + Black, // 0 + DarkRed, // 1 + DarkGreen, // 2 + DarkYellow, // 3 + DarkBlue, // 4 + DarkMagenta, // 5 + DarkCyan, // 6 + Grey, // 7 + DarkGrey, // 8 + Red, // 9 + Green, // 10 + Yellow, // 11 + Blue, // 12 + Magenta, // 13 + Cyan, // 14 + White, // 15 + ] + .get(n as usize) + .copied() + .unwrap_or(Color::AnsiValue(n)) + } + + // 24 bit colors: `2;;;` + 2 => Color::Rgb { + r: parse_next_u8(values)?, + g: parse_next_u8(values)?, + b: parse_next_u8(values)?, + }, + + _ => return None, + }; + // If there's another value, it's unexpected so return None. + if values.next().is_some() { + return None; + } + Some(color) + } +} + +impl TryFrom<&str> for Color { + type Error = (); + + /// Try to create a `Color` from the string representation. This returns an error if the string does not match. + fn try_from(src: &str) -> Result { + let src = src.to_lowercase(); + + match src.as_ref() { + "reset" => Ok(Color::Reset), + "black" => Ok(Color::Black), + "dark_grey" => Ok(Color::DarkGrey), + "red" => Ok(Color::Red), + "dark_red" => Ok(Color::DarkRed), + "green" => Ok(Color::Green), + "dark_green" => Ok(Color::DarkGreen), + "yellow" => Ok(Color::Yellow), + "dark_yellow" => Ok(Color::DarkYellow), + "blue" => Ok(Color::Blue), + "dark_blue" => Ok(Color::DarkBlue), + "magenta" => Ok(Color::Magenta), + "dark_magenta" => Ok(Color::DarkMagenta), + "cyan" => Ok(Color::Cyan), + "dark_cyan" => Ok(Color::DarkCyan), + "white" => Ok(Color::White), + "grey" => Ok(Color::Grey), + _ => Err(()), + } + } +} + +impl FromStr for Color { + type Err = (); + + /// Creates a `Color` from the string representation. + /// + /// # Notes + /// + /// * Returns `Color::White` in case of an unknown color. + /// * Does not return `Err` and you can safely unwrap. + fn from_str(src: &str) -> Result { + Ok(Color::try_from(src).unwrap_or(Color::White)) + } +} + +impl From<(u8, u8, u8)> for Color { + /// Creates a 'Color' from the tuple representation. + fn from(val: (u8, u8, u8)) -> Self { + let (r, g, b) = val; + Self::Rgb { r, g, b } + } +} + +#[cfg(feature = "serde")] +impl serde::ser::Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + let str = match *self { + Color::Reset => "reset", + Color::Black => "black", + Color::DarkGrey => "dark_grey", + Color::Red => "red", + Color::DarkRed => "dark_red", + Color::Green => "green", + Color::DarkGreen => "dark_green", + Color::Yellow => "yellow", + Color::DarkYellow => "dark_yellow", + Color::Blue => "blue", + Color::DarkBlue => "dark_blue", + Color::Magenta => "magenta", + Color::DarkMagenta => "dark_magenta", + Color::Cyan => "cyan", + Color::DarkCyan => "dark_cyan", + Color::White => "white", + Color::Grey => "grey", + _ => "", + }; + + if str == "" { + match *self { + Color::AnsiValue(value) => { + return serializer.serialize_str(&format!("ansi_({})", value)); + } + Color::Rgb { r, g, b } => { + return serializer.serialize_str(&format!("rgb_({},{},{})", r, g, b)); + } + _ => { + return Err(serde::ser::Error::custom("Could not serialize enum type")); + } + } + } else { + return serializer.serialize_str(str); + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::de::Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + struct ColorVisitor; + impl<'de> serde::de::Visitor<'de> for ColorVisitor { + type Value = Color; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "`reset`, `black`, `blue`, `dark_blue`, `cyan`, `dark_cyan`, `green`, `dark_green`, `grey`, `dark_grey`, `magenta`, `dark_magenta`, `red`, `dark_red`, `white`, `yellow`, `dark_yellow`, `ansi_(value)`, or `rgb_(r,g,b)` or `#rgbhex`", + ) + } + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if let Ok(c) = Color::try_from(value) { + Ok(c) + } else { + if value.contains("ansi") { + // strip away `ansi_(..)' and get the inner value between parenthesis. + let results = value.replace("ansi_(", "").replace(")", ""); + + let ansi_val = results.parse::(); + + if let Ok(ansi) = ansi_val { + return Ok(Color::AnsiValue(ansi)); + } + } else if value.contains("rgb") { + // strip away `rgb_(..)' and get the inner values between parenthesis. + let results = value + .replace("rgb_(", "") + .replace(")", "") + .split(',') + .map(|x| x.to_string()) + .collect::>(); + + if results.len() == 3 { + let r = results[0].parse::(); + let g = results[1].parse::(); + let b = results[2].parse::(); + + if r.is_ok() && g.is_ok() && b.is_ok() { + return Ok(Color::Rgb { + r: r.unwrap(), + g: g.unwrap(), + b: b.unwrap(), + }); + } + } + } else if let Some(hex) = value.strip_prefix('#') { + if hex.is_ascii() && hex.len() == 6 { + let r = u8::from_str_radix(&hex[0..2], 16); + let g = u8::from_str_radix(&hex[2..4], 16); + let b = u8::from_str_radix(&hex[4..6], 16); + + if r.is_ok() && g.is_ok() && b.is_ok() { + return Ok(Color::Rgb { + r: r.unwrap(), + g: g.unwrap(), + b: b.unwrap(), + }); + } + } + } + + Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)) + } + } + } + + deserializer.deserialize_str(ColorVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::Color; + + #[test] + fn test_known_color_conversion() { + assert_eq!("reset".parse(), Ok(Color::Reset)); + assert_eq!("grey".parse(), Ok(Color::Grey)); + assert_eq!("dark_grey".parse(), Ok(Color::DarkGrey)); + assert_eq!("red".parse(), Ok(Color::Red)); + assert_eq!("dark_red".parse(), Ok(Color::DarkRed)); + assert_eq!("green".parse(), Ok(Color::Green)); + assert_eq!("dark_green".parse(), Ok(Color::DarkGreen)); + assert_eq!("yellow".parse(), Ok(Color::Yellow)); + assert_eq!("dark_yellow".parse(), Ok(Color::DarkYellow)); + assert_eq!("blue".parse(), Ok(Color::Blue)); + assert_eq!("dark_blue".parse(), Ok(Color::DarkBlue)); + assert_eq!("magenta".parse(), Ok(Color::Magenta)); + assert_eq!("dark_magenta".parse(), Ok(Color::DarkMagenta)); + assert_eq!("cyan".parse(), Ok(Color::Cyan)); + assert_eq!("dark_cyan".parse(), Ok(Color::DarkCyan)); + assert_eq!("white".parse(), Ok(Color::White)); + assert_eq!("black".parse(), Ok(Color::Black)); + } + + #[test] + fn test_unknown_color_conversion_yields_white() { + assert_eq!("foo".parse(), Ok(Color::White)); + } + + #[test] + fn test_know_rgb_color_conversion() { + assert_eq!(Color::from((0, 0, 0)), Color::Rgb { r: 0, g: 0, b: 0 }); + assert_eq!( + Color::from((255, 255, 255)), + Color::Rgb { + r: 255, + g: 255, + b: 255 + } + ); + } +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod serde_tests { + use super::Color; + use serde_json; + + #[test] + fn test_deserial_known_color_conversion() { + assert_eq!( + serde_json::from_str::("\"Reset\"").unwrap(), + Color::Reset + ); + assert_eq!( + serde_json::from_str::("\"reset\"").unwrap(), + Color::Reset + ); + assert_eq!( + serde_json::from_str::("\"Red\"").unwrap(), + Color::Red + ); + assert_eq!( + serde_json::from_str::("\"red\"").unwrap(), + Color::Red + ); + assert_eq!( + serde_json::from_str::("\"dark_red\"").unwrap(), + Color::DarkRed + ); + assert_eq!( + serde_json::from_str::("\"grey\"").unwrap(), + Color::Grey + ); + assert_eq!( + serde_json::from_str::("\"dark_grey\"").unwrap(), + Color::DarkGrey + ); + assert_eq!( + serde_json::from_str::("\"green\"").unwrap(), + Color::Green + ); + assert_eq!( + serde_json::from_str::("\"dark_green\"").unwrap(), + Color::DarkGreen + ); + assert_eq!( + serde_json::from_str::("\"yellow\"").unwrap(), + Color::Yellow + ); + assert_eq!( + serde_json::from_str::("\"dark_yellow\"").unwrap(), + Color::DarkYellow + ); + assert_eq!( + serde_json::from_str::("\"blue\"").unwrap(), + Color::Blue + ); + assert_eq!( + serde_json::from_str::("\"dark_blue\"").unwrap(), + Color::DarkBlue + ); + assert_eq!( + serde_json::from_str::("\"magenta\"").unwrap(), + Color::Magenta + ); + assert_eq!( + serde_json::from_str::("\"dark_magenta\"").unwrap(), + Color::DarkMagenta + ); + assert_eq!( + serde_json::from_str::("\"cyan\"").unwrap(), + Color::Cyan + ); + assert_eq!( + serde_json::from_str::("\"dark_cyan\"").unwrap(), + Color::DarkCyan + ); + assert_eq!( + serde_json::from_str::("\"white\"").unwrap(), + Color::White + ); + assert_eq!( + serde_json::from_str::("\"black\"").unwrap(), + Color::Black + ); + } + + #[test] + fn test_deserial_unknown_color_conversion() { + assert!(serde_json::from_str::("\"unknown\"").is_err()); + } + + #[test] + fn test_deserial_ansi_value() { + assert_eq!( + serde_json::from_str::("\"ansi_(255)\"").unwrap(), + Color::AnsiValue(255) + ); + } + + #[test] + fn test_deserial_unvalid_ansi_value() { + assert!(serde_json::from_str::("\"ansi_(256)\"").is_err()); + assert!(serde_json::from_str::("\"ansi_(-1)\"").is_err()); + } + + #[test] + fn test_deserial_rgb() { + assert_eq!( + serde_json::from_str::("\"rgb_(255,255,255)\"").unwrap(), + Color::from((255, 255, 255)) + ); + } + + #[test] + fn test_deserial_unvalid_rgb() { + assert!(serde_json::from_str::("\"rgb_(255,255,255,255)\"").is_err()); + assert!(serde_json::from_str::("\"rgb_(256,255,255)\"").is_err()); + } + + #[test] + fn test_deserial_rgb_hex() { + assert_eq!( + serde_json::from_str::("\"#ffffff\"").unwrap(), + Color::from((255, 255, 255)) + ); + assert_eq!( + serde_json::from_str::("\"#FFFFFF\"").unwrap(), + Color::from((255, 255, 255)) + ); + } + + #[test] + fn test_deserial_unvalid_rgb_hex() { + assert!(serde_json::from_str::("\"#FFFFFFFF\"").is_err()); + assert!(serde_json::from_str::("\"#FFGFFF\"").is_err()); + // Ferris is 4 bytes so this will be considered the correct length. + assert!(serde_json::from_str::("\"#ff🦀\"").is_err()); + } +} diff --git a/keyfork-crossterm/src/style/types/colored.rs b/keyfork-crossterm/src/style/types/colored.rs new file mode 100644 index 0000000..85f921b --- /dev/null +++ b/keyfork-crossterm/src/style/types/colored.rs @@ -0,0 +1,320 @@ +use parking_lot::Once; +use std::fmt::{self, Formatter}; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::style::{parse_next_u8, Color}; + +/// Represents a foreground or background color. +/// +/// This can be converted to a [Colors](struct.Colors.html) by calling `into()` and applied +/// using the [SetColors](struct.SetColors.html) command. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum Colored { + /// A foreground color. + ForegroundColor(Color), + /// A background color. + BackgroundColor(Color), + /// An underline color. + /// Imporant: doesnt work on windows 10 or lower. + 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 + /// various configuration files. + /// + /// # Examples + /// + /// ``` + /// use crossterm::style::{Colored::{self, ForegroundColor, BackgroundColor}, Color}; + /// + /// assert_eq!(Colored::parse_ansi("38;5;0"), Some(ForegroundColor(Color::Black))); + /// assert_eq!(Colored::parse_ansi("38;5;26"), Some(ForegroundColor(Color::AnsiValue(26)))); + /// assert_eq!(Colored::parse_ansi("48;2;50;60;70"), Some(BackgroundColor(Color::Rgb { r: 50, g: 60, b: 70 }))); + /// assert_eq!(Colored::parse_ansi("49"), Some(BackgroundColor(Color::Reset))); + /// assert_eq!(Colored::parse_ansi("invalid color"), None); + /// ``` + /// + /// Currently, 3/4 bit color values aren't supported so return `None`. + /// + /// See also: [`Color::parse_ansi`]. + pub fn parse_ansi(ansi: &str) -> Option { + use Colored::{BackgroundColor, ForegroundColor, UnderlineColor}; + + let values = &mut ansi.split(';'); + + let output = match parse_next_u8(values)? { + 38 => return Color::parse_ansi_iter(values).map(ForegroundColor), + 48 => return Color::parse_ansi_iter(values).map(BackgroundColor), + 58 => return Color::parse_ansi_iter(values).map(UnderlineColor), + + 39 => ForegroundColor(Color::Reset), + 49 => BackgroundColor(Color::Reset), + 59 => UnderlineColor(Color::Reset), + + _ => return None, + }; + + if values.next().is_some() { + return None; + } + + 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 { + return f.write_str("39"); + } else { + f.write_str("38;")?; + color = new_color; + } + } + Colored::BackgroundColor(new_color) => { + if new_color == Color::Reset { + return f.write_str("49"); + } else { + f.write_str("48;")?; + color = new_color; + } + } + Colored::UnderlineColor(new_color) => { + if new_color == Color::Reset { + return f.write_str("59"); + } else { + f.write_str("58;")?; + color = new_color; + } + } + } + + match color { + Color::Black => f.write_str("5;0"), + Color::DarkGrey => f.write_str("5;8"), + Color::Red => f.write_str("5;9"), + Color::DarkRed => f.write_str("5;1"), + Color::Green => f.write_str("5;10"), + Color::DarkGreen => f.write_str("5;2"), + Color::Yellow => f.write_str("5;11"), + Color::DarkYellow => f.write_str("5;3"), + Color::Blue => f.write_str("5;12"), + Color::DarkBlue => f.write_str("5;4"), + Color::Magenta => f.write_str("5;13"), + Color::DarkMagenta => f.write_str("5;5"), + Color::Cyan => f.write_str("5;14"), + Color::DarkCyan => f.write_str("5;6"), + Color::White => f.write_str("5;15"), + Color::Grey => f.write_str("5;7"), + Color::Rgb { r, g, b } => write!(f, "2;{r};{g};{b}"), + Color::AnsiValue(val) => write!(f, "5;{val}"), + _ => Ok(()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::style::{Color, Colored}; + + fn check_format_color(colored: Colored, expected: &str) { + Colored::set_ansi_color_disabled(true); + assert_eq!(colored.to_string(), ""); + Colored::set_ansi_color_disabled(false); + assert_eq!(colored.to_string(), expected); + } + + #[test] + fn test_format_fg_color() { + let colored = Colored::ForegroundColor(Color::Red); + check_format_color(colored, "38;5;9"); + } + + #[test] + fn test_format_bg_color() { + let colored = Colored::BackgroundColor(Color::Red); + check_format_color(colored, "48;5;9"); + } + + #[test] + fn test_format_reset_fg_color() { + let colored = Colored::ForegroundColor(Color::Reset); + check_format_color(colored, "39"); + } + + #[test] + fn test_format_reset_bg_color() { + let colored = Colored::BackgroundColor(Color::Reset); + check_format_color(colored, "49"); + } + + #[test] + fn test_format_fg_rgb_color() { + let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 }); + check_format_color(colored, "48;2;1;2;3"); + } + + #[test] + fn test_format_fg_ansi_color() { + let colored = Colored::ForegroundColor(Color::AnsiValue(255)); + check_format_color(colored, "38;5;255"); + } + + #[test] + fn test_parse_ansi_fg() { + test_parse_ansi(Colored::ForegroundColor) + } + + #[test] + fn test_parse_ansi_bg() { + test_parse_ansi(Colored::ForegroundColor) + } + + /// Used for test_parse_ansi_fg and test_parse_ansi_bg + fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) { + /// Formats a re-parses `color` to check the result. + macro_rules! test { + ($color:expr) => { + let colored = bg_or_fg($color); + assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored)); + }; + } + + use Color::*; + + test!(Reset); + test!(Black); + test!(DarkGrey); + test!(Red); + test!(DarkRed); + test!(Green); + test!(DarkGreen); + test!(Yellow); + test!(DarkYellow); + test!(Blue); + test!(DarkBlue); + test!(Magenta); + test!(DarkMagenta); + test!(Cyan); + test!(DarkCyan); + test!(White); + test!(Grey); + + // n in 0..=15 will give us the color values above back. + for n in 16..=255 { + test!(AnsiValue(n)); + } + + for r in 0..=255 { + for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() { + for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() { + test!(Rgb { r, g, b }); + } + } + } + } + + #[test] + fn test_parse_invalid_ansi_color() { + /// Checks that trying to parse `s` yields None. + fn test(s: &str) { + assert_eq!(Colored::parse_ansi(s), None); + } + test(""); + test(";"); + test(";;"); + test(";;"); + test("0"); + test("1"); + test("12"); + test("100"); + test("100048949345"); + test("39;"); + test("49;"); + test("39;2"); + test("49;2"); + test("38"); + test("38;"); + test("38;0"); + test("38;5"); + test("38;5;0;"); + test("38;5;0;2"); + test("38;5;80;"); + test("38;5;80;2"); + test("38;5;257"); + test("38;2"); + test("38;2;"); + test("38;2;0"); + test("38;2;0;2"); + test("38;2;0;2;257"); + test("38;2;0;2;25;"); + test("38;2;0;2;25;3"); + test("48"); + test("48;"); + test("48;0"); + test("48;5"); + test("48;5;0;"); + test("48;5;0;2"); + test("48;5;80;"); + test("48;5;80;2"); + test("48;5;257"); + test("48;2"); + test("48;2;"); + test("48;2;0"); + test("48;2;0;2"); + test("48;2;0;2;257"); + test("48;2;0;2;25;"); + test("48;2;0;2;25;3"); + } + + #[test] + fn test_no_color() { + std::env::set_var("NO_COLOR", "1"); + assert!(Colored::ansi_color_disabled()); + std::env::set_var("NO_COLOR", "XXX"); + assert!(Colored::ansi_color_disabled()); + std::env::set_var("NO_COLOR", ""); + assert!(!Colored::ansi_color_disabled()); + std::env::remove_var("NO_COLOR"); + assert!(!Colored::ansi_color_disabled()); + } +} diff --git a/keyfork-crossterm/src/style/types/colors.rs b/keyfork-crossterm/src/style/types/colors.rs new file mode 100644 index 0000000..0b4afbf --- /dev/null +++ b/keyfork-crossterm/src/style/types/colors.rs @@ -0,0 +1,234 @@ +use crate::style::{Color, Colored}; + +/// Represents, optionally, a foreground and/or a background color. +/// +/// It can be applied using the `SetColors` command. +/// +/// It can also be created from a [Colored](enum.Colored.html) value or a tuple of +/// `(Color, Color)` in the order `(foreground, background)`. +/// +/// The [then](#method.then) method can be used to combine `Colors` values. +/// +/// For example: +/// ```no_run +/// use crossterm::style::{Color, Colors, Colored}; +/// +/// // An example color, loaded from a config, file in ANSI format. +/// let config_color = "38;2;23;147;209"; +/// +/// // Default to green text on a black background. +/// let default_colors = Colors::new(Color::Green, Color::Black); +/// // Load a colored value from a config and override the default colors +/// let colors = match Colored::parse_ansi(config_color) { +/// Some(colored) => default_colors.then(&colored.into()), +/// None => default_colors, +/// }; +/// ``` +/// +/// See [Color](enum.Color.html). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Colors { + pub foreground: Option, + pub background: Option, +} + +impl Colors { + /// Returns a new `Color` which, when applied, has the same effect as applying `self` and *then* + /// `other`. + pub fn then(&self, other: &Colors) -> Colors { + Colors { + foreground: other.foreground.or(self.foreground), + background: other.background.or(self.background), + } + } +} + +impl Colors { + pub fn new(foreground: Color, background: Color) -> Colors { + Colors { + foreground: Some(foreground), + background: Some(background), + } + } +} + +impl From for Colors { + fn from(colored: Colored) -> Colors { + match colored { + Colored::ForegroundColor(color) => Colors { + foreground: Some(color), + background: None, + }, + Colored::BackgroundColor(color) => Colors { + foreground: None, + background: Some(color), + }, + Colored::UnderlineColor(color) => Colors { + foreground: None, + background: Some(color), + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::style::{Color, Colors}; + + #[test] + fn test_colors_then() { + use Color::*; + + assert_eq!( + Colors { + foreground: None, + background: None, + } + .then(&Colors { + foreground: None, + background: None, + }), + Colors { + foreground: None, + background: None, + } + ); + + assert_eq!( + Colors { + foreground: None, + background: None, + } + .then(&Colors { + foreground: Some(Black), + background: None, + }), + Colors { + foreground: Some(Black), + background: None, + } + ); + + assert_eq!( + Colors { + foreground: None, + background: None, + } + .then(&Colors { + foreground: None, + background: Some(Grey), + }), + Colors { + foreground: None, + background: Some(Grey), + } + ); + + assert_eq!( + Colors { + foreground: None, + background: None, + } + .then(&Colors::new(White, Grey)), + Colors::new(White, Grey), + ); + + assert_eq!( + Colors { + foreground: None, + background: Some(Blue), + } + .then(&Colors::new(White, Grey)), + Colors::new(White, Grey), + ); + + assert_eq!( + Colors { + foreground: Some(Blue), + background: None, + } + .then(&Colors::new(White, Grey)), + Colors::new(White, Grey), + ); + + assert_eq!( + Colors::new(Blue, Green).then(&Colors::new(White, Grey)), + Colors::new(White, Grey), + ); + + assert_eq!( + Colors { + foreground: Some(Blue), + background: Some(Green), + } + .then(&Colors { + foreground: None, + background: Some(Grey), + }), + Colors { + foreground: Some(Blue), + background: Some(Grey), + } + ); + + assert_eq!( + Colors { + foreground: Some(Blue), + background: Some(Green), + } + .then(&Colors { + foreground: Some(White), + background: None, + }), + Colors { + foreground: Some(White), + background: Some(Green), + } + ); + + assert_eq!( + Colors { + foreground: Some(Blue), + background: Some(Green), + } + .then(&Colors { + foreground: None, + background: None, + }), + Colors { + foreground: Some(Blue), + background: Some(Green), + } + ); + + assert_eq!( + Colors { + foreground: None, + background: Some(Green), + } + .then(&Colors { + foreground: None, + background: None, + }), + Colors { + foreground: None, + background: Some(Green), + } + ); + + assert_eq!( + Colors { + foreground: Some(Blue), + background: None, + } + .then(&Colors { + foreground: None, + background: None, + }), + Colors { + foreground: Some(Blue), + background: None, + } + ); + } +} diff --git a/keyfork-crossterm/src/terminal.rs b/keyfork-crossterm/src/terminal.rs new file mode 100644 index 0000000..e7406be --- /dev/null +++ b/keyfork-crossterm/src/terminal.rs @@ -0,0 +1,568 @@ +//! # Terminal +//! +//! The `terminal` module provides functionality to work with the terminal. +//! +//! This documentation does not contain a lot of examples. The reason is that it's fairly +//! obvious how to use this crate. Although, we do provide +//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository +//! to demonstrate the capabilities. +//! +//! Most terminal actions can be performed with commands. +//! Please have a look at [command documentation](../index.html#command-api) for a more detailed documentation. +//! +//! ## Screen Buffer +//! +//! A screen buffer is a two-dimensional array of character +//! and color data which is displayed in a terminal screen. +//! +//! The terminal has several of those buffers and is able to switch between them. +//! The default screen in which you work is called the 'main screen'. +//! The other screens are called the 'alternative screen'. +//! +//! It is important to understand that crossterm does not yet support creating screens, +//! or switch between more than two buffers, and only offers the ability to change +//! between the 'alternate' and 'main screen'. +//! +//! ### Alternate Screen +//! +//! By default, you will be working on the main screen. +//! There is also another screen called the 'alternative' screen. +//! This screen is slightly different from the main screen. +//! For example, it has the exact dimensions of the terminal window, +//! without any scroll-back area. +//! +//! Crossterm offers the possibility to switch to the 'alternative' screen, +//! make some modifications, and move back to the 'main' screen again. +//! The main screen will stay intact and will have the original data as we performed all +//! operations on the alternative screen. +//! +//! An good example of this is Vim. +//! When it is launched from bash, a whole new buffer is used to modify a file. +//! Then, when the modification is finished, it closes again and continues on the main screen. +//! +//! ### Raw Mode +//! +//! By default, the terminal functions in a certain way. +//! For example, it will move the cursor to the beginning of the next line when the input hits the end of a line. +//! Or that the backspace is interpreted for character removal. +//! +//! Sometimes these default modes are irrelevant, +//! and in this case, we can turn them off. +//! This is what happens when you enable raw modes. +//! +//! Those modes will be set when enabling raw modes: +//! +//! - Input will not be forwarded to screen +//! - Input will not be processed on enter press +//! - Input will not be line buffered (input sent byte-by-byte to input buffer) +//! - Special keys like backspace and CTRL+C will not be processed by terminal driver +//! - New line character will not be processed therefore `println!` can't be used, use `write!` instead +//! +//! Raw mode can be enabled/disabled with the [enable_raw_mode](terminal::enable_raw_mode) and [disable_raw_mode](terminal::disable_raw_mode) functions. +//! +//! ## Examples +//! +//! ```no_run +//! use std::io::{self, Write}; +//! use crossterm::{execute, terminal::{ScrollUp, SetSize, size}}; +//! +//! fn main() -> io::Result<()> { +//! let (cols, rows) = size()?; +//! // Resize terminal and scroll up. +//! execute!( +//! io::stdout(), +//! SetSize(10, 10), +//! ScrollUp(5) +//! )?; +//! +//! // Be a good citizen, cleanup +//! execute!(io::stdout(), SetSize(cols, rows))?; +//! Ok(()) +//! } +//! ``` +//! +//! For manual execution control check out [crossterm::queue](../macro.queue.html). + +use std::{fmt, io}; + +#[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, + pub columns: u16, + pub width: u16, + pub height: u16, +} + +/// Returns the terminal size `[WindowSize]`. +/// +/// The width and height in pixels may not be reliably implemented or default to 0. +/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused". +/// For windows it is not implemented. +pub fn window_size() -> io::Result { + sys::window_size() +} + +/// Disables line wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableLineWrap; + +impl Command for DisableLineWrap { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?7l")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + let screen_buffer = ScreenBuffer::current()?; + let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); + let new_mode = console_mode.mode()? & !ENABLE_WRAP_AT_EOL_OUTPUT; + console_mode.set_mode(new_mode)?; + Ok(()) + } +} + +/// Enable line wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableLineWrap; + +impl Command for EnableLineWrap { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?7h")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + let screen_buffer = ScreenBuffer::current()?; + let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); + let new_mode = console_mode.mode()? | ENABLE_WRAP_AT_EOL_OUTPUT; + console_mode.set_mode(new_mode)?; + Ok(()) + } +} + +/// A command that switches to alternate screen. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [LeaveAlternateScreen](./struct.LeaveAlternateScreen.html) command to leave the entered alternate screen. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{self, Write}; +/// use crossterm::{execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen}}; +/// +/// fn main() -> io::Result<()> { +/// execute!(io::stdout(), EnterAlternateScreen)?; +/// +/// // Do anything on the alternate screen +/// +/// execute!(io::stdout(), LeaveAlternateScreen) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnterAlternateScreen; + +impl Command for EnterAlternateScreen { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?1049h")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + let alternate_screen = ScreenBuffer::create()?; + alternate_screen.show()?; + Ok(()) + } +} + +/// A command that switches back to the main screen. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [EnterAlternateScreen](./struct.EnterAlternateScreen.html) to enter the alternate screen. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{self, Write}; +/// use crossterm::{execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen}}; +/// +/// fn main() -> io::Result<()> { +/// execute!(io::stdout(), EnterAlternateScreen)?; +/// +/// // Do anything on the alternate screen +/// +/// execute!(io::stdout(), LeaveAlternateScreen) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LeaveAlternateScreen; + +impl Command for LeaveAlternateScreen { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?1049l")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + let screen_buffer = ScreenBuffer::from(Handle::current_out_handle()?); + screen_buffer.show()?; + Ok(()) + } +} + +/// Different ways to clear the terminal buffer. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum ClearType { + /// All cells. + All, + /// All plus history + Purge, + /// All cells from the cursor position downwards. + FromCursorDown, + /// All cells from the cursor position upwards. + FromCursorUp, + /// All cells at the cursor row. + CurrentLine, + /// All cells from the cursor position until the new line. + UntilNewLine, +} + +/// A command that scrolls the terminal screen a given number of rows up. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScrollUp(pub u16); + +impl Command for ScrollUp { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + if self.0 != 0 { + write!(f, csi!("{}S"), self.0)?; + } + 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. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScrollDown(pub u16); + +impl Command for ScrollDown { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + if self.0 != 0 { + write!(f, csi!("{}T"), self.0)?; + } + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + sys::scroll_down(self.0) + } +} + +/// A command that clears the terminal screen buffer. +/// +/// See the [`ClearType`](enum.ClearType.html) enum. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Clear(pub ClearType); + +impl Command for Clear { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(match self.0 { + ClearType::All => csi!("2J"), + ClearType::Purge => csi!("3J"), + ClearType::FromCursorDown => csi!("J"), + ClearType::FromCursorUp => csi!("1J"), + ClearType::CurrentLine => csi!("2K"), + 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)`. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetSize(pub u16, pub u16); + +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 +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetTitle(pub T); + +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. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [EndSynchronizedUpdate](./struct.EndSynchronizedUpdate.html) command to leave the entered alternate screen. +/// +/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and +/// renders its current state. With applications updating the screen at a higher frequency this can cause tearing. +/// +/// This mode attempts to mitigate that. +/// +/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. +/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled +/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect +/// by unintentionally rendering in the middle a of an application screen update. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{self, Write}; +/// use crossterm::{execute, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; +/// +/// fn main() -> io::Result<()> { +/// execute!(io::stdout(), BeginSynchronizedUpdate)?; +/// +/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. +/// +/// execute!(io::stdout(), EndSynchronizedUpdate)?; +/// Ok(()) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BeginSynchronizedUpdate; + +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. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [BeginSynchronizedUpdate](./struct.BeginSynchronizedUpdate.html) to enter the alternate screen. +/// +/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and +/// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. +/// +/// This mode attempts to mitigate that. +/// +/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. +/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled +/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect +/// by unintentionally rendering in the middle a of an application screen update. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{self, Write}; +/// use crossterm::{execute, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; +/// +/// fn main() -> io::Result<()> { +/// execute!(io::stdout(), BeginSynchronizedUpdate)?; +/// +/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. +/// +/// execute!(io::stdout(), EndSynchronizedUpdate)?; +/// Ok(()) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EndSynchronizedUpdate; + +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); +impl_display!(for ScrollDown); +impl_display!(for SetSize); +impl_display!(for Clear); + +#[cfg(test)] +mod tests { + use std::{io::stdout, thread, time}; + + use crate::execute; + + use super::*; + + // Test is disabled, because it's failing on Travis CI + #[test] + #[ignore] + fn test_resize_ansi() { + let (width, height) = size().unwrap(); + + execute!(stdout(), SetSize(35, 35)).unwrap(); + + // see issue: https://github.com/eminence/terminal-size/issues/11 + thread::sleep(time::Duration::from_millis(30)); + + assert_eq!((35, 35), size().unwrap()); + + // reset to previous size + execute!(stdout(), SetSize(width, height)).unwrap(); + + // see issue: https://github.com/eminence/terminal-size/issues/11 + thread::sleep(time::Duration::from_millis(30)); + + assert_eq!((width, height), size().unwrap()); + } + + #[test] + fn test_raw_mode() { + // check we start from normal mode (may fail on some test harnesses) + assert!(!is_raw_mode_enabled().unwrap()); + + // enable the raw mode + if enable_raw_mode().is_err() { + // Enabling raw mode doesn't work on the ci + // So we just ignore it + return; + } + + // check it worked (on unix it doesn't really check the underlying + // tty but rather check that the code is consistent) + assert!(is_raw_mode_enabled().unwrap()); + + // enable it again, this should not change anything + enable_raw_mode().unwrap(); + + // check we're still in raw mode + assert!(is_raw_mode_enabled().unwrap()); + + // now let's disable it + disable_raw_mode().unwrap(); + + // check we're back to normal mode + assert!(!is_raw_mode_enabled().unwrap()); + } +} diff --git a/keyfork-crossterm/src/terminal/sys.rs b/keyfork-crossterm/src/terminal/sys.rs new file mode 100644 index 0000000..9dde47d --- /dev/null +++ b/keyfork-crossterm/src/terminal/sys.rs @@ -0,0 +1,27 @@ +//! 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/keyfork-crossterm/src/terminal/sys/file_descriptor.rs b/keyfork-crossterm/src/terminal/sys/file_descriptor.rs new file mode 100644 index 0000000..8df9620 --- /dev/null +++ b/keyfork-crossterm/src/terminal/sys/file_descriptor.rs @@ -0,0 +1,89 @@ +use std::{ + fs, io, + os::unix::{ + io::{IntoRawFd, RawFd}, + prelude::AsRawFd, + }, +}; + +use libc::size_t; + +/// 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)] +pub struct FileDesc { + fd: RawFd, + close_on_drop: bool, +} + +impl FileDesc { + /// Constructs a new `FileDesc` with the given `RawFd`. + /// + /// # Arguments + /// + /// * `fd` - raw file descriptor + /// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped + pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc { + FileDesc { fd, close_on_drop } + } + + pub fn read(&self, buffer: &mut [u8], size: usize) -> io::Result { + let result = unsafe { + libc::read( + self.fd, + buffer.as_mut_ptr() as *mut libc::c_void, + size 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 + } +} + +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() + } +} + +/// 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)) +} diff --git a/keyfork-crossterm/src/terminal/sys/unix.rs b/keyfork-crossterm/src/terminal/sys/unix.rs new file mode 100644 index 0000000..ed545c5 --- /dev/null +++ b/keyfork-crossterm/src/terminal/sys/unix.rs @@ -0,0 +1,240 @@ +//! UNIX related logic for terminal manipulation. + +use crate::terminal::{ + sys::file_descriptor::{tty_fd, FileDesc}, + WindowSize, +}; +use libc::{ + cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, + TIOCGWINSZ, +}; +use parking_lot::Mutex; +use std::fs::File; + +use std::os::unix::io::{IntoRawFd, RawFd}; + +use std::{io, mem, process}; + +// 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() +} + +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)] +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()) +} + +#[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()) +} + +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(()) +} + +/// 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. +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(()) +} + +/// 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, + } +} + +// Transform the given mode into an raw mode (non-canonical) mode. +fn raw_terminal_attr(termios: &mut Termios) { + unsafe { cfmakeraw(termios) } +} + +fn get_terminal_attr(fd: RawFd) -> io::Result { + unsafe { + let mut termios = mem::zeroed(); + wrap_with_result(tcgetattr(fd, &mut termios))?; + Ok(termios) + } +} + +fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { + wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) +} + +fn wrap_with_result(result: i32) -> io::Result<()> { + if result == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} diff --git a/keyfork-crossterm/src/terminal/sys/windows.rs b/keyfork-crossterm/src/terminal/sys/windows.rs new file mode 100644 index 0000000..2e408b5 --- /dev/null +++ b/keyfork-crossterm/src/terminal/sys/windows.rs @@ -0,0 +1,471 @@ +//! 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_value() - width { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "terminal width too large", + )); + } + + new_size.width = window.left + width; + resize_buffer = true; + } + let height = height as i16; + if current_size.height < window.top + height { + if window.top >= i16::max_value() - height { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "terminal height too large", + )); + } + + new_size.height = window.top + height; + resize_buffer = true; + } + + if resize_buffer { + screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?; + } + + let mut window = window; + + // preserve the position, but change the size. + window.bottom = window.top + height - 1; + window.right = window.left + width - 1; + console.set_console_info(true, window)?; + + // if we resized the buffer, un-resize it. + if resize_buffer { + screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?; + } + + let bounds = console.largest_window_size()?; + + if width > bounds.x { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("terminal width {width} too large"), + )); + } + if height > bounds.y { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("terminal height {height} too large"), + )); + } + + Ok(()) +} + +pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> { + struct Utf16Encoder(Vec); + 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/keyfork-crossterm/src/tty.rs b/keyfork-crossterm/src/tty.rs new file mode 100644 index 0000000..78e32aa --- /dev/null +++ b/keyfork-crossterm/src/tty.rs @@ -0,0 +1,46 @@ +//! 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(unix)] +impl IsTty for S { + fn is_tty(&self) -> bool { + let fd = self.as_raw_fd(); + unsafe { libc::isatty(fd) == 1 } + } +} + +/// 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 + } +}