Skip to content

Commit

Permalink
Get windows console colors working
Browse files Browse the repository at this point in the history
  • Loading branch information
pksunkara committed Apr 26, 2020
1 parent 5e75ff1 commit 20c740f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 11 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ documentation = "https://docs.rs/console"
readme = "README.md"

[features]
default = ["unicode-width", "ansi-parsing"]
default = ["unicode-width", "ansi-parsing", "windows-console-colors"]
windows-console-colors = ["ansi-parsing", "winapi-util"]
ansi-parsing = ["regex"]

[dependencies]
Expand All @@ -27,5 +28,6 @@ unicode-width = { version = "0.1", optional = true }
termios = "0.3"

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winbase","winuser","consoleapi","processenv","wincon"] }
winapi = { version = "0.3", features = ["winbase", "winuser", "consoleapi", "processenv", "wincon"] }
winapi-util = { version = "0.1", optional = true }
encode_unicode = "0.3"
20 changes: 20 additions & 0 deletions src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,23 @@ fn test_ansi_iter_re() {
assert_eq!(iter.rest_slice(), "");
assert_eq!(iter.next(), None);
}

#[test]
fn test_ansi_iter_re_on_multi() {
use crate::style;
let s = format!("{}", style("a").red().bold().force_styling(true));
let mut iter = AnsiCodeIterator::new(&s);
assert_eq!(iter.next(), Some(("\x1b[31m", true)));
assert_eq!(iter.current_slice(), "\x1b[31m");
assert_eq!(iter.rest_slice(), "\x1b[1ma\x1b[0m");
assert_eq!(iter.next(), Some(("\x1b[1m", true)));
assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1m");
assert_eq!(iter.rest_slice(), "a\x1b[0m");
assert_eq!(iter.next(), Some(("a", false)));
assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1ma");
assert_eq!(iter.rest_slice(), "\x1b[0m");
assert_eq!(iter.next(), Some(("\x1b[0m", true)));
assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1ma\x1b[0m");
assert_eq!(iter.rest_slice(), "");
assert_eq!(iter.next(), None);
}
37 changes: 28 additions & 9 deletions src/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,10 @@ impl Term {
Ok(())
}

#[inline]
/// Checks if the terminal is indeed a terminal.
///
/// This is a shortcut for `features().is_attended()`.
#[inline]
pub fn is_term(&self) -> bool {
self.features().is_attended()
}
Expand Down Expand Up @@ -324,28 +324,28 @@ impl Term {
terminal_size()
}

#[inline]
/// Moves the cursor to `x` and `y`
#[inline]
pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> {
move_cursor_to(self, x, y)
}

#[inline]
/// Moves the cursor up `n` lines
#[inline]
pub fn move_cursor_up(&self, n: usize) -> io::Result<()> {
move_cursor_up(self, n)
}

#[inline]
/// Moves the cursor down `n` lines
#[inline]
pub fn move_cursor_down(&self, n: usize) -> io::Result<()> {
move_cursor_down(self, n)
}

#[inline]
/// Clears the current line.
///
/// The positions the cursor at the beginning of the line again.
#[inline]
pub fn clear_line(&self) -> io::Result<()> {
clear_line(self)
}
Expand All @@ -364,14 +364,14 @@ impl Term {
Ok(())
}

#[inline]
/// Clears the entire screen.
#[inline]
pub fn clear_screen(&self) -> io::Result<()> {
clear_screen(self)
}

#[inline]
/// Clears the last char in the the current line.
#[inline]
pub fn clear_chars(&self, n: usize) -> io::Result<()> {
common_term::clear_chars(self, n)
}
Expand All @@ -384,21 +384,40 @@ impl Term {
set_title(title);
}

#[inline]
/// Makes cursor visible again
#[inline]
pub fn show_cursor(&self) -> io::Result<()> {
show_cursor(self)
}

#[inline]
/// Hides cursor
#[inline]
pub fn hide_cursor(&self) -> io::Result<()> {
hide_cursor(self)
}

// helpers

#[cfg(all(windows, features = "windows-console-colors"))]
fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
if self.features().is_msys_tty() {
self.write_through_common(bytes)
} else {
use winapi_util::console::Console;

match self.inner.target {
TermTarget::Stdout => console_colors(self, Console::stdout(), bytes),
TermTarget::Stderr => console_colors(self, Console::stderr(), bytes),
}
}
}

#[cfg(not(all(windows, features = "windows-console-colors")))]
fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
self.write_through_common(bytes)
}

pub(crate) fn write_through_common(&self, bytes: &[u8]) -> io::Result<()> {
match self.inner.target {
TermTarget::Stdout => {
io::stdout().write_all(bytes)?;
Expand Down
66 changes: 66 additions & 0 deletions src/windows_term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use std::slice;

use encode_unicode::error::InvalidUtf16Tuple;
use encode_unicode::CharExt;
#[cfg(feature = "windows-console-colors")]
use regex::Regex;
use winapi::ctypes::c_void;
use winapi::shared::minwindef::DWORD;
use winapi::shared::minwindef::MAX_PATH;
Expand All @@ -27,11 +29,20 @@ use winapi::um::wincon::{
KEY_EVENT, KEY_EVENT_RECORD,
};
use winapi::um::winnt::{CHAR, HANDLE, INT, WCHAR};
#[cfg(feature = "windows-console-colors")]
use winapi_util::console::{Color, Console, Intense};

use crate::common_term;
use crate::kb::Key;
use crate::term::{Term, TermTarget};

#[cfg(feature = "windows-console-colors")]
lazy_static::lazy_static! {
static ref INTENSE_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)8;5;(8|9|1[0-5])m").unwrap();
static ref NORMAL_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)([0-7])m").unwrap();
static ref ATTR_RE: Regex = Regex::new(r"\x1b\[([1-8])m").unwrap();
}

pub const DEFAULT_WIDTH: u16 = 79;

pub fn as_handle(term: &Term) -> HANDLE {
Expand Down Expand Up @@ -371,4 +382,59 @@ pub fn set_title<T: Display>(title: T) {
}
}

#[cfg(feature = "windows-console-colors")]
pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result<()> {
use crate::ansi::AnsiCodeIterator;
use std::str::from_utf8;

let s = from_utf8(bytes).expect("data to be printed is not an ansi string");
let mut iter = AnsiCodeIterator::new(s);

while !iter.rest_slice().is_empty() {
if let Some((part, is_esc)) = iter.next() {
if !is_esc {
out.write_through_common(part.as_bytes())?;
} else if part == "\x1b[0m" {
con.reset()?;
} else if let Some(cap) = INTENSE_COLOR_RE.captures(part) {
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());

match cap.get(1).unwrap().as_str() {
"3" => con.fg(Intense::Yes, color)?,
"4" => con.bg(Intense::Yes, color)?,
_ => unreachable!(),
};
} else if let Some(cap) = NORMAL_COLOR_RE.captures(part) {
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());

match cap.get(1).unwrap().as_str() {
"3" => con.fg(Intense::No, color)?,
"4" => con.bg(Intense::No, color)?,
_ => unreachable!(),
};
} else if !ATTR_RE.is_match(part) {
out.write_through_common(part.as_bytes())?;
}
}
}

Ok(())
}

#[cfg(feature = "windows-console-colors")]
fn get_color_from_ansi(ansi: &str) -> Color {
match ansi {
"0" | "8" => Color::Black,
"1" | "9" => Color::Red,
"2" | "10" => Color::Green,
"3" | "11" => Color::Yellow,
"4" | "12" => Color::Blue,
"5" | "13" => Color::Magenta,
"6" | "14" => Color::Cyan,
"7" | "15" => Color::White,
_ => unreachable!(),
}
}

// TODO: Might not work on windows console, also clear_chars
pub use crate::common_term::{hide_cursor, show_cursor};

0 comments on commit 20c740f

Please sign in to comment.