fix: refresh diff colors on terminal theme changes
This commit is contained in:
Generated
+72
-2
@@ -79,6 +79,9 @@ name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -214,18 +217,24 @@ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags 2.13.2",
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"filedescriptor",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"temp-env",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -433,6 +442,17 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
@@ -1238,12 +1258,27 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -1294,6 +1329,32 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555"
|
||||
dependencies = [
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"scc",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -1451,6 +1512,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temp-env"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termina"
|
||||
version = "0.3.3"
|
||||
|
||||
@@ -29,3 +29,11 @@ tree-sitter-typescript = "0.23.2"
|
||||
tree-sitter-yaml = "0.7.2"
|
||||
unicode-segmentation = "1.12"
|
||||
unicode-width = "0.2"
|
||||
|
||||
[patch.crates-io]
|
||||
# Remove once upstream Crossterm decodes DEC 2031 color-scheme notifications.
|
||||
crossterm = { path = "vendor/crossterm" }
|
||||
|
||||
[workspace]
|
||||
members = ["vendor/crossterm"]
|
||||
default-members = ["."]
|
||||
|
||||
@@ -6,7 +6,7 @@ Contributor-facing module boundaries and implementation contracts. Project termi
|
||||
|
||||
## Module ownership
|
||||
|
||||
One crate; no workspace or terminal-coordinate types in the editing backend:
|
||||
One editor crate; the workspace also includes its patched Crossterm dependency. Terminal-coordinate types remain outside the editing backend:
|
||||
|
||||
- `editor`: owns buffer lifecycle and per-buffer text, saved state, primary/selection set, modes, search, and undo/redo; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, key-sequence/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
||||
@@ -46,11 +46,13 @@ Frontends borrow `snippet_selections()` for the active argument's end-exclusive
|
||||
|
||||
## Terminal lifecycle and layout
|
||||
|
||||
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, mouse capture, focus reporting, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
||||
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, mouse capture, focus reporting, DEC 2031 color-scheme notifications, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
||||
|
||||
Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-update commands (mode 2026) in a reusable byte buffer, then write and flush the complete frame to the terminal. Frame computation never suspends terminal presentation; synchronization covers transmitted painting rather than layout/highlighting work. The hardware cursor is hidden before painting even when synchronization is unsupported. Terminal cleanup releases synchronization before leaving the alternate screen or invoking caller panic diagnostics. Backend-writer access is enabled solely for this staging seam.
|
||||
|
||||
`terminal_colors` intercepts crossterm's key-shaped OSC 11/4 replies before editor dispatch, assembles a bounded RGB background/red/green triple, and preserves unrelated events and opaque bracketed paste. Queries never block input and start after painting, so syntax initialization does not spend their reply deadline. Startup and focus-regain queries replace cached diff tints atomically; an unanswered/partial query expires after 250 ms and removes stale tints rather than assuming a dark theme. Valid late replies are consumed without becoming editing commands. The frontend precomputes rounded RGB blends from the original background (12% row, 28% character), propagating them to active, parked, and newly created panes.
|
||||
`terminal_colors` intercepts crossterm's key-shaped OSC 11/4 replies before editor dispatch, assembles a bounded RGB background/red/green triple, and preserves unrelated events and opaque bracketed paste. Queries never block input and start after painting, so syntax initialization does not spend their reply deadline. Startup, color-scheme notifications, and fallback focus-regain queries replace cached diff tints atomically; an unanswered/partial query expires after 250 ms and removes stale tints rather than assuming a dark theme. Valid late replies are consumed without becoming editing commands. The frontend precomputes rounded RGB blends from the original background (18% row, 38% character), propagating them to active, parked, and newly created panes.
|
||||
|
||||
`vendor/crossterm` retains Crossterm 0.29.0's upstream source and MIT license, with a small Unix decoder patch exposing `Event::ColorSchemeChanged` for `CSI ? 997 ; 1/2 n`. The report signals a palette refresh, not a choice of hard-coded light/dark colors. Valid fragmented reports and malformed color reports leave following keys intact. Cargo patches both direct and Ratatui dependency paths to this one copy; remove the patch/workspace member once upstream supports these notifications.
|
||||
|
||||
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
||||
|
||||
|
||||
+5
-1
@@ -8,14 +8,18 @@ Local verification and implementation constraints. See [architecture](architectu
|
||||
|
||||
```sh
|
||||
cargo test
|
||||
cargo test -p crossterm color_scheme_notifications
|
||||
cargo test -p crossterm --doc
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo fmt --check
|
||||
cargo fmt --package vamp --check
|
||||
for file in src/ruby_*.rb examples/config.rb examples/plugins/greeting.rb; do
|
||||
ruby -c "$file" || exit
|
||||
done
|
||||
rubocop
|
||||
```
|
||||
|
||||
The default workspace member is the editor. `vendor/crossterm` is included to test its minimal DEC 2031 notification decoder patch; the rest of its source retains upstream formatting. The decoder checks cover fragmented reports and keyboard recovery, and its doctests verify the added event does not break upstream examples.
|
||||
|
||||
## Ruby types
|
||||
|
||||
Use YARD `@param`, `@return`, and `@yieldparam` annotations for Ruby APIs, and `@type` for locals where inference needs help. Examples annotate DSL block parameters before the call, without adding runtime assignments or dependencies. `Vamp::Action` / `Vamp::Mode` field types and the dynamically defined mode methods use Solargraph-supported directives.
|
||||
|
||||
+1
-1
@@ -300,7 +300,7 @@ In split diffs, changed text rows use muted red backgrounds in Before and muted
|
||||
|
||||
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
||||
|
||||
Diff backgrounds blend the terminal's default background with its red/green palette colors: a subtle 12% accent for changed rows and a stronger 28% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint and again on terminal focus regain; cached colors apply across editor panes. If you switch terminal themes while vamp remains focused, switch away and back to refresh the tints. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
||||
Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint and whenever a subscribed DEC 2031 color-scheme notification arrives, so supported terminals update tints while the editor stays focused, including changes between two themes with the same light/dark preference. Focus-regain queries remain the fallback for terminals without notifications; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
||||
|
||||
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
|
||||
|
||||
|
||||
+50
-17
@@ -62,6 +62,19 @@ struct TerminalSession {
|
||||
hook: Arc<Mutex<HookState>>,
|
||||
}
|
||||
|
||||
struct ColorSchemeNotifications(bool);
|
||||
|
||||
impl crossterm::Command for ColorSchemeNotifications {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
f.write_str(if self.0 { "\x1b[?2031h" } else { "\x1b[?2031l" })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_terminal() {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(
|
||||
@@ -70,6 +83,7 @@ fn restore_terminal() {
|
||||
DisableBracketedPaste,
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false),
|
||||
LeaveAlternateScreen,
|
||||
Show
|
||||
);
|
||||
@@ -109,7 +123,8 @@ impl TerminalSession {
|
||||
EnterAlternateScreen,
|
||||
EnableBracketedPaste,
|
||||
EnableMouseCapture,
|
||||
EnableFocusChange
|
||||
EnableFocusChange,
|
||||
ColorSchemeNotifications(true)
|
||||
)?;
|
||||
body()
|
||||
}));
|
||||
@@ -787,7 +802,7 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
pending = following;
|
||||
}
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
Event::FocusGained => query_theme = true,
|
||||
Event::FocusGained | Event::ColorSchemeChanged => query_theme = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2982,8 +2997,8 @@ impl DiffColors {
|
||||
Color::Rgb(channels[0], channels[1], channels[2])
|
||||
};
|
||||
Self {
|
||||
row: [blend(theme.red, 12), blend(theme.green, 12)],
|
||||
character: [blend(theme.red, 28), blend(theme.green, 28)],
|
||||
row: [blend(theme.red, 18), blend(theme.green, 18)],
|
||||
character: [blend(theme.red, 38), blend(theme.green, 38)],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4247,19 +4262,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn character_diff_backgrounds_preserve_common_unicode_and_syntax_across_viewports() {
|
||||
for (theme, row, character) in [
|
||||
(
|
||||
Some(LIGHT_DIFF_THEME),
|
||||
[Color::Rgb(245, 219, 217), Color::Rgb(222, 234, 218)],
|
||||
[Color::Rgb(239, 185, 187), Color::Rgb(186, 218, 190)],
|
||||
),
|
||||
(
|
||||
Some(DARK_DIFF_THEME),
|
||||
[Color::Rgb(44, 29, 36), Color::Rgb(22, 44, 38)],
|
||||
[Color::Rgb(76, 35, 44), Color::Rgb(26, 68, 50)],
|
||||
),
|
||||
(None, [Color::Reset; 2], [Color::Reset; 2]),
|
||||
] {
|
||||
for theme in [Some(LIGHT_DIFF_THEME), Some(DARK_DIFF_THEME), None] {
|
||||
let mut diff = diff_comparison(vec![syntax_diff_file(
|
||||
"changes.rs",
|
||||
"let 界e\u{301} = old + old;",
|
||||
@@ -4272,6 +4275,36 @@ mod tests {
|
||||
..WheelPanels::default()
|
||||
};
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let row = panels.diff_colors.row;
|
||||
let character = panels.diff_colors.character;
|
||||
if let Some(theme) = theme {
|
||||
for (index, accent) in [theme.red, theme.green].into_iter().enumerate() {
|
||||
let Color::Rgb(r, g, b) = row[index] else {
|
||||
panic!("theme row background must be RGB");
|
||||
};
|
||||
let Color::Rgb(cr, cg, cb) = character[index] else {
|
||||
panic!("theme character background must be RGB");
|
||||
};
|
||||
for ((common, changed), (background, accent)) in [r, g, b]
|
||||
.into_iter()
|
||||
.zip([cr, cg, cb])
|
||||
.zip(theme.background.into_iter().zip(accent))
|
||||
{
|
||||
assert!(
|
||||
(background.min(accent)..=background.max(accent)).contains(&common)
|
||||
);
|
||||
assert!(
|
||||
(background.min(accent)..=background.max(accent)).contains(&changed)
|
||||
);
|
||||
if background != accent {
|
||||
assert!(changed.abs_diff(accent) < common.abs_diff(accent));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert_eq!(row, [Color::Reset; 2]);
|
||||
assert_eq!(character, [Color::Reset; 2]);
|
||||
}
|
||||
for index in 0..2 {
|
||||
let pane = panels.diff.as_ref().unwrap().regions[index + 1].0;
|
||||
let buffer = terminal.backend().buffer();
|
||||
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "crossterm"
|
||||
version = "0.29.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 = false
|
||||
edition = "2021"
|
||||
rust-version = "1.63.0"
|
||||
categories = ["command-line-interface", "command-line-utilities"]
|
||||
|
||||
[lib]
|
||||
name = "crossterm"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
default = ["bracketed-paste", "events", "windows", "derive-more"]
|
||||
|
||||
#! ### Default features
|
||||
## Enables triggering [`Event::Paste`](event::Event::Paste) when pasting text into the terminal.
|
||||
bracketed-paste = []
|
||||
|
||||
## Enables reading input/events from the system using the [`event`] module.
|
||||
events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"]
|
||||
|
||||
## Enables windows specific crates.
|
||||
windows = ["dep:winapi", "dep:crossterm_winapi"]
|
||||
|
||||
#! ### Optional Features
|
||||
|
||||
## Enables the [EventStream](event::EventStream) struct for async event reading.
|
||||
event-stream = ["dep:futures-core", "events"]
|
||||
|
||||
## Enables [`serde`] for various types.
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
## Enables raw file descriptor polling / selecting instead of mio.
|
||||
use-dev-tty = ["filedescriptor", "rustix/process"]
|
||||
|
||||
## Enables `is_*` helper functions for event enums.
|
||||
derive-more = ["dep:derive_more"]
|
||||
|
||||
## Enables interacting with a host clipboard via [`clipboard`](clipboard/index.html)
|
||||
osc52 = ["dep:base64"]
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.22", optional = true }
|
||||
bitflags = { version = "2.9" }
|
||||
derive_more = { version = "2.0.0", features = ["is_variant"], optional = true }
|
||||
document-features = "0.2.11"
|
||||
futures-core = { version = "0.3", optional = true, default-features = false }
|
||||
parking_lot = "0.12"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
# Windows dependencies
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
crossterm_winapi = { version = "0.9.1", optional = true }
|
||||
winapi = { version = "0.3.9", optional = true, features = ["winuser", "winerror"] }
|
||||
|
||||
# UNIX dependencies
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
filedescriptor = { version = "0.8", optional = true }
|
||||
# Default to using rustix for UNIX systems, but provide an option to use libc for backwards
|
||||
# compatibility.
|
||||
libc = { version = "0.2", default-features = false, optional = true }
|
||||
mio = { version = "1.0", features = ["os-poll"], optional = true }
|
||||
rustix = { version = "1", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
signal-hook = { version = "0.3.17", optional = true }
|
||||
signal-hook-mio = { version = "0.2.4", features = ["support-v1_0"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
serial_test = "3.0.0"
|
||||
temp-env = "0.3.6"
|
||||
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+46
@@ -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)
|
||||
}
|
||||
Vendored
+308
@@ -0,0 +1,308 @@
|
||||
//! # Clipboard
|
||||
//!
|
||||
//! The `clipboard` module provides functionality to work with a host clipboard.
|
||||
//!
|
||||
//! ## Implemented operations:
|
||||
//!
|
||||
//! - Copy: [`CopyToClipboard`](struct.CopyToClipboard.html)
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{osc, Command};
|
||||
|
||||
/// Different clipboard types
|
||||
///
|
||||
/// Some operating systems and desktop environments support multiple buffers
|
||||
/// for copy/cut/paste. Their details differ between operating systems.
|
||||
/// See <https://specifications.freedesktop.org/clipboard-spec/latest/>
|
||||
/// for a detailed survey of supported types based on the X window system.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardType {
|
||||
/// Default clipboard when using Ctrl+C or Ctrl+V
|
||||
Clipboard,
|
||||
|
||||
/// Clipboard on Linux/X/Wayland when using selection and middle mouse button
|
||||
Primary,
|
||||
|
||||
/// Other clipboard type not explicitly supported by crossterm
|
||||
///
|
||||
/// See
|
||||
/// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
|
||||
/// for potential values.
|
||||
///
|
||||
/// Note that support for these in terminal emulators is very limited.
|
||||
Other(char),
|
||||
}
|
||||
|
||||
impl From<&ClipboardType> for char {
|
||||
fn from(val: &ClipboardType) -> Self {
|
||||
match val {
|
||||
ClipboardType::Clipboard => 'c',
|
||||
ClipboardType::Primary => 'p',
|
||||
ClipboardType::Other(other) => *other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for ClipboardType {
|
||||
fn from(value: char) -> Self {
|
||||
match value {
|
||||
'c' => ClipboardType::Clipboard,
|
||||
'p' => ClipboardType::Primary,
|
||||
other => ClipboardType::Other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of clipboard types
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ClipboardSelection(
|
||||
/// An ordered list of clipboards which will be the destination for the copied selection.
|
||||
///
|
||||
/// Order matters due to implementations deviating from the
|
||||
/// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
|
||||
/// reference. Some terminal emulators may only interpret the first character of this
|
||||
/// parameter. For differences, see
|
||||
/// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support).
|
||||
pub Vec<ClipboardType>,
|
||||
);
|
||||
|
||||
impl ClipboardSelection {
|
||||
/// Returns a String corresponsing to the "Pc" parameter of the OSC52
|
||||
/// sequence.
|
||||
fn to_osc52_pc(&self) -> String {
|
||||
self.0.iter().map(Into::<char>::into).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ClipboardSelection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.to_osc52_pc())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ClipboardSelection {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ClipboardSelection(
|
||||
s.chars().map(From::<char>::from).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that copies to clipboard
|
||||
///
|
||||
/// This command uses OSC control sequence `Pr = 5 2` (See
|
||||
/// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) )
|
||||
/// to copy data to the terminal host clipboard.
|
||||
///
|
||||
/// This only works if it is enabled on the user's terminal emulator. If a terminal multiplexer
|
||||
/// is used, the multiplexer must support it, too.
|
||||
///
|
||||
/// Commands must be executed/queued for execution otherwise they do nothing.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crossterm::execute;
|
||||
/// use crossterm::clipboard::CopyToClipboard;
|
||||
/// // Copy foo to clipboard
|
||||
/// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo"));
|
||||
/// // Copy bar to primary
|
||||
/// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("bar"));
|
||||
/// ```
|
||||
///
|
||||
/// See also examples/copy-to-clipboard.rs.
|
||||
///
|
||||
/// # Terminal Support
|
||||
///
|
||||
/// The following table shows what destinations are filled by different terminal emulators when
|
||||
/// asked to copy to different destination sequences.
|
||||
///
|
||||
/// | Terminal (Version) | dest '' | dest 'c' | dest 'p' | dest 'cp' | dest'pc' |
|
||||
/// | --------------------- | --------- | --------- | -------- | ------------- | ------------- |
|
||||
/// | xterm (397) *3 | primary | clipboard | primary | clipb., prim. | clipb., prim. |
|
||||
/// | Alacritty (0.15.1) *3 | clipboard | clipboard | primary | clipb. | prim. |
|
||||
/// | Wezterm (*1) *3 | clipboard | clipboard | primary | clipb. | clipb. |
|
||||
/// | Konsole (24.12.3) *3 | clipboard | clipboard | primary | clipb., prim. | clipb., prim. |
|
||||
/// | Kitty (0.40.0) *3 | clipboard | clipboard | primary | clipb. | clipb. |
|
||||
/// | foot (1.20.2) *3 | clipboard | clipboard | primary | clipb., prim. | clipb., prim. |
|
||||
/// | tmux (3.5a) *2 *3 | primary | clipboard | primary | clipb., prim. | clipb., prim. |
|
||||
///
|
||||
/// Asterisks:
|
||||
/// 1. 20240203-110809-5046fc22
|
||||
/// 2. set-clipboard set to [external](https://github.com/tmux/tmux/wiki/Clipboard#how-it-works),
|
||||
/// i.e. this is OSC52 pass-through.
|
||||
/// 3. This was tested on wayland with the
|
||||
/// [primary selection protocol](https://wayland.app/protocols/primary-selection-unstable-v1)
|
||||
/// enabled.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CopyToClipboard<T> {
|
||||
/// Content to be copied
|
||||
pub content: T,
|
||||
/// Sequence of copy destinations
|
||||
///
|
||||
/// Not all sequences are equally supported by terminal emulators. See
|
||||
/// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support).
|
||||
pub destination: ClipboardSelection,
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> Command for CopyToClipboard<T> {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
osc!("52;{destination};{encoded_text}"),
|
||||
destination = self.destination.to_osc52_pc(),
|
||||
encoded_text = BASE64_STANDARD.encode(&self.content)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
use std::io;
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Copying is not implemented for the Windows API.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> CopyToClipboard<T> {
|
||||
/// Construct a [`CopyToClipboard`] that writes content into the
|
||||
/// "clipboard" (or 'c') clipboard selection.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crossterm::{execute, Command};
|
||||
/// use crossterm::clipboard::CopyToClipboard;
|
||||
/// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo"));
|
||||
/// ```
|
||||
pub fn to_clipboard_from(content: T) -> CopyToClipboard<T> {
|
||||
CopyToClipboard {
|
||||
content,
|
||||
destination: ClipboardSelection(vec![ClipboardType::Clipboard]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a [`CopyToClipboard`] that writes content into the "primary"
|
||||
/// (or 'p') clipboard selection.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crossterm::execute;
|
||||
/// use crossterm::clipboard::CopyToClipboard;
|
||||
/// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("foo"));
|
||||
/// ```
|
||||
pub fn to_primary_from(content: T) -> CopyToClipboard<T> {
|
||||
CopyToClipboard {
|
||||
content,
|
||||
destination: ClipboardSelection(vec![ClipboardType::Primary]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_string_to_selection() {
|
||||
assert_eq!(
|
||||
ClipboardSelection::from_str("p").unwrap(),
|
||||
ClipboardSelection(vec![ClipboardType::Primary])
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection::from_str("").unwrap(),
|
||||
ClipboardSelection(vec![])
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection::from_str("cp").unwrap(),
|
||||
ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_clipboard_selection_to_osc52_pc() {
|
||||
assert_eq!(ClipboardSelection(vec![]).to_osc52_pc(), "");
|
||||
assert_eq!(
|
||||
ClipboardSelection(vec![ClipboardType::Clipboard]).to_osc52_pc(),
|
||||
"c"
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection(vec![ClipboardType::Primary]).to_osc52_pc(),
|
||||
"p"
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard])
|
||||
.to_osc52_pc(),
|
||||
"pc"
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary])
|
||||
.to_osc52_pc(),
|
||||
"cp"
|
||||
);
|
||||
assert_eq!(
|
||||
ClipboardSelection(vec![ClipboardType::Other('s')]).to_osc52_pc(),
|
||||
"s"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_copy_string_osc52() {
|
||||
let mut buffer = String::new();
|
||||
super::CopyToClipboard {
|
||||
content: "foo",
|
||||
destination: ClipboardSelection(vec![ClipboardType::Clipboard]),
|
||||
}
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\");
|
||||
|
||||
buffer.clear();
|
||||
super::CopyToClipboard {
|
||||
content: "foo",
|
||||
destination: ClipboardSelection(vec![ClipboardType::Primary]),
|
||||
}
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\");
|
||||
|
||||
buffer.clear();
|
||||
super::CopyToClipboard {
|
||||
content: "foo",
|
||||
destination: ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard]),
|
||||
}
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;pc;Zm9v\x1b\\");
|
||||
|
||||
buffer.clear();
|
||||
super::CopyToClipboard {
|
||||
content: "foo",
|
||||
destination: ClipboardSelection(vec![]),
|
||||
}
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;;Zm9v\x1b\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_copy_string_osc52_constructor() {
|
||||
let mut buffer = String::new();
|
||||
super::CopyToClipboard::to_clipboard_from("foo")
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\");
|
||||
|
||||
let mut buffer = String::new();
|
||||
super::CopyToClipboard::to_primary_from("foo")
|
||||
.write_ansi(&mut buffer)
|
||||
.unwrap();
|
||||
assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\");
|
||||
}
|
||||
}
|
||||
Vendored
+295
@@ -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)
|
||||
}
|
||||
Vendored
+504
@@ -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(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
//! This module provides platform related functions.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::unix::position;
|
||||
#[cfg(windows)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::windows::position;
|
||||
#[cfg(windows)]
|
||||
pub(crate) use self::windows::{
|
||||
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||
move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub(crate) mod unix;
|
||||
+56
@@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
//! 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 upper = u32::from(position.x as u16) << 16;
|
||||
let lower = u32::from(position.y as u16);
|
||||
let bits = u64::from(upper | lower);
|
||||
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);
|
||||
}
|
||||
}
|
||||
Vendored
+1753
File diff suppressed because it is too large
Load Diff
Vendored
+115
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(unix)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent,
|
||||
KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct InternalEventFilter;
|
||||
|
||||
impl Filter for InternalEventFilter {
|
||||
fn eval(&self, _: &InternalEvent) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[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)));
|
||||
}
|
||||
}
|
||||
Vendored
+436
@@ -0,0 +1,436 @@
|
||||
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::Event, EventSource, Filter, InternalEvent, InternalEventReader};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct InternalEventFilter;
|
||||
|
||||
impl Filter for InternalEventFilter {
|
||||
fn eval(&self, _: &InternalEvent) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+27
@@ -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;
|
||||
}
|
||||
+11
@@ -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;
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
use std::{collections::VecDeque, io, time::Duration};
|
||||
|
||||
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||
use signal_hook_mio::v1_0::Signals;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{
|
||||
source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent,
|
||||
};
|
||||
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||
|
||||
// Tokens to identify file descriptor
|
||||
const TTY_TOKEN: Token = Token(0);
|
||||
const SIGNAL_TOKEN: Token = Token(1);
|
||||
#[cfg(feature = "event-stream")]
|
||||
const WAKE_TOKEN: Token = Token(2);
|
||||
|
||||
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||
// is enough.
|
||||
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||
|
||||
pub(crate) struct UnixInternalEventSource {
|
||||
poll: Poll,
|
||||
events: Events,
|
||||
parser: Parser,
|
||||
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||
tty_fd: FileDesc<'static>,
|
||||
signals: Signals,
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker: Waker,
|
||||
}
|
||||
|
||||
impl UnixInternalEventSource {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||
let poll = Poll::new()?;
|
||||
let registry = poll.registry();
|
||||
|
||||
let tty_raw_fd = input_fd.raw_fd();
|
||||
let mut tty_ev = SourceFd(&tty_raw_fd);
|
||||
registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?;
|
||||
|
||||
let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?;
|
||||
registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let waker = Waker::new(registry, WAKE_TOKEN)?;
|
||||
|
||||
Ok(UnixInternalEventSource {
|
||||
poll,
|
||||
events: Events::with_capacity(3),
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty_fd: input_fd,
|
||||
signals,
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for UnixInternalEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) {
|
||||
// Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds.
|
||||
// Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned).
|
||||
// https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes
|
||||
if e.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if self.events.is_empty() {
|
||||
// No readiness events = timeout
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for token in self.events.iter().map(|x| x.token()) {
|
||||
match token {
|
||||
TTY_TOKEN => {
|
||||
loop {
|
||||
match self.tty_fd.read(&mut self.tty_buffer) {
|
||||
Ok(read_count) => {
|
||||
if read_count > 0 {
|
||||
self.parser.advance(
|
||||
&self.tty_buffer[..read_count],
|
||||
read_count == TTY_BUFFER_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// No more data to read at the moment. We will receive another event
|
||||
if e.kind() == io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
// once more data is available to read.
|
||||
else if e.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
SIGNAL_TOKEN => {
|
||||
if self.signals.pending().next() == Some(signal_hook::consts::SIGWINCH) {
|
||||
// TODO Should we remove tput?
|
||||
//
|
||||
// This can take a really long time, because terminal::size can
|
||||
// launch new process (tput) and then it parses its output. It's
|
||||
// not a really long time from the absolute time point of view, but
|
||||
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||
// point of view.
|
||||
let new_size = crate::terminal::size()?;
|
||||
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||
new_size.0, new_size.1,
|
||||
))));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "event-stream")]
|
||||
WAKE_TOKEN => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
"Poll operation was woken up by `Waker::wake`",
|
||||
));
|
||||
}
|
||||
_ => unreachable!("Synchronize Evented handle registration & token handling"),
|
||||
}
|
||||
}
|
||||
|
||||
// Processing above can take some time, check if timeout expired
|
||||
if timeout.elapsed() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker {
|
||||
self.waker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Following `Parser` structure exists for two reasons:
|
||||
//
|
||||
// * mimic anes Parser interface
|
||||
// * move the advancing, parsing, ... stuff out of the `try_read` method
|
||||
//
|
||||
#[derive(Debug)]
|
||||
struct Parser {
|
||||
buffer: Vec<u8>,
|
||||
internal_events: VecDeque<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Parser {
|
||||
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||
// it smaller?
|
||||
//
|
||||
// Probably not worth spending more time on this as "there's a plan"
|
||||
// to use the anes crate parser.
|
||||
buffer: Vec::with_capacity(256),
|
||||
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||
// fit? What is an average sequence length? Let's guess here
|
||||
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||
// when processing large amounts of data.
|
||||
//
|
||||
// There's no need to make it bigger, because when you look at the `try_read`
|
||||
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||
// is processed -> events pushed.
|
||||
internal_events: VecDeque::with_capacity(128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||
for (idx, byte) in buffer.iter().enumerate() {
|
||||
let more = idx + 1 < buffer.len() || more;
|
||||
|
||||
self.buffer.push(*byte);
|
||||
|
||||
match parse_event(&self.buffer, more) {
|
||||
Ok(Some(ie)) => {
|
||||
self.internal_events.push_back(ie);
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event can't be parsed, because we don't have enough bytes for
|
||||
// the current sequence. Keep the buffer and process next bytes.
|
||||
}
|
||||
Err(_) => {
|
||||
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||
// Clear the buffer and continue with another sequence.
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = InternalEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.internal_events.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Parser;
|
||||
use crate::event::{Event, InternalEvent, KeyCode};
|
||||
|
||||
#[test]
|
||||
fn color_scheme_notifications_preserve_following_keys() {
|
||||
for report in [b"\x1B[?997;1n", b"\x1B[?997;2n"] {
|
||||
for split in 1..report.len() {
|
||||
let mut parser = Parser::default();
|
||||
parser.advance(&report[..split], true);
|
||||
assert_eq!(parser.next(), None);
|
||||
parser.advance(&report[split..], false);
|
||||
parser.advance(b"x", false);
|
||||
assert_eq!(
|
||||
parser.next(),
|
||||
Some(InternalEvent::Event(Event::ColorSchemeChanged))
|
||||
);
|
||||
assert_eq!(
|
||||
parser.next(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Char('x').into())))
|
||||
);
|
||||
assert_eq!(parser.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_color_scheme_notifications_do_not_trap_keys() {
|
||||
for report in [
|
||||
b"\x1B[?997n".as_slice(),
|
||||
b"\x1B[?997;0n",
|
||||
b"\x1B[?997;3n",
|
||||
b"\x1B[?997;01n",
|
||||
b"\x1B[?997;1;2n",
|
||||
b"\x1B[?997;1A",
|
||||
] {
|
||||
let mut parser = Parser::default();
|
||||
parser.advance(report, false);
|
||||
assert_eq!(parser.next(), None);
|
||||
parser.advance(b"x", false);
|
||||
assert_eq!(
|
||||
parser.next(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Char('x').into())))
|
||||
);
|
||||
assert_eq!(parser.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
#[cfg(feature = "libc")]
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration};
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::fd::{AsFd, AsRawFd};
|
||||
|
||||
use signal_hook::low_level::pipe;
|
||||
|
||||
use crate::event::timeout::PollTimeout;
|
||||
use crate::event::Event;
|
||||
use filedescriptor::{poll, pollfd, POLLIN};
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent};
|
||||
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||
|
||||
/// Holds a prototypical Waker and a receiver we can wait on when doing select().
|
||||
#[cfg(feature = "event-stream")]
|
||||
struct WakePipe {
|
||||
receiver: UnixStream,
|
||||
waker: Waker,
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
impl WakePipe {
|
||||
fn new() -> io::Result<Self> {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
Ok(WakePipe {
|
||||
receiver,
|
||||
waker: Waker::new(sender),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||
// is enough.
|
||||
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||
|
||||
pub(crate) struct UnixInternalEventSource {
|
||||
parser: Parser,
|
||||
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||
tty: FileDesc<'static>,
|
||||
winch_signal_receiver: UnixStream,
|
||||
#[cfg(feature = "event-stream")]
|
||||
wake_pipe: WakePipe,
|
||||
}
|
||||
|
||||
fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> {
|
||||
let (receiver, sender) = UnixStream::pair()?;
|
||||
receiver.set_nonblocking(true)?;
|
||||
sender.set_nonblocking(true)?;
|
||||
Ok((receiver, sender))
|
||||
}
|
||||
|
||||
impl UnixInternalEventSource {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||
Ok(UnixInternalEventSource {
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty: input_fd,
|
||||
winch_signal_receiver: {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
// Unregistering is unnecessary because EventSource is a singleton
|
||||
#[cfg(feature = "libc")]
|
||||
pipe::register(libc::SIGWINCH, sender)?;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pipe::register(rustix::process::Signal::WINCH.as_raw(), sender)?;
|
||||
receiver
|
||||
},
|
||||
#[cfg(feature = "event-stream")]
|
||||
wake_pipe: WakePipe::new()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// read_complete reads from a non-blocking file descriptor
|
||||
/// until the buffer is full or it would block.
|
||||
///
|
||||
/// Similar to `std::io::Read::read_to_end`, except this function
|
||||
/// only fills the given buffer and does not read beyond that.
|
||||
fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
match fd.read(buf) {
|
||||
Ok(x) => return Ok(x),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::WouldBlock => return Ok(0),
|
||||
io::ErrorKind::Interrupted => continue,
|
||||
_ => return Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for UnixInternalEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
fn make_pollfd<F: AsRawFd>(fd: &F) -> pollfd {
|
||||
pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: POLLIN,
|
||||
revents: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
];
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
make_pollfd(&self.wake_pipe.receiver),
|
||||
];
|
||||
|
||||
while timeout.leftover().map_or(true, |t| !t.is_zero()) {
|
||||
// check if there are buffered events from the last read
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
match poll(&mut fds, timeout.leftover()) {
|
||||
Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => {
|
||||
match e.kind() {
|
||||
// retry on EINTR
|
||||
io::ErrorKind::Interrupted => continue,
|
||||
_ => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("got unexpected error while polling: {:?}", e),
|
||||
))
|
||||
}
|
||||
Ok(_) => (),
|
||||
};
|
||||
if fds[0].revents & POLLIN != 0 {
|
||||
loop {
|
||||
let read_count = read_complete(&self.tty, &mut self.tty_buffer)?;
|
||||
if read_count > 0 {
|
||||
self.parser.advance(
|
||||
&self.tty_buffer[..read_count],
|
||||
read_count == TTY_BUFFER_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if fds[1].revents & POLLIN != 0 {
|
||||
#[cfg(feature = "libc")]
|
||||
let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false);
|
||||
#[cfg(not(feature = "libc"))]
|
||||
let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd());
|
||||
// drain the pipe
|
||||
while read_complete(&fd, &mut [0; 1024])? != 0 {}
|
||||
// TODO Should we remove tput?
|
||||
//
|
||||
// This can take a really long time, because terminal::size can
|
||||
// launch new process (tput) and then it parses its output. It's
|
||||
// not a really long time from the absolute time point of view, but
|
||||
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||
// point of view.
|
||||
let new_size = crate::terminal::size()?;
|
||||
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||
new_size.0, new_size.1,
|
||||
))));
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
if fds[2].revents & POLLIN != 0 {
|
||||
#[cfg(feature = "libc")]
|
||||
let fd = FileDesc::new(self.wake_pipe.receiver.as_raw_fd(), false);
|
||||
#[cfg(not(feature = "libc"))]
|
||||
let fd = FileDesc::Borrowed(self.wake_pipe.receiver.as_fd());
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
+100
@@ -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 i32 + 1) as u16,
|
||||
(record.size.y as i32 + 1) as u16,
|
||||
))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
Vendored
+146
@@ -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();
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) mod waker;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub(crate) mod parse;
|
||||
+1514
File diff suppressed because it is too large
Load Diff
+11
@@ -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_all(&[0])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+48
@@ -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(())
|
||||
}
|
||||
+378
@@ -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,
|
||||
}))
|
||||
}
|
||||
+86
@@ -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()
|
||||
}
|
||||
}
|
||||
+40
@@ -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()
|
||||
}
|
||||
}
|
||||
+92
@@ -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));
|
||||
}
|
||||
}
|
||||
Vendored
+263
@@ -0,0 +1,263 @@
|
||||
#![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)
|
||||
//! - Module [`clipboard`](clipboard/index.html) (requires
|
||||
//! [`feature = "osc52"`](#optional-features))
|
||||
//! - Clipboard - [`CopyToClipboard`](clipboard/struct.CopyToClipboard.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(())
|
||||
//! }
|
||||
//!```
|
||||
//! ## Feature Flags
|
||||
#![doc = document_features::document_features!()]
|
||||
//!
|
||||
//! [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;
|
||||
|
||||
/// A module for clipboard interaction
|
||||
#[cfg(feature = "osc52")]
|
||||
pub mod clipboard;
|
||||
|
||||
#[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.");
|
||||
Vendored
+378
@@ -0,0 +1,378 @@
|
||||
/// Concatenate string literals while prepending a ANSI control sequence introducer (`"\x1b["`)
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! csi {
|
||||
($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) };
|
||||
}
|
||||
|
||||
/// Concatenate string literals while prepending a xterm Operating System Commands (OSC)
|
||||
/// introducer (`"\x1b]"`) and appending a BEL (`"\x07"`).
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! osc {
|
||||
($( $l:expr ),*) => { concat!("\x1B]", $( $l ),*, "\x1B\\") };
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
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"))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+621
@@ -0,0 +1,621 @@
|
||||
//! # 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 {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Check if we're running in a pseudo TTY, which supports true color.
|
||||
// Fall back to env vars otherwise for other terminals on Windows.
|
||||
if crate::ansi_support::supports_ansi() {
|
||||
return u16::MAX;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT: u16 = 8;
|
||||
env::var("COLORTERM")
|
||||
.or_else(|_| env::var("TERM"))
|
||||
.map_or(DEFAULT, |x| match x {
|
||||
_ if x.contains("24bit") || x.contains("truecolor") => u16::MAX,
|
||||
_ if x.contains("256") => 256,
|
||||
_ => DEFAULT,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Writing both foreground and background colors in one command resulted in about 20% more
|
||||
// FPS (20 to 24 fps) on a fullscreen (171x51) app that writes every cell with a different
|
||||
// foreground and background color, compared to separately using the SetForegroundColor and
|
||||
// SetBackgroundColor commands (iTerm2, M2 Macbook Pro). `Esc[38;5;<fg>mEsc[48;5;<bg>m` (16
|
||||
// chars) vs `Esc[38;5;<fg>;48;5;<bg>m` (14 chars)
|
||||
match (self.0.foreground, self.0.background) {
|
||||
(Some(fg), Some(bg)) => {
|
||||
write!(
|
||||
f,
|
||||
csi!("{};{}m"),
|
||||
Colored::ForegroundColor(fg),
|
||||
Colored::BackgroundColor(bg)
|
||||
)
|
||||
}
|
||||
(Some(fg), None) => write!(f, csi!("{}m"), Colored::ForegroundColor(fg)),
|
||||
(None, Some(bg)) => write!(f, csi!("{}m"), Colored::BackgroundColor(bg)),
|
||||
(None, None) => 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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// On Windows many env var tests will fail so we need to conditionally check for ANSI support.
|
||||
// This allows other terminals on Windows to still assert env var support.
|
||||
macro_rules! skip_windows_ansi_supported {
|
||||
() => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if crate::ansi_support::supports_ansi() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, test)]
|
||||
#[cfg(windows)]
|
||||
fn windows_always_truecolor() {
|
||||
// This should always be true on supported Windows 10+,
|
||||
// but downlevel Windows clients and other terminals may fail `cargo test` otherwise.
|
||||
if crate::ansi_support::supports_ansi() {
|
||||
assert_eq!(u16::MAX, available_color_count());
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colorterm_overrides_term() {
|
||||
skip_windows_ansi_supported!();
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("COLORTERM", Some("truecolor")),
|
||||
("TERM", Some("xterm-256color")),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(u16::MAX, available_color_count());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn term_24bits() {
|
||||
skip_windows_ansi_supported!();
|
||||
temp_env::with_vars(
|
||||
[("COLORTERM", None), ("TERM", Some("xterm-24bits"))],
|
||||
|| {
|
||||
assert_eq!(u16::MAX, available_color_count());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn term_256color() {
|
||||
skip_windows_ansi_supported!();
|
||||
temp_env::with_vars(
|
||||
[("COLORTERM", None), ("TERM", Some("xterm-256color"))],
|
||||
|| {
|
||||
assert_eq!(256u16, available_color_count());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_color_count() {
|
||||
skip_windows_ansi_supported!();
|
||||
temp_env::with_vars([("COLORTERM", None::<&str>), ("TERM", None)], || {
|
||||
assert_eq!(8, available_color_count());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_term_colorterm_values() {
|
||||
skip_windows_ansi_supported!();
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("COLORTERM", Some("gibberish")),
|
||||
("TERM", Some("gibberish")),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(8u16, available_color_count());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
+43
@@ -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
|
||||
}
|
||||
}
|
||||
+77
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
+200
@@ -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));
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
+204
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors};
|
||||
|
||||
mod attribute;
|
||||
mod color;
|
||||
mod colored;
|
||||
mod colors;
|
||||
+183
@@ -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#Select_Graphic_Rendition_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()
|
||||
}
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
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.is_empty() {
|
||||
match *self {
|
||||
Color::AnsiValue(value) => serializer.serialize_str(&format!("ansi_({})", value)),
|
||||
Color::Rgb { r, g, b } => {
|
||||
serializer.serialize_str(&format!("rgb_({},{},{})", r, g, b))
|
||||
}
|
||||
_ => Err(serde::ser::Error::custom("Could not serialize enum type")),
|
||||
}
|
||||
} else {
|
||||
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 serde::de::Visitor<'_> 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 let (Ok(r), Ok(g), Ok(b)) = (r, g, b) {
|
||||
return Ok(Color::Rgb { r, g, b });
|
||||
}
|
||||
}
|
||||
} 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 let (Ok(r), Ok(g), Ok(b)) = (r, g, b) {
|
||||
return Ok(Color::Rgb { r, g, b });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
+320
@@ -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.
|
||||
/// Important: doesn't 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());
|
||||
}
|
||||
}
|
||||
+234
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+568
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+27
@@ -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,154 @@
|
||||
use std::io;
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::size_t;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
fs,
|
||||
marker::PhantomData,
|
||||
os::unix::{
|
||||
io::{IntoRawFd, RawFd},
|
||||
prelude::AsRawFd,
|
||||
},
|
||||
};
|
||||
|
||||
/// A file descriptor wrapper.
|
||||
///
|
||||
/// It allows to retrieve raw file descriptor, write to the file descriptor and
|
||||
/// mainly it closes the file descriptor once dropped.
|
||||
#[derive(Debug)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub struct FileDesc<'a> {
|
||||
fd: RawFd,
|
||||
close_on_drop: bool,
|
||||
phantom: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub enum FileDesc<'a> {
|
||||
Owned(OwnedFd),
|
||||
Borrowed(BorrowedFd<'a>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl FileDesc<'_> {
|
||||
/// Constructs a new `FileDesc` with the given `RawFd`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - raw file descriptor
|
||||
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped
|
||||
pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> {
|
||||
FileDesc {
|
||||
fd,
|
||||
close_on_drop,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let result = unsafe {
|
||||
libc::read(
|
||||
self.fd,
|
||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||
buffer.len() as size_t,
|
||||
)
|
||||
};
|
||||
|
||||
if result < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(result as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying file descriptor.
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl FileDesc<'_> {
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let fd = match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
};
|
||||
let result = rustix::io::read(fd, buffer)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_raw_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_raw_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl Drop for FileDesc<'_> {
|
||||
fn drop(&mut self) {
|
||||
if self.close_on_drop {
|
||||
// Note that errors are ignored when closing a file descriptor. The
|
||||
// reason for this is that if an error occurs we don't actually know if
|
||||
// the file descriptor was closed or not, and if we retried (for
|
||||
// something like EINTR), we might close another valid file descriptor
|
||||
// opened after we closed ours.
|
||||
let _ = unsafe { libc::close(self.fd) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for FileDesc<'_> {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl AsFd for FileDesc<'_> {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
|
||||
(libc::STDIN_FILENO, false)
|
||||
} else {
|
||||
(
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/tty")?
|
||||
.into_raw_fd(),
|
||||
true,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(FileDesc::new(fd, close_on_drop))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
use std::fs::File;
|
||||
|
||||
let stdin = rustix::stdio::stdin();
|
||||
let fd = if rustix::termios::isatty(stdin) {
|
||||
FileDesc::Borrowed(stdin)
|
||||
} else {
|
||||
let dev_tty = File::options().read(true).write(true).open("/dev/tty")?;
|
||||
FileDesc::Owned(dev_tty.into())
|
||||
};
|
||||
Ok(fd)
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
//! UNIX related logic for terminal manipulation.
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
use crate::event::KeyboardEnhancementFlags;
|
||||
use crate::terminal::{
|
||||
sys::file_descriptor::{tty_fd, FileDesc},
|
||||
WindowSize,
|
||||
};
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::{
|
||||
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
|
||||
TIOCGWINSZ,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::{
|
||||
fd::AsFd,
|
||||
termios::{Termios, Winsize},
|
||||
};
|
||||
|
||||
use std::{fs::File, io, process};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
mem,
|
||||
os::unix::io::{IntoRawFd, RawFd},
|
||||
};
|
||||
|
||||
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
||||
// None -> we're not in the raw mode
|
||||
static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> bool {
|
||||
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl From<winsize> for WindowSize {
|
||||
fn from(size: winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl From<Winsize> for WindowSize {
|
||||
fn from(size: Winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
||||
let mut size = winsize {
|
||||
ws_row: 0,
|
||||
ws_col: 0,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.raw_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
STDOUT_FILENO
|
||||
};
|
||||
|
||||
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
||||
return Ok(size.into());
|
||||
}
|
||||
|
||||
Err(std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
let file = File::open("/dev/tty").map(|file| FileDesc::Owned(file.into()));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.as_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
rustix::stdio::stdout()
|
||||
};
|
||||
let size = rustix::termios::tcgetwinsize(fd)?;
|
||||
Ok(size.into())
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
if let Ok(window_size) = window_size() {
|
||||
return Ok((window_size.columns, window_size.rows));
|
||||
}
|
||||
|
||||
tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let fd = tty.raw_fd();
|
||||
let mut ios = get_terminal_attr(fd)?;
|
||||
let original_mode_ios = ios;
|
||||
raw_terminal_attr(&mut ios);
|
||||
set_terminal_attr(fd, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let mut ios = get_terminal_attr(&tty)?;
|
||||
let original_mode_ios = ios.clone();
|
||||
ios.make_raw();
|
||||
set_terminal_attr(&tty, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the raw mode.
|
||||
///
|
||||
/// More precisely, reset the whole termios mode to what it was before the first call
|
||||
/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
|
||||
/// effectively disabling the raw mode and doing nothing else.
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(&tty, original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn get_terminal_attr(fd: impl AsFd) -> io::Result<Termios> {
|
||||
let result = rustix::termios::tcgetattr(fd)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> {
|
||||
rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
||||
query_keyboard_enhancement_flags().map(|flags| flags.is_some())
|
||||
}
|
||||
|
||||
/// Queries the terminal's currently active keyboard enhancement flags.
|
||||
///
|
||||
/// 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 query_keyboard_enhancement_flags() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
if is_raw_mode_enabled() {
|
||||
query_keyboard_enhancement_flags_raw()
|
||||
} else {
|
||||
query_keyboard_enhancement_flags_nonraw()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_keyboard_enhancement_flags_nonraw() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
enable_raw_mode()?;
|
||||
let flags = query_keyboard_enhancement_flags_raw();
|
||||
disable_raw_mode()?;
|
||||
flags
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn query_keyboard_enhancement_flags_raw() -> io::Result<Option<KeyboardEnhancementFlags>> {
|
||||
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(Some(current_flags));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The keyboard enhancement status could not be read within a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// execute tput with the given argument and parse
|
||||
/// the output as a u16.
|
||||
///
|
||||
/// The arg should be "cols" or "lines"
|
||||
fn tput_value(arg: &str) -> Option<u16> {
|
||||
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
||||
let value = output
|
||||
.stdout
|
||||
.into_iter()
|
||||
.filter_map(|b| char::from(b).to_digit(10))
|
||||
.fold(0, |v, n| v * 10 + n as u16);
|
||||
|
||||
if value > 0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size of the screen as determined by tput.
|
||||
///
|
||||
/// This alternate way of computing the size is useful
|
||||
/// when in a subshell.
|
||||
fn tput_size() -> Option<(u16, u16)> {
|
||||
match (tput_value("cols"), tput_value("lines")) {
|
||||
(Some(w), Some(h)) => Some((w, h)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
// Transform the given mode into an raw mode (non-canonical) mode.
|
||||
fn raw_terminal_attr(termios: &mut Termios) {
|
||||
unsafe { cfmakeraw(termios) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
|
||||
unsafe {
|
||||
let mut termios = mem::zeroed();
|
||||
wrap_with_result(tcgetattr(fd, &mut termios))?;
|
||||
Ok(termios)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
|
||||
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn wrap_with_result(result: i32) -> io::Result<()> {
|
||||
if result == -1 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+471
@@ -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 - width {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.width = window.left + width;
|
||||
resize_buffer = true;
|
||||
}
|
||||
let height = height as i16;
|
||||
if current_size.height < window.top + height {
|
||||
if window.top >= i16::MAX - height {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.height = window.top + height;
|
||||
resize_buffer = true;
|
||||
}
|
||||
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?;
|
||||
}
|
||||
|
||||
let mut window = window;
|
||||
|
||||
// preserve the position, but change the size.
|
||||
window.bottom = window.top + height - 1;
|
||||
window.right = window.left + width - 1;
|
||||
console.set_console_info(true, window)?;
|
||||
|
||||
// if we resized the buffer, un-resize it.
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?;
|
||||
}
|
||||
|
||||
let bounds = console.largest_window_size()?;
|
||||
|
||||
if width > bounds.x {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal width {width} too large"),
|
||||
));
|
||||
}
|
||||
if height > bounds.y {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal height {height} too large"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> {
|
||||
struct Utf16Encoder(Vec<u16>);
|
||||
impl Write for Utf16Encoder {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0.extend(s.encode_utf16());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut title_utf16 = Utf16Encoder(Vec::new());
|
||||
write!(title_utf16, "{title}").expect("formatting failed");
|
||||
title_utf16.0.push(0);
|
||||
let title = title_utf16.0;
|
||||
|
||||
let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
|
||||
if result != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_after_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (mut x, mut y) = (location.x, location.y);
|
||||
|
||||
// if cursor position is at the outer right position
|
||||
if x > buffer_size.width {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_before_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (xpos, ypos) = (location.x, location.y);
|
||||
|
||||
// one cell after cursor position
|
||||
let x = 0;
|
||||
// one at row of cursor position
|
||||
let y = 0;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
|
||||
|
||||
// clear everything before cursor position
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> {
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, 0);
|
||||
|
||||
// clear the entire screen
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 0,0
|
||||
cursor::sys::move_to(0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_current_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, location.y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32;
|
||||
|
||||
// clear the whole current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 1 on current row
|
||||
cursor::sys::move_to(0, location.y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_until_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (x, y) = (location.x, location.y);
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width - x) as u32;
|
||||
|
||||
// clear until the current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at original cursor position before we did the clearing
|
||||
cursor::sys::move_to(x as u16, y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_winapi(
|
||||
start_location: Coord,
|
||||
cells_to_write: u32,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let console = Console::from(Handle::current_out_handle()?);
|
||||
console.fill_whit_character(start_location, cells_to_write, ' ')?;
|
||||
console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// Create a new screen buffer to avoid changing the terminal the test
|
||||
// is running within.
|
||||
pub fn temp_screen_buffer() -> std::io::Result<ScreenBuffer> {
|
||||
let alternate_screen = ScreenBuffer::create()?;
|
||||
alternate_screen.show().unwrap();
|
||||
Ok(alternate_screen)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
|
||||
use crossterm_winapi::ScreenBuffer;
|
||||
use serial_test::serial;
|
||||
use winapi::um::wincon::GetConsoleTitleW;
|
||||
|
||||
use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer};
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_resize_winapi_20_21() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
// The values 20 and 21 are arbitrary and different from each other
|
||||
// just to see they're not crossed over.
|
||||
set_size(20, 21).unwrap();
|
||||
assert_eq!((20, 21), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// This is similar to test_resize_winapi_20_21() above. This verifies that
|
||||
// another test of similar functionality runs independently (that a testing
|
||||
// race condition has been addressed).
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
fn test_resize_winapi_30_31() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
set_size(30, 31).unwrap();
|
||||
assert_eq!((30, 31), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_down_winapi() {
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_down(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top + 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom + 2);
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_up_winapi() {
|
||||
// move the terminal buffer down before moving it up
|
||||
test_scroll_down_winapi();
|
||||
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_up(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top - 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom - 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_set_title_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let test_title = "this is a crossterm test title";
|
||||
set_window_title(test_title).unwrap();
|
||||
|
||||
let mut raw = [0_u16; 128];
|
||||
let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
|
||||
assert_ne!(0, length);
|
||||
|
||||
let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
|
||||
assert_eq!(test_title, &console_title[..]);
|
||||
}
|
||||
}
|
||||
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
//! Making it a little more convenient and safe to query whether
|
||||
//! something is a terminal teletype or not.
|
||||
//! This module defines the IsTty trait and the is_tty method to
|
||||
//! return true if the item represents a terminal.
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
#[cfg(windows)]
|
||||
use winapi::um::consoleapi::GetConsoleMode;
|
||||
|
||||
/// Adds the `is_tty` method to types that might represent a terminal
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::io::stdout;
|
||||
/// use crossterm::tty::IsTty;
|
||||
///
|
||||
/// let is_tty: bool = stdout().is_tty();
|
||||
/// ```
|
||||
pub trait IsTty {
|
||||
/// Returns true when an instance is a terminal teletype, otherwise false.
|
||||
fn is_tty(&self) -> bool;
|
||||
}
|
||||
|
||||
/// On UNIX, the `isatty()` function returns true if a file
|
||||
/// descriptor is a terminal.
|
||||
#[cfg(all(unix, feature = "libc"))]
|
||||
impl<S: AsRawFd> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let fd = self.as_raw_fd();
|
||||
unsafe { libc::isatty(fd) == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(feature = "libc")))]
|
||||
impl<S: AsRawFd> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let fd = self.as_raw_fd();
|
||||
rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) })
|
||||
}
|
||||
}
|
||||
|
||||
/// On windows, `GetConsoleMode` will return true if we are in a terminal.
|
||||
/// Otherwise false.
|
||||
#[cfg(windows)]
|
||||
impl<S: AsRawHandle> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let mut mode = 0;
|
||||
let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) };
|
||||
ok == 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user