a module for terminal colors and escape sequences

This commit is contained in:
Bruce Leidl 2019-04-02 14:57:29 -04:00
parent 43800cdc6e
commit 4bd8c3626f
6 changed files with 1742 additions and 0 deletions

View File

@ -0,0 +1,241 @@
use crate::Result;
use crate::terminal::{RawTerminal, Color, Base16Scheme};
use std::io::{self,Read,Write,Stdout};
pub struct AnsiControl(String);
impl AnsiControl {
const ESC: char = '\x1B';
const CSI: char = '[';
const OSC: char = ']';
const ST: char = '\\';
pub fn new() -> Self {
AnsiControl(String::new())
}
pub fn osc(n: u32) -> Self {
AnsiControl::new()
.push(AnsiControl::ESC)
.push(AnsiControl::OSC)
.num(n)
}
pub fn csi() -> Self {
AnsiControl::new()
.push(AnsiControl::ESC)
.push(AnsiControl::CSI)
}
pub fn bold() -> Self {
AnsiControl::csi().push_str("1m")
}
pub fn unbold() -> Self {
AnsiControl::csi().push_str("22m")
}
pub fn clear() -> Self {
AnsiControl::csi().push_str("2J")
}
pub fn goto(x: u16, y: u16) -> Self {
AnsiControl::csi().push_str(x.to_string()).push(';').push_str(y.to_string()).push('H')
}
pub fn set_window_title<S: AsRef<str>>(title: S) -> AnsiControl {
// AnsiControl::osc(2).sep().push_str(title.as_ref()).st()
AnsiControl::osc(0).sep().push_str(title.as_ref()).st()
}
pub fn window_title_push_stack() -> AnsiControl {
AnsiControl::csi().push_str("22;2t")
}
pub fn window_title_pop_stack() -> AnsiControl {
AnsiControl::csi().push_str("23;2t")
}
pub fn sep(self) -> Self {
self.push(';')
}
pub fn color(self, color: Color) -> Self {
self.push_str(color.to_string())
}
pub fn num(self, n: u32) -> Self {
self.push_str(n.to_string())
}
pub fn st(self) -> Self {
self.push(AnsiControl::ESC).push(AnsiControl::ST)
}
pub fn push_str<S: AsRef<str>>(mut self, s: S) -> Self {
self.0.push_str(s.as_ref());
self
}
pub fn push(mut self, c: char) -> Self {
self.0.push(c);
self
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn as_str(&self) -> &str {
&self.0
}
fn parse_color_response(s: &str) -> Result<Vec<(u32, Color)>> {
let prefix = AnsiControl::osc(4).sep();
let suffix = AnsiControl::new().st();
let mut res = Vec::new();
let mut ptr = s;
while ptr.starts_with(prefix.as_str()) {
let s = ptr.trim_start_matches(prefix.as_str());
let offset = match s.find(suffix.as_str()) {
Some(idx) => idx,
None => bail!(":("),
};
let (elem, s) = s.split_at(offset);
res.push(AnsiControl::parse_idx_color_pair(elem)?);
ptr = s.trim_start_matches(suffix.as_str());
}
Ok(res)
}
fn parse_idx_color_pair(s: &str) -> Result<(u32, Color)> {
let v = s.split(";").collect::<Vec<_>>();
if v.len() != 2 {
bail!("bad elem {}", s);
}
let idx = v[0].parse::<u32>()?;
let color = Color::parse(&v[1])?;
Ok((idx, color))
}
pub fn write_stdout(&self) -> Result<()> {
self.write_to(io::stdout())
}
pub fn print(&self) {
io::stdout().write_all(self.as_bytes()).unwrap();
io::stdout().flush().unwrap();
}
pub fn write_to<W: Write>(&self, mut writer: W) -> Result<()> {
writer.write_all(self.as_bytes())?;
writer.flush()?;
Ok(())
}
}
pub struct AnsiTerminal {
raw: RawTerminal<Stdout>,
}
impl AnsiTerminal {
pub fn new() -> Result<Self> {
let raw = RawTerminal::create(io::stdout())?;
Ok(AnsiTerminal{ raw })
}
pub fn clear_screen(&mut self) -> Result<()> {
self.write_code(AnsiControl::clear())?;
self.write_code(AnsiControl::goto(1,1))
}
pub fn set_window_title(&mut self, title: &str) -> Result<()> {
AnsiControl::set_window_title(title).write_to(&mut self.raw)
}
pub fn set_palette_color(&mut self, idx: u32, color: Color) -> Result<()> {
self.write_code(AnsiControl::osc(4)
.sep().num(idx).sep()
.color(color)
.st())
}
pub fn set_palette_bg(&mut self, color: Color) -> Result<()> {
self.write_code(AnsiControl::osc(11).sep().color(color).st())
}
pub fn set_palette_fg(&mut self, color: Color) -> Result<()> {
self.write_code(AnsiControl::osc(10).sep().color(color).st())
}
pub fn read_palette_bg(&mut self) -> Result<Color> {
let prefix = AnsiControl::osc(11).sep();
let suffix = AnsiControl::new().st();
self.write_code(AnsiControl::osc(11).sep().push('?').st())?;
let response = self.read_response()?;
let color = Color::parse(response.trim_start_matches(prefix.as_str()).trim_end_matches(suffix.as_str()))?;
Ok(color)
}
pub fn read_palette_fg(&mut self) -> Result<Color> {
let prefix = AnsiControl::osc(10).sep();
let suffix = AnsiControl::new().st();
self.write_code(AnsiControl::osc(10).sep().push('?').st())?;
let response = self.read_response()?;
let color = Color::parse(response.trim_start_matches(prefix.as_str()).trim_end_matches(suffix.as_str()))?;
Ok(color)
}
pub fn read_palette_color(&mut self, idx: u32) -> Result<Color> {
let colors = self.read_palette_colors(&[idx])?;
Ok(colors[0])
}
pub fn read_palette_colors(&mut self, idxs: &[u32]) -> Result<Vec<Color>> {
let mut ansi = AnsiControl::osc(4);
for idx in idxs {
ansi = ansi.sep().num(*idx).sep().push('?');
}
self.write_code(ansi.st())?;
let response = self.read_response()?;
let parsed = AnsiControl::parse_color_response(&response)?;
if !parsed.iter().zip(idxs).all(|(a,&b)| a.0 == b) {
bail!("color index does not have expected value");
}
Ok(parsed.iter().map(|&(_,c)| c).collect())
}
fn write_code(&mut self, sequence: AnsiControl) -> Result<()> {
self.raw.write_all(sequence.as_bytes())?;
self.raw.flush()?;
Ok(())
}
fn read_response(&mut self) -> Result<String> {
let stdin = io::stdin();
let mut input = stdin.lock();
let mut buffer = Vec::new();
input.read_to_end(&mut buffer)?;
let s = String::from_utf8(buffer)?;
Ok(s)
}
pub fn apply_base16(&mut self, base16: &Base16Scheme) -> Result<()> {
self.set_palette_fg(base16.terminal_foreground())?;
self.set_palette_bg(base16.terminal_background())?;
for i in 0..22 {
self.set_palette_color(i, base16.terminal_palette_color(i as usize))?;
}
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,118 @@
use std::fs;
use std::path::Path;
use crate::terminal::Base16Scheme;
use crate::Result;
const TEMPLATE: &str = r##"
if [ -n "$TMUX" ]; then
# Tell tmux to pass the escape sequences through
# (Source: http://permalink.gmane.org/gmane.comp.terminal-emulators.tmux.user/1324)
put_template() { printf '\033Ptmux;\033\033]4;%d;rgb:%s\033\033\\\033\\' $@; }
put_template_var() { printf '\033Ptmux;\033\033]%d;rgb:%s\033\033\\\033\\' $@; }
put_template_custom() { printf '\033Ptmux;\033\033]%s%s\033\033\\\033\\' $@; }
elif [ "${TERM%%[-.]*}" = "screen" ]; then
# GNU screen (screen, screen-256color, screen-256color-bce)
put_template() { printf '\033P\033]4;%d;rgb:%s\007\033\\' $@; }
put_template_var() { printf '\033P\033]%d;rgb:%s\007\033\\' $@; }
put_template_custom() { printf '\033P\033]%s%s\007\033\\' $@; }
elif [ "${TERM%%-*}" = "linux" ]; then
put_template() { [ $1 -lt 16 ] && printf "\e]P%x%s" $1 $(echo $2 | sed 's/\///g'); }
put_template_var() { true; }
put_template_custom() { true; }
else
put_template() { printf '\033]4;%d;rgb:%s\033\\' $@; }
put_template_var() { printf '\033]%d;rgb:%s\033\\' $@; }
put_template_custom() { printf '\033]%s%s\033\\' $@; }
fi
# 16 color space
put_template 0 $color00
put_template 1 $color01
put_template 2 $color02
put_template 3 $color03
put_template 4 $color04
put_template 5 $color05
put_template 6 $color06
put_template 7 $color07
put_template 8 $color08
put_template 9 $color09
put_template 10 $color10
put_template 11 $color11
put_template 12 $color12
put_template 13 $color13
put_template 14 $color14
put_template 15 $color15
# 256 color space
put_template 16 $color16
put_template 17 $color17
put_template 18 $color18
put_template 19 $color19
put_template 20 $color20
put_template 21 $color21
put_template_var 10 $color_foreground
put_template_var 11 $color_background
put_template_custom 12 ";7" # cursor (reverse video)
# clean up
unset -f put_template
unset -f put_template_var
unset -f put_template_custom
"##;
pub struct Base16Shell {
scheme: Base16Scheme,
output: String,
}
impl Base16Shell {
pub fn write_script<P: AsRef<Path>>(path: P, scheme: &Base16Scheme) -> Result<()> {
let output = Base16Shell::new(scheme.clone()).build();
fs::write(path.as_ref(), output)?;
Ok(())
}
fn new(scheme: Base16Scheme) -> Self {
Base16Shell{ scheme, output: TEMPLATE.to_string() }
}
fn build(self) -> String {
self.color("$color_foreground", 5)
.color("$color_background", 0)
.color("$color00", 0x0)
.color("$color01", 0x8)
.color("$color02", 0xB)
.color("$color03", 0xA)
.color("$color04", 0xD)
.color("$color05", 0xE)
.color("$color06", 0xC)
.color("$color07", 0x5)
.color("$color08", 0x3)
.color("$color09", 0x8)
.color("$color10", 0xB)
.color("$color11", 0xA)
.color("$color12", 0xD)
.color("$color13", 0xE)
.color("$color14", 0xC)
.color("$color15", 0x7)
.color("$color16", 0x9)
.color("$color17", 0xF)
.color("$color18", 0x1)
.color("$color19", 0x2)
.color("$color20", 0x4)
.color("$color21", 0x6)
.output.clone()
}
fn color_str(&self, idx: usize) -> String {
let (r,g,b) = self.scheme.color(idx).rgb();
format!("{:02x}/{:02x}/{:02x}", r, g, b)
}
fn color(mut self, tag: &str, idx: usize) -> Self {
self.output = self.output.replace(tag, &self.color_str(idx));
self
}
}

View File

@ -0,0 +1,97 @@
use std::fmt;
use crate::Result;
use crate::terminal::AnsiTerminal;
#[derive(Copy,Clone,Default,Debug)]
pub struct Color(u16,u16,u16);
impl Color {
pub fn new(r: u16, g: u16, b: u16) -> Color {
Color(r, g, b)
}
pub fn parse(s: &str) -> Result<Color> {
if s.starts_with("rgb:") {
let v = s.trim_start_matches("rgb:").split("/").collect::<Vec<_>>();
if v.len() == 3 {
let r = u16::from_str_radix(&v[0], 16)?;
let g = u16::from_str_radix(&v[1], 16)?;
let b = u16::from_str_radix(&v[2], 16)?;
return Ok(Color(r, g, b))
}
}
Err(format_err!("Cannot parse '{}'", s))
}
pub fn rgb(&self) -> (u16,u16,u16) {
(self.0, self.1, self.2)
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.0 > 0xFF || self.1 > 0xFF || self.2 > 0xFF {
write!(f, "rgb:{:04x}/{:04x}/{:04x}", self.0, self.1, self.2)
} else {
write!(f, "rgb:{:02x}/{:02x}/{:02x}", self.0, self.1, self.2)
}
}
}
#[derive(Default,Clone)]
pub struct TerminalPalette {
bg: Color,
fg: Color,
palette: [Color; 22],
}
impl TerminalPalette {
pub fn set_background(&mut self, color: Color) {
self.bg = color;
}
pub fn set_foreground(&mut self, color: Color) {
self.fg = color;
}
pub fn set_palette_color(&mut self, idx: usize, color: Color) {
self.palette[idx] = color;
}
pub fn palette_color(&self, idx: usize) -> Color {
self.palette[idx]
}
pub fn background(&self) -> Color {
self.bg
}
pub fn foreground(&self) -> Color {
self.fg
}
pub fn apply(&self, terminal: &mut AnsiTerminal) -> Result<()> {
terminal.set_palette_fg(self.fg)?;
terminal.set_palette_bg(self.bg)?;
for i in 0..self.palette.len() {
terminal.set_palette_color(i as u32, self.palette[i])?;
}
Ok(())
}
pub fn load(&mut self, terminal: &mut AnsiTerminal) -> Result<()> {
self.bg = terminal.read_palette_bg()?;
self.fg = terminal.read_palette_fg()?;
let idxs = (0..22).collect::<Vec<_>>();
let colors = terminal.read_palette_colors(&idxs)?;
for i in 0..self.palette.len() {
self.palette[i] = colors[i];
}
Ok(())
}
}

View File

@ -0,0 +1,12 @@
mod base16;
mod base16_shell;
mod ansi;
mod raw;
mod color;
pub use self::raw::RawTerminal;
pub use self::base16::Base16Scheme;
pub use self::color::{Color,TerminalPalette};
pub use self::ansi::{AnsiTerminal,AnsiControl};
pub use self::base16_shell::Base16Shell;

View File

@ -0,0 +1,94 @@
use std::mem;
use std::io::{self,Write};
use libc::c_int;
pub use libc::termios as Termios;
use crate::Result;
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();
if tcgetattr(0, &mut termios) == -1 {
return Err(io::Error::last_os_error())
}
Ok(termios)
}
}
fn set_terminal_attr(termios: &Termios) -> io::Result<()> {
extern "C" {
pub fn tcsetattr(fd: c_int, opt: c_int, termptr: *const Termios) -> c_int;
}
unsafe {
if tcsetattr(0, 0, termios) == -1 {
return Err(io::Error::last_os_error())
}
Ok(())
}
}
fn raw_terminal_attr(termios: &mut Termios) {
extern "C" {
pub fn cfmakeraw(termptr: *mut Termios);
}
unsafe { cfmakeraw(termios) }
}
pub struct RawTerminal<W: Write> {
output: W,
prev_ios: Termios,
raw_ios: Termios,
}
impl <W: Write> RawTerminal<W> {
pub fn raw_terminal_attr() -> Result<Termios> {
let mut ios = get_terminal_attr()?;
raw_terminal_attr(&mut ios);
ios.c_cc[libc::VMIN] = 0;
ios.c_cc[libc::VTIME] = 1;
Ok(ios)
}
pub fn create_with_termios(output: W, raw_ios: Termios) -> Result<Self> {
let prev_ios = get_terminal_attr()?;
set_terminal_attr(&raw_ios)?;
Ok(RawTerminal{ output, prev_ios, raw_ios })
}
pub fn create(output: W) -> Result<Self> {
let raw_ios = RawTerminal::<W>::raw_terminal_attr()?;
RawTerminal::create_with_termios(output, raw_ios)
}
pub fn suspend_raw_mode(&self) -> Result<()> {
set_terminal_attr(&self.prev_ios)?;
Ok(())
}
pub fn activate_raw_mode(&self) -> Result<()> {
set_terminal_attr(&self.raw_ios)?;
Ok(())
}
}
impl <W: Write> Drop for RawTerminal<W> {
fn drop(&mut self) {
self.suspend_raw_mode().unwrap()
}
}
impl<W: Write> Write for RawTerminal<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.output.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.output.flush()
}
}