├── .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 | 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 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](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 | --------------------------------------------------------------------------------