├── .gitignore
├── .idea
├── .gitignore
├── encodings.xml
├── misc.xml
├── vcs.xml
├── modules.xml
└── terminal_backend.iml
├── examples
├── foo.rs
├── readme.rs
├── attribute.rs
├── event.rs
├── basic.rs
├── alternate-raw.rs
└── style.rs
├── docs
├── terminal_full.png
├── termion-logo.png
├── CHANGELOG.md
├── backend-specification.md
└── CONTRIBUTING.md
├── src
├── backend
│ ├── crossterm
│ │ ├── mod.rs
│ │ ├── implementation.rs
│ │ └── mapping.rs
│ ├── termion
│ │ ├── mod.rs
│ │ ├── cursor.rs
│ │ ├── mapping.rs
│ │ └── implementation.rs
│ ├── crosscurses
│ │ ├── mod.rs
│ │ ├── current_style.rs
│ │ ├── constants.rs
│ │ ├── mapping.rs
│ │ └── implementation.rs
│ ├── resize.rs
│ └── mod.rs
├── enums.rs
├── lib.rs
├── enums
│ ├── terminal.rs
│ ├── style.rs
│ └── event.rs
├── error.rs
├── action.rs
└── terminal.rs
├── Cargo.toml
├── .github
└── workflows
│ └── rust.yml
├── README.md
└── Cargo.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /workspace.xml
--------------------------------------------------------------------------------
/examples/foo.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | terminal::stdout();
3 | }
4 |
--------------------------------------------------------------------------------
/docs/terminal_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crossterm-rs/terminal/HEAD/docs/terminal_full.png
--------------------------------------------------------------------------------
/docs/termion-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crossterm-rs/terminal/HEAD/docs/termion-logo.png
--------------------------------------------------------------------------------
/src/backend/crossterm/mod.rs:
--------------------------------------------------------------------------------
1 | pub use self::implementation::BackendImpl;
2 |
3 | mod implementation;
4 | mod mapping;
5 |
--------------------------------------------------------------------------------
/src/backend/termion/mod.rs:
--------------------------------------------------------------------------------
1 | pub use self::implementation::BackendImpl;
2 |
3 | mod cursor;
4 | mod implementation;
5 | mod mapping;
6 |
--------------------------------------------------------------------------------
/src/backend/crosscurses/mod.rs:
--------------------------------------------------------------------------------
1 | mod constants;
2 | mod current_style;
3 | mod implementation;
4 | mod mapping;
5 |
6 | pub use self::implementation::BackendImpl;
7 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/enums.rs:
--------------------------------------------------------------------------------
1 | pub use self::{
2 | event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent},
3 | style::{Attribute, Color},
4 | terminal::Clear,
5 | };
6 |
7 | mod event;
8 | mod style;
9 | mod terminal;
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version 0.2.1
2 | - Fix panic occurred in `BackendImpl::drop`
3 | - Upgrade deps: (crossterm to 0.15, signal-hook to 0.1.13)
4 |
5 | # Version 0.2
6 |
7 | - Crosscurses/pancurses backend implemented.
8 |
9 | # Version 0.1
10 |
11 | - Initial project setup
12 | - Termion Backend Implemented
13 | - Corssterm Backend Implemented
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(unused_imports, unused_must_use)]
2 |
3 | pub use self::{
4 | action::{Action, Retrieved, Value},
5 | enums::{
6 | Attribute, Clear, Color, Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent,
7 | },
8 | terminal::{stderr, stdout, Terminal, TerminalLock},
9 | };
10 |
11 | pub mod error;
12 |
13 | pub(crate) mod action;
14 | pub(crate) mod backend;
15 | pub(crate) mod enums;
16 | pub(crate) mod terminal;
17 |
--------------------------------------------------------------------------------
/src/backend/crosscurses/current_style.rs:
--------------------------------------------------------------------------------
1 | use crate::Color;
2 | use crosscurses::Attributes;
3 |
4 | pub(crate) struct CurrentStyle {
5 | pub(crate) foreground: Color,
6 | pub(crate) background: Color,
7 | pub(crate) attributes: Attributes,
8 | }
9 |
10 | impl CurrentStyle {
11 | pub(crate) fn new() -> CurrentStyle {
12 | CurrentStyle {
13 | foreground: Color::Reset,
14 | background: Color::Reset,
15 | attributes: Attributes::new(),
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/enums/terminal.rs:
--------------------------------------------------------------------------------
1 | /// Different ways to clear the terminal buffer.
2 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
4 | pub enum Clear {
5 | /// All cells.
6 | All,
7 | /// All cells from the cursor position downwards.
8 | FromCursorDown,
9 | /// All cells from the cursor position upwards.
10 | FromCursorUp,
11 | /// All cells at the cursor row.
12 | CurrentLine,
13 | /// All cells from the cursor position until the new line.
14 | UntilNewLine,
15 | }
16 |
--------------------------------------------------------------------------------
/src/backend/crosscurses/constants.rs:
--------------------------------------------------------------------------------
1 | /// A mask that can be used to track all mouse events.
2 | pub(crate) const MOUSE_EVENT_MASK: u32 =
3 | crosscurses::ALL_MOUSE_EVENTS | crosscurses::REPORT_MOUSE_POSITION;
4 |
5 | /// A sequence of escape codes to enable terminal mouse support.
6 | /// We use this directly instead of using `MouseTerminal` from termion.
7 | pub(crate) const ENABLE_MOUSE_CAPTURE: &str = "\x1B[?1002h";
8 |
9 | /// A sequence of escape codes to disable terminal mouse support.
10 | /// We use this directly instead of using `MouseTerminal` from termion.
11 | pub(crate) const DISABLE_MOUSE_CAPTURE: &str = "\x1B[?1002l";
12 |
--------------------------------------------------------------------------------
/examples/readme.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 | use terminal::{error, Action, Clear, Retrieved, Value};
3 |
4 | pub fn main() -> error::Result<()> {
5 | let mut terminal = terminal::stdout();
6 |
7 | // perform an single action.
8 | terminal.act(Action::ClearTerminal(Clear::All))?;
9 |
10 | // batch multiple actions.
11 | for i in 0..20 {
12 | terminal.batch(Action::MoveCursorTo(0, i))?;
13 | terminal.write(format!("{}", i).as_bytes());
14 | }
15 |
16 | // execute batch.
17 | terminal.flush_batch();
18 |
19 | // get an terminal value.
20 | if let Retrieved::TerminalSize(x, y) = terminal.get(Value::TerminalSize)? {
21 | println!("\nx: {}, y: {}", x, y);
22 | }
23 |
24 | Ok(())
25 | }
26 |
--------------------------------------------------------------------------------
/src/backend/resize.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | sync::{
3 | atomic::{AtomicBool, Ordering},
4 | Arc,
5 | },
6 | thread,
7 | };
8 |
9 | use crossbeam_channel::Sender;
10 | use signal_hook::iterator::Signals;
11 |
12 | /// This starts a new thread to listen for SIGWINCH signals
13 | #[allow(unused)]
14 | pub fn start_resize_thread(resize_sender: Sender<()>, resize_running: Arc) {
15 | let signals = Signals::new(&[libc::SIGWINCH]).unwrap();
16 | thread::spawn(move || {
17 | // This thread will listen to SIGWINCH events and report them.
18 | while resize_running.load(Ordering::Relaxed) {
19 | // We know it will only contain SIGWINCH signals, so no need to check.
20 | if signals.wait().count() > 0 {
21 | resize_sender.send(()).unwrap();
22 | }
23 | }
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/backend/mod.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use crate::{error, Action, Retrieved, Value};
4 |
5 | #[cfg(feature = "crosscurses-backend")]
6 | pub(crate) use self::crosscurses::BackendImpl;
7 | #[cfg(feature = "crossterm-backend")]
8 | pub(crate) use self::crossterm::BackendImpl;
9 | #[cfg(feature = "termion-backend")]
10 | pub(crate) use self::termion::BackendImpl;
11 |
12 | #[cfg(feature = "crossterm-backend")]
13 | mod crossterm;
14 |
15 | #[cfg(feature = "termion-backend")]
16 | mod termion;
17 |
18 | #[cfg(feature = "termion-backend")]
19 | mod resize;
20 |
21 | #[cfg(feature = "crosscurses-backend")]
22 | mod crosscurses;
23 |
24 | /// Interface to an backend library.
25 | pub trait Backend {
26 | fn create(buffer: W) -> Self;
27 | fn act(&mut self, action: Action) -> error::Result<()>;
28 | fn batch(&mut self, action: Action) -> error::Result<()>;
29 | fn flush_batch(&mut self) -> error::Result<()>;
30 | fn get(&self, retrieve_operation: Value) -> error::Result;
31 | }
32 |
--------------------------------------------------------------------------------
/.idea/terminal_backend.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/backend/termion/cursor.rs:
--------------------------------------------------------------------------------
1 | use std::{io, io::Write};
2 |
3 | use termion::get_tty;
4 |
5 | use crate::error;
6 | use std::io::BufRead;
7 |
8 | /// Termion's cursor detections is terrible.
9 | /// It panics a lot.
10 | /// Although this solution it is not perfect, it works in most cases.
11 | /// This can be used until it is fixed.
12 | ///
13 | /// https://gitlab.redox-os.org/redox-os/termion/merge_requests/145
14 | /// https://gitlab.redox-os.org/redox-os/termion/issues/173/
15 | pub fn position() -> error::Result<(u16, u16)> {
16 | // Where is the cursor.unwrap()
17 | // Use `ESC [ 6 n`.
18 | let mut tty = get_tty().unwrap();
19 | let stdin = io::stdin();
20 |
21 | // Write command
22 | tty.write_all(b"\x1B[6n").unwrap();
23 | tty.flush().unwrap();
24 |
25 | stdin.lock().read_until(b'[', &mut vec![]).unwrap();
26 |
27 | let mut rows = vec![];
28 | stdin.lock().read_until(b';', &mut rows).unwrap();
29 |
30 | let mut cols = vec![];
31 | stdin.lock().read_until(b'R', &mut cols).unwrap();
32 |
33 | // remove delimiter
34 | rows.pop();
35 | cols.pop();
36 |
37 | let rows = String::from_utf8(rows).unwrap().parse::().unwrap();
38 | let cols = String::from_utf8(cols).unwrap().parse::().unwrap();
39 |
40 | Ok((cols - 1, rows - 1))
41 | }
42 |
--------------------------------------------------------------------------------
/examples/attribute.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::cognitive_complexity)]
2 |
3 | use std::{io::Write, thread, time::Duration};
4 | use terminal::{error::Result, stdout, Action, Attribute, TerminalLock};
5 |
6 | const ATTRIBUTES: [(Attribute, Attribute); 7] = [
7 | (Attribute::Bold, Attribute::BoldOff),
8 | (Attribute::Italic, Attribute::ItalicOff),
9 | (Attribute::Underlined, Attribute::UnderlinedOff),
10 | (Attribute::Reversed, Attribute::ReversedOff),
11 | (Attribute::Crossed, Attribute::CrossedOff),
12 | (Attribute::SlowBlink, Attribute::BlinkOff),
13 | (Attribute::Conceal, Attribute::ConcealOff),
14 | ];
15 |
16 | fn display_attributes(w: &mut TerminalLock) -> Result<()> {
17 | let mut y = 2;
18 | w.write(b"Display attributes");
19 |
20 | for (on, off) in &ATTRIBUTES {
21 | w.act(Action::MoveCursorTo(0, y));
22 |
23 | w.batch(Action::SetAttribute(*on));
24 | w.write(format!("{:>width$} ", format!("{:?}", on), width = 35).as_bytes());
25 | w.batch(Action::SetAttribute(*off));
26 | w.write(format!("{:>width$}", format!("{:?}", off), width = 35).as_bytes());
27 | w.batch(Action::ResetColor);
28 |
29 | w.flush_batch();
30 |
31 | y += 1;
32 | }
33 |
34 | Ok(())
35 | }
36 |
37 | pub fn main() {
38 | let stdout = stdout();
39 | let mut lock = stdout.lock_mut().unwrap();
40 |
41 | display_attributes(&mut lock);
42 |
43 | thread::sleep(Duration::from_millis(5000))
44 | }
45 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "terminal"
3 | version = "0.2.1"
4 | authors = ["T. Post"]
5 | description = "Unified API over different TUI libraries."
6 | edition = "2018"
7 | repository = "https://github.com/crossterm-rs/terminal"
8 | documentation = "https://docs.rs/terminal/"
9 | license = "MIT"
10 | keywords = ["console", "cli", "tty", "terminal"]
11 | exclude = ["target", "Cargo.lock"]
12 | readme = "README.md"
13 | categories = ["command-line-interface", "command-line-utilities"]
14 |
15 | #
16 | # Build documentation with all features, BackendImpl is availible.
17 | #
18 | [package.metadata.docs.rs]
19 | features = ["crossterm-backend"]
20 |
21 | #
22 | # Features
23 | #
24 | [features]
25 | default = ["crossterm-backend"]
26 | termion-backend = ["termion", "signal-hook", "libc", "crossbeam-channel"]
27 | crossterm-backend = ["crossterm"]
28 | crosscurses-backend = ["crosscurses", "libc"]
29 |
30 | #
31 | # Shared dependencies
32 | #
33 | [dependencies]
34 | bitflags = "1.2.1"
35 |
36 | #
37 | # Backend dependencies
38 | #
39 | [dependencies.termion]
40 | optional = true
41 | version = "1.5.3"
42 |
43 | [dependencies.crossterm]
44 | optional = true
45 | version = "0.15"
46 |
47 | [dependencies.crosscurses]
48 | optional = true
49 | version = "0.1.0"
50 | features = ["wide"]
51 |
52 | #
53 | # UNIX dependencies
54 | #
55 | [target.'cfg(unix)'.dependencies]
56 | signal-hook = { version = "0.1.13", optional = true }
57 | libc = { version = "0.2.66", optional = true }
58 | crossbeam-channel = { version = "0.4.0", optional = true }
59 |
--------------------------------------------------------------------------------
/examples/event.rs:
--------------------------------------------------------------------------------
1 | use bitflags::_core::time::Duration;
2 |
3 | use terminal::{error, stdout, Action, Event, KeyCode, KeyEvent, Retrieved, Value};
4 |
5 | fn main() {
6 | with_duration_read();
7 | }
8 |
9 | /// Block read indefinitely for events.
10 | fn block_read() -> error::Result<()> {
11 | let terminal = stdout();
12 |
13 | terminal.act(Action::EnableRawMode)?;
14 |
15 | loop {
16 | if let Retrieved::Event(event) = terminal.get(Value::Event(None))? {
17 | match event {
18 | Some(Event::Key(KeyEvent {
19 | code: KeyCode::Esc, ..
20 | })) => return Ok(()),
21 | Some(event) => {
22 | println!("{:?}\r", event);
23 | }
24 | _ => {}
25 | }
26 | }
27 | }
28 | }
29 |
30 | /// Reads events withing a certain duration.
31 | fn with_duration_read() -> error::Result<()> {
32 | let terminal = stdout();
33 |
34 | terminal.act(Action::EnableRawMode)?;
35 | terminal.act(Action::EnableMouseCapture)?;
36 |
37 | loop {
38 | if let Retrieved::Event(event) =
39 | terminal.get(Value::Event(Some(Duration::from_millis(500))))?
40 | {
41 | match event {
42 | Some(Event::Key(KeyEvent {
43 | code: KeyCode::Esc, ..
44 | })) => return Ok(()),
45 | Some(event) => {
46 | println!("{:?}\r", event);
47 | }
48 | None => println!("...\r"),
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/docs/backend-specification.md:
--------------------------------------------------------------------------------
1 | # Supportability by Backend
2 |
3 | | Backend | `Action` Not Supported |
4 | | :------ | :------ |
5 | | `crosscurses` | ScrollUp, ScrollDown, Enter/Leave alternate screen (default alternate screen) |
6 | | `termion` | ScrollUp, ScrollDown, |
7 | | `crossterm` | |
8 |
9 |
10 | | Backend | `Attribute` Not Supported |
11 | | :------ | :------ |
12 | | `crosscurses` | Fraktur, NormalIntensity, Framed |
13 | | `termion` | ConcealOn, ConcealOff, Fraktur, NormalIntensity |
14 | | `crossterm` | |
15 |
16 | # Backend Evaluation
17 |
18 | This section describes the pros and cons of each backend.
19 |
20 |
21 | ### Crossterm
22 |
23 | feature flag: (crossterm-backend)
24 |
25 | **pros**
26 | - Written in pure Rust
27 | - Works crossplatform
28 | - Performant
29 | - Updates Regularly
30 | - Supports all features of this library.
31 | - Works without threads or spinning loops.
32 | - Supports advanced event / modifier support.
33 |
34 | **cons**
35 | - Uses stdout for cursor position.
36 |
37 | ### Termion (termion-backend)
38 |
39 | feature flag: (crosscurses-backend)
40 |
41 | **pros**
42 | - Written in pure Rust
43 | - Released as a marjor version crate
44 | - Performant
45 | - Supports Redox
46 |
47 | **cons**
48 | - Works on Unix systems only
49 | - Uses threads for reading resize events and input
50 | - Maintenance is limited
51 | - Limited Modifier support.
52 | - Fires thread to read input.
53 | - Fires thread to capture terminal resize events.
54 | - Uses stdout for terminal size
55 | - Uses `/dev/tty` and stdin for cursor position.
56 |
57 | ### Crosscurses
58 |
59 | feature flag: (crosscurses-backend)
60 |
61 | **pros**
62 | - Based on ncurses and pdcurses.
63 | - Works crossplatform
64 | - Supports advanced event / modifier support.
65 |
66 | **cons**
67 | - Depends on C ncurses library.
68 | - Maintenance is limited
69 | - Lacks some features (see above).
70 | - Uses /dev/tty by default, falls back to stdout if not supported.
71 | it is not possible to customize its buffer. Tough you do have full control over refreshing terminal screen.
--------------------------------------------------------------------------------
/examples/basic.rs:
--------------------------------------------------------------------------------
1 | use std::{fs::File, thread, time::Duration};
2 |
3 | use terminal::{error, stderr, stdout, Action, Clear, Retrieved, Terminal, Value};
4 |
5 | fn different_buffers() {
6 | let _stdout = stdout();
7 | let _stderr = stderr();
8 | let _file = Terminal::custom(File::create("./test.txt").unwrap());
9 | }
10 |
11 | /// Gets values from the terminal.
12 | fn get_value() -> error::Result<()> {
13 | let stdout = stdout();
14 |
15 | if let Retrieved::CursorPosition(x, y) = stdout.get(Value::CursorPosition)? {
16 | println!("X: {}, Y: {}", x, y);
17 | }
18 |
19 | if let Retrieved::TerminalSize(column, row) = stdout.get(Value::TerminalSize)? {
20 | println!("columns: {}, rows: {}", column, row);
21 | }
22 |
23 | // see '/examples/event.rs'
24 | if let Retrieved::Event(event) = stdout.get(Value::Event(None))? {
25 | println!("Event: {:?}\r", event);
26 | }
27 |
28 | Ok(())
29 | }
30 |
31 | fn perform_action() -> error::Result<()> {
32 | let stdout = stdout();
33 | stdout.act(Action::MoveCursorTo(10, 10))
34 | }
35 |
36 | /// Batches multiple actions before executing.
37 | fn batch_actions() -> error::Result<()> {
38 | let terminal = stdout();
39 | terminal.batch(Action::ClearTerminal(Clear::All))?;
40 | terminal.batch(Action::MoveCursorTo(5, 5))?;
41 |
42 | thread::sleep(Duration::from_millis(2000));
43 | println!("@");
44 |
45 | terminal.flush_batch()
46 | }
47 |
48 | /// Acquires lock once, and uses that lock to do actions.
49 | fn lock_terminal() -> error::Result<()> {
50 | let terminal = Terminal::custom(File::create("./test.txt").unwrap());
51 |
52 | let mut lock = terminal.lock_mut()?;
53 |
54 | for i in 0..10000 {
55 | println!("{}", i);
56 |
57 | if i % 100 == 0 {
58 | lock.act(Action::ClearTerminal(Clear::All))?;
59 | lock.act(Action::MoveCursorTo(0, 0))?;
60 | }
61 | thread::sleep(Duration::from_millis(10));
62 | }
63 |
64 | Ok(())
65 | }
66 |
67 | fn main() {
68 | get_value().unwrap();
69 | }
70 |
--------------------------------------------------------------------------------
/examples/alternate-raw.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use terminal::{stdout, Action, Clear, Event, KeyCode, KeyEvent, Retrieved, TerminalLock, Value};
4 |
5 | fn main() {
6 | let terminal = stdout();
7 |
8 | let mut lock = terminal.lock_mut().unwrap();
9 |
10 | lock.act(Action::EnterAlternateScreen).unwrap();
11 | lock.act(Action::EnableRawMode).unwrap();
12 | lock.act(Action::HideCursor).unwrap();
13 |
14 | write_alt_screen_msg(&mut lock);
15 |
16 | lock.flush_batch().unwrap();
17 |
18 | loop {
19 | if let Retrieved::Event(Some(Event::Key(key))) = lock.get(Value::Event(None)).unwrap() {
20 | match key {
21 | KeyEvent {
22 | code: KeyCode::Char('q'),
23 | ..
24 | } => {
25 | break;
26 | }
27 | KeyEvent {
28 | code: KeyCode::Char('1'),
29 | ..
30 | } => {
31 | lock.act(Action::LeaveAlternateScreen).unwrap();
32 | }
33 | KeyEvent {
34 | code: KeyCode::Char('2'),
35 | ..
36 | } => {
37 | lock.act(Action::EnterAlternateScreen).unwrap();
38 | write_alt_screen_msg(&mut lock);
39 | }
40 | _ => {}
41 | };
42 | }
43 | }
44 |
45 | lock.act(Action::DisableRawMode).unwrap();
46 | lock.act(Action::ShowCursor).unwrap();
47 | }
48 |
49 | fn write_alt_screen_msg(screen: &mut TerminalLock) {
50 | screen.act(Action::ClearTerminal(Clear::All)).unwrap();
51 | screen.act(Action::MoveCursorTo(1, 1)).unwrap();
52 |
53 | print!("Welcome to the alternate screen.\n\r");
54 | screen.act(Action::MoveCursorTo(1, 3)).unwrap();
55 | print!("Press '1' to switch to the main screen or '2' to switch to the alternate screen.\n\r");
56 | screen.act(Action::MoveCursorTo(1, 4)).unwrap();
57 | print!("Press 'q' to exit (and switch back to the main screen).\n\r");
58 | }
59 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::{self, Display, Formatter},
3 | io,
4 | };
5 |
6 | /// The `terminal` result type.
7 | pub type Result = std::result::Result;
8 |
9 | /// Wrapper for all errors that can occur in `terminal`.
10 | #[derive(Debug)]
11 | pub enum ErrorKind {
12 | FlushingBatchFailed,
13 | /// Attempt to lock the terminal failed.
14 | AttemptToAcquireLock(String),
15 | /// Action is not supported by the current backend.
16 | ActionNotSupported(String),
17 | /// Atribute is not supported by the current backend.
18 | AttributeNotSupported(String),
19 | /// IO error occurred
20 | IoError(io::Error),
21 | #[doc(hidden)]
22 | __Nonexhaustive,
23 | }
24 |
25 | impl From for ErrorKind {
26 | fn from(error: io::Error) -> Self {
27 | ErrorKind::IoError(error)
28 | }
29 | }
30 |
31 | impl std::error::Error for ErrorKind {
32 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
33 | match self {
34 | ErrorKind::IoError(e) => Some(e),
35 | _ => None,
36 | }
37 | }
38 | }
39 |
40 | impl Display for ErrorKind {
41 | fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
42 | match &*self {
43 | ErrorKind::FlushingBatchFailed => {
44 | write!(fmt, "An error occurred with an attempt to flush the buffer")
45 | }
46 | ErrorKind::AttemptToAcquireLock(reason) => write!(
47 | fmt,
48 | "Attempted to acquire lock mutably more than once. {}",
49 | reason
50 | ),
51 | ErrorKind::ActionNotSupported(action_name) => {
52 | write!(fmt, "Action '{}' is not supported by backend.", action_name)
53 | }
54 | ErrorKind::AttributeNotSupported(attribute_name) => write!(
55 | fmt,
56 | "Attribute '{}' is not supported by backend.",
57 | attribute_name
58 | ),
59 | _ => write!(fmt, "Some error has occurred"),
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/examples/style.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use bitflags::_core::time::Duration;
4 | use std::thread;
5 | use terminal::{error, stdout, Action, Clear, Color, TerminalLock};
6 |
7 | fn draw_color_values_matrix_16x16(
8 | w: &mut TerminalLock,
9 | title: &str,
10 | color: F,
11 | ) -> error::Result<()>
12 | where
13 | W: Write,
14 | F: Fn(u16, u16) -> Color,
15 | {
16 | w.batch(Action::ClearTerminal(Clear::All))?;
17 |
18 | write!(w, "{}", title);
19 | w.flush();
20 |
21 | for idx in 0..=15 {
22 | w.batch(Action::MoveCursorTo(1, idx + 4))?;
23 | write!(w, "{}", format!("{:>width$}", idx, width = 2));
24 |
25 | w.batch(Action::MoveCursorTo(idx * 3 + 3, 3))?;
26 | write!(w, "{}", format!("{:>width$}", idx, width = 3));
27 | }
28 |
29 | for row in 0..=15u16 {
30 | w.batch(Action::MoveCursorTo(4, row + 4))?;
31 |
32 | for col in 0..=15u16 {
33 | w.batch(Action::SetForegroundColor(color(col, row)))?;
34 | write!(w, "███");
35 | }
36 |
37 | w.batch(Action::SetForegroundColor(Color::White))?;
38 | write!(w, "{}", format!("{:>width$} ..= ", row * 16, width = 3));
39 | write!(w, "{}", format!("{:>width$}", row * 16 + 15, width = 3));
40 | }
41 |
42 | w.flush_batch()?;
43 |
44 | Ok(())
45 | }
46 |
47 | fn rgb(lock: &mut TerminalLock) {
48 | draw_color_values_matrix_16x16(lock, "Color::Rgb values", |col, row| {
49 | Color::AnsiValue((row * 16 + col) as u8)
50 | })
51 | .unwrap();
52 | }
53 |
54 | fn rgb_red_values(w: &mut TerminalLock) -> error::Result<()> {
55 | draw_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| {
56 | Color::Rgb((row * 16 + col) as u8, 0 as u8, 0)
57 | })
58 | }
59 |
60 | fn rgb_green_values(w: &mut TerminalLock) -> error::Result<()> {
61 | draw_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| {
62 | Color::Rgb(0, (row * 16 + col) as u8, 0)
63 | })
64 | }
65 |
66 | fn rgb_blue_values(w: &mut TerminalLock) -> error::Result<()> {
67 | draw_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| {
68 | Color::Rgb(0, 0, (row * 16 + col) as u8)
69 | })
70 | }
71 |
72 | fn main() {
73 | let terminal = stdout();
74 | let mut lock = terminal.lock_mut().unwrap();
75 |
76 | rgb(&mut lock);
77 |
78 | thread::sleep(Duration::from_millis(2000))
79 | }
80 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Terminal Adapter Test
2 |
3 | on:
4 | # Build master branch only
5 | push:
6 | branches:
7 | - master
8 | # Build pull requests targeting master branch only
9 | pull_request:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | test:
15 | name: ${{matrix.rust}} on ${{ matrix.os }}
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | backend_features: [crossterm-backend, termion-backend]
20 | os: [ubuntu-latest, windows-2019, macOS-latest]
21 | rust: [stable, nightly]
22 | # Allow failures on nightly, it's just informative
23 | include:
24 | - rust: stable
25 | can-fail: false
26 | - rust: nightly
27 | can-fail: true
28 | steps:
29 | - name: Checkout Repository
30 | uses: actions/checkout@v1
31 | with:
32 | fetch-depth: 1
33 | - name: Install Rust
34 | uses: hecrj/setup-rust-action@master
35 | with:
36 | rust-version: ${{ matrix.rust }}
37 | components: rustfmt,clippy
38 | - name: Toolchain Information
39 | run: |
40 | rustc --version
41 | rustfmt --version
42 | rustup --version
43 | cargo --version
44 | - name: Check Formatting
45 | if: matrix.rust == 'stable'
46 | run: cargo fmt --all -- --check
47 | continue-on-error: ${{ matrix.can-fail }}
48 | - name: Clippy
49 | run: cargo clippy -- -D clippy::all
50 | continue-on-error: ${{ matrix.can-fail }}
51 |
52 | - name: Build with feature crosterm-backend
53 | run: cargo build --no-default-features --features="crossterm-backend"
54 | continue-on-error: ${{ matrix.can-fail }}
55 | - name: Build with feature termion-backend
56 | if: matrix.os != 'windows-2019'
57 | run: cargo build --no-default-features --features="termion-backend"
58 | continue-on-error: ${{ matrix.can-fail }}
59 |
60 | - name: Test with feature crosterm-backend
61 | run: cargo test --no-default-features --features="crossterm-backend"
62 | continue-on-error: ${{ matrix.can-fail }}
63 | - name: Test with feature termion-backend
64 | if: matrix.os != 'windows-2019'
65 | run: cargo test --no-default-features --features="termion-backend"
66 | continue-on-error: ${{ matrix.can-fail }}
67 |
68 | - name: Test Packaging
69 | if: matrix.rust == 'stable'
70 | run: cargo package
71 | continue-on-error: ${{ matrix.can-fail }}
72 |
--------------------------------------------------------------------------------
/src/action.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use crate::{Attribute, Clear, Color, Event};
4 |
5 | /// A value that can be retrieved from the terminal.
6 | ///
7 | /// A [Value](enum.Value.html) can be retrieved with [Terminal::get](struct.Terminal.html#method.get).
8 | pub enum Value {
9 | /// Get the terminal size.
10 | TerminalSize,
11 | /// Get the cursor position.
12 | CursorPosition,
13 | /// Try to get an event within the given duration.
14 | /// The application will wait indefinitely when `None`.
15 | /// It will wait for some duration if `Some(duration)` is given.
16 | Event(Option),
17 | }
18 |
19 | /// A result that is returned from a request for a [Value](enum.Value.html).
20 | ///
21 | /// A [Value](enum.Value.html) can be retrieved with [Terminal::get](struct.Terminal.html#method.get).
22 | pub enum Retrieved {
23 | /// The terminal size is returned number of (column, row)s.
24 | TerminalSize(u16, u16),
25 | /// The cursor position is returned (column, row).
26 | /// The top left cell is represented 0,0.
27 | CursorPosition(u16, u16),
28 | /// An event is returned.
29 | /// Timeout occurred if `None` is returned.
30 | Event(Option),
31 | }
32 |
33 | /// An action that can be performed on the terminal.
34 | ///
35 | /// To perform an [Action](enum.Action.html) use [Terminal::act](struct.Terminal.html#method.act).
36 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
38 | pub enum Action {
39 | /// Moves the terminal cursor to the given position (column, row).
40 | MoveCursorTo(u16, u16),
41 | /// Hides the terminal cursor.
42 | HideCursor,
43 | /// Shows the terminal cursor.
44 | ShowCursor,
45 | /// Enables blinking of the terminal cursor.
46 | EnableBlinking,
47 | /// Disables blinking of the terminal cursor.
48 | DisableBlinking,
49 | /// Clears the terminal screen buffer.
50 | ClearTerminal(Clear),
51 | /// Sets the terminal size (columns, rows).
52 | SetTerminalSize(u16, u16),
53 | /// Scrolls the terminal screen a given number of rows up.
54 | ScrollUp(u16),
55 | /// Scrolls the terminal screen a given number of rows down.
56 | ScrollDown(u16),
57 |
58 | /// Enables raw mode.
59 | EnableRawMode,
60 | /// Disables raw mode.
61 | DisableRawMode,
62 | /// Switches to alternate screen.
63 | EnterAlternateScreen,
64 | /// Switches back to the main screen.
65 | LeaveAlternateScreen,
66 |
67 | /// Enables mouse event capturing.
68 | EnableMouseCapture,
69 | /// Disables mouse event capturing.
70 | DisableMouseCapture,
71 |
72 | /// Sets the the foreground color.
73 | SetForegroundColor(Color),
74 | /// Sets the the background color.
75 | SetBackgroundColor(Color),
76 | /// Sets an attribute.
77 | SetAttribute(Attribute),
78 | /// Resets the colors back to default.
79 | ResetColor,
80 | }
81 |
82 | impl From for String {
83 | fn from(action: Action) -> Self {
84 | format!("{:?}", action)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/backend/termion/mapping.rs:
--------------------------------------------------------------------------------
1 | use termion::{event, event::Key};
2 |
3 | use crate::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
4 |
5 | impl From for MouseButton {
6 | fn from(buttons: event::MouseButton) -> Self {
7 | match buttons {
8 | event::MouseButton::Left => MouseButton::Left,
9 | event::MouseButton::Right => MouseButton::Right,
10 | event::MouseButton::Middle => MouseButton::Middle,
11 | _ => { unreachable!("Wheel up and down are handled at MouseEvent level. Code should not be able to reach this.") }
12 | }
13 | }
14 | }
15 |
16 | fn to_0_based(x: u16, y: u16) -> (u16, u16) {
17 | // to 0-based position.
18 | (x - 1, y - 1)
19 | }
20 |
21 | impl From for MouseEvent {
22 | fn from(event: event::MouseEvent) -> Self {
23 | match event {
24 | event::MouseEvent::Press(btn, x, y) => {
25 | // to 0-based position.
26 | let (x, y) = to_0_based(x, y);
27 |
28 | if btn == event::MouseButton::WheelDown {
29 | MouseEvent::ScrollDown(x, y, KeyModifiers::empty())
30 | } else if btn == event::MouseButton::WheelUp {
31 | MouseEvent::ScrollUp(x, y, KeyModifiers::empty())
32 | } else {
33 | MouseEvent::Down(btn.into(), x, y, KeyModifiers::empty())
34 | }
35 | }
36 | event::MouseEvent::Release(x, y) => {
37 | // to 0-based position.
38 | let (x, y) = to_0_based(x, y);
39 |
40 | MouseEvent::Up(MouseButton::Unknown, x, y, KeyModifiers::empty())
41 | }
42 | event::MouseEvent::Hold(x, y) => {
43 | // to 0-based position.
44 | let (x, y) = to_0_based(x, y);
45 |
46 | MouseEvent::Drag(MouseButton::Unknown, x, y, KeyModifiers::empty())
47 | }
48 | }
49 | }
50 | }
51 |
52 | impl From for KeyEvent {
53 | fn from(code: event::Key) -> Self {
54 | match code {
55 | event::Key::Backspace => KeyCode::Backspace.into(),
56 | event::Key::Left => KeyCode::Left.into(),
57 | event::Key::Right => KeyCode::Right.into(),
58 | event::Key::Up => KeyCode::Up.into(),
59 | event::Key::Down => KeyCode::Down.into(),
60 | event::Key::Home => KeyCode::Home.into(),
61 | event::Key::End => KeyCode::End.into(),
62 | event::Key::PageUp => KeyCode::PageUp.into(),
63 | event::Key::PageDown => KeyCode::PageDown.into(),
64 | event::Key::BackTab => KeyCode::BackTab.into(),
65 | event::Key::Delete => KeyCode::Delete.into(),
66 | event::Key::Insert => KeyCode::Insert.into(),
67 | event::Key::F(f) => KeyCode::F(f).into(),
68 | event::Key::Char('\n') => KeyCode::Enter.into(),
69 | event::Key::Char('\t') => KeyCode::Tab.into(),
70 | event::Key::Char(c) => KeyCode::Char(c).into(),
71 | event::Key::Null => KeyCode::Null.into(),
72 | event::Key::Esc => KeyCode::Esc.into(),
73 |
74 | Key::Alt(char) => {
75 | let mut modifiers = KeyModifiers::empty();
76 | modifiers |= KeyModifiers::ALT;
77 |
78 | KeyEvent::new(KeyCode::Char(char), modifiers)
79 | }
80 | Key::Ctrl(char) => {
81 | let mut modifiers = KeyModifiers::empty();
82 | modifiers |= KeyModifiers::CONTROL;
83 |
84 | KeyEvent::new(KeyCode::Char(char), modifiers)
85 | }
86 |
87 | Key::__IsNotComplete => KeyCode::Tab.into(),
88 | }
89 | }
90 | }
91 |
92 | impl From for Event {
93 | fn from(event: event::Event) -> Self {
94 | match event {
95 | event::Event::Key(key) => Event::Key(KeyEvent::from(key)),
96 | event::Event::Mouse(mouse) => Event::Mouse(MouseEvent::from(mouse)),
97 | event::Event::Unsupported(_data) => Event::Unknown,
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | I would appreciate any contributions to this crate. However, some things are handy to know.
4 |
5 | ## Code Style
6 |
7 | ### Import Order
8 |
9 | All imports are semantically grouped and ordered. The order is:
10 |
11 | - standard library (`use std::...`)
12 | - external crates (`use rand::...`)
13 | - current crate (`use crate::...`)
14 | - parent module (`use super::..`)
15 | - current module (`use self::...`)
16 | - module declaration (`mod ...`)
17 |
18 | There must be an empty line between groups. An example:
19 |
20 | ```rust
21 | use crossterm_utils::{csi, write_cout, Result};
22 |
23 | use crate::sys::{get_cursor_position, show_cursor};
24 |
25 | use super::Cursor;
26 | ```
27 |
28 | #### CLion Tips
29 |
30 | The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts
31 | imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation
32 | to get proper grouping & sorting:
33 |
34 | * _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports
35 | * `cargo fmt` - fix ordering within the group
36 |
37 | Second step can be automated via _CLion_ -> _Preferences_ ->
38 | _Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_.
39 |
40 | ### Max Line Length
41 |
42 | | Type | Max line length |
43 | | :--- | ---: |
44 | | Code | 100 |
45 | | Comments in the code | 120 |
46 | | Documentation | 120 |
47 |
48 | 100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width)
49 | default value.
50 |
51 | 120 is because of the GitHub. The editor & viewer width there is +- 123 characters.
52 |
53 | ### Warnings
54 |
55 | The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings.
56 | If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required.
57 |
58 | Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs.
59 |
60 | ### Forbidden Warnings
61 |
62 | Search for `#![deny(...)]` in the code:
63 |
64 | * `unused_must_use`
65 | * `unused_imports`
66 |
67 | ## Implementing Backend
68 |
69 | 1. Consider to create an issue for potential support.
70 | 2. Add folder with the name of '{YOUR_BACKEND}' in /src/backend.
71 | 3. Add `mod.rs`, `implementation.rs` files to this folder.
72 | 4. Create `BackendImpl` struct and implement `Backend` trait.
73 | _maybe the code is out to date, then just implement the `Backend` trait._
74 |
75 | ```rust
76 | pub struct BackendImpl {
77 | _phantom: PhantomData,
78 | }
79 | ```
80 |
81 | 5. Implement Backend, check `/crossterm/implementation.rs` and `/termion/implementation.rs` out for references.
82 |
83 | ```rust
84 | use crate::{backend::Backend, error};
85 |
86 | impl Backend for BackendImpl {
87 | fn create() -> Self {
88 | unimplemented!()
89 | }
90 |
91 | fn act(&mut self, action: Action, buffer: &mut W) -> error::Result<()> {
92 | unimplemented!()
93 | }
94 |
95 | fn batch(&mut self, action: Action, buffer: &mut W) -> error::Result<()> {
96 | unimplemented!()
97 | }
98 |
99 | fn flush_batch(&mut self, buffer: &mut W) -> error::Result<()> {
100 | unimplemented!()
101 | }
102 |
103 | fn get(&self, retrieve_operation: Value) -> error::Result<()> {
104 | unimplemented!()
105 | }
106 | }
107 | ```
108 | 6. Reexport `{YOUR_BACKEND}::BackendImpl` in the module file you created at 3.
109 | `pub use self::implementation::BackendImpl;`.
110 |
111 | 7. Last but not least, export your module in `/src/backend/mod.rs`
112 |
113 | ```rust
114 | #[cfg(feature = "your_backend")]
115 | pub(crate) mod your_backend;
116 |
117 | #[cfg(feature = "your_backend")]
118 | pub(crate) use self::your_backend::BackendImpl;
119 | ```
120 |
121 | 8. Finaly, submit your PR.
--------------------------------------------------------------------------------
/src/enums/style.rs:
--------------------------------------------------------------------------------
1 | /// Represents an color.
2 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
4 | pub enum Color {
5 | /// Resets the terminal color.
6 | Reset,
7 | /// Black color.
8 | Black,
9 | /// Dark grey color.
10 | DarkGrey,
11 | /// Light red color.
12 | Red,
13 | /// Dark red color.
14 | DarkRed,
15 | /// Light green color.
16 | Green,
17 | /// Dark green color.
18 | DarkGreen,
19 | /// Light yellow color.
20 | Yellow,
21 | /// Dark yellow color.
22 | DarkYellow,
23 | /// Light blue color.
24 | Blue,
25 | /// Dark blue color.
26 | DarkBlue,
27 | /// Light magenta color.
28 | Magenta,
29 | /// Dark magenta color.
30 | DarkMagenta,
31 | /// Light cyan color.
32 | Cyan,
33 | /// Dark cyan color.
34 | DarkCyan,
35 | /// White color.
36 | White,
37 | /// Grey color.
38 | Grey,
39 | /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info.
40 | ///
41 | /// Most UNIX terminals and Windows 10 supported only.
42 | /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info.
43 | Rgb(u8, u8, u8),
44 |
45 | /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info.
46 | ///
47 | /// Most UNIX terminals and Windows 10 supported only.
48 | /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info.
49 | AnsiValue(u8),
50 | }
51 |
52 | impl From for Color {
53 | fn from(n: u8) -> Self {
54 | match n {
55 | 0 => Color::Black,
56 | 1 => Color::Red,
57 | 2 => Color::Green,
58 | 3 => Color::Yellow,
59 | 4 => Color::Blue,
60 | 5 => Color::Magenta,
61 | 6 => Color::Cyan,
62 | 7 => Color::White,
63 |
64 | 8 => Color::Black,
65 | 9 => Color::DarkRed,
66 | 10 => Color::DarkGreen,
67 | 11 => Color::DarkYellow,
68 | 12 => Color::DarkBlue,
69 | 13 => Color::DarkMagenta,
70 | 14 => Color::DarkCyan,
71 | 15 => Color::Grey,
72 |
73 | // parsing: https://stackoverflow.com/questions/27159322/rgb-values-of-the-colors-in-the-ansi-extended-colors-index-17-255
74 | _ if n > 15 && n < 232 => {
75 | let rgb_r = ((n - 16) / 36) * 51;
76 | let rgb_g = (((n - 16) % 36) / 6) * 51;
77 | let rgb_b = ((n - 16) % 6) * 51;
78 |
79 | Color::Rgb(rgb_r, rgb_g, rgb_b)
80 | }
81 | _ if n >= 232 => {
82 | let value = (n - 232) * 10 + 8;
83 | Color::Rgb(value, value, value)
84 | }
85 | _ => unreachable!(),
86 | }
87 | }
88 | }
89 |
90 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
91 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
92 | pub enum Attribute {
93 | /// Resets all the attributes.
94 | Reset,
95 |
96 | /// Increases the text intensity.
97 | Bold,
98 | /// Decreases the text intensity.
99 | BoldOff,
100 |
101 | /// Emphasises the text.
102 | Italic,
103 | /// Turns off the `Italic` attribute.
104 | ItalicOff,
105 |
106 | /// Underlines the text.
107 | Underlined,
108 | /// Turns off the `Underlined` attribute.
109 | UnderlinedOff,
110 |
111 | /// Makes the text blinking (< 150 per minute).
112 | SlowBlink,
113 | /// Makes the text blinking (>= 150 per minute).
114 | RapidBlink,
115 | /// Turns off the text blinking (`SlowBlink` or `RapidBlink`).
116 | BlinkOff,
117 |
118 | /// Crosses the text.
119 | Crossed,
120 | /// Turns off the `CrossedOut` attribute.
121 | CrossedOff,
122 |
123 | /// Swaps foreground and background colors.
124 | Reversed,
125 | /// Turns off the `Reverse` attribute.
126 | ReversedOff,
127 |
128 | /// Hides the text (also known as hidden).
129 | Conceal,
130 | /// Turns off the `Hidden` attribute.
131 | ConcealOff,
132 |
133 | /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface.
134 | ///
135 | /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols).
136 | Fraktur,
137 |
138 | /// Turns off the `Bold` attribute.
139 | NormalIntensity,
140 |
141 | /// Switches the text back to normal intensity (no bold, italic).
142 | BoldItalicOff,
143 | /// Makes the text framed.
144 | Framed,
145 |
146 | #[doc(hidden)]
147 | __Nonexhaustive,
148 | }
149 |
150 | impl From for String {
151 | fn from(attr: Attribute) -> Self {
152 | format!("{:?}", attr)
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/enums/event.rs:
--------------------------------------------------------------------------------
1 | use bitflags::bitflags;
2 |
3 | /// Represents an event.
4 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
5 | #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
6 | pub enum Event {
7 | /// A single key event with additional pressed modifiers.
8 | Key(KeyEvent),
9 | /// A singe mouse event with additional pressed modifiers.
10 | Mouse(MouseEvent),
11 | /// An resize event with new dimensions after resize (columns, rows).
12 | Resize,
13 | /// An event was not supported by the backend.
14 | Unknown,
15 | }
16 |
17 | /// Represents a mouse event.
18 | ///
19 | /// # Platform-specific Notes
20 | ///
21 | /// ## Mouse Buttons
22 | ///
23 | /// Some platforms/terminals do not report mouse button for the
24 | /// `MouseEvent::Up` and `MouseEvent::Drag` events. `MouseButton::Left`
25 | /// is returned if we don't know which button was used.
26 | ///
27 | /// ## Key Modifiers
28 | ///
29 | /// Some platforms/terminals does not report all key modifiers
30 | /// combinations for all mouse event types. For example - macOS reports
31 | /// `Ctrl` + left mouse button click as a right mouse button click.
32 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33 | #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
34 | pub enum MouseEvent {
35 | /// Pressed mouse button.
36 | ///
37 | /// Contains mouse button, pressed pointer location (column, row), and additional key modifiers.
38 | Down(MouseButton, u16, u16, KeyModifiers),
39 | /// Released mouse button.
40 | ///
41 | /// Contains mouse button, released pointer location (column, row), and additional key modifiers.
42 | Up(MouseButton, u16, u16, KeyModifiers),
43 | /// Moved mouse pointer while pressing a mouse button.
44 | ///
45 | /// Contains the pressed mouse button, released pointer location (column, row), and additional key modifiers.
46 | Drag(MouseButton, u16, u16, KeyModifiers),
47 | /// Scrolled mouse wheel downwards (towards the user).
48 | ///
49 | /// Contains the scroll location (column, row), and additional key modifiers.
50 | ScrollDown(u16, u16, KeyModifiers),
51 | /// Scrolled mouse wheel upwards (away from the user).
52 | ///
53 | /// Contains the scroll location (column, row), and additional key modifiers.
54 | ScrollUp(u16, u16, KeyModifiers),
55 | }
56 |
57 | impl MouseEvent {
58 | /// Returns the button used by this event, if any.
59 | ///
60 | /// Returns `None` if `self` is `WheelUp` or `WheelDown`.
61 | pub fn button(self) -> Option {
62 | match self {
63 | MouseEvent::Down(btn, ..) | MouseEvent::Up(btn, ..) | MouseEvent::Drag(btn, ..) => {
64 | Some(btn)
65 | }
66 | _ => None,
67 | }
68 | }
69 | }
70 |
71 | /// Represents a mouse button.
72 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73 | #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
74 | pub enum MouseButton {
75 | /// Left mouse button.
76 | Left,
77 | /// Right mouse button.
78 | Right,
79 | /// Middle mouse button.
80 | Middle,
81 | /// An mouse button was not supported by the backend.
82 | Unknown,
83 | }
84 |
85 | bitflags! {
86 | /// Represents key modifiers (shift, control, alt).
87 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88 | pub struct KeyModifiers: u8 {
89 | const SHIFT = 0b0000_0001;
90 | const CONTROL = 0b0000_0010;
91 | const ALT = 0b0000_0100;
92 | }
93 | }
94 |
95 | /// Represents a key event.
96 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97 | #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
98 | pub struct KeyEvent {
99 | /// The key itself.
100 | pub code: KeyCode,
101 | /// Additional key modifiers.
102 | pub modifiers: KeyModifiers,
103 | }
104 |
105 | impl KeyEvent {
106 | pub fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
107 | KeyEvent { code, modifiers }
108 | }
109 | }
110 |
111 | impl From for KeyEvent {
112 | fn from(code: KeyCode) -> Self {
113 | KeyEvent {
114 | code,
115 | modifiers: KeyModifiers::empty(),
116 | }
117 | }
118 | }
119 |
120 | /// Represents a key.
121 | #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
122 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
123 | pub enum KeyCode {
124 | /// Backspace key.
125 | Backspace,
126 | /// Enter key.
127 | Enter,
128 | /// Left arrow key.
129 | Left,
130 | /// Right arrow key.
131 | Right,
132 | /// Up arrow key.
133 | Up,
134 | /// Down arrow key.
135 | Down,
136 | /// Home key.
137 | Home,
138 | /// End key.
139 | End,
140 | /// Page up key.
141 | PageUp,
142 | /// Page dow key.
143 | PageDown,
144 | /// Tab key.
145 | Tab,
146 | /// Shift + Tab key.
147 | BackTab,
148 | /// Delete key.
149 | Delete,
150 | /// Insert key.
151 | Insert,
152 | /// F key.
153 | ///
154 | /// `KeyEvent::F(1)` represents F1 key, etc.
155 | F(u8),
156 | /// A character.
157 | ///
158 | /// `KeyEvent::Char('c')` represents `c` character, etc.
159 | Char(char),
160 | /// Null.
161 | Null,
162 | /// Escape key.
163 | Esc,
164 | }
165 |
--------------------------------------------------------------------------------
/src/backend/crossterm/implementation.rs:
--------------------------------------------------------------------------------
1 | use std::{io, io::Write};
2 |
3 | use crossterm::{
4 | cursor, event, style, terminal,
5 | terminal::{disable_raw_mode, enable_raw_mode},
6 | ExecutableCommand, QueueableCommand,
7 | };
8 |
9 | use crate::{backend::Backend, error, error::ErrorKind, Action, Event, Retrieved, Value};
10 |
11 | pub struct BackendImpl {
12 | // The internal buffer on which operations are performed and written to.
13 | buffer: W,
14 | // Crossterm panics if we disable the mouse event capture before we enabled it.
15 | // We need to check in the `drop` if we enabled it to prevent this.
16 | // Should be fixed in later crossterm releases.
17 | mouse_capture_enabled: bool,
18 | }
19 |
20 | impl Backend for BackendImpl {
21 | fn create(buffer: W) -> BackendImpl {
22 | BackendImpl {
23 | buffer,
24 | mouse_capture_enabled: false,
25 | }
26 | }
27 |
28 | fn act(&mut self, action: Action) -> error::Result<()> {
29 | self.batch(action)?;
30 | self.flush_batch()
31 | }
32 |
33 | #[allow(clippy::cognitive_complexity)]
34 | fn batch(&mut self, action: Action) -> error::Result<()> {
35 | let buffer = &mut self.buffer;
36 |
37 | let _ = match action {
38 | Action::MoveCursorTo(column, row) => buffer.queue(cursor::MoveTo(column, row))?,
39 | Action::HideCursor => buffer.queue(cursor::Hide)?,
40 | Action::ShowCursor => buffer.queue(cursor::Show)?,
41 | Action::EnableBlinking => buffer.queue(cursor::EnableBlinking)?,
42 | Action::DisableBlinking => buffer.queue(cursor::DisableBlinking)?,
43 | Action::ClearTerminal(clear_type) => {
44 | buffer.queue(terminal::Clear(terminal::ClearType::from(clear_type)))?
45 | }
46 | Action::SetTerminalSize(column, row) => buffer.queue(terminal::SetSize(column, row))?,
47 | Action::ScrollUp(rows) => buffer.queue(terminal::ScrollUp(rows))?,
48 | Action::ScrollDown(rows) => buffer.queue(terminal::ScrollDown(rows))?,
49 | Action::EnterAlternateScreen => {
50 | buffer.queue(terminal::EnterAlternateScreen)?;
51 | buffer
52 | }
53 | Action::LeaveAlternateScreen => {
54 | buffer.queue(terminal::LeaveAlternateScreen)?;
55 | buffer
56 | }
57 | Action::SetForegroundColor(color) => {
58 | buffer.queue(style::SetForegroundColor(style::Color::from(color)))?
59 | }
60 | Action::SetBackgroundColor(color) => {
61 | buffer.queue(style::SetBackgroundColor(style::Color::from(color)))?
62 | }
63 | Action::SetAttribute(attr) => {
64 | buffer.queue(style::SetAttribute(style::Attribute::from(attr)))?
65 | }
66 | Action::ResetColor => buffer.queue(style::ResetColor)?,
67 | Action::EnableRawMode => {
68 | enable_raw_mode()?;
69 | return Ok(());
70 | }
71 | Action::DisableRawMode => {
72 | disable_raw_mode()?;
73 | return Ok(());
74 | }
75 | Action::EnableMouseCapture => {
76 | self.mouse_capture_enabled = true;
77 | buffer.queue(event::EnableMouseCapture)?
78 | }
79 | Action::DisableMouseCapture => {
80 | self.mouse_capture_enabled = false;
81 | buffer.queue(event::DisableMouseCapture)?
82 | }
83 | };
84 |
85 | Ok(())
86 | }
87 |
88 | fn flush_batch(&mut self) -> error::Result<()> {
89 | self.buffer
90 | .flush()
91 | .map_err(|_| ErrorKind::FlushingBatchFailed)
92 | }
93 |
94 | fn get(&self, retrieve_operation: Value) -> error::Result {
95 | Ok(match retrieve_operation {
96 | Value::TerminalSize => {
97 | let size = terminal::size()?;
98 | Retrieved::TerminalSize(size.0, size.1)
99 | }
100 | Value::CursorPosition => {
101 | let position = cursor::position()?;
102 | Retrieved::CursorPosition(position.0, position.1)
103 | }
104 | Value::Event(duration) => {
105 | if let Some(duration) = duration {
106 | if event::poll(duration)? {
107 | let event = event::read()?;
108 | Retrieved::Event(Some(Event::from(event)))
109 | } else {
110 | Retrieved::Event(None)
111 | }
112 | } else {
113 | let event = event::read()?;
114 | Retrieved::Event(Some(Event::from(event)))
115 | }
116 | }
117 | })
118 | }
119 | }
120 |
121 | impl Drop for BackendImpl {
122 | fn drop(&mut self) {
123 | io::stdout()
124 | .execute(terminal::LeaveAlternateScreen)
125 | .unwrap();
126 |
127 | disable_raw_mode().unwrap();
128 |
129 | if self.mouse_capture_enabled {
130 | io::stdout().execute(event::DisableMouseCapture).unwrap();
131 | }
132 | }
133 | }
134 |
135 | impl Write for BackendImpl {
136 | fn write(&mut self, buf: &[u8]) -> Result {
137 | self.buffer.write(buf)
138 | }
139 |
140 | fn flush(&mut self) -> Result<(), io::Error> {
141 | self.buffer.flush()
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2)
4 | [![Latest Version][crate-badge]][crate-link]
5 | [![docs][docs-badge]][docs-link]
6 | ![Lines of Code][loc-badge]
7 | [![MIT][license-badge]][license-link]
8 | [![Join us on Discord][discord-badge]][discord-link]
9 | [![Stable Status][actions-stable-badge]][actions-link]
10 | [![Beta Status][actions-nightly-badge]][actions-link]
11 |
12 | # Unified API over different TUI libraries.
13 |
14 | This library offers a universal API over various terminal libraries such as
15 | [termion][termion], [crossterm][crossterm], [ncurses][ncurses], [pancurses][pancurses], and [console][console].
16 |
17 | Why would I need this library? Three main reasons:
18 | 1. Being less dependent on a specific terminal library with certain features.
19 | 2. Support different features depending on the chosen backend and allow you to change at any given time.
20 | 3. Hides implementation details (raw mode, write to the buffer, batch operations).
21 | 4. Hides the differences (cursor 0 or 1 based, cleaning resources, event handling, performing actions. )
22 | 5. Reduces backend mapping duplication in the ecosystem ([cursive][cursive], [tui][tui], [termimad][termimad], ...)
23 |
24 | This library is still quite young.
25 | If you experience problems, feel free to make an issue.
26 | I'd fix it as soon as possible.
27 |
28 | ## Table of Contents
29 |
30 | * [Features](#features)
31 | * [Implemented Backends](#implemented-backends)
32 | * [Getting Started](#getting-started)
33 | * [Other Resources](#other-resources)
34 | * [Contributing](#contributing)
35 |
36 | ## Features
37 |
38 | - Batching multiple terminal commands before executing (flush).
39 | - Complete control over the underlying buffer.
40 | - Locking the terminal for a certain duration.
41 | - Backend of your choice.
42 |
43 |
44 |
47 |
48 | ### Implemented Backends
49 |
50 | - [Crossterm][crossterm] (Pure rust and crossplatform)
51 | - [Termion][termion] (Pure rust for UNIX systems)
52 | - [Crosscurses][crosscurses] (crossplatform but requires ncurses C dependency (**fork pancurses**))
53 |
54 | Use **one** of the below feature flags to choose an backend.
55 |
56 | | Feature | Description |
57 | | :------ | :------ |
58 | | `crossterm-backend` | crossterm backend will be used.|
59 | | `termion-backend` | termion backend will be used.|
60 | | `crosscurses-backend` | crosscurses backend will be used.|
61 |
62 | _like_
63 | ```toml
64 | [dependencies.terminal]
65 | version = "0.2"
66 | features = ["crossterm-backend"]
67 | ```
68 |
69 | In the [backend-specification](docs/backend-specification.md) document you will find each backend and it's benefits described.
70 |
71 | ### Yet to Implement
72 | - [ncurses][ncurses]
73 |
74 | ## Getting Started
75 |
76 |
77 |
78 | Click to show Cargo.toml.
79 |
80 |
81 | ```toml
82 | [dependencies]
83 | terminal = "0.2"
84 | features = ["your_backend_choice"]
85 | ```
86 |
87 |
88 |
89 |
90 | ```rust
91 | use terminal::{Action, Clear, error, Retrieved, Value};
92 | use std::io::Write;
93 |
94 | pub fn main() -> error::Result<()> {
95 | let mut terminal = terminal::stdout();
96 |
97 | // perform an single action.
98 | terminal.act(Action::ClearTerminal(Clear::All))?;
99 |
100 | // batch multiple actions.
101 | for i in 0..20 {
102 | terminal.batch(Action::MoveCursorTo(0, i))?;
103 | terminal.write(format!("{}", i).as_bytes());
104 | }
105 |
106 | // execute batch.
107 | terminal.flush_batch();
108 |
109 | // get an terminal value.
110 | if let Retrieved::TerminalSize(x, y) = terminal.get(Value::TerminalSize)? {
111 | println!("\nx: {}, y: {}", x, y);
112 | }
113 |
114 | Ok(())
115 | }
116 | ```
117 |
118 | ### Other Resources
119 |
120 | - [API documentation](https://docs.rs/terminal/)
121 | - [Examples repository](/examples)
122 | - [Backend Specification](docs/backend-specification.md)
123 |
124 | ## Contributing
125 |
126 | I would appreciate any kind of contribution. Before you do, please,
127 | read the [Contributing](docs/CONTRIBUTING.md) guidelines.
128 |
129 | ## Authors
130 |
131 | * **Timon Post** - *Project Owner & creator*
132 |
133 | ## License
134 |
135 | This project, `terminal` are licensed under the MIT
136 | License - see the [LICENSE](https://github.com/crossterm-rs/terminal/blob/master/LICENSE) file for details.
137 |
138 | [crate-badge]: https://img.shields.io/crates/v/terminal.svg
139 | [crate-link]: https://crates.io/crates/terminal
140 |
141 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
142 | [license-link]: terminal/LICENSE
143 |
144 | [docs-badge]: https://docs.rs/terminal/badge.svg
145 | [docs-link]: https://docs.rs/terminal/
146 |
147 | [discord-badge]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
148 | [discord-link]: https://discord.gg/K4nyTDB
149 |
150 | [actions-link]: https://github.com/crossterm-rs/terminal/actions
151 | [actions-stable-badge]: https://github.com/crossterm-rs/terminal/workflows/Terminal%20Adapter%20Test/badge.svg
152 | [actions-nightly-badge]: https://github.com/crossterm-rs/terminal/workflows/Terminal%20Adapter%20Test/badge.svg
153 |
154 | [loc-badge]: https://tokei.rs/b1/github/crossterm-rs/terminal?category=code
155 |
156 | [termion]: https://crates.io/crates/termion
157 | [crossterm]: https://crates.io/crates/crossterm
158 | [cursive]: https://crates.io/crates/cursive
159 | [tui]: https://crates.io/crates/tui
160 | [termimad]: https://crates.io/crates/termimad
161 | [ncurses]: https://crates.io/crates/ncurses
162 | [crosscurses]: https://crates.io/crates/crosscurses
163 | [pancurses]: https://crates.io/crates/pancurses
164 | [console]: https://crates.io/crates/console
165 |
--------------------------------------------------------------------------------
/src/terminal.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io::{self, Stderr, Stdout, Write},
3 | sync::{RwLock, RwLockWriteGuard},
4 | };
5 |
6 | use crate::{
7 | backend::{Backend as _, BackendImpl},
8 | error, Action, Retrieved, Value,
9 | };
10 |
11 | /// Creates a [Stdout](https://doc.rust-lang.org/std/io/struct.Stdout.html) buffered [Terminal](struct.Terminal.html).
12 | pub fn stdout() -> Terminal {
13 | Terminal::custom(io::stdout())
14 | }
15 |
16 | /// Creates a [Stderr](https://doc.rust-lang.org/std/io/struct.Stdout.html) buffered [Terminal](struct.Terminal.html).
17 | pub fn stderr() -> Terminal {
18 | Terminal::custom(io::stderr())
19 | }
20 |
21 | /// A simple interface to perform operations on the terminal.
22 | /// It also allows terminal values to be queried.
23 | ///
24 | /// # Examples
25 | ///
26 | /// ```no_run
27 | /// use terminal::{Clear, Action, Value, Retrieved, error};
28 | ///
29 | /// pub fn main() -> error::Result<()> {
30 | /// let terminal = terminal::stdout();
31 | ///
32 | /// // perform an single action.
33 | /// terminal.act(Action::ClearTerminal(Clear::All))?;
34 | ///
35 | /// // batch multiple actions.
36 | /// for i in 0..100 {
37 | /// terminal.batch(Action::MoveCursorTo(0, i))?;
38 | /// }
39 | ///
40 | /// // execute batch.
41 | /// terminal.flush_batch();
42 | ///
43 | /// // get an terminal value.
44 | /// if let Retrieved::TerminalSize(x, y) = terminal.get(Value::TerminalSize)? {
45 | /// println!("x: {}, y: {}", x, y);
46 | /// }
47 | ///
48 | /// Ok(())
49 | /// }
50 | /// ```
51 | ///
52 | /// # Notes
53 | pub struct Terminal {
54 | // Access to the `Terminal` internals is ONLY allowed if this lock is acquired,
55 | // use `lock_mut()`.
56 | lock: RwLock>,
57 | }
58 |
59 | impl Terminal {
60 | /// Creates a custom buffered [Terminal](struct.Terminal.html) with the given buffer.
61 | pub fn custom(buffer: W) -> Terminal {
62 | Terminal {
63 | lock: RwLock::new(BackendImpl::create(buffer)),
64 | }
65 | }
66 |
67 | /// Locks this [Terminal](struct.Terminal.html), returning a mutable lock guard.
68 | /// A deadlock is not possible, instead an error will be returned if a lock is already in use.
69 | /// Make sure this lock is only used at one place.
70 | /// The lock is released when the returned lock goes out of scope.
71 | pub fn lock_mut(&self) -> error::Result> {
72 | if let Ok(lock) = self.lock.try_write() {
73 | Ok(TerminalLock::new(lock))
74 | } else {
75 | Err(error::ErrorKind::AttemptToAcquireLock(
76 | "`Terminal` can only be mutably borrowed once.".to_string(),
77 | ))
78 | }
79 | }
80 |
81 | /// Performs an action on the terminal.
82 | ///
83 | /// # Note
84 | ///
85 | /// Acquires an lock for underlying mutability,
86 | /// this can be prevented with [lock_mut](struct.Terminal.html#method.lock_mut).
87 | pub fn act(&self, action: Action) -> error::Result<()> {
88 | let mut lock = self.lock_mut()?;
89 | lock.act(action)
90 | }
91 |
92 | /// Batches an action for later execution.
93 | /// You can flush/execute the batched actions with [batch](struct.Terminal.html#method.flush_batch).
94 | ///
95 | /// # Note
96 | ///
97 | /// Acquires an lock for underlying mutability,
98 | /// this can be prevented with [lock_mut](struct.Terminal.html#method.lock_mut).
99 | pub fn batch(&self, action: Action) -> error::Result<()> {
100 | let mut lock = self.lock_mut()?;
101 | lock.batch(action)
102 | }
103 |
104 | /// Flushes the batched actions, this executes the actions in the order that they were batched.
105 | /// You can batch an action with [batch](struct.Terminal.html#method.batch).
106 | ///
107 | /// # Note
108 | ///
109 | /// Acquires an lock for underlying mutability,
110 | /// this can be prevented with [lock_mut](struct.Terminal.html#method.lock_mut).
111 | pub fn flush_batch(&self) -> error::Result<()> {
112 | let mut lock = self.lock_mut()?;
113 | lock.flush_batch()
114 | }
115 |
116 | /// Gets an value from the terminal.
117 | pub fn get(&self, value: Value) -> error::Result {
118 | let lock = self.lock_mut()?;
119 | lock.get(value)
120 | }
121 | }
122 |
123 | impl<'a, W: Write> Write for Terminal {
124 | fn write(&mut self, buf: &[u8]) -> io::Result {
125 | let mut lock = self.lock_mut().unwrap();
126 | lock.backend.write(buf)
127 | }
128 |
129 | fn flush(&mut self) -> io::Result<()> {
130 | let mut lock = self.lock_mut().unwrap();
131 | lock.backend.flush()
132 | }
133 | }
134 |
135 | /// A mutable lock to the [Terminal](struct.Terminal.html).
136 | pub struct TerminalLock<'a, W: Write> {
137 | backend: RwLockWriteGuard<'a, BackendImpl>,
138 | }
139 |
140 | impl<'a, W: Write> TerminalLock<'a, W> {
141 | pub fn new(locked_backend: RwLockWriteGuard<'a, BackendImpl>) -> TerminalLock<'a, W> {
142 | TerminalLock {
143 | backend: locked_backend,
144 | }
145 | }
146 |
147 | /// See [Terminal::act](struct.Terminal.html#method.act).
148 | pub fn act(&mut self, action: Action) -> error::Result<()> {
149 | self.backend.act(action)
150 | }
151 |
152 | /// See [Terminal::batch](struct.Terminal.html#method.batch).
153 | pub fn batch(&mut self, action: Action) -> error::Result<()> {
154 | self.backend.batch(action)
155 | }
156 |
157 | /// See [Terminal::flush_batch](struct.Terminal.html#method.flush_batch).
158 | pub fn flush_batch(&mut self) -> error::Result<()> {
159 | self.backend.flush_batch()
160 | }
161 |
162 | /// See [Terminal::get](struct.Terminal.html#method.get).
163 | pub fn get(&self, value: Value) -> error::Result {
164 | self.backend.get(value)
165 | }
166 | }
167 |
168 | impl<'a, W: Write> Write for TerminalLock<'a, W> {
169 | fn write(&mut self, buf: &[u8]) -> io::Result {
170 | self.backend.write(buf)
171 | }
172 |
173 | fn flush(&mut self) -> io::Result<()> {
174 | self.backend.flush()
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/backend/crossterm/mapping.rs:
--------------------------------------------------------------------------------
1 | use std::{error::Error, io};
2 |
3 | use crossterm::{event, style, terminal};
4 |
5 | use crate::{
6 | error::ErrorKind, Attribute, Clear, Color, Event, KeyCode, KeyEvent, KeyModifiers, MouseButton,
7 | MouseEvent,
8 | };
9 |
10 | impl From for style::Attribute {
11 | fn from(attribute: Attribute) -> Self {
12 | match attribute {
13 | Attribute::Reset => style::Attribute::Reset,
14 | Attribute::Bold => style::Attribute::Bold,
15 | Attribute::BoldItalicOff => style::Attribute::Dim,
16 | Attribute::Italic => style::Attribute::Italic,
17 | Attribute::Underlined => style::Attribute::Underlined,
18 | Attribute::SlowBlink => style::Attribute::SlowBlink,
19 | Attribute::RapidBlink => style::Attribute::RapidBlink,
20 | Attribute::Reversed => style::Attribute::Reverse,
21 | Attribute::Conceal => style::Attribute::Hidden,
22 | Attribute::Crossed => style::Attribute::CrossedOut,
23 | Attribute::Fraktur => style::Attribute::Fraktur,
24 | Attribute::BoldOff => style::Attribute::NoBold,
25 | Attribute::NormalIntensity => style::Attribute::NormalIntensity,
26 | Attribute::ItalicOff => style::Attribute::NoItalic,
27 | Attribute::UnderlinedOff => style::Attribute::NoUnderline,
28 | Attribute::BlinkOff => style::Attribute::NoBlink,
29 | Attribute::ReversedOff => style::Attribute::NoReverse,
30 | Attribute::ConcealOff => style::Attribute::NoHidden,
31 | Attribute::CrossedOff => style::Attribute::NotCrossedOut,
32 | Attribute::Framed => style::Attribute::Framed,
33 | Attribute::__Nonexhaustive => style::Attribute::__Nonexhaustive,
34 | }
35 | }
36 | }
37 |
38 | impl From for style::Color {
39 | fn from(color: Color) -> Self {
40 | match color {
41 | Color::Reset => style::Color::Reset,
42 | Color::Black => style::Color::Black,
43 | Color::DarkGrey => style::Color::DarkGrey,
44 | Color::Red => style::Color::Red,
45 | Color::DarkRed => style::Color::DarkRed,
46 | Color::Green => style::Color::Green,
47 | Color::DarkGreen => style::Color::DarkGreen,
48 | Color::Yellow => style::Color::Yellow,
49 | Color::DarkYellow => style::Color::DarkYellow,
50 | Color::Blue => style::Color::Blue,
51 | Color::DarkBlue => style::Color::DarkBlue,
52 | Color::Magenta => style::Color::Magenta,
53 | Color::DarkMagenta => style::Color::DarkMagenta,
54 | Color::Cyan => style::Color::Cyan,
55 | Color::DarkCyan => style::Color::DarkCyan,
56 | Color::White => style::Color::White,
57 | Color::Grey => style::Color::Grey,
58 | Color::Rgb(r, g, b) => style::Color::Rgb { r, g, b },
59 | Color::AnsiValue(val) => style::Color::AnsiValue(val),
60 | }
61 | }
62 | }
63 |
64 | impl From for terminal::ClearType {
65 | fn from(clear_type: Clear) -> Self {
66 | match clear_type {
67 | Clear::All => terminal::ClearType::All,
68 | Clear::FromCursorDown => terminal::ClearType::FromCursorDown,
69 | Clear::FromCursorUp => terminal::ClearType::FromCursorUp,
70 | Clear::CurrentLine => terminal::ClearType::CurrentLine,
71 | Clear::UntilNewLine => terminal::ClearType::UntilNewLine,
72 | }
73 | }
74 | }
75 |
76 | impl From for MouseButton {
77 | fn from(buttons: event::MouseButton) -> Self {
78 | match buttons {
79 | event::MouseButton::Left => MouseButton::Left,
80 | event::MouseButton::Right => MouseButton::Right,
81 | event::MouseButton::Middle => MouseButton::Middle,
82 | }
83 | }
84 | }
85 |
86 | impl From for MouseEvent {
87 | fn from(event: event::MouseEvent) -> Self {
88 | match event {
89 | event::MouseEvent::Down(btn, x, y, modifiers) => {
90 | MouseEvent::Down(btn.into(), x, y, modifiers.into())
91 | }
92 | event::MouseEvent::Up(btn, x, y, modifiers) => {
93 | MouseEvent::Up(btn.into(), x, y, modifiers.into())
94 | }
95 | event::MouseEvent::Drag(btn, x, y, modifiers) => {
96 | MouseEvent::Drag(btn.into(), x, y, modifiers.into())
97 | }
98 | event::MouseEvent::ScrollDown(x, y, modifiers) => {
99 | MouseEvent::ScrollUp(x, y, modifiers.into())
100 | }
101 | event::MouseEvent::ScrollUp(x, y, modifiers) => {
102 | MouseEvent::ScrollDown(x, y, modifiers.into())
103 | }
104 | }
105 | }
106 | }
107 |
108 | impl From for KeyModifiers {
109 | fn from(modifiers: event::KeyModifiers) -> Self {
110 | let shift = modifiers.contains(event::KeyModifiers::SHIFT);
111 | let ctrl = modifiers.contains(event::KeyModifiers::CONTROL);
112 | let alt = modifiers.contains(event::KeyModifiers::ALT);
113 |
114 | let mut modifiers = KeyModifiers::empty();
115 |
116 | if shift {
117 | modifiers |= KeyModifiers::SHIFT;
118 | }
119 | if ctrl {
120 | modifiers |= KeyModifiers::CONTROL;
121 | }
122 | if alt {
123 | modifiers |= KeyModifiers::ALT;
124 | }
125 |
126 | modifiers
127 | }
128 | }
129 |
130 | impl From for KeyCode {
131 | fn from(code: event::KeyCode) -> Self {
132 | match code {
133 | event::KeyCode::Backspace => KeyCode::Backspace,
134 | event::KeyCode::Enter => KeyCode::Enter,
135 | event::KeyCode::Left => KeyCode::Left,
136 | event::KeyCode::Right => KeyCode::Right,
137 | event::KeyCode::Up => KeyCode::Up,
138 | event::KeyCode::Down => KeyCode::Down,
139 | event::KeyCode::Home => KeyCode::Home,
140 | event::KeyCode::End => KeyCode::End,
141 | event::KeyCode::PageUp => KeyCode::PageUp,
142 | event::KeyCode::PageDown => KeyCode::PageDown,
143 | event::KeyCode::Tab => KeyCode::Tab,
144 | event::KeyCode::BackTab => KeyCode::BackTab,
145 | event::KeyCode::Delete => KeyCode::Delete,
146 | event::KeyCode::Insert => KeyCode::Insert,
147 | event::KeyCode::F(f) => KeyCode::F(f),
148 | event::KeyCode::Char(c) => KeyCode::Char(c),
149 | event::KeyCode::Null => KeyCode::Null,
150 | event::KeyCode::Esc => KeyCode::Esc,
151 | }
152 | }
153 | }
154 |
155 | impl From for KeyEvent {
156 | fn from(event: event::KeyEvent) -> Self {
157 | KeyEvent {
158 | code: KeyCode::from(event.code),
159 | modifiers: KeyModifiers::from(event.modifiers),
160 | }
161 | }
162 | }
163 |
164 | impl From for Event {
165 | fn from(event: event::Event) -> Self {
166 | match event {
167 | event::Event::Key(key) => Event::Key(KeyEvent::from(key)),
168 | event::Event::Mouse(mouse) => Event::Mouse(MouseEvent::from(mouse)),
169 | event::Event::Resize(_x, _y) => Event::Resize,
170 | }
171 | }
172 | }
173 |
174 | impl From for ErrorKind {
175 | fn from(error: crossterm::ErrorKind) -> Self {
176 | match error {
177 | crossterm::ErrorKind::IoError(e) => ErrorKind::IoError(e),
178 | e => ErrorKind::IoError(io::Error::new(io::ErrorKind::Other, e.description())),
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/backend/termion/implementation.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt,
3 | fmt::{Display, Formatter},
4 | fs::File,
5 | io,
6 | io::Write,
7 | result,
8 | sync::{
9 | atomic::{AtomicBool, Ordering},
10 | Arc,
11 | },
12 | thread,
13 | };
14 |
15 | use crossbeam_channel::{select, unbounded, Receiver};
16 | use termion::{
17 | clear, color, cursor, get_tty,
18 | input::TermRead,
19 | raw::{IntoRawMode, RawTerminal},
20 | screen, style, terminal_size,
21 | };
22 |
23 | use crate::{
24 | backend::{resize, termion::cursor::position, Backend},
25 | error,
26 | error::ErrorKind,
27 | Action, Attribute, Clear, Color, Event, Retrieved, Value,
28 | };
29 |
30 | /// A sequence of escape codes to enable terminal mouse support.
31 | /// We use this directly instead of using `MouseTerminal` from termion.
32 | const ENABLE_MOUSE_CAPTURE: &str = "\x1B[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h";
33 |
34 | /// A sequence of escape codes to disable terminal mouse support.
35 | /// We use this directly instead of using `MouseTerminal` from termion.
36 | const DISABLE_MOUSE_CAPTURE: &str = "\x1B[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l";
37 |
38 | /// Writer which writes either an foreground or background color escape code to the formatter.
39 | struct ColorCodeWriter {
40 | color: T,
41 | is_fg: bool,
42 | }
43 |
44 | impl ColorCodeWriter {
45 | pub fn new(color: T, is_fg: bool) -> ColorCodeWriter {
46 | ColorCodeWriter { color, is_fg }
47 | }
48 | }
49 |
50 | impl Display for ColorCodeWriter {
51 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
52 | if self.is_fg {
53 | self.color.write_fg(f)
54 | } else {
55 | self.color.write_bg(f)
56 | }
57 | }
58 | }
59 |
60 | pub struct BackendImpl {
61 | // Write operations are forwarded to this type when raw mode is enabled.
62 | // termion wraps raw mode in an struct which requires owner ship of the buffer.
63 | // We can't give ownership to the buffer, because it is owned by `Terminal`.
64 | // Also we can't change the buffer type to `RawTerminal` at run time because of the generic type.
65 | raw_buffer: Option>>,
66 | buffer: W,
67 |
68 | input_receiver: Option>,
69 | resize_receiver: Option>,
70 |
71 | is_raw_mode_enabled: bool,
72 | }
73 |
74 | impl BackendImpl {
75 | /// Write the given color to the given buffer.
76 | pub fn w_color(&mut self, color: T, is_fg: bool) -> io::Result<()> {
77 | if let Some(ref mut terminal) = self.raw_buffer {
78 | write!(terminal, "{}", ColorCodeWriter::new(color, is_fg))
79 | } else {
80 | write!(self.buffer, "{}", ColorCodeWriter::new(color, is_fg))
81 | }
82 | }
83 |
84 | /// Format the given color and write it to the given buffer.
85 | pub fn f_color(&mut self, color: Color, is_fg: bool) -> io::Result<()> {
86 | match color {
87 | Color::Reset => self.w_color(color::Reset, is_fg),
88 | Color::Black => self.w_color(color::Black, is_fg),
89 | Color::DarkGrey => self.w_color(color::Black, is_fg),
90 | Color::Red => self.w_color(color::LightRed, is_fg),
91 | Color::DarkRed => self.w_color(color::Red, is_fg),
92 | Color::Green => self.w_color(color::LightGreen, is_fg),
93 | Color::DarkGreen => self.w_color(color::Green, is_fg),
94 | Color::Yellow => self.w_color(color::LightYellow, is_fg),
95 | Color::DarkYellow => self.w_color(color::Yellow, is_fg),
96 | Color::Blue => self.w_color(color::LightBlue, is_fg),
97 | Color::DarkBlue => self.w_color(color::Blue, is_fg),
98 | Color::Magenta => self.w_color(color::LightMagenta, is_fg),
99 | Color::DarkMagenta => self.w_color(color::Magenta, is_fg),
100 | Color::Cyan => self.w_color(color::LightCyan, is_fg),
101 | Color::DarkCyan => self.w_color(color::Cyan, is_fg),
102 | Color::White => self.w_color(color::White, is_fg),
103 | Color::Grey => self.w_color(color::LightWhite, is_fg),
104 | Color::Rgb(r, g, b) => self.w_color(color::Rgb(r, g, b), is_fg),
105 | Color::AnsiValue(val) => self.w_color(color::AnsiValue(val), is_fg),
106 | }
107 | }
108 |
109 | /// Write displayable type to the given buffer.
110 | pub fn w_display(&mut self, displayable: &dyn Display) -> io::Result<()> {
111 | if let Some(ref mut terminal) = self.raw_buffer {
112 | write!(terminal, "{}", displayable)
113 | } else {
114 | write!(self.buffer, "{}", displayable)
115 | }
116 | }
117 |
118 | /// Format the given attribute and write it to the given buffer.
119 | pub fn f_attribute(&mut self, attribute: Attribute) -> error::Result<()> {
120 | match attribute {
121 | Attribute::SlowBlink => self.w_display(&style::Blink)?,
122 | Attribute::RapidBlink => self.w_display(&style::Blink)?,
123 | Attribute::BlinkOff => self.w_display(&style::NoBlink)?,
124 |
125 | Attribute::Bold => self.w_display(&style::Bold)?,
126 | Attribute::BoldOff => self.w_display(&style::NoBold)?,
127 |
128 | Attribute::Crossed => self.w_display(&style::CrossedOut)?,
129 | Attribute::CrossedOff => self.w_display(&style::NoCrossedOut)?,
130 |
131 | Attribute::BoldItalicOff => self.w_display(&style::Faint)?,
132 |
133 | Attribute::Framed => self.w_display(&style::Framed)?,
134 |
135 | Attribute::Reversed => self.w_display(&style::Invert)?,
136 | Attribute::ReversedOff => self.w_display(&style::NoInvert)?,
137 |
138 | Attribute::Italic => self.w_display(&style::Italic)?,
139 | Attribute::ItalicOff => self.w_display(&style::NoItalic)?,
140 |
141 | Attribute::Underlined => self.w_display(&style::Underline)?,
142 | Attribute::UnderlinedOff => self.w_display(&style::NoUnderline)?,
143 |
144 | Attribute::Reset => self.w_display(&style::Reset)?,
145 | _ => {
146 | // ConcealOff, ConcealOff, Fraktur, NormalIntensity not supported.
147 | return Err(error::ErrorKind::AttributeNotSupported(String::from(
148 | attribute,
149 | )));
150 | }
151 | };
152 |
153 | Ok(())
154 | }
155 | }
156 |
157 | impl Backend for BackendImpl {
158 | fn create(buffer: W) -> Self {
159 | let (input_sender, input_receiver) = unbounded::();
160 | let (resize_sender, resize_receiver) = unbounded();
161 |
162 | let running = Arc::new(AtomicBool::new(true));
163 |
164 | #[cfg(unix)]
165 | resize::start_resize_thread(resize_sender, Arc::clone(&running));
166 |
167 | // termion is blocking by default, read input from a separate thread.
168 | thread::spawn(move || {
169 | let input = termion::get_tty().unwrap();
170 | let mut events = input.events();
171 |
172 | while let Some(Ok(event)) = events.next() {
173 | // If we can't send, then receiving side closed, stop thread.
174 | if input_sender.send(Event::from(event)).is_err() {
175 | break;
176 | }
177 | }
178 |
179 | running.store(false, Ordering::Relaxed);
180 | });
181 |
182 | BackendImpl {
183 | raw_buffer: None,
184 | buffer,
185 | resize_receiver: Some(resize_receiver),
186 | input_receiver: Some(input_receiver),
187 | is_raw_mode_enabled: false,
188 | }
189 | }
190 |
191 | fn act(&mut self, action: Action) -> error::Result<()> {
192 | self.batch(action)?;
193 | self.flush_batch()
194 | }
195 |
196 | #[allow(clippy::cognitive_complexity)]
197 | fn batch(&mut self, action: Action) -> error::Result<()> {
198 | match action {
199 | Action::MoveCursorTo(column, row) => {
200 | self.w_display(&cursor::Goto(column + 1, row + 1))?
201 | }
202 | Action::HideCursor => self.w_display(&cursor::Hide)?,
203 | Action::ShowCursor => self.w_display(&cursor::Show)?,
204 | Action::ClearTerminal(clear_type) => match clear_type {
205 | Clear::All => {
206 | self.w_display(&clear::All)?;
207 | }
208 | Clear::FromCursorDown => self.w_display(&clear::AfterCursor)?,
209 | Clear::FromCursorUp => self.w_display(&clear::BeforeCursor)?,
210 | Clear::CurrentLine => self.w_display(&clear::CurrentLine)?,
211 | Clear::UntilNewLine => self.w_display(&clear::UntilNewline)?,
212 | },
213 | Action::EnterAlternateScreen => self.w_display(&screen::ToAlternateScreen)?,
214 | Action::LeaveAlternateScreen => self.w_display(&screen::ToMainScreen)?,
215 | Action::SetForegroundColor(color) => self.f_color(color, true)?,
216 | Action::SetBackgroundColor(color) => self.f_color(color, false)?,
217 | Action::SetAttribute(attr) => self.f_attribute(attr)?,
218 | Action::ResetColor => self.w_display(&format!(
219 | "{}{}",
220 | color::Reset.fg_str(),
221 | color::Reset.bg_str()
222 | ))?,
223 | Action::EnableRawMode => {
224 | self.raw_buffer = Some(Box::new(termion::get_tty()?.into_raw_mode().unwrap()));
225 | self.is_raw_mode_enabled = true;
226 | }
227 | Action::DisableRawMode => {
228 | if self.raw_buffer.is_some() {
229 | self.raw_buffer = None;
230 | self.is_raw_mode_enabled = false;
231 | }
232 | }
233 | Action::EnableMouseCapture => {
234 | self.buffer.write_all(ENABLE_MOUSE_CAPTURE.as_bytes())?;
235 | }
236 | Action::DisableMouseCapture => {
237 | self.buffer.write_all(DISABLE_MOUSE_CAPTURE.as_bytes())?;
238 | }
239 | Action::SetTerminalSize(..)
240 | | Action::EnableBlinking
241 | | Action::DisableBlinking
242 | | Action::ScrollUp(_)
243 | | Action::ScrollDown(_) => {
244 | return Err(error::ErrorKind::ActionNotSupported(String::from(action)))
245 | }
246 | };
247 |
248 | self.flush_batch()
249 | }
250 |
251 | fn flush_batch(&mut self) -> error::Result<()> {
252 | self.buffer
253 | .flush()
254 | .map_err(|_| ErrorKind::FlushingBatchFailed)
255 | }
256 |
257 | fn get(&self, retrieve_operation: Value) -> error::Result {
258 | Ok(match retrieve_operation {
259 | Value::TerminalSize => {
260 | let size = terminal_size()?;
261 | Retrieved::TerminalSize(size.0, size.1)
262 | }
263 | Value::CursorPosition => {
264 | // if raw mode is disabled, we need to enable and disable it.
265 | // Otherwise the position is written to the console window.
266 | let (x, y) = if self.is_raw_mode_enabled {
267 | position()?
268 | } else {
269 | get_tty()?.into_raw_mode()?;
270 | position()?
271 | };
272 |
273 | Retrieved::CursorPosition(x, y)
274 | }
275 | Value::Event(duration) => {
276 | if let Some(ref input_receiver) = self.input_receiver {
277 | if let Some(ref resize_receiver) = self.resize_receiver {
278 | let event = if let Some(duration) = duration {
279 | select! {
280 | recv(input_receiver) -> event => event.ok(),
281 | recv(resize_receiver) -> _ => Some(Event::Resize),
282 | default(duration) => None,
283 | }
284 | } else {
285 | select! {
286 | recv(input_receiver) -> event => event.ok(),
287 | recv(resize_receiver) -> _ => Some(Event::Resize),
288 | }
289 | };
290 | return Ok(event.map_or(Retrieved::Event(None), |event| {
291 | Retrieved::Event(Some(event))
292 | }));
293 | };
294 | };
295 |
296 | Retrieved::Event(None)
297 | }
298 | })
299 | }
300 | }
301 |
302 | impl Write for BackendImpl {
303 | fn write(&mut self, buf: &[u8]) -> result::Result {
304 | self.buffer.write(buf)
305 | }
306 |
307 | fn flush(&mut self) -> result::Result<(), io::Error> {
308 | self.buffer.flush()
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/backend/crosscurses/mapping.rs:
--------------------------------------------------------------------------------
1 | use crate::{Color, Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
2 | use crosscurses::{mmask_t, Input};
3 | use std::io::Write;
4 |
5 | impl super::BackendImpl {
6 | pub fn parse_next(&self, input: crosscurses::Input) -> Event {
7 | // Try to map the crosscurses input event to an `KeyEvent` with possible modifiers.
8 | let key_event = self.try_parse_key(input).map_or(
9 | self.try_map_shift_key(input).map_or(
10 | self.try_map_ctrl_key(input)
11 | .map_or(self.try_map_ctrl_alt_key(input), Some),
12 | Some,
13 | ),
14 | Some,
15 | );
16 |
17 | match key_event {
18 | Some(key_event) => Event::Key(key_event),
19 | None => {
20 | // TODO, if your event is not mapped, feel free to add it.
21 | // Although other backends have to support it as well.
22 | // The point of this library is to support the most of the important keys.
23 |
24 | self.try_map_non_key_event(input)
25 | .map_or(Event::Unknown, |e| e)
26 | }
27 | }
28 | }
29 |
30 | /// Matches on keys without modifiers, returns `None` if the key has modifiers or is not supported.
31 | pub fn try_parse_key(&self, input: crosscurses::Input) -> Option {
32 | let empty = KeyModifiers::empty();
33 |
34 | let key_code = match input {
35 | Input::Character(c) => match c {
36 | '\r' | '\n' => Some(KeyCode::Enter.into()),
37 | '\t' => Some(KeyCode::Tab.into()),
38 | '\x7F' => Some(KeyCode::Backspace.into()),
39 | '\u{8}' => Some(KeyCode::Backspace.into()),
40 | c @ '\x01'..='\x1A' => Some(KeyEvent::new(
41 | KeyCode::Char((c as u8 - 0x1 + b'a') as char),
42 | KeyModifiers::CONTROL,
43 | )),
44 | c @ '\x1C'..='\x1F' => Some(KeyEvent::new(
45 | KeyCode::Char((c as u8 - 0x1C + b'4') as char),
46 | KeyModifiers::CONTROL,
47 | )),
48 | _ if (c as u32) <= 26 => Some(KeyEvent::new(
49 | KeyCode::Char((b'a' - 1 + c as u8) as char),
50 | KeyModifiers::CONTROL,
51 | )),
52 | '\u{1b}' => Some(KeyCode::Esc.into()),
53 | c => Some(KeyCode::Char(c).into()),
54 | },
55 | Input::KeyDown => Some(KeyEvent {
56 | code: KeyCode::Down,
57 | modifiers: empty,
58 | }),
59 | Input::KeyUp => Some(KeyEvent {
60 | code: KeyCode::Up,
61 | modifiers: empty,
62 | }),
63 | Input::KeyLeft => Some(KeyEvent {
64 | code: KeyCode::Left,
65 | modifiers: empty,
66 | }),
67 | Input::KeyRight => Some(KeyEvent {
68 | code: KeyCode::Right,
69 | modifiers: empty,
70 | }),
71 | Input::KeyHome => Some(KeyEvent {
72 | code: KeyCode::Home,
73 | modifiers: empty,
74 | }),
75 | Input::KeyBackspace => Some(KeyEvent {
76 | code: KeyCode::Backspace,
77 | modifiers: empty,
78 | }),
79 | Input::KeyF0 => Some(KeyEvent {
80 | code: KeyCode::F(0),
81 | modifiers: empty,
82 | }),
83 | Input::KeyF1 => Some(KeyEvent {
84 | code: KeyCode::F(1),
85 | modifiers: empty,
86 | }),
87 | Input::KeyF2 => Some(KeyEvent {
88 | code: KeyCode::F(2),
89 | modifiers: empty,
90 | }),
91 | Input::KeyF3 => Some(KeyEvent {
92 | code: KeyCode::F(3),
93 | modifiers: empty,
94 | }),
95 | Input::KeyF4 => Some(KeyEvent {
96 | code: KeyCode::F(4),
97 | modifiers: empty,
98 | }),
99 | Input::KeyF5 => Some(KeyEvent {
100 | code: KeyCode::F(5),
101 | modifiers: empty,
102 | }),
103 | Input::KeyF6 => Some(KeyEvent {
104 | code: KeyCode::F(6),
105 | modifiers: empty,
106 | }),
107 | Input::KeyF7 => Some(KeyEvent {
108 | code: KeyCode::F(7),
109 | modifiers: empty,
110 | }),
111 | Input::KeyF8 => Some(KeyEvent {
112 | code: KeyCode::F(8),
113 | modifiers: empty,
114 | }),
115 | Input::KeyF9 => Some(KeyEvent {
116 | code: KeyCode::F(9),
117 | modifiers: empty,
118 | }),
119 | Input::KeyF10 => Some(KeyEvent {
120 | code: KeyCode::F(10),
121 | modifiers: empty,
122 | }),
123 | Input::KeyF11 => Some(KeyEvent {
124 | code: KeyCode::F(11),
125 | modifiers: empty,
126 | }),
127 | Input::KeyF12 => Some(KeyEvent {
128 | code: KeyCode::F(12),
129 | modifiers: empty,
130 | }),
131 | Input::KeyF13 => Some(KeyEvent {
132 | code: KeyCode::F(13),
133 | modifiers: empty,
134 | }),
135 | Input::KeyF14 => Some(KeyEvent {
136 | code: KeyCode::F(14),
137 | modifiers: empty,
138 | }),
139 | Input::KeyF15 => Some(KeyEvent {
140 | code: KeyCode::F(15),
141 | modifiers: empty,
142 | }),
143 | Input::KeyDL => Some(KeyEvent {
144 | code: KeyCode::Delete,
145 | modifiers: empty,
146 | }),
147 | Input::KeyIC => Some(KeyEvent {
148 | code: KeyCode::Insert,
149 | modifiers: empty,
150 | }),
151 | Input::KeyNPage => Some(KeyEvent {
152 | code: KeyCode::PageDown,
153 | modifiers: empty,
154 | }),
155 | Input::KeyPPage => Some(KeyEvent {
156 | code: KeyCode::PageUp,
157 | modifiers: empty,
158 | }),
159 | Input::KeyEnter => Some(KeyEvent {
160 | code: KeyCode::Enter,
161 | modifiers: empty,
162 | }),
163 | Input::KeyEnd => Some(KeyEvent {
164 | code: KeyCode::End,
165 | modifiers: empty,
166 | }),
167 | _ => None,
168 | };
169 |
170 | key_code.map(|e| e)
171 | }
172 |
173 | /// Matches on shift keys, returns `None` if the key does not have an SHIFT modifier or is not supported.
174 | pub fn try_map_shift_key(&self, input: crosscurses::Input) -> Option {
175 | let key_code = match input {
176 | Input::KeySF => Some(KeyCode::Down),
177 | Input::KeySR => Some(KeyCode::Up),
178 | Input::KeySTab => Some(KeyCode::Tab),
179 | Input::KeySDC => Some(KeyCode::Delete),
180 | Input::KeySEnd => Some(KeyCode::End),
181 | Input::KeySHome => Some(KeyCode::Home),
182 | Input::KeySIC => Some(KeyCode::Insert),
183 | Input::KeySLeft => Some(KeyCode::Left),
184 | Input::KeySNext => Some(KeyCode::PageDown),
185 | Input::KeySPrevious => Some(KeyCode::PageDown),
186 | Input::KeySPrint => Some(KeyCode::End),
187 | Input::KeySRight => Some(KeyCode::Right),
188 | Input::KeyBTab => Some(KeyCode::BackTab),
189 | _ => None,
190 | };
191 |
192 | key_code.map(|e| KeyEvent::new(e, KeyModifiers::SHIFT))
193 | }
194 |
195 | /// Matches on CTRL keys, returns `None` if the key does not have an CTRL modifier or is not supported.
196 | pub fn try_map_ctrl_key(&self, input: crosscurses::Input) -> Option {
197 | let key_code = match input {
198 | Input::KeyCTab => Some(KeyCode::Tab),
199 | _ => None,
200 | };
201 |
202 | key_code.map(|e| KeyEvent::new(e, KeyModifiers::CONTROL))
203 | }
204 |
205 | /// Matches on CTRL + ALT keys, returns `None` if the key does not have an SHIFT + ALT modifier or is not supported.
206 | pub fn try_map_ctrl_alt_key(&self, input: crosscurses::Input) -> Option {
207 | let key_code = match input {
208 | Input::KeyCATab => Some(KeyCode::Tab),
209 | _ => None,
210 | };
211 |
212 | key_code.map(|e| KeyEvent::new(e, KeyModifiers::CONTROL | KeyModifiers::ALT))
213 | }
214 |
215 | /// Matches on non key events, returns `None` if the key is not a non-key event or is not supported.
216 | pub fn try_map_non_key_event(&self, input: crosscurses::Input) -> Option {
217 | // No key event, handle non key events e.g resize
218 | match input {
219 | Input::KeyResize => {
220 | // Let crosscurses adjust their structures when the
221 | // window is resized.
222 | crosscurses::resize_term(0, 0);
223 |
224 | Some(Event::Resize)
225 | }
226 | Input::KeyMouse => Some(self.map_mouse_event()),
227 | Input::Unknown(code) => {
228 | Some(
229 | self.key_codes
230 | // crosscurses does some weird keycode mapping
231 | .get(&(code + 256 + 48))
232 | .cloned()
233 | .unwrap_or_else(|| Event::Unknown),
234 | )
235 | }
236 | _ => None,
237 | }
238 | }
239 |
240 | fn map_mouse_event(&self) -> Event {
241 | let mut mevent = match crosscurses::getmouse() {
242 | Err(_) => return Event::Unknown,
243 | Ok(event) => event,
244 | };
245 |
246 | let shift = (mevent.bstate & crosscurses::BUTTON_SHIFT as mmask_t) != 0;
247 | let alt = (mevent.bstate & crosscurses::BUTTON_ALT as mmask_t) != 0;
248 | let ctrl = (mevent.bstate & crosscurses::BUTTON_CTRL as mmask_t) != 0;
249 |
250 | let mut modifiers = KeyModifiers::empty();
251 |
252 | if shift {
253 | modifiers |= KeyModifiers::SHIFT;
254 | }
255 | if ctrl {
256 | modifiers |= KeyModifiers::CONTROL;
257 | }
258 | if alt {
259 | modifiers |= KeyModifiers::ALT;
260 | }
261 |
262 | mevent.bstate &= !(crosscurses::BUTTON_SHIFT
263 | | crosscurses::BUTTON_ALT
264 | | crosscurses::BUTTON_CTRL) as mmask_t;
265 |
266 | let (x, y) = (mevent.x as u16, mevent.y as u16);
267 |
268 | if mevent.bstate == crosscurses::REPORT_MOUSE_POSITION as mmask_t {
269 | // The event is either a mouse drag event,
270 | // or a weird double-release event. :S
271 | self.last_btn()
272 | .map(|btn| Event::Mouse(MouseEvent::Drag(btn, x, y, modifiers)))
273 | .unwrap_or_else(|| {
274 | // We got a mouse drag, but no last mouse pressed?
275 | Event::Unknown
276 | })
277 | } else {
278 | // Identify the button
279 | let mut bare_event = mevent.bstate & ((1 << 25) - 1);
280 |
281 | let mut event = None;
282 | while bare_event != 0 {
283 | let single_event = 1 << bare_event.trailing_zeros();
284 | bare_event ^= single_event;
285 |
286 | // Process single_event
287 | self.on_mouse_event(
288 | single_event,
289 | |e| {
290 | if event.is_none() {
291 | event = Some(e);
292 | } else {
293 | self.update_stored_event(Event::Mouse(e));
294 | }
295 | },
296 | x,
297 | y,
298 | modifiers,
299 | );
300 | }
301 |
302 | if let Some(event) = event {
303 | if let Some(btn) = event.button() {
304 | self.update_last_btn(btn);
305 | }
306 |
307 | Event::Mouse(event)
308 | } else {
309 | // No event parsed?...
310 | Event::Unknown
311 | }
312 | }
313 | }
314 |
315 | /// Parse the given code into one or more event.
316 | ///
317 | /// If the given event code should expend into multiple events
318 | /// (for instance click expends into PRESS + RELEASE),
319 | /// the returned Vec will include those queued events.
320 | ///
321 | /// The main event is returned separately to avoid allocation in most cases.
322 | fn on_mouse_event(
323 | &self,
324 | bare_event: mmask_t,
325 | mut f: F,
326 | x: u16,
327 | y: u16,
328 | modifiers: KeyModifiers,
329 | ) where
330 | F: FnMut(MouseEvent),
331 | {
332 | let button = self.map_mouse_button(bare_event);
333 | match bare_event {
334 | crosscurses::BUTTON4_PRESSED => f(MouseEvent::ScrollUp(x, y, modifiers)),
335 | crosscurses::BUTTON5_PRESSED => f(MouseEvent::ScrollDown(x, y, modifiers)),
336 | crosscurses::BUTTON1_RELEASED
337 | | crosscurses::BUTTON2_RELEASED
338 | | crosscurses::BUTTON3_RELEASED
339 | | crosscurses::BUTTON4_RELEASED
340 | | crosscurses::BUTTON5_RELEASED => f(MouseEvent::Up(button, x, y, modifiers)),
341 | crosscurses::BUTTON1_PRESSED
342 | | crosscurses::BUTTON2_PRESSED
343 | | crosscurses::BUTTON3_PRESSED => f(MouseEvent::Down(button, x, y, modifiers)),
344 | crosscurses::BUTTON1_CLICKED
345 | | crosscurses::BUTTON2_CLICKED
346 | | crosscurses::BUTTON3_CLICKED
347 | | crosscurses::BUTTON4_CLICKED
348 | | crosscurses::BUTTON5_CLICKED => {
349 | f(MouseEvent::Down(button, x, y, modifiers));
350 | f(MouseEvent::Up(button, x, y, modifiers));
351 | }
352 | // Well, we disabled click detection
353 | crosscurses::BUTTON1_DOUBLE_CLICKED
354 | | crosscurses::BUTTON2_DOUBLE_CLICKED
355 | | crosscurses::BUTTON3_DOUBLE_CLICKED
356 | | crosscurses::BUTTON4_DOUBLE_CLICKED
357 | | crosscurses::BUTTON5_DOUBLE_CLICKED => {
358 | for _ in 0..2 {
359 | f(MouseEvent::Down(button, x, y, modifiers));
360 | f(MouseEvent::Up(button, x, y, modifiers));
361 | }
362 | }
363 | crosscurses::BUTTON1_TRIPLE_CLICKED
364 | | crosscurses::BUTTON2_TRIPLE_CLICKED
365 | | crosscurses::BUTTON3_TRIPLE_CLICKED
366 | | crosscurses::BUTTON4_TRIPLE_CLICKED
367 | | crosscurses::BUTTON5_TRIPLE_CLICKED => {
368 | for _ in 0..3 {
369 | f(MouseEvent::Down(button, x, y, modifiers));
370 | f(MouseEvent::Up(button, x, y, modifiers));
371 | }
372 | }
373 | _ => { // Unknown event: {:032b}", bare_event }
374 | }
375 | }
376 | }
377 |
378 | /// Returns the Key enum corresponding to the given crosscurses event.
379 | fn map_mouse_button(&self, bare_event: mmask_t) -> MouseButton {
380 | match bare_event {
381 | crosscurses::BUTTON1_RELEASED
382 | | crosscurses::BUTTON1_PRESSED
383 | | crosscurses::BUTTON1_CLICKED
384 | | crosscurses::BUTTON1_DOUBLE_CLICKED
385 | | crosscurses::BUTTON1_TRIPLE_CLICKED => MouseButton::Left,
386 | crosscurses::BUTTON2_RELEASED
387 | | crosscurses::BUTTON2_PRESSED
388 | | crosscurses::BUTTON2_CLICKED
389 | | crosscurses::BUTTON2_DOUBLE_CLICKED
390 | | crosscurses::BUTTON2_TRIPLE_CLICKED => MouseButton::Middle,
391 | crosscurses::BUTTON3_RELEASED
392 | | crosscurses::BUTTON3_PRESSED
393 | | crosscurses::BUTTON3_CLICKED
394 | | crosscurses::BUTTON3_DOUBLE_CLICKED
395 | | crosscurses::BUTTON3_TRIPLE_CLICKED => MouseButton::Right,
396 | crosscurses::BUTTON4_RELEASED
397 | | crosscurses::BUTTON4_PRESSED
398 | | crosscurses::BUTTON4_CLICKED
399 | | crosscurses::BUTTON4_DOUBLE_CLICKED
400 | | crosscurses::BUTTON4_TRIPLE_CLICKED => MouseButton::Unknown,
401 | crosscurses::BUTTON5_RELEASED
402 | | crosscurses::BUTTON5_PRESSED
403 | | crosscurses::BUTTON5_CLICKED
404 | | crosscurses::BUTTON5_DOUBLE_CLICKED
405 | | crosscurses::BUTTON5_TRIPLE_CLICKED => MouseButton::Unknown,
406 | _ => MouseButton::Unknown,
407 | }
408 | }
409 | }
410 |
411 | pub fn find_closest(color: Color, max_colors: i16) -> i16 {
412 | // translate ansi value to rgb
413 | let color = if let Color::AnsiValue(val) = color {
414 | Color::from(val)
415 | } else {
416 | color
417 | };
418 |
419 | // translate to closest supported color.
420 | match color {
421 | // Dark colors
422 | Color::Black => crosscurses::COLOR_BLACK,
423 | Color::DarkRed => crosscurses::COLOR_RED,
424 | Color::DarkGreen => crosscurses::COLOR_GREEN,
425 | Color::DarkYellow => crosscurses::COLOR_YELLOW,
426 | Color::DarkBlue => crosscurses::COLOR_BLUE,
427 | Color::DarkMagenta => crosscurses::COLOR_MAGENTA,
428 | Color::DarkCyan => crosscurses::COLOR_CYAN,
429 | Color::Grey => crosscurses::COLOR_WHITE,
430 |
431 | // Light colors
432 | Color::Red => 9 % max_colors,
433 | Color::Green => 10 % max_colors,
434 | Color::Yellow => 11 % max_colors,
435 | Color::Blue => 12 % max_colors,
436 | Color::Magenta => 13 % max_colors,
437 | Color::Cyan => 14 % max_colors,
438 | Color::White => 15 % max_colors,
439 | Color::Rgb(r, g, b) if max_colors >= 256 => {
440 | // If r = g = b, it may be a grayscale value!
441 | if r == g && g == b && r != 0 && r < 250 {
442 | // Grayscale
443 | // (r = g = b) = 8 + 10 * n
444 | // (r - 8) / 10 = n
445 | let n = (r - 8) / 10;
446 | i16::from(232 + n)
447 | } else {
448 | // Generic RGB
449 | let r = 6 * u16::from(r) / 256;
450 | let g = 6 * u16::from(g) / 256;
451 | let b = 6 * u16::from(b) / 256;
452 | (16 + 36 * r + 6 * g + b) as i16
453 | }
454 | }
455 | Color::Rgb(r, g, b) => {
456 | let r = if r > 127 { 1 } else { 0 };
457 | let g = if g > 127 { 1 } else { 0 };
458 | let b = if b > 127 { 1 } else { 0 };
459 | (r + 2 * g + 4 * b) as i16
460 | }
461 | _ => -1, // -1 represents default color
462 | }
463 | }
464 |
--------------------------------------------------------------------------------
/src/backend/crosscurses/implementation.rs:
--------------------------------------------------------------------------------
1 | use crate::backend::crosscurses::constants;
2 | use crate::{
3 | backend::{
4 | crosscurses::{current_style::CurrentStyle, mapping::find_closest},
5 | Backend,
6 | },
7 | error, Action, Attribute, Clear, Color, Event, KeyCode, KeyEvent, KeyModifiers, MouseButton,
8 | Retrieved, Value,
9 | };
10 | use crosscurses::{ToChtype, Window, COLORS};
11 | use std::{
12 | collections::HashMap, ffi::CStr, fs::File, io, io::Write, os::unix::io::IntoRawFd, result,
13 | sync::RwLock,
14 | };
15 |
16 | /// Checks if the expression result is an error.
17 | /// Returns an Error based on the error code.
18 | /// Does nothing if there's no error.
19 | macro_rules! check {
20 | ($expr:expr) => (match $expr {
21 | 0 => {},
22 | -1 => {
23 | return Err($crate::error::ErrorKind::IoError(std::io::Error::new(std::io::ErrorKind::Other, "Some error occurred while executing the action")))
24 | }
25 | 3 => {
26 | return Err($crate::error::ErrorKind::ActionNotSupported("The action is not supported by crosscurses. Either work around it or use an other backend.".to_string()))
27 | }
28 | _ => {}
29 | });
30 | }
31 |
32 | #[derive(Default)]
33 | struct InputCache {
34 | // The mouse on event doesn't have a button,
35 | // so we have to save it with the mouse down event
36 | last_mouse_button: Option,
37 |
38 | stored_event: Option,
39 | }
40 |
41 | pub struct BackendImpl {
42 | buffer: W,
43 | // We can batch commands in the crosscurses window.
44 | // The moment we call `refresh` these are executed.
45 | window: crosscurses::Window,
46 |
47 | // The cache needed to parse input.
48 | input_cache: RwLock,
49 |
50 | // ncurses stores color values in pairs (fg, bg) color.
51 | // We store those pairs in this hashmap on order to keep track of the pairs we initialized.
52 | color_pairs: HashMap,
53 |
54 | // Some key code definitions from which we can construct events.
55 | pub(crate) key_codes: HashMap,
56 |
57 | // This is necessary to know the style that is currently set.
58 | current_style: CurrentStyle,
59 | }
60 |
61 | impl BackendImpl {
62 | /// Prints the given string-like value into the window.
63 | fn print>(&mut self, asref: S) -> error::Result<()> {
64 | if cfg!(windows) {
65 | // PDCurses does an extra intermediate CString allocation, so we just
66 | // print out each character one at a time to avoid that.
67 | asref.as_ref().chars().all(|c| self.print_char(c).is_ok());
68 | } else {
69 | // NCurses, it seems, doesn't do the intermediate allocation and also uses
70 | // a faster routine for printing a whole string at once.
71 | self.window.printw(asref.as_ref());
72 | }
73 |
74 | Ok(())
75 | }
76 |
77 | /// Prints the given character into the window.
78 | fn print_char(&mut self, character: T) -> error::Result<()> {
79 | self.window.addch(character);
80 | Ok(())
81 | }
82 |
83 | /// Updates the stored event.
84 | pub(crate) fn update_stored_event(&self, btn: Event) {
85 | let mut lock = self.input_cache.write().unwrap();
86 | lock.stored_event = Some(btn);
87 | }
88 |
89 | /// Tries to read event from temporary cache.
90 | fn try_take(&self) -> Option {
91 | self.input_cache.write().unwrap().stored_event.take()
92 | }
93 |
94 | /// Updates the last used button with a new button.
95 | pub(crate) fn update_last_btn(&self, btn: MouseButton) {
96 | let mut lock = self.input_cache.write().unwrap();
97 | lock.last_mouse_button = Some(btn);
98 | }
99 |
100 | /// Retrieves the last printed mouse button.
101 | pub(crate) fn last_btn(&self) -> Option {
102 | self.input_cache.read().unwrap().last_mouse_button
103 | }
104 |
105 | /// Retrieves the foreground color index.
106 | pub(crate) fn get_fg_index(&mut self, fg_color: Color) -> i32 {
107 | let closest_fg_color = find_closest(fg_color, COLORS() as i16);
108 | let closest_bg_color = find_closest(self.current_style.background, COLORS() as i16);
109 |
110 | self.get_or_insert(closest_fg_color, closest_fg_color, closest_bg_color)
111 | }
112 |
113 | /// Retrieves the background color index.
114 | fn get_bg_index(&mut self, bg_color: Color) -> i32 {
115 | let closest_fg_color = find_closest(self.current_style.foreground, COLORS() as i16);
116 | let closest_bg_color = find_closest(bg_color, COLORS() as i16);
117 |
118 | self.get_or_insert(closest_bg_color, closest_fg_color, closest_bg_color)
119 | }
120 |
121 | /// Retrieves the color pair index, if the given color pair doesn't exist yet,
122 | /// it will be created and the index will be returned.
123 | fn get_or_insert(&mut self, key: i16, fg_color: i16, bg_color: i16) -> i32 {
124 | let index = self.new_color_pair_index();
125 |
126 | *self.color_pairs.entry(key).or_insert_with(|| {
127 | crosscurses::init_pair(index as i16, fg_color, bg_color);
128 | index
129 | })
130 | }
131 |
132 | /// Returns a new color pair index.
133 | fn new_color_pair_index(&mut self) -> i32 {
134 | let n = 1 + self.color_pairs.len() as i32;
135 |
136 | if 256 > n {
137 | // We still have plenty of space for everyone.
138 | n
139 | } else {
140 | // resize color pairs
141 | let target = n - 1;
142 | // Remove the mapping to n-1
143 | self.color_pairs.retain(|_, &mut v| v != target);
144 | target
145 | }
146 | }
147 | }
148 |
149 | fn init_stdout_window() -> Window {
150 | // Windows currently will only work on stdout because of this default crosscurses initialisation.
151 | // TODO: support using `newterm` like `init_unix_window` so that we are not depended on stdout.
152 | crosscurses::initscr()
153 | }
154 |
155 | #[cfg(unix)]
156 | fn init_custom_window() -> Window {
157 | // By default crosscurses use stdout.
158 | // We can change this by calling `new_term` with an FILE pointer to the source.
159 | // Which is /dev/tty in our case.
160 | let file = File::create("/dev/tty").unwrap();
161 |
162 | let c_file = unsafe {
163 | libc::fdopen(
164 | file.into_raw_fd(),
165 | CStr::from_bytes_with_nul_unchecked(b"w+\0").as_ptr(),
166 | )
167 | };
168 |
169 | if cfg!(unix)
170 | && std::env::var("TERM")
171 | .map(|var| var.is_empty())
172 | .unwrap_or(false)
173 | {
174 | init_stdout_window()
175 | } else {
176 | // Create screen pointer which we will be using for this backend.
177 | let screen = crosscurses::newterm(None, c_file, c_file);
178 |
179 | // Set the created screen as active.
180 | crosscurses::set_term(screen);
181 |
182 | // Get `Window` of the created screen.
183 | crosscurses::stdscr()
184 | }
185 | }
186 |
187 | impl Backend for BackendImpl {
188 | fn create(buffer: W) -> Self {
189 | // The delay is the time ncurses wait after pressing ESC
190 | // to see if it's an escape sequence.
191 | // Default delay is way too long. 25 is imperceptible yet works fine.
192 | ::std::env::set_var("ESCDELAY", "25");
193 |
194 | #[cfg(windows)]
195 | let window = init_stdout_window();
196 |
197 | #[cfg(unix)]
198 | let window = init_custom_window();
199 |
200 | // Some default settings
201 | window.keypad(true);
202 | crosscurses::start_color();
203 | crosscurses::use_default_colors();
204 | crosscurses::mousemask(constants::MOUSE_EVENT_MASK, ::std::ptr::null_mut());
205 |
206 | // Initialize the default fore and background.
207 | let mut map = HashMap::::new();
208 | map.insert(-1, 0);
209 | crosscurses::init_pair(0, -1, -1);
210 |
211 | BackendImpl {
212 | window,
213 | input_cache: RwLock::new(InputCache::default()),
214 | color_pairs: map,
215 | key_codes: initialize_keymap(),
216 | current_style: CurrentStyle::new(),
217 | buffer,
218 | }
219 | }
220 |
221 | fn act(&mut self, action: Action) -> error::Result<()> {
222 | self.batch(action)?;
223 | self.flush_batch()
224 | }
225 |
226 | #[allow(clippy::cognitive_complexity)]
227 | fn batch(&mut self, action: Action) -> error::Result<()> {
228 | match action {
229 | Action::MoveCursorTo(x, y) => {
230 | // Coordinates are reversed here
231 | check!(self.window.mv(y as i32, x as i32));
232 | }
233 | Action::HideCursor => {
234 | check!(crosscurses::curs_set(0));
235 | }
236 | Action::ShowCursor => {
237 | check!(crosscurses::curs_set(1));
238 | }
239 | Action::EnableBlinking => {
240 | check!(crosscurses::set_blink(true));
241 | }
242 | Action::DisableBlinking => {
243 | check!(crosscurses::set_blink(false));
244 | }
245 | Action::ClearTerminal(clear_type) => {
246 | check!(match clear_type {
247 | Clear::All => self.window.clear(),
248 | Clear::FromCursorDown => self.window.clrtobot(),
249 | Clear::UntilNewLine => self.window.clrtoeol(),
250 | Clear::FromCursorUp => 3, // TODO, not supported by crosscurses
251 | Clear::CurrentLine => 3, // TODO, not supported by crosscurses
252 | });
253 | }
254 | Action::SetTerminalSize(cols, rows) => {
255 | crosscurses::resize_term(rows as i32, cols as i32);
256 | }
257 | Action::EnableRawMode => {
258 | check!(crosscurses::noecho());
259 | check!(crosscurses::raw());
260 | check!(crosscurses::nonl());
261 | }
262 | Action::DisableRawMode => {
263 | check!(crosscurses::echo());
264 | check!(crosscurses::noraw());
265 | check!(crosscurses::nl());
266 | }
267 | Action::EnableMouseCapture => {
268 | self.buffer
269 | .write_all(constants::ENABLE_MOUSE_CAPTURE.as_bytes())?;
270 | self.buffer.flush()?;
271 | }
272 | Action::DisableMouseCapture => {
273 | self.buffer
274 | .write_all(constants::DISABLE_MOUSE_CAPTURE.as_bytes())?;
275 | self.buffer.flush()?;
276 | }
277 | Action::ResetColor => {
278 | let style = crosscurses::COLOR_PAIR(0 as crosscurses::chtype);
279 | check!(self.window.attron(style));
280 | check!(self.window.attroff(self.current_style.attributes));
281 | check!(self.window.refresh());
282 | }
283 | Action::SetForegroundColor(color) => {
284 | self.current_style.foreground = color;
285 | let index = self.get_fg_index(color);
286 | let style = crosscurses::COLOR_PAIR(index as crosscurses::chtype);
287 | check!(self.window.attron(style));
288 | check!(self.window.refresh());
289 | }
290 | Action::SetBackgroundColor(color) => {
291 | self.current_style.background = color;
292 | let index = self.get_bg_index(color);
293 | let style = crosscurses::COLOR_PAIR(index as crosscurses::chtype);
294 | check!(self.window.attron(style));
295 | check!(self.window.refresh());
296 | }
297 | Action::SetAttribute(attr) => {
298 | let no_match1 = match attr {
299 | Attribute::Reset => Some(crosscurses::Attribute::Normal),
300 | Attribute::Bold => Some(crosscurses::Attribute::Bold),
301 | Attribute::Italic => Some(crosscurses::Attribute::Italic),
302 | Attribute::Underlined => Some(crosscurses::Attribute::Underline),
303 | Attribute::SlowBlink | Attribute::RapidBlink => {
304 | Some(crosscurses::Attribute::Blink)
305 | }
306 | Attribute::Crossed => Some(crosscurses::Attribute::Strikeout),
307 | Attribute::Reversed => Some(crosscurses::Attribute::Reverse),
308 | Attribute::Conceal => Some(crosscurses::Attribute::Invisible),
309 | _ => None, // OFF attributes and Fraktur, NormalIntensity, Framed
310 | }
311 | .map(|attribute| {
312 | self.window.attron(attribute);
313 | self.current_style.attributes = self.current_style.attributes | attribute;
314 | });
315 |
316 | let no_match2 = match attr {
317 | Attribute::BoldOff => Some(crosscurses::Attribute::Bold),
318 | Attribute::ItalicOff => Some(crosscurses::Attribute::Italic),
319 | Attribute::UnderlinedOff => Some(crosscurses::Attribute::Underline),
320 | Attribute::BlinkOff => Some(crosscurses::Attribute::Blink),
321 | Attribute::CrossedOff => Some(crosscurses::Attribute::Strikeout),
322 | Attribute::ReversedOff => Some(crosscurses::Attribute::Reverse),
323 | Attribute::ConcealOff => Some(crosscurses::Attribute::Invisible),
324 | _ => None, // OFF attributes and Fraktur, NormalIntensity, Framed
325 | }
326 | .map(|attribute| {
327 | self.window.attroff(attribute);
328 | self.current_style.attributes = self.current_style.attributes ^ attribute;
329 | });
330 |
331 | if no_match1.is_none() && no_match2.is_none() {
332 | return Err(error::ErrorKind::AttributeNotSupported(String::from(attr)));
333 | }
334 | }
335 | Action::EnterAlternateScreen
336 | | Action::LeaveAlternateScreen
337 | | Action::ScrollUp(_)
338 | | Action::ScrollDown(_) => check!(3),
339 | };
340 |
341 | Ok(())
342 | }
343 |
344 | fn flush_batch(&mut self) -> error::Result<()> {
345 | self.window.refresh();
346 | Ok(())
347 | }
348 |
349 | fn get(&self, retrieve_operation: Value) -> error::Result {
350 | match retrieve_operation {
351 | Value::TerminalSize => {
352 | // Coordinates are reversed here
353 | let (y, x) = self.window.get_max_yx();
354 | Ok(Retrieved::TerminalSize(x as u16, y as u16))
355 | }
356 | Value::CursorPosition => {
357 | let (y, x) = self.window.get_cur_yx();
358 | Ok(Retrieved::CursorPosition(y as u16, x as u16))
359 | }
360 | Value::Event(duration) => {
361 | if let Some(event) = self.try_take() {
362 | return Ok(Retrieved::Event(Some(event)));
363 | }
364 |
365 | let duration = duration.map_or(-1, |f| f.as_millis() as i32);
366 |
367 | self.window.timeout(duration);
368 |
369 | if let Some(input) = self.window.getch() {
370 | return Ok(Retrieved::Event(Some(self.parse_next(input))));
371 | }
372 |
373 | Ok(Retrieved::Event(None))
374 | }
375 | }
376 | }
377 | }
378 |
379 | impl Drop for BackendImpl {
380 | fn drop(&mut self) {
381 | let _ = self.act(Action::DisableMouseCapture);
382 | crosscurses::endwin();
383 | }
384 | }
385 |
386 | impl Write for BackendImpl {
387 | fn write(&mut self, buf: &[u8]) -> result::Result {
388 | let string = std::str::from_utf8(buf).unwrap();
389 | let len = string.len();
390 | // We need to write strings to crosscurses window instead of directly to the buffer.
391 | self.print(string).unwrap();
392 | Ok(len)
393 | }
394 |
395 | fn flush(&mut self) -> result::Result<(), io::Error> {
396 | self.window.refresh();
397 | Ok(())
398 | }
399 | }
400 |
401 | fn initialize_keymap() -> HashMap {
402 | let mut map = HashMap::default();
403 |
404 | fill_key_codes(&mut map, crosscurses::keyname);
405 |
406 | map
407 | }
408 |
409 | #[allow(clippy::eq_op)]
410 | fn fill_key_codes(target: &mut HashMap, f: F)
411 | where
412 | F: Fn(i32) -> Option,
413 | {
414 | let mut key_names = HashMap::<&str, KeyCode>::new();
415 | key_names.insert("DC", KeyCode::Delete);
416 | key_names.insert("DN", KeyCode::Down);
417 | key_names.insert("END", KeyCode::End);
418 | key_names.insert("HOM", KeyCode::Home);
419 | key_names.insert("IC", KeyCode::Insert);
420 | key_names.insert("LFT", KeyCode::Left);
421 | key_names.insert("NXT", KeyCode::PageDown);
422 | key_names.insert("PRV", KeyCode::PageUp);
423 | key_names.insert("RIT", KeyCode::Right);
424 | key_names.insert("UP", KeyCode::Up);
425 |
426 | for code in 512..1024 {
427 | let name = match f(code) {
428 | Some(name) => name,
429 | None => continue,
430 | };
431 |
432 | if !name.starts_with('k') {
433 | continue;
434 | }
435 |
436 | let (key_name, modifier) = name[1..].split_at(name.len() - 2);
437 | let key = match key_names.get(key_name) {
438 | Some(&key) => key,
439 | None => continue,
440 | };
441 |
442 | let event = match modifier {
443 | "3" => Event::Key(KeyEvent {
444 | code: key,
445 | modifiers: KeyModifiers::ALT,
446 | }),
447 | "4" => Event::Key(KeyEvent {
448 | code: key,
449 | modifiers: KeyModifiers::ALT | KeyModifiers::SHIFT,
450 | }),
451 | "5" => Event::Key(KeyEvent {
452 | code: key,
453 | modifiers: KeyModifiers::CONTROL,
454 | }),
455 | "6" => Event::Key(KeyEvent {
456 | code: key,
457 | modifiers: (KeyModifiers::CONTROL | KeyModifiers::CONTROL),
458 | }),
459 | "7" => Event::Key(KeyEvent {
460 | code: key,
461 | modifiers: (KeyModifiers::CONTROL | KeyModifiers::ALT),
462 | }),
463 | _ => continue,
464 | };
465 |
466 | target.insert(code, event);
467 | }
468 | }
469 |
470 | #[cfg(test)]
471 | mod test {
472 | use crate::error;
473 |
474 | fn a(return_val: i32) -> error::Result<()> {
475 | check!(return_val);
476 | Ok(())
477 | }
478 |
479 | #[test]
480 | fn test_check_macro() {
481 | assert!(a(0).is_ok());
482 | assert!(a(1).is_ok());
483 | assert!(a(3).is_err());
484 | assert!(a(-1).is_err());
485 | }
486 | }
487 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | [[package]]
4 | name = "arc-swap"
5 | version = "0.4.4"
6 | source = "registry+https://github.com/rust-lang/crates.io-index"
7 |
8 | [[package]]
9 | name = "autocfg"
10 | version = "0.1.7"
11 | source = "registry+https://github.com/rust-lang/crates.io-index"
12 |
13 | [[package]]
14 | name = "bitflags"
15 | version = "1.2.1"
16 | source = "registry+https://github.com/rust-lang/crates.io-index"
17 |
18 | [[package]]
19 | name = "cc"
20 | version = "1.0.50"
21 | source = "registry+https://github.com/rust-lang/crates.io-index"
22 |
23 | [[package]]
24 | name = "cfg-if"
25 | version = "0.1.10"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 |
28 | [[package]]
29 | name = "cloudabi"
30 | version = "0.0.3"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | dependencies = [
33 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
34 | ]
35 |
36 | [[package]]
37 | name = "crossbeam-channel"
38 | version = "0.4.0"
39 | source = "registry+https://github.com/rust-lang/crates.io-index"
40 | dependencies = [
41 | "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
42 | ]
43 |
44 | [[package]]
45 | name = "crossbeam-utils"
46 | version = "0.7.0"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | dependencies = [
49 | "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
50 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
51 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
52 | ]
53 |
54 | [[package]]
55 | name = "crosscurses"
56 | version = "0.1.0"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | dependencies = [
59 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
60 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
61 | "ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)",
62 | "pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
63 | "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
64 | ]
65 |
66 | [[package]]
67 | name = "crossterm"
68 | version = "0.15.0"
69 | source = "registry+https://github.com/rust-lang/crates.io-index"
70 | dependencies = [
71 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
72 | "crossterm_winapi 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
73 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
74 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
75 | "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
76 | "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
77 | "signal-hook 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
78 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
79 | ]
80 |
81 | [[package]]
82 | name = "crossterm_winapi"
83 | version = "0.6.1"
84 | source = "registry+https://github.com/rust-lang/crates.io-index"
85 | dependencies = [
86 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
87 | ]
88 |
89 | [[package]]
90 | name = "fuchsia-zircon"
91 | version = "0.3.3"
92 | source = "registry+https://github.com/rust-lang/crates.io-index"
93 | dependencies = [
94 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
95 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
96 | ]
97 |
98 | [[package]]
99 | name = "fuchsia-zircon-sys"
100 | version = "0.3.3"
101 | source = "registry+https://github.com/rust-lang/crates.io-index"
102 |
103 | [[package]]
104 | name = "iovec"
105 | version = "0.1.4"
106 | source = "registry+https://github.com/rust-lang/crates.io-index"
107 | dependencies = [
108 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
109 | ]
110 |
111 | [[package]]
112 | name = "kernel32-sys"
113 | version = "0.2.2"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | dependencies = [
116 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
117 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
118 | ]
119 |
120 | [[package]]
121 | name = "lazy_static"
122 | version = "1.4.0"
123 | source = "registry+https://github.com/rust-lang/crates.io-index"
124 |
125 | [[package]]
126 | name = "libc"
127 | version = "0.2.66"
128 | source = "registry+https://github.com/rust-lang/crates.io-index"
129 |
130 | [[package]]
131 | name = "lock_api"
132 | version = "0.3.3"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | dependencies = [
135 | "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
136 | ]
137 |
138 | [[package]]
139 | name = "log"
140 | version = "0.4.8"
141 | source = "registry+https://github.com/rust-lang/crates.io-index"
142 | dependencies = [
143 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
144 | ]
145 |
146 | [[package]]
147 | name = "mio"
148 | version = "0.6.21"
149 | source = "registry+https://github.com/rust-lang/crates.io-index"
150 | dependencies = [
151 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
152 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
153 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
154 | "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
155 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
156 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
157 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
158 | "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
159 | "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
160 | "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
161 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
162 | ]
163 |
164 | [[package]]
165 | name = "miow"
166 | version = "0.2.1"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | dependencies = [
169 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
170 | "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
171 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
172 | "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
173 | ]
174 |
175 | [[package]]
176 | name = "ncurses"
177 | version = "5.99.0"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | dependencies = [
180 | "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
181 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
182 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
183 | ]
184 |
185 | [[package]]
186 | name = "net2"
187 | version = "0.2.33"
188 | source = "registry+https://github.com/rust-lang/crates.io-index"
189 | dependencies = [
190 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
191 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
192 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
193 | ]
194 |
195 | [[package]]
196 | name = "numtoa"
197 | version = "0.1.0"
198 | source = "registry+https://github.com/rust-lang/crates.io-index"
199 |
200 | [[package]]
201 | name = "parking_lot"
202 | version = "0.10.0"
203 | source = "registry+https://github.com/rust-lang/crates.io-index"
204 | dependencies = [
205 | "lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
206 | "parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
207 | ]
208 |
209 | [[package]]
210 | name = "parking_lot_core"
211 | version = "0.7.0"
212 | source = "registry+https://github.com/rust-lang/crates.io-index"
213 | dependencies = [
214 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
215 | "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
216 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
217 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
218 | "smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
219 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
220 | ]
221 |
222 | [[package]]
223 | name = "pdcurses-sys"
224 | version = "0.7.1"
225 | source = "registry+https://github.com/rust-lang/crates.io-index"
226 | dependencies = [
227 | "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
228 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
229 | ]
230 |
231 | [[package]]
232 | name = "pkg-config"
233 | version = "0.3.17"
234 | source = "registry+https://github.com/rust-lang/crates.io-index"
235 |
236 | [[package]]
237 | name = "redox_syscall"
238 | version = "0.1.56"
239 | source = "registry+https://github.com/rust-lang/crates.io-index"
240 |
241 | [[package]]
242 | name = "redox_termios"
243 | version = "0.1.1"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | dependencies = [
246 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
247 | ]
248 |
249 | [[package]]
250 | name = "scopeguard"
251 | version = "1.0.0"
252 | source = "registry+https://github.com/rust-lang/crates.io-index"
253 |
254 | [[package]]
255 | name = "signal-hook"
256 | version = "0.1.13"
257 | source = "registry+https://github.com/rust-lang/crates.io-index"
258 | dependencies = [
259 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
260 | "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
261 | "signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
262 | ]
263 |
264 | [[package]]
265 | name = "signal-hook-registry"
266 | version = "1.2.0"
267 | source = "registry+https://github.com/rust-lang/crates.io-index"
268 | dependencies = [
269 | "arc-swap 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
270 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
271 | ]
272 |
273 | [[package]]
274 | name = "slab"
275 | version = "0.4.2"
276 | source = "registry+https://github.com/rust-lang/crates.io-index"
277 |
278 | [[package]]
279 | name = "smallvec"
280 | version = "1.1.0"
281 | source = "registry+https://github.com/rust-lang/crates.io-index"
282 |
283 | [[package]]
284 | name = "terminal"
285 | version = "0.2.1"
286 | dependencies = [
287 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
288 | "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
289 | "crosscurses 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
290 | "crossterm 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
291 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
292 | "signal-hook 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
293 | "termion 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
294 | ]
295 |
296 | [[package]]
297 | name = "termion"
298 | version = "1.5.4"
299 | source = "registry+https://github.com/rust-lang/crates.io-index"
300 | dependencies = [
301 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
302 | "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
303 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
304 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
305 | ]
306 |
307 | [[package]]
308 | name = "winapi"
309 | version = "0.2.8"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 |
312 | [[package]]
313 | name = "winapi"
314 | version = "0.3.8"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | dependencies = [
317 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
318 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
319 | ]
320 |
321 | [[package]]
322 | name = "winapi-build"
323 | version = "0.1.1"
324 | source = "registry+https://github.com/rust-lang/crates.io-index"
325 |
326 | [[package]]
327 | name = "winapi-i686-pc-windows-gnu"
328 | version = "0.4.0"
329 | source = "registry+https://github.com/rust-lang/crates.io-index"
330 |
331 | [[package]]
332 | name = "winapi-x86_64-pc-windows-gnu"
333 | version = "0.4.0"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 |
336 | [[package]]
337 | name = "winreg"
338 | version = "0.6.2"
339 | source = "registry+https://github.com/rust-lang/crates.io-index"
340 | dependencies = [
341 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
342 | ]
343 |
344 | [[package]]
345 | name = "ws2_32-sys"
346 | version = "0.2.1"
347 | source = "registry+https://github.com/rust-lang/crates.io-index"
348 | dependencies = [
349 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
350 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
351 | ]
352 |
353 | [metadata]
354 | "checksum arc-swap 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d7b8a9123b8027467bce0099fe556c628a53c8d83df0507084c31e9ba2e39aff"
355 | "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
356 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
357 | "checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
358 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
359 | "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
360 | "checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c"
361 | "checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
362 | "checksum crosscurses 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fe7d4ba27dc0057256a86ba49fc62e483992c2ffaf781271285c5edecb138570"
363 | "checksum crossterm 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "207a948d1b4ff59e5aec9bb9426cc4fd3d17b719e5c7b74e27f0a60c4cc2d095"
364 | "checksum crossterm_winapi 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "057b7146d02fb50175fd7dbe5158f6097f33d02831f43b4ee8ae4ddf67b68f5c"
365 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
366 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
367 | "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
368 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
369 | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
370 | "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
371 | "checksum lock_api 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b"
372 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
373 | "checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f"
374 | "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
375 | "checksum ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82"
376 | "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
377 | "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
378 | "checksum parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc"
379 | "checksum parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1"
380 | "checksum pdcurses-sys 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b"
381 | "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
382 | "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
383 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
384 | "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
385 | "checksum signal-hook 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "10b9f3a1686a29f53cfd91ee5e3db3c12313ec02d33765f02c1a9645a1811e2c"
386 | "checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
387 | "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
388 | "checksum smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44e59e0c9fa00817912ae6e4e6e3c4fe04455e75699d06eedc7d85917ed8e8f4"
389 | "checksum termion 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "818ef3700c2a7b447dca1a1dd28341fe635e6ee103c806c636bb9c929991b2cd"
390 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
391 | "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
392 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
393 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
394 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
395 | "checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
396 | "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
397 |
--------------------------------------------------------------------------------