Merge remote-tracking branch 'origin/master'

This commit is contained in:
Matthias Devlamynck 2019-01-28 19:55:09 +01:00
commit 5301d8621a
23 changed files with 550 additions and 252 deletions

66
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,66 @@
image: "rust:latest"
stages:
- build
- test
before_script:
- rustup toolchain add $toolchain
cache:
paths:
- target/
build:stable:
stage: build
variables:
toolchain: stable
script:
- cargo +stable build --verbose
- cargo +stable build --release --verbose
test:stable:
stage: test
variables:
toolchain: stable
dependencies:
- build:stable
script:
- script -q -c "cargo +stable test --verbose"
- script -q -c "cargo +stable test --release --verbose"
build:beta:
stage: build
variables:
toolchain: beta
script:
- cargo +beta build --verbose
- cargo +beta build --release --verbose
test:beta:
stage: test
variables:
toolchain: beta
dependencies:
- build:beta
script:
- script -q -c "cargo +beta test --verbose"
- script -q -c "cargo +beta test --release --verbose"
build:nightly:
stage: build
variables:
toolchain: nightly
script:
- cargo +nightly build --verbose
- cargo +nightly build --release --verbose
test:nightly:
stage: test
variables:
toolchain: nightly
dependencies:
- build:nightly
script:
- script -q -c "cargo +nightly test --verbose"
- script -q -c "cargo +nightly test --release --verbose"

View File

@ -1,13 +1,20 @@
[package] [package]
name = "termion" name = "termion"
version = "1.4.0" version = "1.5.1"
authors = ["ticki <Ticki@users.noreply.github.com>", "gycos <alexandre.bury@gmail.com>", "IGI-111 <igi-111@protonmail.com>"] authors = ["ticki <Ticki@users.noreply.github.com>", "gycos <alexandre.bury@gmail.com>", "IGI-111 <igi-111@protonmail.com>"]
description = "A bindless library for manipulating terminals." description = "A bindless library for manipulating terminals."
repository = "https://github.com/ticki/termion" repository = "https://gitlab.redox-os.org/redox-os/termion"
documentation = "https://docs.rs/termion" documentation = "https://docs.rs/termion"
license = "MIT" license = "MIT"
keywords = ["tty", "color", "terminal", "password", "tui"] keywords = ["tty", "color", "terminal", "password", "tui"]
exclude = ["target", "CHANGELOG.md", "image.png", "Cargo.lock"] exclude = ["target", "CHANGELOG.md", "image.png", "Cargo.lock"]
[dependencies]
numtoa = { version = "0.1.0", features = ["std"]}
[target.'cfg(not(target_os = "redox"))'.dependencies] [target.'cfg(not(target_os = "redox"))'.dependencies]
libc = "0.2.8" libc = "0.2.8"
[target.'cfg(target_os = "redox")'.dependencies]
redox_syscall = "0.1"
redox_termios = "0.1"

View File

@ -1,8 +1,8 @@
<p align="center"> <p align="center">
<img alt="Termion logo" src="https://rawgit.com/ticki/termion/master/logo.svg" /> <img alt="Termion logo" src="https://rawgit.com/redox-os/termion/master/logo.svg" />
</p> </p>
[![Build Status](https://travis-ci.org/ticki/termion.svg?branch=master)](https://travis-ci.org/ticki/termion) [![Latest Version](https://img.shields.io/crates/v/termion.svg)](https://crates.io/crates/termion) | [Documentation](https://docs.rs/termion) | [Examples](https://github.com/Ticki/termion/tree/master/examples) | [Changelog](https://github.com/Ticki/termion/tree/master/CHANGELOG.md) | [Tutorial](http://ticki.github.io/blog/making-terminal-applications-in-rust-with-termion/) [![Build Status](https://travis-ci.org/redox-os/termion.svg?branch=master)](https://travis-ci.org/redox-os/termion) [![Latest Version](https://img.shields.io/crates/v/termion.svg)](https://crates.io/crates/termion) | [Documentation](https://docs.rs/termion) | [Examples](https://github.com/redox-os/termion/tree/master/examples) | [Changelog](https://github.com/redox-os/termion/tree/master/CHANGELOG.md) | [Tutorial](http://ticki.github.io/blog/making-terminal-applications-in-rust-with-termion/)
|----|----|----|----|---- |----|----|----|----|----

View File

@ -6,5 +6,5 @@ fn main() {
println!("{}Red", color::Fg(color::Red)); println!("{}Red", color::Fg(color::Red));
println!("{}Blue", color::Fg(color::Blue)); println!("{}Blue", color::Fg(color::Blue));
println!("{}Blue'n'Bold{}", style::Bold, style::Reset); println!("{}Blue'n'Bold{}", style::Bold, style::Reset);
println!("{}Just plain italic", style::Italic); println!("{}Just plain italic{}", style::Italic, style::Reset);
} }

View File

@ -2,7 +2,30 @@ use std::io::{self, Read};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use tty; use sys::tty::get_tty;
/// Construct an asynchronous handle to the TTY standard input, with a delimiter byte.
///
/// This has the same advantages as async_stdin(), but also allows specifying a delimiter byte. The
/// reader will stop reading after consuming the delimiter byte.
pub fn async_stdin_until(delimiter: u8) -> AsyncReader {
let (send, recv) = mpsc::channel();
thread::spawn(move || for i in get_tty().unwrap().bytes() {
match i {
Ok(byte) => {
let end_of_stream = &byte == &delimiter;
let send_error = send.send(Ok(byte)).is_err();
if end_of_stream || send_error { return; }
},
Err(_) => { return; }
}
});
AsyncReader { recv: recv }
}
/// Construct an asynchronous handle to the TTY standard input. /// Construct an asynchronous handle to the TTY standard input.
/// ///
@ -17,7 +40,7 @@ use tty;
pub fn async_stdin() -> AsyncReader { pub fn async_stdin() -> AsyncReader {
let (send, recv) = mpsc::channel(); let (send, recv) = mpsc::channel();
thread::spawn(move || for i in tty::get_tty().unwrap().bytes() { thread::spawn(move || for i in get_tty().unwrap().bytes() {
if send.send(i).is_err() { if send.send(i).is_err() {
return; return;
} }

View File

@ -18,6 +18,7 @@ use std::io::{self, Write, Read};
use std::time::{SystemTime, Duration}; use std::time::{SystemTime, Duration};
use async::async_stdin; use async::async_stdin;
use std::env; use std::env;
use numtoa::NumToA;
/// A terminal color. /// A terminal color.
pub trait Color { pub trait Color {
@ -36,14 +37,24 @@ macro_rules! derive_color {
impl Color for $name { impl Color for $name {
#[inline] #[inline]
fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("38;5;", $value, "m")) f.write_str(self.fg_str())
} }
#[inline] #[inline]
fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("48;5;", $value, "m")) f.write_str(self.bg_str())
} }
} }
impl $name {
#[inline]
/// Returns the ANSI escape sequence as a string.
pub fn fg_str(&self) -> &'static str { csi!("38;5;", $value, "m") }
#[inline]
/// Returns the ANSI escape sequences as a string.
pub fn bg_str(&self) -> &'static str { csi!("48;5;", $value, "m") }
}
}; };
} }
@ -110,15 +121,31 @@ impl AnsiValue {
} }
} }
impl AnsiValue {
/// Returns the ANSI sequence as a string.
pub fn fg_string(self) -> String {
let mut x = [0u8; 20];
let x = self.0.numtoa_str(10, &mut x);
[csi!("38;5;"), x, "m"].concat()
}
/// Returns the ANSI sequence as a string.
pub fn bg_string(self) -> String {
let mut x = [0u8; 20];
let x = self.0.numtoa_str(10, &mut x);
[csi!("48;5;"), x, "m"].concat()
}
}
impl Color for AnsiValue { impl Color for AnsiValue {
#[inline] #[inline]
fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("38;5;{}m"), self.0) f.write_str(&self.fg_string())
} }
#[inline] #[inline]
fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("48;5;{}m"), self.0) f.write_str(&self.bg_string())
} }
} }
@ -126,15 +153,41 @@ impl Color for AnsiValue {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rgb(pub u8, pub u8, pub u8); pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
/// Returns the ANSI sequence as a string.
pub fn fg_string(self) -> String {
let (mut x, mut y, mut z) = ([0u8; 20], [0u8; 20], [0u8; 20]);
let (x, y, z) = (
self.0.numtoa_str(10, &mut x),
self.1.numtoa_str(10, &mut y),
self.2.numtoa_str(10, &mut z),
);
[csi!("38;2;"), x, ";", y, ";", z, "m"].concat()
}
/// Returns the ANSI sequence as a string.
pub fn bg_string(self) -> String {
let (mut x, mut y, mut z) = ([0u8; 20], [0u8; 20], [0u8; 20]);
let (x, y, z) = (
self.0.numtoa_str(10, &mut x),
self.1.numtoa_str(10, &mut y),
self.2.numtoa_str(10, &mut z),
);
[csi!("48;2;"), x, ";", y, ";", z, "m"].concat()
}
}
impl Color for Rgb { impl Color for Rgb {
#[inline] #[inline]
fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("38;2;{};{};{}m"), self.0, self.1, self.2) f.write_str(&self.fg_string())
} }
#[inline] #[inline]
fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("48;2;{};{};{}m"), self.0, self.1, self.2) f.write_str(&self.bg_string())
} }
} }
@ -142,15 +195,25 @@ impl Color for Rgb {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Reset; pub struct Reset;
const RESET_FG: &str = csi!("39m");
const RESET_BG: &str = csi!("49m");
impl Reset {
/// Returns the ANSI sequence as a string.
pub fn fg_str(self) -> &'static str { RESET_FG }
/// Returns the ANSI sequence as a string.
pub fn bg_str(self) -> &'static str { RESET_BG }
}
impl Color for Reset { impl Color for Reset {
#[inline] #[inline]
fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("39m")) f.write_str(RESET_FG)
} }
#[inline] #[inline]
fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result { fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("49m")) f.write_str(RESET_BG)
} }
} }

View File

@ -3,13 +3,17 @@
use std::fmt; use std::fmt;
use std::ops; use std::ops;
use std::io::{self, Write, Error, ErrorKind, Read}; use std::io::{self, Write, Error, ErrorKind, Read};
use async::async_stdin; use async::async_stdin_until;
use std::time::{SystemTime, Duration}; use std::time::{SystemTime, Duration};
use raw::CONTROL_SEQUENCE_TIMEOUT; use raw::CONTROL_SEQUENCE_TIMEOUT;
use numtoa::NumToA;
derive_csi_sequence!("Hide the cursor.", Hide, "?25l"); derive_csi_sequence!("Hide the cursor.", Hide, "?25l");
derive_csi_sequence!("Show the cursor.", Show, "?25h"); derive_csi_sequence!("Show the cursor.", Show, "?25h");
derive_csi_sequence!("Restore the cursor.", Restore, "u");
derive_csi_sequence!("Save the cursor.", Save, "s");
/// Goto some position ((1,1)-based). /// Goto some position ((1,1)-based).
/// ///
/// # Why one-based? /// # Why one-based?
@ -30,6 +34,13 @@ derive_csi_sequence!("Show the cursor.", Show, "?25h");
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Goto(pub u16, pub u16); pub struct Goto(pub u16, pub u16);
impl From<Goto> for String {
fn from(this: Goto) -> String {
let (mut x, mut y) = ([0u8; 20], [0u8; 20]);
["\x1B[", this.1.numtoa_str(10, &mut x), ";", this.0.numtoa_str(10, &mut y), "H"].concat()
}
}
impl Default for Goto { impl Default for Goto {
fn default() -> Goto { fn default() -> Goto {
Goto(1, 1) Goto(1, 1)
@ -39,8 +50,7 @@ impl Default for Goto {
impl fmt::Display for Goto { impl fmt::Display for Goto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
debug_assert!(self != &Goto(0, 0), "Goto is one-based."); debug_assert!(self != &Goto(0, 0), "Goto is one-based.");
f.write_str(&String::from(*self))
write!(f, csi!("{};{}H"), self.1, self.0)
} }
} }
@ -48,9 +58,16 @@ impl fmt::Display for Goto {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Left(pub u16); pub struct Left(pub u16);
impl From<Left> for String {
fn from(this: Left) -> String {
let mut buf = [0u8; 20];
["\x1B[", this.0.numtoa_str(10, &mut buf), "D"].concat()
}
}
impl fmt::Display for Left { impl fmt::Display for Left {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("{}D"), self.0) f.write_str(&String::from(*self))
} }
} }
@ -58,9 +75,16 @@ impl fmt::Display for Left {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Right(pub u16); pub struct Right(pub u16);
impl From<Right> for String {
fn from(this: Right) -> String {
let mut buf = [0u8; 20];
["\x1B[", this.0.numtoa_str(10, &mut buf), "C"].concat()
}
}
impl fmt::Display for Right { impl fmt::Display for Right {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("{}C"), self.0) f.write_str(&String::from(*self))
} }
} }
@ -68,9 +92,16 @@ impl fmt::Display for Right {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Up(pub u16); pub struct Up(pub u16);
impl From<Up> for String {
fn from(this: Up) -> String {
let mut buf = [0u8; 20];
["\x1B[", this.0.numtoa_str(10, &mut buf), "A"].concat()
}
}
impl fmt::Display for Up { impl fmt::Display for Up {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("{}A"), self.0) f.write_str(&String::from(*self))
} }
} }
@ -78,9 +109,16 @@ impl fmt::Display for Up {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct Down(pub u16); pub struct Down(pub u16);
impl From<Down> for String {
fn from(this: Down) -> String {
let mut buf = [0u8; 20];
["\x1B[", this.0.numtoa_str(10, &mut buf), "B"].concat()
}
}
impl fmt::Display for Down { impl fmt::Display for Down {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, csi!("{}B"), self.0) f.write_str(&String::from(*self))
} }
} }
@ -92,7 +130,8 @@ pub trait DetectCursorPos {
impl<W: Write> DetectCursorPos for W { impl<W: Write> DetectCursorPos for W {
fn cursor_pos(&mut self) -> io::Result<(u16, u16)> { fn cursor_pos(&mut self) -> io::Result<(u16, u16)> {
let mut stdin = async_stdin(); let delimiter = b'R';
let mut stdin = async_stdin_until(delimiter);
// Where is the cursor? // Where is the cursor?
// Use `ESC [ 6 n`. // Use `ESC [ 6 n`.
@ -106,13 +145,13 @@ impl<W: Write> DetectCursorPos for W {
let now = SystemTime::now(); let now = SystemTime::now();
// Either consume all data up to R or wait for a timeout. // Either consume all data up to R or wait for a timeout.
while buf[0] != b'R' && now.elapsed().unwrap() < timeout { while buf[0] != delimiter && now.elapsed().unwrap() < timeout {
if stdin.read(&mut buf)? > 0 { if stdin.read(&mut buf)? > 0 {
read_chars.push(buf[0]); read_chars.push(buf[0]);
} }
} }
if read_chars.len() == 0 { if read_chars.is_empty() {
return Err(Error::new(ErrorKind::Other, "Cursor position detection timed out.")); return Err(Error::new(ErrorKind::Other, "Cursor position detection timed out."));
} }

View File

@ -1,7 +1,6 @@
//! Mouse and key events. //! Mouse and key events.
use std::io::{Error, ErrorKind}; use std::io::{Error, ErrorKind};
use std::ascii::AsciiExt;
use std::str; use std::str;
/// An event reported by the terminal. /// An event reported by the terminal.

View File

@ -27,16 +27,29 @@ impl<R: Read> Iterator for Keys<R> {
} }
/// An iterator over input events. /// An iterator over input events.
pub struct Events<R> { pub struct Events<R> {
source: R, inner: EventsAndRaw<R>
leftover: Option<u8>,
} }
impl<R: Read> Iterator for Events<R> { impl<R: Read> Iterator for Events<R> {
type Item = Result<Event, io::Error>; type Item = Result<Event, io::Error>;
fn next(&mut self) -> Option<Result<Event, io::Error>> { fn next(&mut self) -> Option<Result<Event, io::Error>> {
let mut source = &mut self.source; self.inner.next().map(|tuple| tuple.map(|(event, _raw)| event))
}
}
/// An iterator over input events and the bytes that define them.
pub struct EventsAndRaw<R> {
source: R,
leftover: Option<u8>,
}
impl<R: Read> Iterator for EventsAndRaw<R> {
type Item = Result<(Event, Vec<u8>), io::Error>;
fn next(&mut self) -> Option<Result<(Event, Vec<u8>), io::Error>> {
let source = &mut self.source;
if let Some(c) = self.leftover { if let Some(c) = self.leftover {
// we have a leftover byte, use it // we have a leftover byte, use it
@ -53,7 +66,7 @@ impl<R: Read> Iterator for Events<R> {
Ok(0) => return None, Ok(0) => return None,
Ok(1) => { Ok(1) => {
match buf[0] { match buf[0] {
b'\x1B' => Ok(Event::Key(Key::Esc)), b'\x1B' => Ok((Event::Key(Key::Esc), vec![b'\x1B'])),
c => parse_event(c, &mut source.bytes()), c => parse_event(c, &mut source.bytes()),
} }
} }
@ -75,7 +88,7 @@ impl<R: Read> Iterator for Events<R> {
} }
} }
fn parse_event<I>(item: u8, iter: &mut I) -> Result<Event, io::Error> fn parse_event<I>(item: u8, iter: &mut I) -> Result<(Event, Vec<u8>), io::Error>
where I: Iterator<Item = Result<u8, io::Error>> where I: Iterator<Item = Result<u8, io::Error>>
{ {
let mut buf = vec![item]; let mut buf = vec![item];
@ -85,7 +98,7 @@ fn parse_event<I>(item: u8, iter: &mut I) -> Result<Event, io::Error>
}); });
event::parse_event(item, &mut iter) event::parse_event(item, &mut iter)
}; };
result.or(Ok(Event::Unsupported(buf))) result.or(Ok(Event::Unsupported(buf.clone()))).map(|e| (e, buf))
} }
@ -113,11 +126,11 @@ pub trait TermRead {
} }
} }
impl<R: Read> TermRead for R {
impl<R: Read + TermReadEventsAndRaw> TermRead for R {
fn events(self) -> Events<Self> { fn events(self) -> Events<Self> {
Events { Events {
source: self, inner: self.events_and_raw()
leftover: None,
} }
} }
fn keys(self) -> Keys<Self> { fn keys(self) -> Keys<Self> {
@ -145,6 +158,21 @@ impl<R: Read> TermRead for R {
} }
} }
/// Extension to `TermRead` trait. A separate trait in order to maintain backwards compatibility.
pub trait TermReadEventsAndRaw {
/// An iterator over input events and the bytes that define them.
fn events_and_raw(self) -> EventsAndRaw<Self> where Self: Sized;
}
impl<R: Read> TermReadEventsAndRaw for R {
fn events_and_raw(self) -> EventsAndRaw<Self> {
EventsAndRaw {
source: self,
leftover: None,
}
}
}
/// A sequence of escape codes to enable terminal mouse support. /// A sequence of escape codes to enable terminal mouse support.
const ENTER_MOUSE_SEQUENCE: &'static str = csi!("?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h"); const ENTER_MOUSE_SEQUENCE: &'static str = csi!("?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
@ -241,6 +269,38 @@ mod test {
assert!(i.next().is_none()); assert!(i.next().is_none());
} }
#[test]
fn test_events_and_raw() {
let input = b"\x1B[\x00bc\x7F\x1B[D\
\x1B[M\x00\x22\x24\x1B[<0;2;4;M\x1B[32;2;4M\x1B[<0;2;4;m\x1B[35;2;4Mb";
let mut output = Vec::<u8>::new();
{
let mut i = input.events_and_raw().map(|res| res.unwrap())
.inspect(|&(_, ref raw)| { output.extend(raw); }).map(|(event, _)| event);
assert_eq!(i.next().unwrap(),
Event::Unsupported(vec![0x1B, b'[', 0x00]));
assert_eq!(i.next().unwrap(), Event::Key(Key::Char('b')));
assert_eq!(i.next().unwrap(), Event::Key(Key::Char('c')));
assert_eq!(i.next().unwrap(), Event::Key(Key::Backspace));
assert_eq!(i.next().unwrap(), Event::Key(Key::Left));
assert_eq!(i.next().unwrap(),
Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, 2, 4)));
assert_eq!(i.next().unwrap(),
Event::Mouse(MouseEvent::Press(MouseButton::Left, 2, 4)));
assert_eq!(i.next().unwrap(),
Event::Mouse(MouseEvent::Press(MouseButton::Left, 2, 4)));
assert_eq!(i.next().unwrap(),
Event::Mouse(MouseEvent::Release(2, 4)));
assert_eq!(i.next().unwrap(),
Event::Mouse(MouseEvent::Release(2, 4)));
assert_eq!(i.next().unwrap(), Event::Key(Key::Char('b')));
assert!(i.next().is_none());
}
assert_eq!(input.iter().map(|b| *b).collect::<Vec<u8>>(), output)
}
#[test] #[test]
fn test_function_keys() { fn test_function_keys() {
let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys(); let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys();

View File

@ -8,24 +8,25 @@
//! //!
//! Supports Redox, Mac OS X, and Linux (or, in general, ANSI terminals). //! Supports Redox, Mac OS X, and Linux (or, in general, ANSI terminals).
//! //!
//! For more information refer to the [README](https://github.com/ticki/termion). //! For more information refer to the [README](https://github.com/redox-os/termion).
#![warn(missing_docs)] #![warn(missing_docs)]
#[cfg(not(target_os = "redox"))] extern crate numtoa;
extern crate libc;
#[cfg(not(target_os = "redox"))] #[cfg(target_os = "redox")]
mod termios; #[path="sys/redox/mod.rs"]
mod sys;
#[cfg(unix)]
#[path="sys/unix/mod.rs"]
mod sys;
pub use sys::size::terminal_size;
pub use sys::tty::{is_tty, get_tty};
mod async; mod async;
pub use async::{AsyncReader, async_stdin}; pub use async::{AsyncReader, async_stdin};
mod size;
pub use size::terminal_size;
mod tty;
pub use tty::{is_tty, get_tty};
#[macro_use] #[macro_use]
mod macros; mod macros;
pub mod clear; pub mod clear;
@ -37,3 +38,26 @@ pub mod raw;
pub mod screen; pub mod screen;
pub mod scroll; pub mod scroll;
pub mod style; pub mod style;
#[cfg(test)]
mod test {
use super::sys;
#[test]
fn test_get_terminal_attr() {
sys::attr::get_terminal_attr().unwrap();
sys::attr::get_terminal_attr().unwrap();
sys::attr::get_terminal_attr().unwrap();
}
#[test]
fn test_set_terminal_attr() {
let ios = sys::attr::get_terminal_attr().unwrap();
sys::attr::set_terminal_attr(&ios).unwrap();
}
#[test]
fn test_size() {
sys::size::terminal_size().unwrap();
}
}

View File

@ -15,5 +15,13 @@ macro_rules! derive_csi_sequence {
write!(f, csi!($value)) write!(f, csi!($value))
} }
} }
impl AsRef<[u8]> for $name {
fn as_ref(&self) -> &'static [u8] { csi!($value).as_bytes() }
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &'static str { csi!($value) }
}
}; };
} }

View File

@ -25,6 +25,9 @@
use std::io::{self, Write}; use std::io::{self, Write};
use std::ops; use std::ops;
use sys::Termios;
use sys::attr::{get_terminal_attr, raw_terminal_attr, set_terminal_attr};
/// The timeout of an escape code control sequence, in milliseconds. /// The timeout of an escape code control sequence, in milliseconds.
pub const CONTROL_SEQUENCE_TIMEOUT: u64 = 100; pub const CONTROL_SEQUENCE_TIMEOUT: u64 = 100;
@ -32,34 +35,14 @@ pub const CONTROL_SEQUENCE_TIMEOUT: u64 = 100;
/// dropped. /// dropped.
/// ///
/// Restoring will entirely bring back the old TTY state. /// Restoring will entirely bring back the old TTY state.
#[cfg(target_os = "redox")]
pub struct RawTerminal<W: Write> {
output: W,
}
#[cfg(target_os = "redox")]
impl<W: Write> Drop for RawTerminal<W> {
fn drop(&mut self) {
let _ = write!(self, csi!("?82l"));
let _ = self.flush();
}
}
#[cfg(not(target_os = "redox"))]
use termios::Termios;
/// A terminal restorer, which keeps the previous state of the terminal, and restores it, when
/// dropped.
#[cfg(not(target_os = "redox"))]
pub struct RawTerminal<W: Write> { pub struct RawTerminal<W: Write> {
prev_ios: Termios, prev_ios: Termios,
output: W, output: W,
} }
#[cfg(not(target_os = "redox"))]
impl<W: Write> Drop for RawTerminal<W> { impl<W: Write> Drop for RawTerminal<W> {
fn drop(&mut self) { fn drop(&mut self) {
use termios::set_terminal_attr; set_terminal_attr(&self.prev_ios).unwrap();
set_terminal_attr(&mut self.prev_ios as *mut _);
} }
} }
@ -103,36 +86,32 @@ pub trait IntoRawMode: Write + Sized {
} }
impl<W: Write> IntoRawMode for W { impl<W: Write> IntoRawMode for W {
#[cfg(not(target_os = "redox"))]
fn into_raw_mode(self) -> io::Result<RawTerminal<W>> { fn into_raw_mode(self) -> io::Result<RawTerminal<W>> {
use termios::{cfmakeraw, get_terminal_attr, set_terminal_attr}; let mut ios = get_terminal_attr()?;
let (mut ios, exit) = get_terminal_attr();
let prev_ios = ios; let prev_ios = ios;
if exit != 0 {
return Err(io::Error::new(io::ErrorKind::Other, "Unable to get Termios attribute."));
}
unsafe { raw_terminal_attr(&mut ios);
cfmakeraw(&mut ios);
}
if set_terminal_attr(&mut ios as *mut _) != 0 { set_terminal_attr(&ios)?;
Err(io::Error::new(io::ErrorKind::Other, "Unable to set Termios attribute."))
} else { Ok(RawTerminal {
let res = RawTerminal { prev_ios: prev_ios,
prev_ios: prev_ios, output: self,
output: self, })
}; }
Ok(res) }
}
impl<W: Write> RawTerminal<W> {
pub fn suspend_raw_mode(&self) -> io::Result<()> {
set_terminal_attr(&self.prev_ios)?;
Ok(())
} }
#[cfg(target_os = "redox")] pub fn activate_raw_mode(&self) -> io::Result<()> {
fn into_raw_mode(mut self) -> io::Result<RawTerminal<W>> { let mut ios = get_terminal_attr()?;
write!(self, csi!("?82h"))?; raw_terminal_attr(&mut ios);
self.flush()?; set_terminal_attr(&ios)?;
Ok(RawTerminal { output: self }) Ok(())
} }
} }
@ -145,6 +124,8 @@ mod test {
fn test_into_raw_mode() { fn test_into_raw_mode() {
let mut out = stdout().into_raw_mode().unwrap(); let mut out = stdout().into_raw_mode().unwrap();
out.write_all(b"this is a test, muahhahahah").unwrap(); out.write_all(b"this is a test, muahhahahah\r\n").unwrap();
drop(out);
} }
} }

View File

@ -1,80 +0,0 @@
use std::io;
#[cfg(not(target_os = "redox"))]
use libc::c_ushort;
#[cfg(not(target_os = "redox"))]
#[repr(C)]
struct TermSize {
row: c_ushort,
col: c_ushort,
_x: c_ushort,
_y: c_ushort,
}
// Since attributes on non-item statements is not stable yet, we use a function.
#[cfg(not(target_os = "android"))]
#[cfg(not(target_os = "redox"))]
#[cfg(target_pointer_width = "64")]
#[cfg(not(target_env = "musl"))]
fn tiocgwinsz() -> u64 {
use termios::TIOCGWINSZ;
TIOCGWINSZ as u64
}
#[cfg(not(target_os = "android"))]
#[cfg(not(target_os = "redox"))]
#[cfg(target_pointer_width = "32")]
#[cfg(not(target_env = "musl"))]
fn tiocgwinsz() -> u32 {
use termios::TIOCGWINSZ;
TIOCGWINSZ as u32
}
#[cfg(any(target_env = "musl", target_os = "android"))]
fn tiocgwinsz() -> i32 {
use termios::TIOCGWINSZ;
TIOCGWINSZ as i32
}
/// Get the size of the terminal.
#[cfg(not(target_os = "redox"))]
pub fn terminal_size() -> io::Result<(u16, u16)> {
use libc::ioctl;
use libc::STDOUT_FILENO;
use std::mem;
unsafe {
let mut size: TermSize = mem::zeroed();
if ioctl(STDOUT_FILENO, tiocgwinsz(), &mut size as *mut _) == 0 {
Ok((size.col as u16, size.row as u16))
} else {
Err(io::Error::new(io::ErrorKind::Other, "Unable to get the terminal size."))
}
}
}
/// Get the size of the terminal.
#[cfg(target_os = "redox")]
pub fn terminal_size() -> io::Result<(u16, u16)> {
use std::env;
let width = try!(env::var("COLUMNS").map_err(|x| io::Error::new(io::ErrorKind::NotFound, x)))
.parse()
.unwrap_or(0);
let height = try!(env::var("LINES").map_err(|x| io::Error::new(io::ErrorKind::NotFound, x)))
.parse()
.unwrap_or(0);
Ok((width, height))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_size() {
assert!(terminal_size().is_ok());
}
}

33
src/sys/redox/attr.rs Normal file
View File

@ -0,0 +1,33 @@
use std::io;
use super::{cvt, syscall, Termios};
pub fn get_terminal_attr() -> io::Result<Termios> {
let mut termios = Termios::default();
let fd = cvt(syscall::dup(0, b"termios"))?;
let res = cvt(syscall::read(fd, &mut termios));
let _ = syscall::close(fd);
if res? == termios.len() {
Ok(termios)
} else {
Err(io::Error::new(io::ErrorKind::Other, "Unable to get the terminal attributes."))
}
}
pub fn set_terminal_attr(termios: &Termios) -> io::Result<()> {
let fd = cvt(syscall::dup(0, b"termios"))?;
let res = cvt(syscall::write(fd, termios));
let _ = syscall::close(fd);
if res? == termios.len() {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::Other, "Unable to set the terminal attributes."))
}
}
pub fn raw_terminal_attr(ios: &mut Termios) {
ios.make_raw()
}

15
src/sys/redox/mod.rs Normal file
View File

@ -0,0 +1,15 @@
extern crate redox_termios;
extern crate syscall;
use std::io;
pub use self::redox_termios::Termios;
pub mod attr;
pub mod size;
pub mod tty;
// Support function for converting syscall error to io error
fn cvt(result: Result<usize, syscall::Error>) -> io::Result<usize> {
result.map_err(|err| io::Error::from_raw_os_error(err.errno))
}

18
src/sys/redox/size.rs Normal file
View File

@ -0,0 +1,18 @@
use std::io;
use super::{cvt, redox_termios, syscall};
/// Get the size of the terminal.
pub fn terminal_size() -> io::Result<(u16, u16)> {
let mut winsize = redox_termios::Winsize::default();
let fd = cvt(syscall::dup(1, b"winsize"))?;
let res = cvt(syscall::read(fd, &mut winsize));
let _ = syscall::close(fd);
if res? == winsize.len() {
Ok((winsize.ws_col, winsize.ws_row))
} else {
Err(io::Error::new(io::ErrorKind::Other, "Unable to get the terminal size."))
}
}

22
src/sys/redox/tty.rs Normal file
View File

@ -0,0 +1,22 @@
use std::{env, fs, io};
use std::os::unix::io::AsRawFd;
use super::syscall;
/// Is this stream a TTY?
pub fn is_tty<T: AsRawFd>(stream: &T) -> bool {
if let Ok(fd) = syscall::dup(stream.as_raw_fd(), b"termios") {
let _ = syscall::close(fd);
true
} else {
false
}
}
/// Get the TTY device.
///
/// This allows for getting stdio representing _only_ the TTY, and not other streams.
pub fn get_tty() -> io::Result<fs::File> {
let tty = try!(env::var("TTY").map_err(|x| io::Error::new(io::ErrorKind::NotFound, x)));
fs::OpenOptions::new().read(true).write(true).open(tty)
}

29
src/sys/unix/attr.rs Normal file
View File

@ -0,0 +1,29 @@
use std::{io, mem};
use super::{cvt, Termios};
use super::libc::c_int;
pub fn get_terminal_attr() -> io::Result<Termios> {
extern "C" {
pub fn tcgetattr(fd: c_int, termptr: *mut Termios) -> c_int;
}
unsafe {
let mut termios = mem::zeroed();
cvt(tcgetattr(0, &mut termios))?;
Ok(termios)
}
}
pub fn set_terminal_attr(termios: &Termios) -> io::Result<()> {
extern "C" {
pub fn tcsetattr(fd: c_int, opt: c_int, termptr: *const Termios) -> c_int;
}
cvt(unsafe { tcsetattr(0, 0, termios) }).and(Ok(()))
}
pub fn raw_terminal_attr(termios: &mut Termios) {
extern "C" {
pub fn cfmakeraw(termptr: *mut Termios);
}
unsafe { cfmakeraw(termios) }
}

33
src/sys/unix/mod.rs Normal file
View File

@ -0,0 +1,33 @@
extern crate libc;
use std::io;
pub use self::libc::termios as Termios;
pub mod attr;
pub mod size;
pub mod tty;
// Support functions for converting libc return values to io errors {
trait IsMinusOne {
fn is_minus_one(&self) -> bool;
}
macro_rules! impl_is_minus_one {
($($t:ident)*) => ($(impl IsMinusOne for $t {
fn is_minus_one(&self) -> bool {
*self == -1
}
})*)
}
impl_is_minus_one! { i8 i16 i32 i64 isize }
fn cvt<T: IsMinusOne>(t: T) -> io::Result<T> {
if t.is_minus_one() {
Err(io::Error::last_os_error())
} else {
Ok(t)
}
}
// } End of support functions

20
src/sys/unix/size.rs Normal file
View File

@ -0,0 +1,20 @@
use std::{io, mem};
use super::cvt;
use super::libc::{c_ushort, ioctl, STDOUT_FILENO, TIOCGWINSZ};
#[repr(C)]
struct TermSize {
row: c_ushort,
col: c_ushort,
_x: c_ushort,
_y: c_ushort,
}
/// Get the size of the terminal.
pub fn terminal_size() -> io::Result<(u16, u16)> {
unsafe {
let mut size: TermSize = mem::zeroed();
cvt(ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut size as *mut _))?;
Ok((size.col as u16, size.row as u16))
}
}

17
src/sys/unix/tty.rs Normal file
View File

@ -0,0 +1,17 @@
use std::{fs, io};
use std::os::unix::io::AsRawFd;
use super::libc;
/// Is this stream a TTY?
pub fn is_tty<T: AsRawFd>(stream: &T) -> bool {
unsafe { libc::isatty(stream.as_raw_fd()) == 1 }
}
/// Get the TTY device.
///
/// This allows for getting stdio representing _only_ the TTY, and not other streams.
pub fn get_tty() -> io::Result<fs::File> {
fs::OpenOptions::new().read(true).write(true).open("/dev/tty")
}

View File

@ -1,45 +0,0 @@
use libc::c_int;
use std::mem;
pub use libc::termios as Termios;
#[cfg(target_os = "linux")]
pub const TIOCGWINSZ: usize = 0x00005413;
#[cfg(not(target_os = "linux"))]
pub const TIOCGWINSZ: usize = 0x40087468;
extern "C" {
pub fn tcgetattr(fd: c_int, termptr: *mut Termios) -> c_int;
pub fn tcsetattr(fd: c_int, opt: c_int, termptr: *mut Termios) -> c_int;
pub fn cfmakeraw(termptr: *mut Termios);
}
pub fn get_terminal_attr() -> (Termios, c_int) {
unsafe {
let mut ios = mem::zeroed();
let attr = tcgetattr(0, &mut ios);
(ios, attr)
}
}
pub fn set_terminal_attr(ios: *mut Termios) -> c_int {
unsafe { tcsetattr(0, 0, ios) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_terminal_attr() {
get_terminal_attr();
get_terminal_attr();
get_terminal_attr();
}
#[test]
fn test_set_terminal_attr() {
let mut ios = get_terminal_attr().0;
set_terminal_attr(&mut ios as *mut _);
}
}

View File

@ -1,34 +0,0 @@
use std::{fs, io};
use std::os::unix::io::AsRawFd;
/// Is this stream an TTY?
#[cfg(not(target_os = "redox"))]
pub fn is_tty<T: AsRawFd>(stream: &T) -> bool {
use libc;
unsafe { libc::isatty(stream.as_raw_fd()) == 1 }
}
/// This will panic.
#[cfg(target_os = "redox")]
pub fn is_tty<T: AsRawFd>(_stream: &T) -> bool {
unimplemented!();
}
/// Get the TTY device.
///
/// This allows for getting stdio representing _only_ the TTY, and not other streams.
#[cfg(target_os = "redox")]
pub fn get_tty() -> io::Result<fs::File> {
use std::env;
let tty = try!(env::var("TTY").map_err(|x| io::Error::new(io::ErrorKind::NotFound, x)));
fs::OpenOptions::new().read(true).write(true).open(tty)
}
/// Get the TTY device.
///
/// This allows for getting stdio representing _only_ the TTY, and not other streams.
#[cfg(not(target_os = "redox"))]
pub fn get_tty() -> io::Result<fs::File> {
fs::OpenOptions::new().read(true).write(true).open("/dev/tty")
}