keyfork-crossterm: initial commit, import project
This commit is contained in:
parent
11ced19bc5
commit
e1347d94c0
|
@ -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
|
|
@ -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<TerminalOutput>` 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.
|
|
@ -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"]
|
|
@ -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.
|
|
@ -0,0 +1,213 @@
|
|||
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
|
||||
|
||||
[![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
|
||||
|
||||
<!--
|
||||
WARNING: Do not change following heading title as it's used in the URL by other crates!
|
||||
-->
|
||||
|
||||
### 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._
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Click to show Cargo.toml.
|
||||
</summary>
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
crossterm = "0.27"
|
||||
```
|
||||
|
||||
</details>
|
||||
<p></p>
|
||||
|
||||
```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<Event>`. |
|
||||
| `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
|
|
@ -0,0 +1 @@
|
|||
book
|
|
@ -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`
|
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,103 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="433.000000pt" viewBox="0 0 700.000000 433.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,433.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M987 3178 c-41 -74 -41 -74 -90 -34 -63 52 -83 48 -112 -19 -14 -30
|
||||
-27 -55 -30 -55 -3 0 -24 14 -47 30 -43 32 -79 38 -95 18 -6 -7 -13 -35 -17
|
||||
-62 -3 -27 -8 -51 -11 -54 -2 -2 -22 3 -45 12 -89 35 -118 16 -106 -69 4 -24
|
||||
3 -46 -2 -49 -6 -3 -35 -1 -67 6 -48 9 -59 9 -72 -5 -13 -13 -14 -22 -4 -61
|
||||
19 -75 19 -75 -57 -78 -61 -3 -67 -5 -70 -26 -2 -13 4 -41 12 -63 25 -61 22
|
||||
-66 -36 -74 -29 -4 -59 -12 -65 -17 -22 -18 -14 -53 22 -95 19 -23 35 -43 35
|
||||
-46 0 -2 -12 -7 -27 -10 -47 -10 -93 -44 -93 -69 0 -13 18 -40 41 -64 l41 -42
|
||||
-46 -27 c-42 -24 -46 -30 -46 -64 0 -33 5 -40 40 -60 22 -12 40 -25 40 -28 0
|
||||
-2 -16 -23 -35 -46 -53 -61 -46 -86 28 -112 31 -11 57 -24 57 -30 0 -5 -14
|
||||
-23 -31 -39 -60 -60 -45 -109 37 -119 25 -2 48 -7 51 -11 3 -3 -2 -25 -11 -49
|
||||
-32 -86 -16 -113 63 -102 31 4 51 3 56 -4 3 -6 1 -37 -5 -67 -16 -78 -4 -90
|
||||
74 -74 30 6 60 9 65 5 6 -3 7 -27 4 -55 -4 -33 -2 -55 7 -65 16 -19 57 -19 90
|
||||
0 46 26 59 20 66 -36 12 -86 45 -100 108 -44 22 19 45 35 51 35 6 0 19 -26 30
|
||||
-58 26 -75 52 -80 112 -22 l43 40 26 -42 c31 -50 41 -58 71 -58 16 0 31 15 54
|
||||
53 l33 52 39 -42 c50 -54 77 -56 106 -8 11 20 24 47 27 60 8 31 14 31 48 0 71
|
||||
-66 107 -57 121 32 4 26 9 51 11 55 3 4 23 -2 47 -12 52 -24 93 -25 101 -4 3
|
||||
9 6 40 6 70 0 61 1 61 76 43 39 -10 48 -9 61 4 14 13 14 24 5 72 -7 32 -9 62
|
||||
-6 67 4 6 25 7 50 3 63 -9 84 0 84 36 0 16 -7 46 -15 66 -8 19 -15 39 -15 44
|
||||
0 5 23 11 51 15 85 11 99 43 49 112 -16 23 -30 45 -30 49 0 5 24 17 53 28 68
|
||||
27 73 53 22 112 -19 23 -35 44 -35 47 0 2 22 17 50 33 41 23 50 33 50 55 0 22
|
||||
-9 32 -50 54 -27 15 -50 32 -50 39 0 6 16 26 35 45 19 19 35 44 35 56 0 25
|
||||
-40 59 -83 69 -15 4 -27 10 -27 14 0 3 14 24 30 45 32 42 38 77 18 94 -7 5
|
||||
-37 13 -66 17 -43 6 -53 10 -49 24 40 138 39 141 -48 141 -36 0 -65 4 -65 9 0
|
||||
5 5 32 12 61 10 44 9 53 -5 67 -14 14 -23 15 -67 5 -29 -7 -55 -12 -60 -12 -5
|
||||
0 -10 30 -12 68 -3 65 -4 67 -31 70 -16 2 -46 -4 -68 -12 -22 -9 -43 -16 -48
|
||||
-16 -5 0 -12 24 -16 53 -4 28 -12 58 -17 65 -17 20 -53 14 -97 -19 -24 -17
|
||||
-44 -29 -44 -27 -59 121 -75 130 -137 74 -22 -20 -44 -36 -48 -36 -4 0 -21 23
|
||||
-37 50 -37 62 -70 69 -98 18z m41 -398 c17 0 44 18 83 55 32 30 66 55 76 55
|
||||
32 0 125 -31 186 -61 l57 -29 0 -88 c0 -62 -4 -94 -15 -109 l-15 -22 -44 19
|
||||
c-90 40 -166 54 -301 54 -105 0 -145 -4 -210 -23 -197 -55 -335 -178 -386
|
||||
-341 -29 -92 -28 -227 1 -315 12 -37 20 -68 19 -69 -10 -9 -107 -66 -112 -66
|
||||
-6 0 -33 66 -53 130 -8 25 -17 95 -21 156 l-6 110 32 13 c17 8 31 17 31 21 0
|
||||
4 12 11 28 15 52 13 72 33 72 73 0 20 -7 50 -15 65 -22 44 -29 117 -13 154 31
|
||||
74 177 207 285 262 23 11 72 29 110 40 l68 19 59 -59 c41 -41 66 -59 84 -59z
|
||||
m206 -307 c68 -22 133 -83 168 -160 l28 -63 85 0 85 0 0 40 c0 39 0 40 19 21
|
||||
10 -10 33 -22 50 -26 17 -4 31 -10 31 -15 0 -5 15 -14 34 -21 33 -12 35 -14
|
||||
38 -73 10 -156 -34 -309 -117 -411 -19 -23 -35 -45 -35 -48 0 -3 -12 -18 -27
|
||||
-33 -26 -26 -28 -27 -88 -15 -120 22 -129 17 -151 -81 -8 -35 -16 -73 -19 -85
|
||||
-11 -47 -251 -92 -387 -73 -40 6 -80 12 -88 15 -131 36 -129 35 -139 68 -6 18
|
||||
-15 56 -21 86 -5 30 -15 60 -21 68 -14 16 -75 17 -135 2 -39 -10 -48 -9 -62 5
|
||||
-19 19 -23 15 63 65 l49 29 31 -23 c46 -36 143 -81 215 -101 90 -25 328 -26
|
||||
420 -1 204 55 336 187 348 350 l4 57 -85 0 -86 0 -7 -37 c-23 -130 -113 -200
|
||||
-274 -217 -182 -18 -314 57 -356 201 -24 82 -16 259 15 328 66 148 229 206
|
||||
415 148z"/>
|
||||
<path d="M4557 2523 c-4 -3 -7 -17 -7 -30 0 -59 -47 -109 -116 -123 -53 -11
|
||||
-54 -12 -54 -46 l0 -34 50 0 50 0 0 -135 c0 -75 5 -145 11 -158 16 -36 64 -59
|
||||
133 -64 115 -10 158 27 172 145 l7 62 -46 0 -46 0 -3 -47 c-3 -41 -6 -48 -25
|
||||
-51 -23 -3 -23 -2 -23 122 l0 126 60 0 60 0 0 40 0 40 -60 0 -60 0 0 80 0 80
|
||||
-48 0 c-27 0 -52 -3 -55 -7z"/>
|
||||
<path d="M2258 2373 l-118 -4 0 -39 c0 -36 2 -39 33 -42 l32 -3 3 -132 3 -133
|
||||
-36 0 c-34 0 -35 -1 -35 -40 l0 -40 190 0 190 0 0 40 0 39 -57 3 -58 3 -3 84
|
||||
c-3 94 7 131 46 161 34 28 42 25 42 -14 0 -44 22 -61 81 -61 63 0 94 28 94 85
|
||||
0 57 -37 91 -104 98 -57 5 -134 -18 -151 -46 -7 -14 -9 -10 -10 16 0 20 -5 31
|
||||
-12 30 -7 -1 -66 -4 -130 -5z"/>
|
||||
<path d="M2867 2370 c-65 -17 -116 -49 -144 -92 -25 -37 -28 -51 -28 -124 0
|
||||
-75 3 -86 30 -125 18 -26 50 -53 80 -68 44 -23 61 -26 160 -26 101 0 115 2
|
||||
168 28 40 20 66 42 85 70 24 36 27 50 27 123 0 75 -3 86 -30 125 -45 64 -108
|
||||
91 -220 95 -49 2 -107 -1 -128 -6z m137 -81 c34 -16 51 -62 50 -139 -1 -108
|
||||
-45 -158 -115 -130 -36 15 -54 60 -54 135 0 113 47 166 119 134z"/>
|
||||
<path d="M3418 2364 c-117 -36 -155 -161 -68 -226 15 -11 78 -29 148 -43 134
|
||||
-27 142 -30 142 -50 0 -18 -34 -28 -87 -27 -61 0 -109 19 -137 53 -20 23 -33
|
||||
29 -65 29 l-41 0 0 -80 0 -80 44 0 c24 0 46 5 48 11 3 8 20 6 58 -5 206 -61
|
||||
407 55 335 193 -19 37 -63 57 -182 81 -142 29 -143 29 -143 44 0 21 57 37 104
|
||||
30 52 -9 92 -29 106 -54 8 -15 21 -20 55 -20 l45 0 0 80 0 80 -39 0 c-24 0
|
||||
-41 -5 -44 -14 -5 -13 -13 -13 -59 0 -67 17 -159 17 -220 -2z"/>
|
||||
<path d="M3968 2363 c-66 -22 -100 -60 -106 -121 -8 -91 25 -115 213 -152 122
|
||||
-24 137 -32 113 -56 -12 -12 -34 -17 -77 -16 -68 1 -125 23 -148 58 -12 18
|
||||
-24 24 -54 24 l-39 0 0 -80 0 -80 38 0 c21 0 42 4 48 10 6 6 27 4 59 -6 113
|
||||
-35 279 -1 328 68 26 35 26 101 0 136 -28 38 -55 49 -188 76 -63 13 -118 26
|
||||
-122 29 -12 13 7 34 36 40 51 11 127 -11 158 -44 22 -23 36 -29 70 -29 l43 0
|
||||
0 80 0 80 -39 0 c-21 0 -44 -6 -50 -14 -9 -11 -19 -11 -63 0 -68 18 -161 17
|
||||
-220 -3z"/>
|
||||
<path d="M5025 2371 c-125 -31 -199 -135 -180 -253 20 -117 99 -176 250 -185
|
||||
162 -11 272 39 300 135 6 21 4 22 -47 22 -45 0 -55 -3 -63 -22 -16 -36 -53
|
||||
-49 -123 -45 -69 3 -105 27 -117 80 l-7 27 181 0 181 0 0 38 c0 51 -41 134
|
||||
-80 162 -17 12 -52 29 -77 36 -48 15 -167 17 -218 5z"/>
|
||||
<path d="M5558 2373 l-118 -4 0 -39 c0 -37 2 -40 28 -40 51 0 53 -7 50 -141
|
||||
l-3 -124 -32 -3 c-31 -3 -33 -6 -33 -43 l0 -39 190 0 190 0 0 40 0 40 -60 0
|
||||
-60 0 0 103 0 103 33 32 c41 40 55 41 49 3 -10 -72 115 -97 165 -32 22 28 16
|
||||
89 -11 116 -44 43 -148 46 -206 5 l-30 -21 0 25 c0 19 -5 25 -17 24 -10 -1
|
||||
-71 -4 -135 -5z"/>
|
||||
<path d="M6327 2366 c-20 -8 -44 -19 -52 -26 -13 -10 -15 -9 -15 9 0 20 -4 21
|
||||
-130 21 l-130 0 0 -39 c0 -37 2 -40 33 -43 l32 -3 3 -132 3 -133 -36 0 c-34 0
|
||||
-35 -1 -35 -40 l0 -40 160 0 160 0 0 39 c0 36 -3 40 -27 43 l-28 3 -3 106 c-3
|
||||
119 5 135 69 145 60 10 73 -10 78 -115 4 -117 -2 -141 -34 -141 -22 0 -25 -4
|
||||
-25 -40 l0 -40 150 0 150 0 0 39 c0 36 -3 40 -27 43 l-28 3 0 108 c0 102 1
|
||||
110 24 128 29 24 72 24 100 2 20 -17 22 -26 19 -128 l-3 -110 -27 -3 c-25 -3
|
||||
-28 -7 -28 -43 l0 -39 160 0 160 0 0 40 c0 39 -1 40 -34 40 -22 0 -36 6 -40
|
||||
16 -3 9 -6 67 -6 129 0 130 -8 156 -59 188 -30 18 -50 22 -122 22 -76 0 -90
|
||||
-3 -126 -27 -37 -25 -43 -26 -55 -11 -34 42 -153 56 -231 29z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.8 KiB |
|
@ -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.
|
|
@ -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
|
|
@ -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,
|
||||
)));
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
}) = event::read()?
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_line() -> io::Result<String> {
|
||||
let mut line = String::new();
|
||||
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
|
||||
match code {
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
line.push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("read line:");
|
||||
println!("{:?}", read_line());
|
||||
println!("read char:");
|
||||
println!("{:?}", read_char());
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "interactive-demo"
|
||||
version = "0.0.1"
|
||||
authors = ["T. Post", "Robert Vojta <rvojta@me.com>"]
|
||||
edition = "2018"
|
||||
description = "Interactive demo for crossterm."
|
||||
license = "MIT"
|
||||
exclude = ["target", "Cargo.lock"]
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
crossterm = { path = "../../" }
|
|
@ -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),
|
||||
_ => { },
|
||||
};
|
||||
)*
|
||||
}
|
||||
}
|
|
@ -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>(w: &mut W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
execute!(w, terminal::EnterAlternateScreen)?;
|
||||
|
||||
terminal::enable_raw_mode()?;
|
||||
|
||||
loop {
|
||||
queue!(
|
||||
w,
|
||||
style::ResetColor,
|
||||
terminal::Clear(ClearType::All),
|
||||
cursor::Hide,
|
||||
cursor::MoveTo(1, 1)
|
||||
)?;
|
||||
|
||||
for line in MENU.split('\n') {
|
||||
queue!(w, style::Print(line), cursor::MoveToNextLine(1))?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
match read_char()? {
|
||||
'1' => test::cursor::run(w)?,
|
||||
'2' => test::color::run(w)?,
|
||||
'3' => test::attribute::run(w)?,
|
||||
'4' => test::event::run(w)?,
|
||||
'5' => test::synchronized_output::run(w)?,
|
||||
'q' => {
|
||||
execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap();
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
execute!(
|
||||
w,
|
||||
style::ResetColor,
|
||||
cursor::Show,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
pub fn read_char() -> std::io::Result<char> {
|
||||
loop {
|
||||
if let Ok(Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: _,
|
||||
state: _,
|
||||
})) = event::read()
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_size() -> io::Result<(u16, u16)> {
|
||||
terminal::size()
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
run(&mut stdout)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub mod attribute;
|
||||
pub mod color;
|
||||
pub mod cursor;
|
||||
pub mod event;
|
||||
pub mod synchronized_output;
|
|
@ -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>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Display attributes"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for (on, off) in &ATTRIBUTES {
|
||||
queue!(
|
||||
w,
|
||||
style::SetAttribute(*on),
|
||||
style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)),
|
||||
style::SetAttribute(*off),
|
||||
style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)),
|
||||
style::ResetColor,
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_set_display_attributes,);
|
||||
Ok(())
|
||||
}
|
|
@ -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>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Foreground colors on the black & white background"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for color in &COLORS {
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(*color),
|
||||
style::SetBackgroundColor(Color::Black),
|
||||
style::Print(format!(
|
||||
"{:>width$} ",
|
||||
format!("{:?} ████████████", color),
|
||||
width = 40
|
||||
)),
|
||||
style::SetBackgroundColor(Color::White),
|
||||
style::Print(format!(
|
||||
"{:>width$}",
|
||||
format!("{:?} ████████████", color),
|
||||
width = 40
|
||||
)),
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_set_background_color<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Background colors with black & white foreground"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for color in &COLORS {
|
||||
queue!(
|
||||
w,
|
||||
style::SetBackgroundColor(*color),
|
||||
style::SetForegroundColor(Color::Black),
|
||||
style::Print(format!(
|
||||
"{:>width$} ",
|
||||
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||
width = 40
|
||||
)),
|
||||
style::SetForegroundColor(Color::White),
|
||||
style::Print(format!(
|
||||
"{:>width$}",
|
||||
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||
width = 40
|
||||
)),
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_color_values_matrix_16x16<W, F>(w: &mut W, title: &str, color: F) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
F: Fn(u16, u16) -> Color,
|
||||
{
|
||||
queue!(w, style::Print(title))?;
|
||||
|
||||
for idx in 0..=15 {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(1, idx + 4),
|
||||
style::Print(format!("{:>width$}", idx, width = 2))
|
||||
)?;
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(idx * 3 + 3, 3),
|
||||
style::Print(format!("{:>width$}", idx, width = 3))
|
||||
)?;
|
||||
}
|
||||
|
||||
for row in 0..=15u16 {
|
||||
queue!(w, cursor::MoveTo(4, row + 4))?;
|
||||
for col in 0..=15u16 {
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(color(col, row)),
|
||||
style::Print("███")
|
||||
)?;
|
||||
}
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(Color::White),
|
||||
style::Print(format!("{:>width$} ..= ", row * 16, width = 3)),
|
||||
style::Print(format!("{:>width$}", row * 16 + 15, width = 3))
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_color_ansi_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| {
|
||||
Color::AnsiValue((row * 16 + col) as u8)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_red_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb {
|
||||
r: (row * 16 + col) as u8,
|
||||
g: 0_u8,
|
||||
b: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_green_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb {
|
||||
r: 0,
|
||||
g: (row * 16 + col) as u8,
|
||||
b: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_blue_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: (row * 16 + col) as u8,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(
|
||||
w,
|
||||
test_set_foreground_color,
|
||||
test_set_background_color,
|
||||
test_color_ansi_values,
|
||||
test_rgb_red_values,
|
||||
test_rgb_green_values,
|
||||
test_rgb_blue_values,
|
||||
);
|
||||
Ok(())
|
||||
}
|
|
@ -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>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_down<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_left<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_right<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_previous_line<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| {
|
||||
cursor::MoveToPreviousLine(1)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_next_line<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1))
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_column<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| {
|
||||
cursor::MoveToColumn(center_x + 1)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_hide_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, style::Print("HideCursor"), cursor::Hide)
|
||||
}
|
||||
|
||||
fn test_show_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, style::Print("ShowCursor"), cursor::Show)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_block<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking Block:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingBlock,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_underscore<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking Underscore:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingUnderScore,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_bar<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking bar:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingBar,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_move_cursor_to<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(
|
||||
w,
|
||||
"MoveTo (x: 1, y: 1) removed from center",
|
||||
|center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_save_restore_cursor_position<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w,
|
||||
cursor::MoveTo(0, 0),
|
||||
style::Print("Save position, print character elsewhere, after three seconds restore to old position."),
|
||||
cursor::MoveToNextLine(2),
|
||||
style::Print("Save ->[ ]<- Position"),
|
||||
cursor::MoveTo(8, 2),
|
||||
cursor::SavePosition,
|
||||
cursor::MoveTo(10,10),
|
||||
style::Print("Move To ->[√]<- Position")
|
||||
)?;
|
||||
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
execute!(w, cursor::RestorePosition, style::Print("√"))
|
||||
}
|
||||
|
||||
/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command.
|
||||
fn draw_cursor_box<W, F, T>(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
F: Fn(u16, u16) -> T,
|
||||
T: Command,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
cursor::Hide,
|
||||
cursor::MoveTo(0, 0),
|
||||
style::SetForegroundColor(style::Color::Red),
|
||||
style::Print(format!(
|
||||
"Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.",
|
||||
description
|
||||
))
|
||||
)?;
|
||||
|
||||
let start_y = 2;
|
||||
let width = 21;
|
||||
let height = 11 + start_y;
|
||||
let center_x = width / 2;
|
||||
let center_y = (height + start_y) / 2;
|
||||
|
||||
for row in start_y..=10 + start_y {
|
||||
for column in 0..=width {
|
||||
if (row == start_y || row == height - 1) || (column == 0 || column == width) {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(column, row),
|
||||
style::PrintStyledContent("▓".red()),
|
||||
)?;
|
||||
} else {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(column, row),
|
||||
style::PrintStyledContent("_".red().on_white())
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(center_x, center_y),
|
||||
style::PrintStyledContent("▀".red().on_white()),
|
||||
cursor::MoveTo(center_x, center_y),
|
||||
)?;
|
||||
queue!(
|
||||
w,
|
||||
cursor_command(center_x, center_y),
|
||||
style::PrintStyledContent("√".magenta().on_white())
|
||||
)?;
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(
|
||||
w,
|
||||
test_hide_cursor,
|
||||
test_show_cursor,
|
||||
test_cursor_blinking_bar,
|
||||
test_cursor_blinking_block,
|
||||
test_cursor_blinking_underscore,
|
||||
test_move_cursor_left,
|
||||
test_move_cursor_right,
|
||||
test_move_cursor_up,
|
||||
test_move_cursor_down,
|
||||
test_move_cursor_to,
|
||||
test_move_cursor_to_next_line,
|
||||
test_move_cursor_to_previous_line,
|
||||
test_move_cursor_to_column,
|
||||
test_save_restore_cursor_position
|
||||
);
|
||||
Ok(())
|
||||
}
|
|
@ -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>(w: &mut W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
execute!(w, EnableMouseCapture)?;
|
||||
|
||||
loop {
|
||||
// Blocking read
|
||||
let event = read()?;
|
||||
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Char('q').into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
execute!(w, DisableMouseCapture)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_event);
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
use std::io::Write;
|
||||
|
||||
use crossterm::{cursor, execute, style::Print, SynchronizedUpdate};
|
||||
|
||||
fn render_slowly<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
for i in 1..10 {
|
||||
execute!(w, Print(format!("{}", i)))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_slow_rendering<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, Print("Rendering without synchronized update:"))?;
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
render_slowly(w)?;
|
||||
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
execute!(w, Print("Rendering with synchronized update:"))?;
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
w.sync_update(render_slowly)??;
|
||||
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_slow_rendering,);
|
||||
Ok(())
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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<W>(write: &mut W) -> io::Result<char>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
queue!(
|
||||
write,
|
||||
EnterAlternateScreen, // enter alternate screen
|
||||
Hide // hide the cursor
|
||||
)?;
|
||||
|
||||
let mut y = 1;
|
||||
for line in TEXT.split('\n') {
|
||||
queue!(write, MoveTo(1, y), Print(line.to_string()))?;
|
||||
y += 1;
|
||||
}
|
||||
|
||||
write.flush()?;
|
||||
|
||||
terminal::enable_raw_mode()?;
|
||||
let user_char = read_char()?; // we wait for the user to hit a key
|
||||
execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen
|
||||
|
||||
terminal::disable_raw_mode()?;
|
||||
|
||||
Ok(user_char)
|
||||
}
|
||||
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
}) = event::read()?
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cargo run --example stderr
|
||||
fn main() {
|
||||
match run_app(&mut io::stderr()).unwrap() {
|
||||
'1' => print!(".."),
|
||||
'2' => print!("/"),
|
||||
'3' => print!("~"),
|
||||
_ => println!("{}", TEXT),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<T: Command + ?Sized> 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<T: Write + ?Sized> 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<T: Write + ?Sized> 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<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result<T>;
|
||||
}
|
||||
|
||||
impl<W: std::io::Write + ?Sized> 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<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result<T> {
|
||||
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<C: Command>(
|
||||
io: &mut (impl io::Write + ?Sized),
|
||||
command: C,
|
||||
) -> io::Result<()> {
|
||||
struct Adapter<T> {
|
||||
inner: T,
|
||||
res: io::Result<()>,
|
||||
}
|
||||
|
||||
impl<T: Write> fmt::Write for Adapter<T> {
|
||||
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::<C>()
|
||||
),
|
||||
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)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<i16> {
|
||||
let window = ScreenBuffer::current()?.info()?;
|
||||
|
||||
let window_size = window.terminal_window();
|
||||
let screen_size = window.terminal_size();
|
||||
|
||||
if y <= screen_size.height {
|
||||
Ok(y)
|
||||
} else {
|
||||
Ok(y - window_size.top)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cursor position (column, row).
|
||||
///
|
||||
/// The top left cell is represented `0,0`.
|
||||
pub fn position() -> io::Result<(u16, u16)> {
|
||||
let cursor = ScreenBufferCursor::output()?;
|
||||
let mut position = cursor.position()?;
|
||||
// if position.y != 0 {
|
||||
position.y = parse_relative_y(position.y)?;
|
||||
// }
|
||||
Ok(position.into())
|
||||
}
|
||||
|
||||
pub(crate) fn show_cursor(show_cursor: bool) -> std::io::Result<()> {
|
||||
ScreenBufferCursor::from(Handle::current_out_handle()?).set_visibility(show_cursor)
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(column: u16, row: u16) -> std::io::Result<()> {
|
||||
let cursor = ScreenBufferCursor::output()?;
|
||||
cursor.move_to(column as i16, row as i16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_up(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column, row - count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_right(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column + count, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_down(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column, row + count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_left(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column - count, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_column(new_column: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(new_column, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_row(new_row: u16) -> std::io::Result<()> {
|
||||
let (col, _) = position()?;
|
||||
move_to(col, new_row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_next_line(count: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(0, row + count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_previous_line(count: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(0, row - count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn save_position() -> std::io::Result<()> {
|
||||
ScreenBufferCursor::output()?.save_position()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn restore_position() -> std::io::Result<()> {
|
||||
ScreenBufferCursor::output()?.restore_position()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// WinAPI wrapper over terminal cursor behaviour.
|
||||
struct ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer,
|
||||
}
|
||||
|
||||
impl ScreenBufferCursor {
|
||||
fn output() -> std::io::Result<ScreenBufferCursor> {
|
||||
Ok(ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn position(&self) -> std::io::Result<Coord> {
|
||||
Ok(self.screen_buffer.info()?.cursor_pos())
|
||||
}
|
||||
|
||||
fn move_to(&self, x: i16, y: i16) -> std::io::Result<()> {
|
||||
if x < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Argument Out of Range Exception when setting cursor position to X: {x}"),
|
||||
));
|
||||
}
|
||||
|
||||
if y < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Argument Out of Range Exception when setting cursor position to Y: {y}"),
|
||||
));
|
||||
}
|
||||
|
||||
let position = COORD { X: x, Y: y };
|
||||
|
||||
unsafe {
|
||||
if result(SetConsoleCursorPosition(
|
||||
**self.screen_buffer.handle(),
|
||||
position,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_visibility(&self, visible: bool) -> std::io::Result<()> {
|
||||
let cursor_info = CONSOLE_CURSOR_INFO {
|
||||
dwSize: 100,
|
||||
bVisible: if visible { TRUE } else { FALSE },
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if result(SetConsoleCursorInfo(
|
||||
**self.screen_buffer.handle(),
|
||||
&cursor_info,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_position(&self) -> std::io::Result<()> {
|
||||
if let Ok(val) = u32::try_from(SAVED_CURSOR_POS.load(Ordering::Relaxed)) {
|
||||
let x = (val >> 16) as i16;
|
||||
let y = val as i16;
|
||||
self.move_to(x, y)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_position(&self) -> std::io::Result<()> {
|
||||
let position = self.position()?;
|
||||
|
||||
let bits = u64::from(u32::from(position.x as u16) << 16 | u32::from(position.y as u16));
|
||||
SAVED_CURSOR_POS.store(bits, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Handle> for ScreenBufferCursor {
|
||||
fn from(handle: Handle) -> Self {
|
||||
ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer::from(handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||
move_to_previous_line, move_to_row, move_up, position, restore_position, save_position,
|
||||
};
|
||||
use crate::terminal::sys::temp_screen_buffer;
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
|
||||
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1));
|
||||
|
||||
move_to(saved_x, saved_y).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x, saved_y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_right_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
move_right(1).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x + 1, saved_y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_left_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(2, 0).unwrap();
|
||||
|
||||
move_left(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_up_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_up(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_next_line_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_next_line(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_previous_line_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_previous_line(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_column_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_column(12).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (12, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_row_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_row(5).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_down_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 0).unwrap();
|
||||
|
||||
move_down(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_save_restore_position_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
|
||||
save_position().unwrap();
|
||||
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||
restore_position().unwrap();
|
||||
|
||||
let (x, y) = position().unwrap();
|
||||
|
||||
assert_eq!(x, saved_x);
|
||||
assert_eq!(y, saved_y);
|
||||
}
|
||||
}
|
|
@ -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<Option<InternalEventReader>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn lock_internal_event_reader() -> MappedMutexGuard<'static, InternalEventReader> {
|
||||
MutexGuard::map(INTERNAL_EVENT_READER.lock(), |reader| {
|
||||
reader.get_or_insert_with(InternalEventReader::default)
|
||||
})
|
||||
}
|
||||
fn try_lock_internal_event_reader_for(
|
||||
duration: Duration,
|
||||
) -> Option<MappedMutexGuard<'static, InternalEventReader>> {
|
||||
Some(MutexGuard::map(
|
||||
INTERNAL_EVENT_READER.try_lock_for(duration)?,
|
||||
|reader| reader.get_or_insert_with(InternalEventReader::default),
|
||||
))
|
||||
}
|
||||
|
||||
/// Checks if there is an [`Event`](enum.Event.html) available.
|
||||
///
|
||||
/// Returns `Ok(true)` if an [`Event`](enum.Event.html) is available otherwise it returns `Ok(false)`.
|
||||
///
|
||||
/// `Ok(true)` guarantees that subsequent call to the [`read`](fn.read.html) function
|
||||
/// won't block.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timeout` - maximum waiting time for event availability
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Return immediately:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{time::Duration, io};
|
||||
/// use crossterm::{event::poll};
|
||||
///
|
||||
/// fn is_event_available() -> io::Result<bool> {
|
||||
/// // Zero duration says that the `poll` function must return immediately
|
||||
/// // with an `Event` availability information
|
||||
/// poll(Duration::from_secs(0))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Wait up to 100ms:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{time::Duration, io};
|
||||
///
|
||||
/// use crossterm::event::poll;
|
||||
///
|
||||
/// fn is_event_available() -> io::Result<bool> {
|
||||
/// // Wait for an `Event` availability for 100ms. It returns immediately
|
||||
/// // if an `Event` is/becomes available.
|
||||
/// poll(Duration::from_millis(100))
|
||||
/// }
|
||||
/// ```
|
||||
pub fn poll(timeout: Duration) -> std::io::Result<bool> {
|
||||
poll_internal(Some(timeout), &EventFilter)
|
||||
}
|
||||
|
||||
/// Reads a single [`Event`](enum.Event.html).
|
||||
///
|
||||
/// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the
|
||||
/// [`poll`](fn.poll.html) function to get non-blocking reads.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Blocking read:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crossterm::event::read;
|
||||
/// use std::io;
|
||||
///
|
||||
/// fn print_events() -> io::Result<bool> {
|
||||
/// loop {
|
||||
/// // Blocks until an `Event` is available
|
||||
/// println!("{:?}", read()?);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Non-blocking read:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::time::Duration;
|
||||
/// use std::io;
|
||||
///
|
||||
/// use crossterm::event::{read, poll};
|
||||
///
|
||||
/// fn print_events() -> io::Result<bool> {
|
||||
/// loop {
|
||||
/// if poll(Duration::from_millis(100))? {
|
||||
/// // It's guaranteed that `read` won't block, because `poll` returned
|
||||
/// // `Ok(true)`.
|
||||
/// println!("{:?}", read()?);
|
||||
/// } else {
|
||||
/// // Timeout expired, no `Event` is available
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn read() -> std::io::Result<Event> {
|
||||
match read_internal(&EventFilter)? {
|
||||
InternalEvent::Event(event) => Ok(event),
|
||||
#[cfg(unix)]
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls to check if there are any `InternalEvent`s that can be read within the given duration.
|
||||
pub(crate) fn poll_internal<F>(timeout: Option<Duration>, filter: &F) -> std::io::Result<bool>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let (mut reader, timeout) = if let Some(timeout) = timeout {
|
||||
let poll_timeout = PollTimeout::new(Some(timeout));
|
||||
if let Some(reader) = try_lock_internal_event_reader_for(timeout) {
|
||||
(reader, poll_timeout.leftover())
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
(lock_internal_event_reader(), None)
|
||||
};
|
||||
reader.poll(timeout, filter)
|
||||
}
|
||||
|
||||
/// Reads a single `InternalEvent`.
|
||||
pub(crate) fn read_internal<F>(filter: &F) -> std::io::Result<InternalEvent>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let mut reader = lock_internal_event_reader();
|
||||
reader.read(filter)
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Represents special flags that tell compatible terminals to add extra information to keyboard events.
|
||||
///
|
||||
/// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement> 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 <https://sw.kovidgoyal.net/kitty/keyboard-protocol/> 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<KeyCode> 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<H: Hasher>(&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);
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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<InternalEvent>,
|
||||
source: Option<Box<dyn EventSource>>,
|
||||
skipped_events: Vec<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for InternalEventReader {
|
||||
fn default() -> Self {
|
||||
#[cfg(windows)]
|
||||
let source = WindowsEventSource::new();
|
||||
#[cfg(unix)]
|
||||
let source = UnixInternalEventSource::new();
|
||||
|
||||
let source = source.ok().map(|x| Box::new(x) as Box<dyn EventSource>);
|
||||
|
||||
InternalEventReader {
|
||||
source,
|
||||
events: VecDeque::with_capacity(32),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalEventReader {
|
||||
/// Returns a `Waker` allowing to wake/force the `poll` method to return `Ok(false)`.
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) fn waker(&self) -> Waker {
|
||||
self.source.as_ref().expect("reader source not set").waker()
|
||||
}
|
||||
|
||||
pub(crate) fn poll<F>(&mut self, timeout: Option<Duration>, filter: &F) -> io::Result<bool>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
for event in &self.events {
|
||||
if filter.eval(event) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let event_source = match self.source.as_mut() {
|
||||
Some(source) => source,
|
||||
None => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to initialize input reader",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let poll_timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
let maybe_event = match event_source.try_read(poll_timeout.leftover()) {
|
||||
Ok(None) => None,
|
||||
Ok(Some(event)) => {
|
||||
if filter.eval(&event) {
|
||||
Some(event)
|
||||
} else {
|
||||
self.skipped_events.push(event);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == io::ErrorKind::Interrupted {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if poll_timeout.elapsed() || maybe_event.is_some() {
|
||||
self.events.extend(self.skipped_events.drain(..));
|
||||
|
||||
if let Some(event) = maybe_event {
|
||||
self.events.push_front(event);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read<F>(&mut self, filter: &F) -> io::Result<InternalEvent>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let mut skipped_events = VecDeque::new();
|
||||
|
||||
loop {
|
||||
while let Some(event) = self.events.pop_front() {
|
||||
if filter.eval(&event) {
|
||||
while let Some(event) = skipped_events.pop_front() {
|
||||
self.events.push_back(event);
|
||||
}
|
||||
|
||||
return Ok(event);
|
||||
} else {
|
||||
// We can not directly write events back to `self.events`.
|
||||
// If we did, we would put our self's into an endless loop
|
||||
// that would enqueue -> dequeue -> enqueue etc.
|
||||
// This happens because `poll` in this function will always return true if there are events in it's.
|
||||
// And because we just put the non-fulfilling event there this is going to be the case.
|
||||
// Instead we can store them into the temporary buffer,
|
||||
// and then when the filter is fulfilled write all events back in order.
|
||||
skipped_events.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.poll(None, filter)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io;
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use super::super::filter::CursorPositionFilter;
|
||||
use super::{
|
||||
super::{filter::InternalEventFilter, Event},
|
||||
EventSource, InternalEvent, InternalEventReader,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_poll_fails_without_event_source() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(10)), &InternalEventFilter)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_true_for_matching_event_in_queue_at_front() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![InternalEvent::Event(Event::Resize(10, 10))].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_poll_returns_true_for_matching_event_in_queue_at_back() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![
|
||||
InternalEvent::Event(Event::Resize(10, 10)),
|
||||
InternalEvent::CursorPosition(10, 20),
|
||||
]
|
||||
.into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &CursorPositionFilter).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_returns_matching_event_in_queue_at_front() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_read_returns_matching_event_in_queue_at_back() {
|
||||
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![InternalEvent::Event(Event::Resize(10, 10)), CURSOR_EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_read_does_not_consume_skipped_event() {
|
||||
const SKIPPED_EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), SKIPPED_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_timeouts_if_source_has_no_events() {
|
||||
let source = FakeSource::default();
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(!reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_true_if_source_has_at_least_one_event() {
|
||||
let source = FakeSource::with_events(&[InternalEvent::Event(Event::Resize(10, 10))]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reads_returns_event_if_source_has_at_least_one_event() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_returns_events_if_source_has_events() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_false_after_all_source_events_are_consumed() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(!reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_propagates_error() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(FakeSource::new(&[]))),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.err()
|
||||
.map(|e| format!("{:?}", &e.kind())),
|
||||
Some(format!("{:?}", io::ErrorKind::Other))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_propagates_error() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(FakeSource::new(&[]))),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reader
|
||||
.read(&InternalEventFilter)
|
||||
.err()
|
||||
.map(|e| format!("{:?}", &e.kind())),
|
||||
Some(format!("{:?}", io::ErrorKind::Other))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_continues_after_error() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(reader.read(&InternalEventFilter).is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_continues_after_error() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(reader.read(&InternalEventFilter).is_err());
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeSource {
|
||||
events: VecDeque<InternalEvent>,
|
||||
error: Option<io::Error>,
|
||||
}
|
||||
|
||||
impl FakeSource {
|
||||
fn new(events: &[InternalEvent]) -> FakeSource {
|
||||
FakeSource {
|
||||
events: events.to_vec().into(),
|
||||
error: Some(io::Error::new(io::ErrorKind::Other, "")),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_events(events: &[InternalEvent]) -> FakeSource {
|
||||
FakeSource {
|
||||
events: events.to_vec().into(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for FakeSource {
|
||||
fn try_read(&mut self, _timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
// Return error if set in case there's just one remaining event
|
||||
if self.events.len() == 1 {
|
||||
if let Some(error) = self.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Return all events from the queue
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
// Return error if there're no more events
|
||||
if let Some(error) = self.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Timeout
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> super::super::sys::Waker {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Duration>) -> io::Result<Option<InternalEvent>>;
|
||||
|
||||
/// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`.
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker;
|
||||
}
|
|
@ -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;
|
|
@ -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<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result<Self> {
|
||||
let poll = Poll::new()?;
|
||||
let registry = poll.registry();
|
||||
|
||||
let tty_raw_fd = input_fd.raw_fd();
|
||||
let mut tty_ev = SourceFd(&tty_raw_fd);
|
||||
registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?;
|
||||
|
||||
let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?;
|
||||
registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let waker = Waker::new(registry, WAKE_TOKEN)?;
|
||||
|
||||
Ok(UnixInternalEventSource {
|
||||
poll,
|
||||
events: Events::with_capacity(3),
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty_fd: input_fd,
|
||||
signals,
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for UnixInternalEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) {
|
||||
// Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds.
|
||||
// Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned).
|
||||
// https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes
|
||||
if e.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if self.events.is_empty() {
|
||||
// No readiness events = timeout
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for token in self.events.iter().map(|x| x.token()) {
|
||||
match token {
|
||||
TTY_TOKEN => {
|
||||
loop {
|
||||
match self.tty_fd.read(&mut self.tty_buffer, 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<u8>,
|
||||
internal_events: VecDeque<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Parser {
|
||||
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||
// it smaller?
|
||||
//
|
||||
// Probably not worth spending more time on this as "there's a plan"
|
||||
// to use the anes crate parser.
|
||||
buffer: Vec::with_capacity(256),
|
||||
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||
// fit? What is an average sequence length? Let's guess here
|
||||
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||
// when processing large amounts of data.
|
||||
//
|
||||
// There's no need to make it bigger, because when you look at the `try_read`
|
||||
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||
// is processed -> events pushed.
|
||||
internal_events: VecDeque::with_capacity(128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||
for (idx, byte) in buffer.iter().enumerate() {
|
||||
let more = idx + 1 < buffer.len() || more;
|
||||
|
||||
self.buffer.push(*byte);
|
||||
|
||||
match parse_event(&self.buffer, more) {
|
||||
Ok(Some(ie)) => {
|
||||
self.internal_events.push_back(ie);
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event can't be parsed, because we don't have enough bytes for
|
||||
// the current sequence. Keep the buffer and process next bytes.
|
||||
}
|
||||
Err(_) => {
|
||||
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||
// Clear the buffer and continue with another sequence.
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = InternalEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.internal_events.pop_front()
|
||||
}
|
||||
}
|
|
@ -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<Self> {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
Ok(WakePipe {
|
||||
receiver,
|
||||
waker: Waker::new(sender),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||
// is enough.
|
||||
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||
|
||||
pub(crate) struct UnixInternalEventSource {
|
||||
parser: Parser,
|
||||
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||
tty: FileDesc,
|
||||
winch_signal_receiver: UnixStream,
|
||||
#[cfg(feature = "event-stream")]
|
||||
wake_pipe: WakePipe,
|
||||
}
|
||||
|
||||
fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> {
|
||||
let (receiver, sender) = UnixStream::pair()?;
|
||||
receiver.set_nonblocking(true)?;
|
||||
sender.set_nonblocking(true)?;
|
||||
Ok((receiver, sender))
|
||||
}
|
||||
|
||||
impl UnixInternalEventSource {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result<Self> {
|
||||
Ok(UnixInternalEventSource {
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty: input_fd,
|
||||
winch_signal_receiver: {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
// Unregistering is unnecessary because EventSource is a singleton
|
||||
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<usize> {
|
||||
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<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
fn make_pollfd<F: AsRawFd>(fd: &F) -> pollfd {
|
||||
pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: POLLIN,
|
||||
revents: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
];
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
make_pollfd(&self.wake_pipe.receiver),
|
||||
];
|
||||
|
||||
while timeout.leftover().map_or(true, |t| !t.is_zero()) {
|
||||
// check if there are buffered events from the last read
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
match poll(&mut fds, timeout.leftover()) {
|
||||
Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => {
|
||||
match e.kind() {
|
||||
// retry on EINTR
|
||||
io::ErrorKind::Interrupted => continue,
|
||||
_ => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("got unexpected error while polling: {:?}", e),
|
||||
))
|
||||
}
|
||||
Ok(_) => (),
|
||||
};
|
||||
if fds[0].revents & POLLIN != 0 {
|
||||
loop {
|
||||
let read_count = read_complete(&self.tty, &mut self.tty_buffer)?;
|
||||
if read_count > 0 {
|
||||
self.parser.advance(
|
||||
&self.tty_buffer[..read_count],
|
||||
read_count == TTY_BUFFER_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if fds[1].revents & POLLIN != 0 {
|
||||
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<u8>,
|
||||
internal_events: VecDeque<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Parser {
|
||||
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||
// it smaller?
|
||||
//
|
||||
// Probably not worth spending more time on this as "there's a plan"
|
||||
// to use the anes crate parser.
|
||||
buffer: Vec::with_capacity(256),
|
||||
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||
// fit? What is an average sequence length? Let's guess here
|
||||
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||
// when processing large amounts of data.
|
||||
//
|
||||
// There's no need to make it bigger, because when you look at the `try_read`
|
||||
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||
// is processed -> events pushed.
|
||||
internal_events: VecDeque::with_capacity(128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||
for (idx, byte) in buffer.iter().enumerate() {
|
||||
let more = idx + 1 < buffer.len() || more;
|
||||
|
||||
self.buffer.push(*byte);
|
||||
|
||||
match parse_event(&self.buffer, more) {
|
||||
Ok(Some(ie)) => {
|
||||
self.internal_events.push_back(ie);
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event can't be parsed, because we don't have enough bytes for
|
||||
// the current sequence. Keep the buffer and process next bytes.
|
||||
}
|
||||
Err(_) => {
|
||||
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||
// Clear the buffer and continue with another sequence.
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = InternalEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.internal_events.pop_front()
|
||||
}
|
||||
}
|
|
@ -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<u16>,
|
||||
mouse_buttons_pressed: MouseButtonsPressed,
|
||||
}
|
||||
|
||||
impl WindowsEventSource {
|
||||
pub(crate) fn new() -> std::io::Result<WindowsEventSource> {
|
||||
let console = Console::from(Handle::current_in_handle()?);
|
||||
Ok(WindowsEventSource {
|
||||
console,
|
||||
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
poll: WinApiPoll::new(),
|
||||
#[cfg(feature = "event-stream")]
|
||||
poll: WinApiPoll::new()?,
|
||||
|
||||
surrogate_buffer: None,
|
||||
mouse_buttons_pressed: MouseButtonsPressed::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for WindowsEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<InternalEvent>> {
|
||||
let poll_timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
if let Some(event_ready) = self.poll.poll(poll_timeout.leftover())? {
|
||||
let number = self.console.number_of_console_input_events()?;
|
||||
if event_ready && number != 0 {
|
||||
let event = match self.console.read_single_input_event()? {
|
||||
InputRecord::KeyEvent(record) => {
|
||||
handle_key_event(record, &mut self.surrogate_buffer)
|
||||
}
|
||||
InputRecord::MouseEvent(record) => {
|
||||
let mouse_event =
|
||||
handle_mouse_event(record, &self.mouse_buttons_pressed);
|
||||
self.mouse_buttons_pressed = MouseButtonsPressed {
|
||||
left: record.button_state.left_button(),
|
||||
right: record.button_state.right_button(),
|
||||
middle: record.button_state.middle_button(),
|
||||
};
|
||||
|
||||
mouse_event
|
||||
}
|
||||
InputRecord::WindowBufferSizeEvent(record) => {
|
||||
// windows starts counting at 0, unix at 1, add one to replicate unix behaviour.
|
||||
Some(Event::Resize(
|
||||
record.size.x as u16 + 1,
|
||||
record.size.y as u16 + 1,
|
||||
))
|
||||
}
|
||||
InputRecord::FocusEvent(record) => {
|
||||
let event = if record.set_focus {
|
||||
Event::FocusGained
|
||||
} else {
|
||||
Event::FocusLost
|
||||
};
|
||||
Some(event)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(event) = event {
|
||||
return Ok(Some(InternalEvent::Event(event)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if poll_timeout.elapsed() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker {
|
||||
self.poll.waker()
|
||||
}
|
||||
}
|
|
@ -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<Event>`.
|
||||
///
|
||||
/// **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<AtomicBool>,
|
||||
stream_wake_task_should_shutdown: Arc<AtomicBool>,
|
||||
task_sender: SyncSender<Task>,
|
||||
}
|
||||
|
||||
impl Default for EventStream {
|
||||
fn default() -> Self {
|
||||
let (task_sender, receiver) = mpsc::sync_channel::<Task>(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<AtomicBool>,
|
||||
stream_wake_task_should_shutdown: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
// 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<Event>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let result = match poll_internal(Some(Duration::from_secs(0)), &EventFilter) {
|
||||
Ok(true) => match read_internal(&EventFilter) {
|
||||
Ok(InternalEvent::Event(event)) => Poll::Ready(Some(Ok(event))),
|
||||
Err(e) => Poll::Ready(Some(Err(e))),
|
||||
#[cfg(unix)]
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Ok(false) => {
|
||||
if !self
|
||||
.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();
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) mod waker;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub(crate) mod parse;
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
|
@ -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<Mutex<::mio::Waker>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Create a new `Waker`.
|
||||
pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(mio::Waker::new(registry, waker_token)?)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||
///
|
||||
/// Readiness is set to `Ready::readable()`.
|
||||
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||
self.inner.lock().unwrap().wake()
|
||||
}
|
||||
|
||||
/// Resets the state so the same waker can be reused.
|
||||
///
|
||||
/// This function is not impl
|
||||
#[allow(dead_code, clippy::clippy::unnecessary_wraps)]
|
||||
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -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<Mutex<UnixStream>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Create a new `Waker`.
|
||||
pub(crate) fn new(writer: UnixStream) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(writer)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||
///
|
||||
/// Readiness is set to `Ready::readable()`.
|
||||
pub(crate) fn wake(&self) -> io::Result<()> {
|
||||
self.inner.lock().unwrap().write(&[0])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
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(())
|
||||
}
|
|
@ -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<Event> {
|
||||
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event, buttons_pressed) {
|
||||
return Some(Event::Mouse(event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
enum WindowsKeyEvent {
|
||||
KeyEvent(KeyEvent),
|
||||
Surrogate(u16),
|
||||
}
|
||||
|
||||
pub(crate) fn handle_key_event(
|
||||
key_event: KeyEventRecord,
|
||||
surrogate_buffer: &mut Option<u16>,
|
||||
) -> Option<Event> {
|
||||
let windows_key_event = parse_key_event_record(&key_event)?;
|
||||
match windows_key_event {
|
||||
WindowsKeyEvent::KeyEvent(key_event) => {
|
||||
// Discard any buffered surrogate value if another valid key event comes before the
|
||||
// next surrogate value.
|
||||
*surrogate_buffer = None;
|
||||
Some(Event::Key(key_event))
|
||||
}
|
||||
WindowsKeyEvent::Surrogate(new_surrogate) => {
|
||||
let ch = handle_surrogate(surrogate_buffer, new_surrogate)?;
|
||||
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||
let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers);
|
||||
Some(Event::Key(key_event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_surrogate(surrogate_buffer: &mut Option<u16>, new_surrogate: u16) -> Option<char> {
|
||||
match *surrogate_buffer {
|
||||
Some(buffered_surrogate) => {
|
||||
*surrogate_buffer = None;
|
||||
std::char::decode_utf16([buffered_surrogate, new_surrogate])
|
||||
.next()
|
||||
.unwrap()
|
||||
.ok()
|
||||
}
|
||||
None => {
|
||||
*surrogate_buffer = Some(new_surrogate);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ControlKeyState> for KeyModifiers {
|
||||
fn from(state: &ControlKeyState) -> Self {
|
||||
let shift = state.has_state(SHIFT_PRESSED);
|
||||
let alt = state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
|
||||
let control = state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
|
||||
|
||||
let mut modifier = KeyModifiers::empty();
|
||||
|
||||
if shift {
|
||||
modifier |= KeyModifiers::SHIFT;
|
||||
}
|
||||
if control {
|
||||
modifier |= KeyModifiers::CONTROL;
|
||||
}
|
||||
if alt {
|
||||
modifier |= KeyModifiers::ALT;
|
||||
}
|
||||
|
||||
modifier
|
||||
}
|
||||
}
|
||||
|
||||
enum CharCase {
|
||||
LowerCase,
|
||||
UpperCase,
|
||||
}
|
||||
|
||||
fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char {
|
||||
match desired_case {
|
||||
CharCase::LowerCase if ch.is_uppercase() => {
|
||||
let mut iter = ch.to_lowercase();
|
||||
// Unwrap is safe; iterator yields one or more chars.
|
||||
let ch_lower = iter.next().unwrap();
|
||||
if iter.next().is_none() {
|
||||
ch_lower
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
}
|
||||
CharCase::UpperCase if ch.is_lowercase() => {
|
||||
let mut iter = ch.to_uppercase();
|
||||
// Unwrap is safe; iterator yields one or more chars.
|
||||
let ch_upper = iter.next().unwrap();
|
||||
if iter.next().is_none() {
|
||||
ch_upper
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
}
|
||||
_ => ch,
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to return the character for a key event accounting for the user's keyboard layout.
|
||||
// The returned character (if any) is capitalized (if applicable) based on shift and capslock state.
|
||||
// Returns None if the key doesn't map to a character or if it is a dead key.
|
||||
// We use the *currently* active keyboard layout (if it can be determined). This layout may not
|
||||
// correspond to the keyboard layout that was active when the user typed their input, since console
|
||||
// applications get their input asynchronously from the terminal. By the time a console application
|
||||
// can process a key input, the user may have changed the active layout. In this case, the character
|
||||
// returned might not correspond to what the user expects, but there is no way for a console
|
||||
// application to know what the keyboard layout actually was for a key event, so this is our best
|
||||
// effort. If a console application processes input in a timely fashion, then it is unlikely that a
|
||||
// user has time to change their keyboard layout before a key event is processed.
|
||||
fn get_char_for_key(key_event: &KeyEventRecord) -> Option<char> {
|
||||
let virtual_key_code = key_event.virtual_key_code as u32;
|
||||
let virtual_scan_code = key_event.virtual_scan_code as u32;
|
||||
let key_state = [0u8; 256];
|
||||
let mut utf16_buf = [0u16, 16];
|
||||
let dont_change_kernel_keyboard_state = 0x4;
|
||||
|
||||
// Best-effort attempt at determining the currently active keyboard layout.
|
||||
// At the time of writing, this works for a console application running in Windows Terminal, but
|
||||
// doesn't work under a Conhost terminal. For Conhost, the window handle returned by
|
||||
// GetForegroundWindow() does not appear to actually be the foreground window which has the
|
||||
// keyboard layout associated with it (or perhaps it is, but also has special protection that
|
||||
// doesn't allow us to query it).
|
||||
// When this determination fails, the returned keyboard layout handle will be null, which is an
|
||||
// acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx
|
||||
// appears to use the keyboard layout associated with the current thread, which will be the
|
||||
// layout that was inherited when the console application started (or possibly when the current
|
||||
// thread was spawned). This is then unfortunately not updated when the user changes their
|
||||
// keyboard layout in the terminal, but it's what we get.
|
||||
let active_keyboard_layout = unsafe {
|
||||
let foreground_window = GetForegroundWindow();
|
||||
let foreground_thread = GetWindowThreadProcessId(foreground_window, std::ptr::null_mut());
|
||||
GetKeyboardLayout(foreground_thread)
|
||||
};
|
||||
|
||||
let ret = unsafe {
|
||||
ToUnicodeEx(
|
||||
virtual_key_code,
|
||||
virtual_scan_code,
|
||||
key_state.as_ptr(),
|
||||
utf16_buf.as_mut_ptr(),
|
||||
utf16_buf.len() as i32,
|
||||
dont_change_kernel_keyboard_state,
|
||||
active_keyboard_layout,
|
||||
)
|
||||
};
|
||||
|
||||
// -1 indicates a dead key.
|
||||
// 0 indicates no character for this key.
|
||||
if ret < 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize));
|
||||
let mut ch = ch_iter.next()?.ok()?;
|
||||
if ch_iter.next().is_some() {
|
||||
// Key doesn't map to a single char.
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_shift_pressed = key_event.control_key_state.has_state(SHIFT_PRESSED);
|
||||
let is_capslock_on = key_event.control_key_state.has_state(CAPSLOCK_ON);
|
||||
let desired_case = if is_shift_pressed ^ is_capslock_on {
|
||||
CharCase::UpperCase
|
||||
} else {
|
||||
CharCase::LowerCase
|
||||
};
|
||||
ch = try_ensure_char_case(ch, desired_case);
|
||||
Some(ch)
|
||||
}
|
||||
|
||||
fn parse_key_event_record(key_event: &KeyEventRecord) -> Option<WindowsKeyEvent> {
|
||||
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||
let virtual_key_code = key_event.virtual_key_code as i32;
|
||||
|
||||
// We normally ignore all key release events, but we will make an exception for an Alt key
|
||||
// release if it carries a u_char value, as this indicates an Alt code.
|
||||
let is_alt_code = virtual_key_code == VK_MENU && !key_event.key_down && key_event.u_char != 0;
|
||||
if is_alt_code {
|
||||
let utf16 = key_event.u_char;
|
||||
match utf16 {
|
||||
surrogate @ 0xD800..=0xDFFF => {
|
||||
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||
}
|
||||
unicode_scalar_value => {
|
||||
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||
// values.
|
||||
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||
let key_code = KeyCode::Char(ch);
|
||||
let kind = if key_event.key_down {
|
||||
KeyEventKind::Press
|
||||
} else {
|
||||
KeyEventKind::Release
|
||||
};
|
||||
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't generate events for numpad key presses when they're producing Alt codes.
|
||||
let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&virtual_key_code);
|
||||
let is_only_alt_modifier = modifiers.contains(KeyModifiers::ALT)
|
||||
&& !modifiers.contains(KeyModifiers::SHIFT | KeyModifiers::CONTROL);
|
||||
if is_only_alt_modifier && is_numpad_numeric_key {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parse_result = match virtual_key_code {
|
||||
VK_SHIFT | VK_CONTROL | VK_MENU => None,
|
||||
VK_BACK => Some(KeyCode::Backspace),
|
||||
VK_ESCAPE => Some(KeyCode::Esc),
|
||||
VK_RETURN => Some(KeyCode::Enter),
|
||||
VK_F1..=VK_F24 => Some(KeyCode::F((key_event.virtual_key_code - 111) as u8)),
|
||||
VK_LEFT => Some(KeyCode::Left),
|
||||
VK_UP => Some(KeyCode::Up),
|
||||
VK_RIGHT => Some(KeyCode::Right),
|
||||
VK_DOWN => Some(KeyCode::Down),
|
||||
VK_PRIOR => Some(KeyCode::PageUp),
|
||||
VK_NEXT => Some(KeyCode::PageDown),
|
||||
VK_HOME => Some(KeyCode::Home),
|
||||
VK_END => Some(KeyCode::End),
|
||||
VK_DELETE => Some(KeyCode::Delete),
|
||||
VK_INSERT => Some(KeyCode::Insert),
|
||||
VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab),
|
||||
VK_TAB => Some(KeyCode::Tab),
|
||||
_ => {
|
||||
let utf16 = key_event.u_char;
|
||||
match utf16 {
|
||||
0x00..=0x1f => {
|
||||
// Some key combinations generate either no u_char value or generate control
|
||||
// codes. To deliver back a KeyCode::Char(...) event we want to know which
|
||||
// character the key normally maps to on the user's keyboard layout.
|
||||
// The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.)
|
||||
// are handled by their virtual key codes above.
|
||||
get_char_for_key(key_event).map(KeyCode::Char)
|
||||
}
|
||||
surrogate @ 0xD800..=0xDFFF => {
|
||||
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||
}
|
||||
unicode_scalar_value => {
|
||||
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||
// values.
|
||||
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||
Some(KeyCode::Char(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(key_code) = parse_result {
|
||||
let kind = if key_event.key_down {
|
||||
KeyEventKind::Press
|
||||
} else {
|
||||
KeyEventKind::Release
|
||||
};
|
||||
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// The 'y' position of a mouse event or resize event is not relative to the window but absolute to screen buffer.
|
||||
// This means that when the mouse cursor is at the top left it will be x: 0, y: 2295 (e.g. y = number of cells conting from the absolute buffer height) instead of relative x: 0, y: 0 to the window.
|
||||
pub fn parse_relative_y(y: i16) -> std::io::Result<i16> {
|
||||
let window_size = ScreenBuffer::current()?.info()?.terminal_window();
|
||||
Ok(y - window_size.top)
|
||||
}
|
||||
|
||||
fn parse_mouse_event_record(
|
||||
event: &crossterm_winapi::MouseEvent,
|
||||
buttons_pressed: &MouseButtonsPressed,
|
||||
) -> std::io::Result<Option<MouseEvent>> {
|
||||
let modifiers = KeyModifiers::from(&event.control_key_state);
|
||||
|
||||
let xpos = event.mouse_position.x as u16;
|
||||
let ypos = parse_relative_y(event.mouse_position.y)? as u16;
|
||||
|
||||
let button_state = event.button_state;
|
||||
|
||||
let kind = match event.event_flags {
|
||||
EventFlags::PressOrRelease | EventFlags::DoubleClick => {
|
||||
if button_state.left_button() && !buttons_pressed.left {
|
||||
Some(MouseEventKind::Down(MouseButton::Left))
|
||||
} else if !button_state.left_button() && buttons_pressed.left {
|
||||
Some(MouseEventKind::Up(MouseButton::Left))
|
||||
} else if button_state.right_button() && !buttons_pressed.right {
|
||||
Some(MouseEventKind::Down(MouseButton::Right))
|
||||
} else if !button_state.right_button() && buttons_pressed.right {
|
||||
Some(MouseEventKind::Up(MouseButton::Right))
|
||||
} else if button_state.middle_button() && !buttons_pressed.middle {
|
||||
Some(MouseEventKind::Down(MouseButton::Middle))
|
||||
} else if !button_state.middle_button() && buttons_pressed.middle {
|
||||
Some(MouseEventKind::Up(MouseButton::Middle))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventFlags::MouseMoved => {
|
||||
let button = if button_state.right_button() {
|
||||
MouseButton::Right
|
||||
} else if button_state.middle_button() {
|
||||
MouseButton::Middle
|
||||
} else {
|
||||
MouseButton::Left
|
||||
};
|
||||
if button_state.release_button() {
|
||||
Some(MouseEventKind::Moved)
|
||||
} else {
|
||||
Some(MouseEventKind::Drag(button))
|
||||
}
|
||||
}
|
||||
EventFlags::MouseWheeled => {
|
||||
// Vertical scroll
|
||||
// from https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str
|
||||
// if `button_state` is negative then the wheel was rotated backward, toward the user.
|
||||
if button_state.scroll_down() {
|
||||
Some(MouseEventKind::ScrollDown)
|
||||
} else if button_state.scroll_up() {
|
||||
Some(MouseEventKind::ScrollUp)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventFlags::MouseHwheeled => {
|
||||
if button_state.scroll_left() {
|
||||
Some(MouseEventKind::ScrollLeft)
|
||||
} else if button_state.scroll_right() {
|
||||
Some(MouseEventKind::ScrollRight)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(kind.map(|kind| MouseEvent {
|
||||
kind,
|
||||
column: xpos,
|
||||
row: ypos,
|
||||
modifiers,
|
||||
}))
|
||||
}
|
|
@ -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<WinApiPoll> {
|
||||
Ok(WinApiPoll {
|
||||
waker: Waker::new()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WinApiPoll {
|
||||
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<bool>> {
|
||||
let dw_millis = if let Some(duration) = timeout {
|
||||
duration.as_millis() as u32
|
||||
} else {
|
||||
INFINITE
|
||||
};
|
||||
|
||||
let console_handle = Handle::current_in_handle()?;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let semaphore = self.waker.semaphore();
|
||||
#[cfg(feature = "event-stream")]
|
||||
let handles = &[*console_handle, **semaphore.handle()];
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
let handles = &[*console_handle];
|
||||
|
||||
let output =
|
||||
unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) };
|
||||
|
||||
match output {
|
||||
output if output == WAIT_OBJECT_0 => {
|
||||
// input handle triggered
|
||||
Ok(Some(true))
|
||||
}
|
||||
#[cfg(feature = "event-stream")]
|
||||
output if output == WAIT_OBJECT_0 + 1 => {
|
||||
// semaphore handle triggered
|
||||
let _ = self.waker.reset();
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Poll operation was woken up by `Waker::wake`",
|
||||
))
|
||||
}
|
||||
WAIT_TIMEOUT | WAIT_ABANDONED_0 => {
|
||||
// timeout elapsed
|
||||
Ok(None)
|
||||
}
|
||||
WAIT_FAILED => Err(io::Error::last_os_error()),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"WaitForMultipleObjects returned unexpected result.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub fn waker(&self) -> Waker {
|
||||
self.waker.clone()
|
||||
}
|
||||
}
|
|
@ -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<Mutex<Semaphore>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Creates a new waker.
|
||||
///
|
||||
/// `Waker` is based on the `Semaphore`. You have to use the semaphore
|
||||
/// handle along with the `WaitForMultipleObjects`.
|
||||
pub(crate) fn new() -> std::io::Result<Self> {
|
||||
let inner = Semaphore::new()?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wakes the `WaitForMultipleObjects`.
|
||||
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||
self.inner.lock().unwrap().release()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the current semaphore with a new one allowing us to reuse the same `Waker`.
|
||||
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||
*self.inner.lock().unwrap() = Semaphore::new()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the semaphore associated with the waker.
|
||||
pub(crate) fn semaphore(&self) -> Semaphore {
|
||||
self.inner.lock().unwrap().clone()
|
||||
}
|
||||
}
|
|
@ -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<Duration>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl PollTimeout {
|
||||
/// Constructs a new `PollTimeout` with the given optional `Duration`.
|
||||
pub fn new(timeout: Option<Duration>) -> PollTimeout {
|
||||
PollTimeout {
|
||||
timeout,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the timeout has elapsed.
|
||||
///
|
||||
/// It always returns `false` if the initial timeout was set to `None`.
|
||||
pub fn elapsed(&self) -> bool {
|
||||
self.timeout
|
||||
.map(|timeout| self.start.elapsed() >= timeout)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns the timeout leftover (initial timeout duration - elapsed duration).
|
||||
pub fn leftover(&self) -> Option<Duration> {
|
||||
self.timeout.map(|timeout| {
|
||||
let elapsed = self.start.elapsed();
|
||||
|
||||
if elapsed >= timeout {
|
||||
Duration::from_secs(0)
|
||||
} else {
|
||||
timeout - elapsed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::PollTimeout;
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_without_duration_does_not_have_leftover() {
|
||||
let timeout = PollTimeout::new(None);
|
||||
assert_eq!(timeout.leftover(), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_without_duration_never_elapses() {
|
||||
let timeout = PollTimeout::new(None);
|
||||
assert!(!timeout.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_elapses() {
|
||||
const TIMEOUT_MILLIS: u64 = 100;
|
||||
|
||||
let timeout = PollTimeout {
|
||||
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||
};
|
||||
|
||||
assert!(timeout.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_elapsed_timeout_has_zero_leftover() {
|
||||
const TIMEOUT_MILLIS: u64 = 100;
|
||||
|
||||
let timeout = PollTimeout {
|
||||
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||
};
|
||||
|
||||
assert!(timeout.elapsed());
|
||||
assert_eq!(timeout.leftover(), Some(Duration::from_millis(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_not_elapsed_timeout_has_positive_leftover() {
|
||||
let timeout = PollTimeout::new(Some(Duration::from_secs(60)));
|
||||
|
||||
assert!(!timeout.elapsed());
|
||||
assert!(timeout.leftover().unwrap() > Duration::from_secs(0));
|
||||
}
|
||||
}
|
|
@ -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.");
|
|
@ -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<usize> {
|
||||
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"))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<D: Display>(val: D) -> StyledContent<D> {
|
||||
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<D: Display>(pub StyledContent<D>);
|
||||
|
||||
impl<D: Display> Command for PrintStyledContent<D> {
|
||||
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<T: Display>(pub T);
|
||||
|
||||
impl<T: Display> Command for Print<T> {
|
||||
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<T: Display> Display for Print<T> {
|
||||
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<String>);
|
||||
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<Item = &'a str>) -> Option<u8> {
|
||||
iter.next().and_then(|s| s.parse().ok())
|
||||
}
|
|
@ -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<Attribute> 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<Attribute> 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<Attribute> 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<Attribute> 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));
|
||||
}
|
||||
}
|
|
@ -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<Color>,
|
||||
/// The background color.
|
||||
pub background_color: Option<Color>,
|
||||
/// The underline color.
|
||||
pub underline_color: Option<Color>,
|
||||
/// List of attributes.
|
||||
pub attributes: Attributes,
|
||||
}
|
||||
|
||||
impl ContentStyle {
|
||||
/// Creates a `StyledContent` by applying the style to the given `val`.
|
||||
#[inline]
|
||||
pub fn apply<D: Display>(self, val: D) -> StyledContent<D> {
|
||||
StyledContent::new(self, val)
|
||||
}
|
||||
|
||||
/// Creates a new `ContentStyle`.
|
||||
#[inline]
|
||||
pub fn new() -> ContentStyle {
|
||||
ContentStyle::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ContentStyle> for ContentStyle {
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl AsMut<ContentStyle> for ContentStyle {
|
||||
fn as_mut(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -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<D: Display> {
|
||||
/// The style (colors, content attributes).
|
||||
style: ContentStyle,
|
||||
/// A content to apply the style on.
|
||||
content: D,
|
||||
}
|
||||
|
||||
impl<D: Display> StyledContent<D> {
|
||||
/// Creates a new `StyledContent`.
|
||||
#[inline]
|
||||
pub fn new(style: ContentStyle, content: D) -> StyledContent<D> {
|
||||
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<D: Display> AsRef<ContentStyle> for StyledContent<D> {
|
||||
fn as_ref(&self) -> &ContentStyle {
|
||||
&self.style
|
||||
}
|
||||
}
|
||||
impl<D: Display> AsMut<ContentStyle> for StyledContent<D> {
|
||||
fn as_mut(&mut self) -> &mut ContentStyle {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Display> Display for StyledContent<D> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
crate::command::execute_fmt(
|
||||
f,
|
||||
PrintStyledContent(StyledContent {
|
||||
style: self.style,
|
||||
content: &self.content,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<ContentStyle> + AsMut<ContentStyle>;
|
||||
|
||||
/// 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<Self>;
|
||||
#[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<D: Display> Stylize for StyledContent<D> {
|
||||
type Styled = StyledContent<D>;
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
|
@ -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<Colored> 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::<u16>::into(colored), FG_INTENSITY | FG_RED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_bg_color() {
|
||||
let colored = Colored::BackgroundColor(Color::Red);
|
||||
assert_eq!(Into::<u16>::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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors};
|
||||
|
||||
mod attribute;
|
||||
mod color;
|
||||
mod colored;
|
||||
mod colors;
|
|
@ -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<Item = Attribute> {
|
||||
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 <https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters>
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
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<Item = &'a str>) -> Option<Self> {
|
||||
let color = match parse_next_u8(values)? {
|
||||
// 8 bit colors: `5;<n>`
|
||||
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;<r>;<g>;<b>`
|
||||
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<Self, Self::Error> {
|
||||
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<Self, Self::Err> {
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Color, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Color, E>
|
||||
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::<u8>();
|
||||
|
||||
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::<Vec<String>>();
|
||||
|
||||
if results.len() == 3 {
|
||||
let r = results[0].parse::<u8>();
|
||||
let g = results[1].parse::<u8>();
|
||||
let b = results[2].parse::<u8>();
|
||||
|
||||
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::<Color>("\"Reset\"").unwrap(),
|
||||
Color::Reset
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"reset\"").unwrap(),
|
||||
Color::Reset
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"Red\"").unwrap(),
|
||||
Color::Red
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"red\"").unwrap(),
|
||||
Color::Red
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_red\"").unwrap(),
|
||||
Color::DarkRed
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"grey\"").unwrap(),
|
||||
Color::Grey
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_grey\"").unwrap(),
|
||||
Color::DarkGrey
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"green\"").unwrap(),
|
||||
Color::Green
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_green\"").unwrap(),
|
||||
Color::DarkGreen
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"yellow\"").unwrap(),
|
||||
Color::Yellow
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_yellow\"").unwrap(),
|
||||
Color::DarkYellow
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"blue\"").unwrap(),
|
||||
Color::Blue
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_blue\"").unwrap(),
|
||||
Color::DarkBlue
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"magenta\"").unwrap(),
|
||||
Color::Magenta
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_magenta\"").unwrap(),
|
||||
Color::DarkMagenta
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"cyan\"").unwrap(),
|
||||
Color::Cyan
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"dark_cyan\"").unwrap(),
|
||||
Color::DarkCyan
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"white\"").unwrap(),
|
||||
Color::White
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"black\"").unwrap(),
|
||||
Color::Black
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_unknown_color_conversion() {
|
||||
assert!(serde_json::from_str::<Color>("\"unknown\"").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_ansi_value() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"ansi_(255)\"").unwrap(),
|
||||
Color::AnsiValue(255)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_unvalid_ansi_value() {
|
||||
assert!(serde_json::from_str::<Color>("\"ansi_(256)\"").is_err());
|
||||
assert!(serde_json::from_str::<Color>("\"ansi_(-1)\"").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_rgb() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"rgb_(255,255,255)\"").unwrap(),
|
||||
Color::from((255, 255, 255))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_unvalid_rgb() {
|
||||
assert!(serde_json::from_str::<Color>("\"rgb_(255,255,255,255)\"").is_err());
|
||||
assert!(serde_json::from_str::<Color>("\"rgb_(256,255,255)\"").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_rgb_hex() {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"#ffffff\"").unwrap(),
|
||||
Color::from((255, 255, 255))
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>("\"#FFFFFF\"").unwrap(),
|
||||
Color::from((255, 255, 255))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserial_unvalid_rgb_hex() {
|
||||
assert!(serde_json::from_str::<Color>("\"#FFFFFFFF\"").is_err());
|
||||
assert!(serde_json::from_str::<Color>("\"#FFGFFF\"").is_err());
|
||||
// Ferris is 4 bytes so this will be considered the correct length.
|
||||
assert!(serde_json::from_str::<Color>("\"#ff🦀\"").is_err());
|
||||
}
|
||||
}
|
|
@ -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 [ <str> 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<Self> {
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -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<Color>,
|
||||
pub background: Option<Color>,
|
||||
}
|
||||
|
||||
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<Colored> 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<bool> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Ok(sys::is_raw_mode_enabled())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
sys::is_raw_mode_enabled()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables raw mode.
|
||||
///
|
||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||
pub fn enable_raw_mode() -> io::Result<()> {
|
||||
sys::enable_raw_mode()
|
||||
}
|
||||
|
||||
/// Disables raw mode.
|
||||
///
|
||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||
pub fn disable_raw_mode() -> io::Result<()> {
|
||||
sys::disable_raw_mode()
|
||||
}
|
||||
|
||||
/// Returns the terminal size `(columns, rows)`.
|
||||
///
|
||||
/// The top left cell is represented `(1, 1)`.
|
||||
pub fn size() -> io::Result<(u16, u16)> {
|
||||
sys::size()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowSize {
|
||||
pub rows: u16,
|
||||
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<WindowSize> {
|
||||
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<T>(pub T);
|
||||
|
||||
impl<T: fmt::Display> Command for SetTitle<T> {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, "\x1B]0;{}\x07", &self.0)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::set_window_title(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that instructs the terminal emulator to begin a synchronized frame.
|
||||
///
|
||||
/// # 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());
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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<usize> {
|
||||
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<FileDesc> {
|
||||
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
|
||||
(libc::STDIN_FILENO, false)
|
||||
} else {
|
||||
(
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/tty")?
|
||||
.into_raw_fd(),
|
||||
true,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(FileDesc::new(fd, close_on_drop))
|
||||
}
|
|
@ -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<Option<Termios>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> bool {
|
||||
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
||||
}
|
||||
|
||||
impl From<winsize> for WindowSize {
|
||||
fn from(size: winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
||||
let mut size = winsize {
|
||||
ws_row: 0,
|
||||
ws_col: 0,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.raw_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
STDOUT_FILENO
|
||||
};
|
||||
|
||||
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
||||
return Ok(size.into());
|
||||
}
|
||||
|
||||
Err(std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[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<bool> {
|
||||
if is_raw_mode_enabled() {
|
||||
read_supports_keyboard_enhancement_raw()
|
||||
} else {
|
||||
read_supports_keyboard_enhancement_flags()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn read_supports_keyboard_enhancement_flags() -> io::Result<bool> {
|
||||
enable_raw_mode()?;
|
||||
let flags = read_supports_keyboard_enhancement_raw();
|
||||
disable_raw_mode()?;
|
||||
flags
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn read_supports_keyboard_enhancement_raw() -> io::Result<bool> {
|
||||
use crate::event::{
|
||||
filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
|
||||
poll_internal, read_internal, InternalEvent,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
// This is the recommended method for testing support for the keyboard enhancement protocol.
|
||||
// We send a query for the flags supported by the terminal and then the primary device attributes
|
||||
// query. If we receive the primary device attributes response but not the keyboard enhancement
|
||||
// flags, none of the flags are supported.
|
||||
//
|
||||
// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
|
||||
|
||||
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
|
||||
// ESC [ c Query primary device attributes.
|
||||
const QUERY: &[u8] = b"\x1B[?u\x1B[c";
|
||||
|
||||
let result = File::open("/dev/tty").and_then(|mut file| {
|
||||
file.write_all(QUERY)?;
|
||||
file.flush()
|
||||
});
|
||||
if result.is_err() {
|
||||
let mut stdout = io::stdout();
|
||||
stdout.write_all(QUERY)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match poll_internal(
|
||||
Some(Duration::from_millis(2000)),
|
||||
&KeyboardEnhancementFlagsFilter,
|
||||
) {
|
||||
Ok(true) => {
|
||||
match read_internal(&KeyboardEnhancementFlagsFilter) {
|
||||
Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => {
|
||||
// Flush the PrimaryDeviceAttributes out of the event queue.
|
||||
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
||||
return Ok(true);
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The keyboard enhancement status could not be read within a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// execute tput with the given argument and parse
|
||||
/// the output as a u16.
|
||||
///
|
||||
/// The arg should be "cols" or "lines"
|
||||
fn tput_value(arg: &str) -> Option<u16> {
|
||||
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
||||
let value = output
|
||||
.stdout
|
||||
.into_iter()
|
||||
.filter_map(|b| char::from(b).to_digit(10))
|
||||
.fold(0, |v, n| v * 10 + n as u16);
|
||||
|
||||
if value > 0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size of the screen as determined by tput.
|
||||
///
|
||||
/// This alternate way of computing the size is useful
|
||||
/// when in a subshell.
|
||||
fn tput_size() -> Option<(u16, u16)> {
|
||||
match (tput_value("cols"), tput_value("lines")) {
|
||||
(Some(w), Some(h)) => Some((w, h)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// 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<Termios> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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<bool> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
Ok(
|
||||
// check none of the "not raw" bits is set
|
||||
dw_mode & NOT_RAW_MODE_MASK == 0,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn enable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode & !NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn disable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode | NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
let terminal_size = ScreenBuffer::current()?.info()?.terminal_size();
|
||||
// windows starts counting at 0, unix at 1, add one to replicated unix behaviour.
|
||||
Ok((
|
||||
(terminal_size.width + 1) as u16,
|
||||
(terminal_size.height + 1) as u16,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Window pixel size not implemented for the Windows API.",
|
||||
))
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// This always returns `Ok(false)` on Windows.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let pos = csbi.cursor_pos();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
let current_attribute = csbi.attributes();
|
||||
|
||||
match clear_type {
|
||||
ClearType::All => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
}
|
||||
ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?,
|
||||
ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?,
|
||||
_ => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
} //TODO: make purge flush the entire screen buffer not just the visible window.
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_up(row_count: u16) -> std::io::Result<()> {
|
||||
let csbi = ScreenBuffer::current()?;
|
||||
let mut window = csbi.info()?.terminal_window();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.top >= count {
|
||||
window.top -= count; // move top down
|
||||
window.bottom -= count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_down(row_count: u16) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
let mut window = csbi.terminal_window();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.bottom < buffer_size.height - count {
|
||||
window.top += count; // move top down
|
||||
window.bottom += count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_size(width: u16, height: u16) -> std::io::Result<()> {
|
||||
if width <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
if height <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
// get the position of the current console window
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let console = Console::from(screen_buffer.handle().clone());
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let current_size = csbi.buffer_size();
|
||||
let window = csbi.terminal_window();
|
||||
|
||||
let mut new_size = Size::new(current_size.width, current_size.height);
|
||||
|
||||
// If the buffer is smaller than this new window size, resize the
|
||||
// buffer to be large enough. Include window position.
|
||||
let mut resize_buffer = false;
|
||||
|
||||
let width = width as i16;
|
||||
if current_size.width < window.left + width {
|
||||
if window.left >= i16::max_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<u16>);
|
||||
impl Write for Utf16Encoder {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0.extend(s.encode_utf16());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut title_utf16 = Utf16Encoder(Vec::new());
|
||||
write!(title_utf16, "{title}").expect("formatting failed");
|
||||
title_utf16.0.push(0);
|
||||
let title = title_utf16.0;
|
||||
|
||||
let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
|
||||
if result != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_after_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (mut x, mut y) = (location.x, location.y);
|
||||
|
||||
// if cursor position is at the outer right position
|
||||
if x > buffer_size.width {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_before_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (xpos, ypos) = (location.x, location.y);
|
||||
|
||||
// one cell after cursor position
|
||||
let x = 0;
|
||||
// one at row of cursor position
|
||||
let y = 0;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
|
||||
|
||||
// clear everything before cursor position
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> {
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, 0);
|
||||
|
||||
// clear the entire screen
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 0,0
|
||||
cursor::sys::move_to(0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_current_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, location.y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32;
|
||||
|
||||
// clear the whole current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 1 on current row
|
||||
cursor::sys::move_to(0, location.y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_until_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (x, y) = (location.x, location.y);
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width - x) as u32;
|
||||
|
||||
// clear until the current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at original cursor position before we did the clearing
|
||||
cursor::sys::move_to(x as u16, y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_winapi(
|
||||
start_location: Coord,
|
||||
cells_to_write: u32,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let console = Console::from(Handle::current_out_handle()?);
|
||||
console.fill_whit_character(start_location, cells_to_write, ' ')?;
|
||||
console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// Create a new screen buffer to avoid changing the terminal the test
|
||||
// is running within.
|
||||
pub fn temp_screen_buffer() -> std::io::Result<ScreenBuffer> {
|
||||
let alternate_screen = ScreenBuffer::create()?;
|
||||
alternate_screen.show().unwrap();
|
||||
Ok(alternate_screen)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
|
||||
use crossterm_winapi::ScreenBuffer;
|
||||
use serial_test::serial;
|
||||
use winapi::um::wincon::GetConsoleTitleW;
|
||||
|
||||
use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer};
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_resize_winapi_20_21() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
// The values 20 and 21 are arbitrary and different from each other
|
||||
// just to see they're not crossed over.
|
||||
set_size(20, 21).unwrap();
|
||||
assert_eq!((20, 21), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// This is similar to test_resize_winapi_20_21() above. This verifies that
|
||||
// another test of similar functionality runs independently (that a testing
|
||||
// race condition has been addressed).
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
fn test_resize_winapi_30_31() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
set_size(30, 31).unwrap();
|
||||
assert_eq!((30, 31), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_down_winapi() {
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_down(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top + 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom + 2);
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_up_winapi() {
|
||||
// move the terminal buffer down before moving it up
|
||||
test_scroll_down_winapi();
|
||||
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_up(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top - 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom - 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_set_title_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let test_title = "this is a crossterm test title";
|
||||
set_window_title(test_title).unwrap();
|
||||
|
||||
let mut raw = [0_u16; 128];
|
||||
let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
|
||||
assert_ne!(0, length);
|
||||
|
||||
let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
|
||||
assert_eq!(test_title, &console_title[..]);
|
||||
}
|
||||
}
|
|
@ -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<S: AsRawFd> 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<S: AsRawHandle> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let mut mode = 0;
|
||||
let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) };
|
||||
ok == 1
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue