From 8f0e433a7dc276ed638f5378781fb189d0da5f63 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 9 May 2017 13:51:44 +0200 Subject: [PATCH] Initial version --- .gitignore | 2 + Cargo.toml | 22 +++ LICENSE | 22 +++ README.md | 16 ++ examples/colors.rs | 9 ++ src/lib.rs | 79 ++++++++++ src/term.rs | 193 ++++++++++++++++++++++++ src/unix_term.rs | 53 +++++++ src/utils.rs | 355 ++++++++++++++++++++++++++++++++++++++++++++ src/windows_term.rs | 81 ++++++++++ 10 files changed, 832 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/colors.rs create mode 100644 src/lib.rs create mode 100644 src/term.rs create mode 100644 src/unix_term.rs create mode 100644 src/utils.rs create mode 100644 src/windows_term.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..de327282 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "console" +description = "A terminal and console abstraction for Rust" +version = "0.1.0" +keywords = ["cli", "terminal", "colors", "console", "ansi"] +authors = ["Armin Ronacher "] +license = "MIT" +homepage = "https://github.com/mitsuhiko/console" +documentation = "https://docs.rs/console" +readme = "README.md" + +[dependencies] +clicolors-control = "0" +lazy_static = "0.2" +libc = "0" +parking_lot = "0" +regex = "0.2" +unicode-width = "0.1" + +[target.'cfg(windows)'.dependencies] +winapi = "0" +kernel32-sys = "0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..dc9a85c1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Armin Ronacher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..1b9fd543 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# indicatif + +A rust library for indicating progress in command line applications to users. + +This currently primarily provides progress bars and spinners as well as basic +color support, but there are bigger plans for the future of this! + +## Examples + + + + + + + + diff --git a/examples/colors.rs b/examples/colors.rs new file mode 100644 index 00000000..85837fc7 --- /dev/null +++ b/examples/colors.rs @@ -0,0 +1,9 @@ +extern crate console; + +use console::style; + +fn main() { + println!("This is red on black: {:010x}", style(42).red().on_black().bold()); + println!("This is reversed: [{}]", style("whatever").reverse()); + println!("This is cyan: {}", style("whatever").cyan()); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..be12c90b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,79 @@ +//! console is a library for Rust that provides access to various terminal +//! features so you can build nicer looking command line interfaces. It +//! comes with various tools and utilities for working with Terminals and +//! formatting text. +//! +//! Best paired with other libraries in the family: +//! +//! * [indicatif](https://crates.io/crates/indicatif) +//! +//! # Terminal Access +//! +//! The terminal is abstracted through the `console::Term` type. It can +//! either directly provide access to the connected terminal or by buffering +//! up commands. A buffered terminal will however not be completely buffered +//! on windows where cursor movements are currently directly passed through. +//! +//! Example usage: +//! +//! ``` +//! # fn test() -> Result<(), Box> { +//! use std::thread; +//! use std::time::Duration; +//! +//! use console::Term; +//! +//! let term = Term::stdout(); +//! term.write_line("Hello World!")?; +//! thread::sleep(Duration::from_millis(2000)); +//! term.clear_line()?; +//! # Ok(()) } fn main() { test().unwrap(); } +//! ``` +//! +//! # Colors and Styles +//! +//! `console` uses `clicolors-control` to control colors. It also +//! provides higher level wrappers for styling text and other things +//! that can be displayed with the `style` function and utility types. +//! +//! Example usage: +//! +//! ``` +//! use console::style; +//! +//! println!("This is {} neat", style("quite").cyan()); +//! ``` +//! +//! You can also store styles and apply them to text later: +//! +//! ``` +//! use console::Style; +//! +//! let cyan = Style::new().cyan(); +//! println!("This is {} neat", cyan.apply_to("quite")); +//! ``` +//! +//! # Working with ANSI Codes +//! +//! The crate provids the function `strip_ansi_codes` to remove ANSI codes +//! from a string as well as `measure_text_width` to calculate the width of a +//! string as it would be displayed by the terminal. Both of those together +//! are useful for more complex formatting. +#[cfg(unix)] extern crate libc; +#[cfg(windows)] extern crate winapi; +#[cfg(windows)] extern crate kernel32; +#[macro_use] extern crate lazy_static; +extern crate regex; +extern crate parking_lot; +extern crate unicode_width; +extern crate clicolors_control; + +pub use term::{Term, user_attended}; +pub use utils::{style, Style, StyledObject, Color, Attribute, + strip_ansi_codes, measure_text_width, + colors_enabled, set_colors_enabled}; + +mod term; +mod utils; +#[cfg(unix)] mod unix_term; +#[cfg(windows)] mod windows_term; diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 00000000..e4d2ff86 --- /dev/null +++ b/src/term.rs @@ -0,0 +1,193 @@ +use std::io; +use std::io::Write; + +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(windows)] +use std::os::windows::io::{AsRawHandle, RawHandle}; + +use parking_lot::Mutex; + +enum TermTarget { + Stdout, + Stderr, +} + +/// Abstraction around a terminal. +pub struct Term { + target: TermTarget, + buffer: Option>>, +} + +impl Term { + /// Return a new unbuffered terminal + #[inline(always)] + pub fn stdout() -> Term { + Term { + target: TermTarget::Stdout, + buffer: None, + } + } + + /// Return a new unbuffered terminal to stderr + #[inline(always)] + pub fn stderr() -> Term { + Term { + target: TermTarget::Stderr, + buffer: None, + } + } + + /// Return a new buffered terminal + pub fn buffered_stdout() -> Term { + Term { + target: TermTarget::Stdout, + buffer: Some(Mutex::new(vec![])), + } + } + + /// Return a new buffered terminal to stderr + pub fn buffered_stderr() -> Term { + Term { + target: TermTarget::Stderr, + buffer: Some(Mutex::new(vec![])), + } + } + + #[doc(hidden)] + pub fn write_str(&self, s: &str) -> io::Result<()> { + match self.buffer { + Some(ref buffer) => buffer.lock().write_all(s.as_bytes()), + None => self.write_through(s.as_bytes()) + } + } + + /// Writes a string to the terminal and adds a newline. + pub fn write_line(&self, s: &str) -> io::Result<()> { + match self.buffer { + Some(ref mutex) => { + let mut buffer = mutex.lock(); + buffer.extend_from_slice(s.as_bytes()); + buffer.push(b'\n'); + Ok(()) + } + None => { + self.write_through(format!("{}\n", s).as_bytes()) + } + } + } + + /// Flushes + pub fn flush(&self) -> io::Result<()> { + match self.buffer { + Some(ref buffer) => { + let mut buffer = buffer.lock(); + if !buffer.is_empty() { + self.write_through(&buffer[..])?; + buffer.clear(); + } + } + None => {} + } + Ok(()) + } + + /// Checks if the terminal is indeed a terminal. + pub fn is_term(&self) -> bool { + is_a_terminal(self) + } + + /// Returns the terminal size or gets sensible defaults. + #[inline(always)] + pub fn size(&self) -> (u16, u16) { + self.size_checked().unwrap_or((24, DEFAULT_WIDTH)) + } + + /// Returns the terminal size in rows and columns. + /// + /// If the size cannot be reliably determined None is returned. + #[inline(always)] + pub fn size_checked(&self) -> Option<(u16, u16)> { + terminal_size() + } + + /// Moves the cursor up `n` lines + pub fn move_cursor_up(&self, n: usize) -> io::Result<()> { + move_cursor_up(self, n) + } + + /// Moves the cursor down `n` lines + pub fn move_cursor_down(&self, n: usize) -> io::Result<()> { + move_cursor_down(self, n) + } + + /// Clears the current line. + pub fn clear_line(&self) -> io::Result<()> { + clear_line(self) + } + + /// Clear the last `n` lines. + pub fn clear_last_lines(&self, n: usize) -> io::Result<()> { + self.move_cursor_up(n)?; + for _ in 0..n { + self.clear_line()?; + self.move_cursor_down(1)?; + } + self.move_cursor_up(n)?; + Ok(()) + } + + // helpers + + fn write_through(&self, bytes: &[u8]) -> io::Result<()> { + match self.target { + TermTarget::Stdout => { + io::stdout().write_all(bytes)?; + io::stdout().flush()?; + } + TermTarget::Stderr => { + io::stderr().write_all(bytes)?; + io::stderr().flush()?; + } + } + Ok(()) + } +} + +/// A fast way to check if the application has a user attended. +/// +/// This means that stdout is connected to a terminal instead of a +/// file or redirected by other means. +pub fn user_attended() -> bool { + Term::stdout().is_term() +} + +#[cfg(unix)] +impl AsRawFd for Term { + + fn as_raw_fd(&self) -> RawFd { + use libc; + match self.target { + TermTarget::Stdout => libc::STDOUT_FILENO, + TermTarget::Stderr => libc::STDERR_FILENO, + } + } +} + +#[cfg(windows)] +impl AsRawHandle for Term { + + fn as_raw_handle(&self) -> RawHandle { + use winapi::{STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + use kernel32::GetStdHandle; + unsafe { + GetStdHandle(match self.target { + TermTarget::Stdout => STD_OUTPUT_HANDLE, + TermTarget::Stderr => STD_ERROR_HANDLE, + }) as RawHandle + } + } +} + +#[cfg(unix)] pub use unix_term::*; +#[cfg(windows)] pub use windows_term::*; diff --git a/src/unix_term.rs b/src/unix_term.rs new file mode 100644 index 00000000..5191c858 --- /dev/null +++ b/src/unix_term.rs @@ -0,0 +1,53 @@ +use std::io; +use std::mem; +use std::os::unix::io::AsRawFd; + +use libc; + +use term::Term; + +pub const DEFAULT_WIDTH: u16 = 80; + + +#[inline(always)] +pub fn is_a_terminal(out: &Term) -> bool { + unsafe { + libc::isatty(out.as_raw_fd()) == 1 + } +} + +pub fn terminal_size() -> Option<(u16, u16)> { + unsafe { + if libc::isatty(libc::STDOUT_FILENO) != 1 { + return None; + } + + let mut winsize: libc::winsize = mem::zeroed(); + libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut winsize); + if winsize.ws_row > 0 && winsize.ws_col > 0 { + Some((winsize.ws_row as u16, winsize.ws_col as u16)) + } else { + None + } + } +} + +pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}B", n)) + } else { + Ok(()) + } +} + +pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}A", n)) + } else { + Ok(()) + } +} + +pub fn clear_line(out: &Term) -> io::Result<()> { + out.write_str(&format!("\r\x1b[2K")) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..3a1acabe --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,355 @@ +use std::fmt; +use std::collections::BTreeSet; +use std::borrow::Cow; + +use regex::Regex; +use unicode_width::UnicodeWidthStr; +use clicolors_control; + +/// Returns `true` if colors should be enabled. +/// +/// This honors the [clicolors spec](http://bixense.com/clicolors/). +/// +/// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. +/// * `CLICOLOR == 0`: Don't output ANSI color escape codes. +/// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. +/// +/// This internally uses `clicolors-control`. +#[inline(always)] +pub fn colors_enabled() -> bool { + clicolors_control::colors_enabled() +} + +/// Forces colorization on or off. +/// +/// This overrides the default for the current process and changes the return value of the +/// `colors_enabled` function. +/// +/// This internally uses `clicolors-control`. +#[inline(always)] +pub fn set_colors_enabled(val: bool) { + clicolors_control::set_colors_enabled(val) +} + +/// Helper function to strip ansi codes. +pub fn strip_ansi_codes(s: &str) -> Cow { + lazy_static! { + static ref STRIP_RE: Regex = Regex::new( + r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]").unwrap(); + } + STRIP_RE.replace_all(s, "") +} + +/// Measure the width of a string in terminal characters. +pub fn measure_text_width(s: &str) -> usize { + strip_ansi_codes(s).width() +} + +/// A terminal color. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, +} + +impl Color { + #[inline(always)] + fn ansi_num(&self) -> usize { + match *self { + Color::Black => 0, + Color::Red => 1, + Color::Green => 2, + Color::Yellow => 3, + Color::Blue => 4, + Color::Magenta => 5, + Color::Cyan => 6, + Color::White => 7, + } + } +} + +/// A terminal style attribute. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +pub enum Attribute { + Bold, + Dim, + Underlined, + Blink, + Reverse, + Hidden, +} + +impl Attribute { + #[inline(always)] + fn ansi_num(&self) -> usize { + match *self { + Attribute::Bold => 1, + Attribute::Dim => 2, + Attribute::Underlined => 4, + Attribute::Blink => 5, + Attribute::Reverse => 7, + Attribute::Hidden => 8, + } + } +} + +/// A stored style that can be applied. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Style { + fg: Option, + bg: Option, + attrs: BTreeSet, + force: Option, +} + +impl Default for Style { + fn default() -> Style { + Style::new() + } +} + +impl Style { + + /// Returns an empty default style. + pub fn new() -> Style { + Style { + fg: None, + bg: None, + attrs: BTreeSet::new(), + force: None, + } + } + + /// Creates a style from a dotted string. + /// + /// Effectively the string is split at each dot and then the + /// terms in between are applied. For instance `red.on_blue` will + /// create a string that is red on blue background. Unknown terms + /// are ignored. + pub fn from_dotted_str(s: &str) -> Style { + let mut rv = Style::new(); + for part in s.split('.') { + rv = match part { + "black" => rv.black(), + "red" => rv.red(), + "green" => rv.green(), + "yellow" => rv.yellow(), + "blue" => rv.blue(), + "magenta" => rv.magenta(), + "cyan" => rv.cyan(), + "white" => rv.white(), + "on_black" => rv.on_black(), + "on_red" => rv.on_red(), + "on_green" => rv.on_green(), + "on_yellow" => rv.on_yellow(), + "on_blue" => rv.on_blue(), + "on_magenta" => rv.on_magenta(), + "on_cyan" => rv.on_cyan(), + "on_white" => rv.on_white(), + "bold" => rv.bold(), + "dim" => rv.dim(), + "underlined" => rv.underlined(), + "blink" => rv.blink(), + "reverse" => rv.reverse(), + "hidden" => rv.hidden(), + _ => { continue; } + }; + } + rv + } + + /// Apply the style to something that can be displayed. + pub fn apply_to(&self, val: D) -> StyledObject { + StyledObject { + style: self.clone(), + val: val + } + } + + /// Forces styling on or off. + /// + /// This overrides the detection from `clicolors-control`. + #[inline(always)] + pub fn force_styling(mut self, value: bool) -> Style { + self.force = Some(value); + self + } + + /// Sets a foreground color. + #[inline(always)] + pub fn fg(mut self, color: Color) -> Style { + self.fg = Some(color); + self + } + + /// Sets a background color. + #[inline(always)] + pub fn bg(mut self, color: Color) -> Style { + self.bg = Some(color); + self + } + + /// Adds a attr. + #[inline(always)] + pub fn attr(mut self, attr: Attribute) -> Style { + self.attrs.insert(attr); + self + } + + #[inline(always)] pub fn black(self) -> Style { self.fg(Color::Black) } + #[inline(always)] pub fn red(self) -> Style { self.fg(Color::Red) } + #[inline(always)] pub fn green(self) -> Style { self.fg(Color::Green) } + #[inline(always)] pub fn yellow(self) -> Style { self.fg(Color::Yellow) } + #[inline(always)] pub fn blue(self) -> Style { self.fg(Color::Blue) } + #[inline(always)] pub fn magenta(self) -> Style { self.fg(Color::Magenta) } + #[inline(always)] pub fn cyan(self) -> Style { self.fg(Color::Cyan) } + #[inline(always)] pub fn white(self) -> Style { self.fg(Color::White) } + #[inline(always)] pub fn on_black(self) -> Style { self.bg(Color::Black) } + #[inline(always)] pub fn on_red(self) -> Style { self.bg(Color::Red) } + #[inline(always)] pub fn on_green(self) -> Style { self.bg(Color::Green) } + #[inline(always)] pub fn on_yellow(self) -> Style { self.bg(Color::Yellow) } + #[inline(always)] pub fn on_blue(self) -> Style { self.bg(Color::Blue) } + #[inline(always)] pub fn on_magenta(self) -> Style { self.bg(Color::Magenta) } + #[inline(always)] pub fn on_cyan(self) -> Style { self.bg(Color::Cyan) } + #[inline(always)] pub fn on_white(self) -> Style { self.bg(Color::White) } + #[inline(always)] pub fn bold(self) -> Style { self.attr(Attribute::Bold) } + #[inline(always)] pub fn dim(self) -> Style { self.attr(Attribute::Dim) } + #[inline(always)] pub fn underlined(self) -> Style { self.attr(Attribute::Underlined) } + #[inline(always)] pub fn blink(self) -> Style { self.attr(Attribute::Blink) } + #[inline(always)] pub fn reverse(self) -> Style { self.attr(Attribute::Reverse) } + #[inline(always)] pub fn hidden(self) -> Style { self.attr(Attribute::Hidden) } +} + +/// Wraps an object for formatting for styling. +/// +/// Example: +/// +/// ```rust,no_run +/// # use console::style; +/// format!("Hello {}", style("World").cyan()); +/// ``` +/// +/// This is a shortcut for making a new style and applying it +/// to a value: +/// +/// ```rust,no_run +/// # use console::Style; +/// format!("Hello {}", Style::new().cyan().apply_to("World")); +/// ``` +pub fn style(val: D) -> StyledObject { + Style::new().apply_to(val) +} + +/// A formatting wrapper that can be styled for a terminal. +#[derive(Clone)] +pub struct StyledObject { + style: Style, + val: D, +} + +impl StyledObject { + /// Forces styling on or off. + /// + /// This overrides the detection from `clicolors-control`. + #[inline(always)] + pub fn force_styling(mut self, value: bool) -> StyledObject { + self.style = self.style.force_styling(value); + self + } + + /// Sets a foreground color. + #[inline(always)] + pub fn fg(mut self, color: Color) -> StyledObject { + self.style = self.style.fg(color); + self + } + + /// Sets a background color. + #[inline(always)] + pub fn bg(mut self, color: Color) -> StyledObject { + self.style = self.style.bg(color); + self + } + + /// Adds a attr. + #[inline(always)] + pub fn attr(mut self, attr: Attribute) -> StyledObject { + self.style = self.style.attr(attr); + self + } + + #[inline(always)] pub fn black(self) -> StyledObject { self.fg(Color::Black) } + #[inline(always)] pub fn red(self) -> StyledObject { self.fg(Color::Red) } + #[inline(always)] pub fn green(self) -> StyledObject { self.fg(Color::Green) } + #[inline(always)] pub fn yellow(self) -> StyledObject { self.fg(Color::Yellow) } + #[inline(always)] pub fn blue(self) -> StyledObject { self.fg(Color::Blue) } + #[inline(always)] pub fn magenta(self) -> StyledObject { self.fg(Color::Magenta) } + #[inline(always)] pub fn cyan(self) -> StyledObject { self.fg(Color::Cyan) } + #[inline(always)] pub fn white(self) -> StyledObject { self.fg(Color::White) } + #[inline(always)] pub fn on_black(self) -> StyledObject { self.bg(Color::Black) } + #[inline(always)] pub fn on_red(self) -> StyledObject { self.bg(Color::Red) } + #[inline(always)] pub fn on_green(self) -> StyledObject { self.bg(Color::Green) } + #[inline(always)] pub fn on_yellow(self) -> StyledObject { self.bg(Color::Yellow) } + #[inline(always)] pub fn on_blue(self) -> StyledObject { self.bg(Color::Blue) } + #[inline(always)] pub fn on_magenta(self) -> StyledObject { self.bg(Color::Magenta) } + #[inline(always)] pub fn on_cyan(self) -> StyledObject { self.bg(Color::Cyan) } + #[inline(always)] pub fn on_white(self) -> StyledObject { self.bg(Color::White) } + #[inline(always)] pub fn bold(self) -> StyledObject { self.attr(Attribute::Bold) } + #[inline(always)] pub fn dim(self) -> StyledObject { self.attr(Attribute::Dim) } + #[inline(always)] pub fn underlined(self) -> StyledObject { self.attr(Attribute::Underlined) } + #[inline(always)] pub fn blink(self) -> StyledObject { self.attr(Attribute::Blink) } + #[inline(always)] pub fn reverse(self) -> StyledObject { self.attr(Attribute::Reverse) } + #[inline(always)] pub fn hidden(self) -> StyledObject { self.attr(Attribute::Hidden) } +} + +macro_rules! impl_fmt { + ($name:ident) => { + impl fmt::$name for StyledObject { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut reset = false; + if self.style.force.unwrap_or_else(colors_enabled) { + if let Some(fg) = self.style.fg { + write!(f, "\x1b[{}m", fg.ansi_num() + 30)?; + reset = true; + } + if let Some(bg) = self.style.bg { + write!(f, "\x1b[{}m", bg.ansi_num() + 40)?; + reset = true; + } + for attr in &self.style.attrs { + write!(f, "\x1b[{}m", attr.ansi_num())?; + reset = true; + } + } + fmt::$name::fmt(&self.val, f)?; + if reset { + write!(f, "\x1b[0m")?; + } + Ok(()) + } + } + } +} + +impl_fmt!(Binary); +impl_fmt!(Debug); +impl_fmt!(Display); +impl_fmt!(LowerExp); +impl_fmt!(LowerHex); +impl_fmt!(Octal); +impl_fmt!(Pointer); +impl_fmt!(UpperExp); +impl_fmt!(UpperHex); + + +#[test] +fn test_text_width() { + let s = style("foo").red().on_black().bold().force_styling(true).to_string(); + assert_eq!(measure_text_width(&s), 3); +} diff --git a/src/windows_term.rs b/src/windows_term.rs new file mode 100644 index 00000000..c8bd3f23 --- /dev/null +++ b/src/windows_term.rs @@ -0,0 +1,81 @@ +use std::io; +use std::mem; +use std::os::windows::io::{RawHandle, AsRawHandle}; + +use winapi::{CHAR, DWORD, HANDLE, STD_OUTPUT_HANDLE, + CONSOLE_SCREEN_BUFFER_INFO, COORD}; +use kernel32::{GetConsoleScreenBufferInfo, + GetConsoleMode, SetConsoleCursorPosition, + FillConsoleOutputCharacterA}; + +use term::Term; + +pub const DEFAULT_WIDTH: u16 = 79; + + +pub fn is_a_terminal(out: &Term) -> bool { + unsafe { + let mut tmp = 0; + GetConsoleMode(out.as_raw_handle(), &mut tmp) != 0 + } +} + +pub fn terminal_size() -> Option<(u16, u16)> { + let hand = STD_OUTPUT_HANDLE as RawHandle; + if let Some((_, csbi)) = get_console_screen_buffer_info(hand) { + Some(((csbi.srWindow.Right - csbi.srWindow.Left) as u16, + (csbi.srWindow.Bottom - csbi.srWindow.Top) as u16)) + } else { + None + } +} + +pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { + if let Some((hand, csbi)) = get_console_screen_buffer_info(out.as_raw_handle()) { + unsafe { + SetConsoleCursorPosition(hand, COORD { + X: 0, + Y: csbi.dwCursorPosition.Y - n as i16, + }); + } + } + Ok(()) +} + +pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { + if let Some((hand, csbi)) = get_console_screen_buffer_info(out.as_raw_handle()) { + unsafe { + SetConsoleCursorPosition(hand, COORD { + X: 0, + Y: csbi.dwCursorPosition.Y + n as i16, + }); + } + } + Ok(()) +} + +pub fn clear_line(out: &Term) -> io::Result<()> { + if let Some((hand, csbi)) = get_console_screen_buffer_info(out.as_raw_handle()) { + unsafe { + let width = csbi.srWindow.Right - csbi.srWindow.Left; + let pos = COORD { + X: 0, + Y: csbi.dwCursorPosition.Y, + }; + let mut written = 0; + FillConsoleOutputCharacterA(hand, b' ' as CHAR, + width as DWORD, pos, &mut written); + } + } + Ok(()) +} + +fn get_console_screen_buffer_info(hand: HANDLE) + -> Option<(HANDLE, CONSOLE_SCREEN_BUFFER_INFO)> +{ + let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() }; + match unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) } { + 0 => None, + _ => Some((hand, csbi)), + } +}