diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e60f6ab --- /dev/null +++ b/.gitlab-ci.yml @@ -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" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index dd2495a..6e88e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,20 @@ [package] name = "termion" -version = "1.4.0" +version = "1.5.1" authors = ["ticki ", "gycos ", "IGI-111 "] 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" license = "MIT" keywords = ["tty", "color", "terminal", "password", "tui"] exclude = ["target", "CHANGELOG.md", "image.png", "Cargo.lock"] +[dependencies] +numtoa = { version = "0.1.0", features = ["std"]} + [target.'cfg(not(target_os = "redox"))'.dependencies] libc = "0.2.8" + +[target.'cfg(target_os = "redox")'.dependencies] +redox_syscall = "0.1" +redox_termios = "0.1" diff --git a/README.md b/README.md index 7bda9d3..25346d9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

-Termion logo +Termion logo

-[![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/) |----|----|----|----|---- diff --git a/examples/color.rs b/examples/color.rs index 16071be..72c521d 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -6,5 +6,5 @@ fn main() { println!("{}Red", color::Fg(color::Red)); println!("{}Blue", color::Fg(color::Blue)); println!("{}Blue'n'Bold{}", style::Bold, style::Reset); - println!("{}Just plain italic", style::Italic); + println!("{}Just plain italic{}", style::Italic, style::Reset); } diff --git a/src/async.rs b/src/async.rs index 6255b78..ea02489 100644 --- a/src/async.rs +++ b/src/async.rs @@ -2,7 +2,30 @@ use std::io::{self, Read}; use std::sync::mpsc; 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. /// @@ -17,7 +40,7 @@ use tty; pub fn async_stdin() -> AsyncReader { 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() { return; } diff --git a/src/color.rs b/src/color.rs index 6f3fd88..73776c2 100644 --- a/src/color.rs +++ b/src/color.rs @@ -18,6 +18,7 @@ use std::io::{self, Write, Read}; use std::time::{SystemTime, Duration}; use async::async_stdin; use std::env; +use numtoa::NumToA; /// A terminal color. pub trait Color { @@ -36,14 +37,24 @@ macro_rules! derive_color { impl Color for $name { #[inline] fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, csi!("38;5;", $value, "m")) + f.write_str(self.fg_str()) } #[inline] 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 { #[inline] 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] 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)] 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 { #[inline] 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] 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)] 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 { #[inline] fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, csi!("39m")) + f.write_str(RESET_FG) } #[inline] fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, csi!("49m")) + f.write_str(RESET_BG) } } diff --git a/src/cursor.rs b/src/cursor.rs index 541507e..bbc0394 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -3,13 +3,17 @@ use std::fmt; use std::ops; use std::io::{self, Write, Error, ErrorKind, Read}; -use async::async_stdin; +use async::async_stdin_until; use std::time::{SystemTime, Duration}; use raw::CONTROL_SEQUENCE_TIMEOUT; +use numtoa::NumToA; derive_csi_sequence!("Hide the cursor.", Hide, "?25l"); 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). /// /// # Why one-based? @@ -30,6 +34,13 @@ derive_csi_sequence!("Show the cursor.", Show, "?25h"); #[derive(Copy, Clone, PartialEq, Eq)] pub struct Goto(pub u16, pub u16); +impl From 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 { fn default() -> Goto { Goto(1, 1) @@ -39,8 +50,7 @@ impl Default for Goto { impl fmt::Display for Goto { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { debug_assert!(self != &Goto(0, 0), "Goto is one-based."); - - write!(f, csi!("{};{}H"), self.1, self.0) + f.write_str(&String::from(*self)) } } @@ -48,9 +58,16 @@ impl fmt::Display for Goto { #[derive(Copy, Clone, PartialEq, Eq)] pub struct Left(pub u16); +impl From 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 { 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)] pub struct Right(pub u16); +impl From 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 { 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)] pub struct Up(pub u16); +impl From 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 { 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)] pub struct Down(pub u16); +impl From 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 { 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 DetectCursorPos for W { 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? // Use `ESC [ 6 n`. @@ -106,13 +145,13 @@ impl DetectCursorPos for W { let now = SystemTime::now(); // 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 { 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.")); } diff --git a/src/event.rs b/src/event.rs index f79cb4c..6e383a1 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,6 @@ //! Mouse and key events. use std::io::{Error, ErrorKind}; -use std::ascii::AsciiExt; use std::str; /// An event reported by the terminal. diff --git a/src/input.rs b/src/input.rs index a9e874d..5c8ecf4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -27,16 +27,29 @@ impl Iterator for Keys { } /// An iterator over input events. -pub struct Events { - source: R, - leftover: Option, +pub struct Events { + inner: EventsAndRaw } impl Iterator for Events { type Item = Result; fn next(&mut self) -> Option> { - 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 { + source: R, + leftover: Option, +} + +impl Iterator for EventsAndRaw { + type Item = Result<(Event, Vec), io::Error>; + + fn next(&mut self) -> Option), io::Error>> { + let source = &mut self.source; if let Some(c) = self.leftover { // we have a leftover byte, use it @@ -53,7 +66,7 @@ impl Iterator for Events { Ok(0) => return None, Ok(1) => { 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()), } } @@ -75,7 +88,7 @@ impl Iterator for Events { } } -fn parse_event(item: u8, iter: &mut I) -> Result +fn parse_event(item: u8, iter: &mut I) -> Result<(Event, Vec), io::Error> where I: Iterator> { let mut buf = vec![item]; @@ -85,7 +98,7 @@ fn parse_event(item: u8, iter: &mut I) -> Result }); 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 TermRead for R { + +impl TermRead for R { fn events(self) -> Events { Events { - source: self, - leftover: None, + inner: self.events_and_raw() } } fn keys(self) -> Keys { @@ -145,6 +158,21 @@ impl 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 where Self: Sized; +} + +impl TermReadEventsAndRaw for R { + fn events_and_raw(self) -> EventsAndRaw { + EventsAndRaw { + source: self, + leftover: None, + } + } +} + /// A sequence of escape codes to enable terminal mouse support. 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()); } + #[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::::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::>(), output) + } + #[test] fn test_function_keys() { let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys(); diff --git a/src/lib.rs b/src/lib.rs index 7e286bb..1d8f66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,24 +8,25 @@ //! //! 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)] -#[cfg(not(target_os = "redox"))] -extern crate libc; +extern crate numtoa; -#[cfg(not(target_os = "redox"))] -mod termios; +#[cfg(target_os = "redox")] +#[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; pub use async::{AsyncReader, async_stdin}; -mod size; -pub use size::terminal_size; - -mod tty; -pub use tty::{is_tty, get_tty}; - #[macro_use] mod macros; pub mod clear; @@ -37,3 +38,26 @@ pub mod raw; pub mod screen; pub mod scroll; 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(); + } +} diff --git a/src/macros.rs b/src/macros.rs index 28370e3..5fd70b9 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -15,5 +15,13 @@ macro_rules! derive_csi_sequence { write!(f, csi!($value)) } } + + impl AsRef<[u8]> for $name { + fn as_ref(&self) -> &'static [u8] { csi!($value).as_bytes() } + } + + impl AsRef for $name { + fn as_ref(&self) -> &'static str { csi!($value) } + } }; } diff --git a/src/raw.rs b/src/raw.rs index bd9c8ec..0dbfb56 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -25,6 +25,9 @@ use std::io::{self, Write}; 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. pub const CONTROL_SEQUENCE_TIMEOUT: u64 = 100; @@ -32,34 +35,14 @@ pub const CONTROL_SEQUENCE_TIMEOUT: u64 = 100; /// dropped. /// /// Restoring will entirely bring back the old TTY state. -#[cfg(target_os = "redox")] -pub struct RawTerminal { - output: W, -} - -#[cfg(target_os = "redox")] -impl Drop for RawTerminal { - 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 { prev_ios: Termios, output: W, } -#[cfg(not(target_os = "redox"))] impl Drop for RawTerminal { fn drop(&mut self) { - use termios::set_terminal_attr; - set_terminal_attr(&mut self.prev_ios as *mut _); + set_terminal_attr(&self.prev_ios).unwrap(); } } @@ -103,36 +86,32 @@ pub trait IntoRawMode: Write + Sized { } impl IntoRawMode for W { - #[cfg(not(target_os = "redox"))] fn into_raw_mode(self) -> io::Result> { - use termios::{cfmakeraw, get_terminal_attr, set_terminal_attr}; - - let (mut ios, exit) = get_terminal_attr(); + let mut ios = get_terminal_attr()?; let prev_ios = ios; - if exit != 0 { - return Err(io::Error::new(io::ErrorKind::Other, "Unable to get Termios attribute.")); - } - unsafe { - cfmakeraw(&mut ios); - } + raw_terminal_attr(&mut ios); - if set_terminal_attr(&mut ios as *mut _) != 0 { - Err(io::Error::new(io::ErrorKind::Other, "Unable to set Termios attribute.")) - } else { - let res = RawTerminal { - prev_ios: prev_ios, - output: self, - }; - Ok(res) - } + set_terminal_attr(&ios)?; + + Ok(RawTerminal { + prev_ios: prev_ios, + output: self, + }) + } +} + +impl RawTerminal { + pub fn suspend_raw_mode(&self) -> io::Result<()> { + set_terminal_attr(&self.prev_ios)?; + Ok(()) } - #[cfg(target_os = "redox")] - fn into_raw_mode(mut self) -> io::Result> { - write!(self, csi!("?82h"))?; - self.flush()?; - Ok(RawTerminal { output: self }) + pub fn activate_raw_mode(&self) -> io::Result<()> { + let mut ios = get_terminal_attr()?; + raw_terminal_attr(&mut ios); + set_terminal_attr(&ios)?; + Ok(()) } } @@ -145,6 +124,8 @@ mod test { fn test_into_raw_mode() { 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); } } diff --git a/src/size.rs b/src/size.rs deleted file mode 100644 index 19f377b..0000000 --- a/src/size.rs +++ /dev/null @@ -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()); - } -} diff --git a/src/sys/redox/attr.rs b/src/sys/redox/attr.rs new file mode 100644 index 0000000..c6489a5 --- /dev/null +++ b/src/sys/redox/attr.rs @@ -0,0 +1,33 @@ +use std::io; + +use super::{cvt, syscall, Termios}; + +pub fn get_terminal_attr() -> io::Result { + 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() +} diff --git a/src/sys/redox/mod.rs b/src/sys/redox/mod.rs new file mode 100644 index 0000000..2a9b875 --- /dev/null +++ b/src/sys/redox/mod.rs @@ -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) -> io::Result { + result.map_err(|err| io::Error::from_raw_os_error(err.errno)) +} diff --git a/src/sys/redox/size.rs b/src/sys/redox/size.rs new file mode 100644 index 0000000..07f64a2 --- /dev/null +++ b/src/sys/redox/size.rs @@ -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.")) + } +} diff --git a/src/sys/redox/tty.rs b/src/sys/redox/tty.rs new file mode 100644 index 0000000..9179b39 --- /dev/null +++ b/src/sys/redox/tty.rs @@ -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(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 { + 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) +} diff --git a/src/sys/unix/attr.rs b/src/sys/unix/attr.rs new file mode 100644 index 0000000..5e21fba --- /dev/null +++ b/src/sys/unix/attr.rs @@ -0,0 +1,29 @@ +use std::{io, mem}; + +use super::{cvt, Termios}; +use super::libc::c_int; + +pub fn get_terminal_attr() -> io::Result { + 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) } +} diff --git a/src/sys/unix/mod.rs b/src/sys/unix/mod.rs new file mode 100644 index 0000000..08d73fe --- /dev/null +++ b/src/sys/unix/mod.rs @@ -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: T) -> io::Result { + if t.is_minus_one() { + Err(io::Error::last_os_error()) + } else { + Ok(t) + } +} +// } End of support functions diff --git a/src/sys/unix/size.rs b/src/sys/unix/size.rs new file mode 100644 index 0000000..17f2515 --- /dev/null +++ b/src/sys/unix/size.rs @@ -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)) + } +} diff --git a/src/sys/unix/tty.rs b/src/sys/unix/tty.rs new file mode 100644 index 0000000..2be9363 --- /dev/null +++ b/src/sys/unix/tty.rs @@ -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(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::OpenOptions::new().read(true).write(true).open("/dev/tty") +} diff --git a/src/termios.rs b/src/termios.rs deleted file mode 100644 index 420408f..0000000 --- a/src/termios.rs +++ /dev/null @@ -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 _); - } -} diff --git a/src/tty.rs b/src/tty.rs deleted file mode 100644 index 9788e54..0000000 --- a/src/tty.rs +++ /dev/null @@ -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(stream: &T) -> bool { - use libc; - - unsafe { libc::isatty(stream.as_raw_fd()) == 1 } -} - -/// This will panic. -#[cfg(target_os = "redox")] -pub fn is_tty(_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 { - 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::OpenOptions::new().read(true).write(true).open("/dev/tty") -}