a module for terminal colors and escape sequences
This commit is contained in:
parent
43800cdc6e
commit
4bd8c3626f
241
libcitadel/src/terminal/ansi.rs
Normal file
241
libcitadel/src/terminal/ansi.rs
Normal 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(())
|
||||
|
||||
}
|
||||
|
||||
}
|
1180
libcitadel/src/terminal/base16.rs
Normal file
1180
libcitadel/src/terminal/base16.rs
Normal file
File diff suppressed because it is too large
Load Diff
118
libcitadel/src/terminal/base16_shell.rs
Normal file
118
libcitadel/src/terminal/base16_shell.rs
Normal 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
|
||||
}
|
||||
}
|
97
libcitadel/src/terminal/color.rs
Normal file
97
libcitadel/src/terminal/color.rs
Normal 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(())
|
||||
}
|
||||
|
||||
}
|
12
libcitadel/src/terminal/mod.rs
Normal file
12
libcitadel/src/terminal/mod.rs
Normal 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;
|
94
libcitadel/src/terminal/raw.rs
Normal file
94
libcitadel/src/terminal/raw.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user