termit-ui 0.0.1

Terminal UI with GUI-like layouts
Documentation
use crate::color::Color;
use std::env;
use std::fmt;
use std::io::{self, Read, Write};
use std::time::{Duration, Instant};
use termion;
use termion::raw::CONTROL_SEQUENCE_TIMEOUT;

pub fn termion_color(
    color: Option<Color>,
    foreground: bool,
    support: ColorSupport,
) -> TermionColor {
    TermionColor {
        foreground,
        color,
        support,
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ColorSupport {
    None,
    Mono,
    Ansi3bit,
    Ansi4bit,
    Ansi8bit,
    RGB24bit,
}

impl Default for ColorSupport {
    fn default() -> ColorSupport {
        ColorSupport::None
    }
}

#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct TermionColor {
    pub color: Option<Color>,
    pub support: ColorSupport,
    pub foreground: bool,
}

impl fmt::Display for TermionColor {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        use self::ColorSupport::*;
        if let Some(Color {
            red: ref r,
            green: ref g,
            blue: ref b,
        }) = self.color
        {
            // handle setting the color
            match (self.foreground, self.support) {
                (_, None) => {
                    Ok((/* ignore colors */))
                }
                (true, Mono) => {
                    if rgb_to_1bit(r, g, b) {
                        termion::color::Fg(termion::color::White).fmt(f)
                    } else {
                        termion::color::Fg(termion::color::Black).fmt(f)
                    }
                }
                (false, Mono) => {
                    if rgb_to_1bit(r, g, b) {
                        termion::color::Bg(termion::color::White).fmt(f)
                    } else {
                        termion::color::Bg(termion::color::Black).fmt(f)
                    }
                }
                (true, Ansi3bit) => {
                    termion::color::Fg(termion::color::AnsiValue(rgb_to_3bit(r, g, b))).fmt(f)
                }
                (false, Ansi3bit) => {
                    termion::color::Bg(termion::color::AnsiValue(rgb_to_3bit(r, g, b))).fmt(f)
                }
                (true, Ansi4bit) => {
                    termion::color::Fg(termion::color::AnsiValue(rgb_to_4bit(r, g, b))).fmt(f)
                }
                (false, Ansi4bit) => {
                    termion::color::Bg(termion::color::AnsiValue(rgb_to_4bit(r, g, b))).fmt(f)
                }
                (true, Ansi8bit) => {
                    termion::color::Fg(termion::color::AnsiValue(rgb_to_ansi(r, g, b))).fmt(f)
                }
                (false, Ansi8bit) => {
                    termion::color::Bg(termion::color::AnsiValue(rgb_to_ansi(r, g, b))).fmt(f)
                }
                (true, RGB24bit) => termion::color::Fg(termion::color::Rgb(*r, *g, *b)).fmt(f),
                (false, RGB24bit) => termion::color::Bg(termion::color::Rgb(*r, *g, *b)).fmt(f),
            }
        } else {
            // handle reset
            match (self.support, self.foreground) {
                (ColorSupport::None, _) => Ok((/* ignore reset */)),
                (_, true) => termion::color::Fg(termion::color::Reset).fmt(f),
                (_, false) => termion::color::Bg(termion::color::Reset).fmt(f),
            }
        }
    }
}

pub fn get_color_support(input: &mut Read, output: &mut Write) -> io::Result<ColorSupport> {
    // Configuration first - save fuel and let the user drive.

    // is this a color terminal?
    if let Ok(colorterm) = env::var("COLORTERM") {
        if colorterm.eq("truecolor") || colorterm.eq("24bit") {
            return Ok(ColorSupport::RGB24bit);
        } else if colorterm.eq("yes") {
            return Ok(ColorSupport::Ansi8bit);
        }
    }

    // does the terminal report 256 colors?
    if let Ok(term) = env::var("TERM") {
        if term.contains("256color") {
            return Ok(ColorSupport::Ansi8bit);
        }
    }

    // let's try detection
    if detect_color(input, output, 0)? {
        // OSC 4 is supported, detect how many colors there are.
        // Do a binary search of the last supported color.
        let mut min = 0u8;
        let mut max = 0xFFu8;
        let mut i = max;
        while max - min > 1 {
            trace!("color min: {}, max: {}, i: {}", min, max, i);
            if detect_color(input, output, i)? {
                min = i
            } else {
                max = i
            }
            i = min + (max - min) / 2;
        }
        trace!("colors: {}", max);
        // some safe fallbacks
        Ok(match max {
            0 => ColorSupport::None,
            1...6 => ColorSupport::Mono,
            7...14 => ColorSupport::Ansi3bit,
            15...0xFE => ColorSupport::Ansi4bit,
            0xFF => ColorSupport::Ansi8bit,
            _ => unreachable!(),
        })
    } else {
        // safe bet
        Ok(ColorSupport::None)
    }
}

/// Detect a color using OSC 4.
fn detect_color(input: &mut Read, output: &mut Write, color: u8) -> io::Result<bool> {
    // Is the color available?
    // Use `ESC ] 4 ; color ; ? BEL`.
    write!(output, "\x1B]4;{};?\x07", color)?;
    output.flush()?;

    let mut buf: [u8; 1] = [0];
    let mut total_read = 0;

    let timeout = Duration::from_millis(CONTROL_SEQUENCE_TIMEOUT);
    let now = Instant::now();
    let bell = 7u8;

    // Either consume all data up to bell or wait for a timeout.
    while buf[0] != bell && now.elapsed() < timeout {
        total_read += input.read(&mut buf)?;
    }

    // If there was a response, the color is supported.
    Ok(total_read > 0)
}

#[inline]
fn bit(byte: &u8, bit: u8) -> u8 {
    1u8 & (byte >> bit)
}

/// Convert 256*256*256 to monochrome black/white
fn rgb_to_1bit(r: &u8, g: &u8, b: &u8) -> bool {
    (r | g | b) & 8 == 8
}
/// Convert monochrome black/white to 256*256*256 RGB
fn rgb_from_1bit(white: &bool) -> (u8, u8, u8) {
    if *white {
        (128, 128, 128)
    } else {
        (0, 0, 0)
    }
}
/// Convert 256*256*256 RGB to 3bit ANSI color 0-8
fn rgb_to_3bit(r: &u8, g: &u8, b: &u8) -> u8 {
    // extract dull and bright RGB:
    // 0-31 => 0,0
    // 32-127 => 0,1
    // 128-255 => 1,x
    let (r, g, b, r_, g_, b_) = (
        bit(r, 6),
        bit(g, 6),
        bit(b, 6),
        bit(r, 7),
        bit(g, 7),
        bit(b, 7),
    );

    // calculate as close as possible 3 bits (bgr)
    let v: u8 = r | r_ | (g << 1) | (g_ << 1) | (b << 2) | (b_ << 2);

    v
}
fn rgb_from_3bit(c: &u8) -> (u8, u8, u8) {
    match c & 0b111 {
        0 => (0, 0, 0),
        1 => (128, 0, 0),
        2 => (0, 128, 0),
        3 => (128, 128, 0),
        4 => (0, 0, 128),
        5 => (128, 0, 128),
        6 => (0, 128, 128),
        7 => (192, 192, 192),
        _ => unreachable!(),
    }
}
/// Convert 256*256*256 RGB to 4bit ANSI color 0-15
fn rgb_to_4bit(r: &u8, g: &u8, b: &u8) -> u8 {
    // it's white (bright white) only when it's white
    if r & g & b == 0xFF {
        return 15;
    }

    // extract dull and bright RGB:
    // 0-31 => 0,0
    // 32-127 => 0,1
    // 128-255 => 1,x
    let (r, g, b, r_, g_, b_) = (
        bit(r, 6),
        bit(g, 6),
        bit(b, 6),
        bit(r, 7),
        bit(g, 7),
        bit(b, 7),
    );

    // calculate as close as possible 4 bits (_bgr)
    let v: u8 = r | r_ | (g << 1) | (g_ << 1) | (b << 2) | (b_ << 2) | ((r_ | g_ | b_) << 3);

    // handle special cases (bright,blue,green,red)
    match v {
        0b1000 => unreachable!(),
        0b0111 => 8, // grey (light black) special case
        0b1111 => 7, // silver (white) special case
        v => v % 16, // just to be sure!
    }
}
fn rgb_from_4bit(c: &u8) -> (u8, u8, u8) {
    match c & 0b1111 {
        0 => (0, 0, 0),
        1 => (128, 0, 0),
        2 => (0, 128, 0),
        3 => (128, 128, 0),
        4 => (0, 0, 128),
        5 => (128, 0, 128),
        6 => (0, 128, 128),
        7 => (192, 192, 192),
        8 => (128, 128, 128),
        9 => (255, 0, 0),
        10 => (0, 255, 0),
        11 => (255, 255, 0),
        12 => (0, 0, 255),
        13 => (255, 0, 255),
        14 => (0, 255, 255),
        15 => (255, 255, 255),
        _ => unreachable!(),
    }
}

fn c256_to_gray(c: &u8) -> u8 {
    232u8.checked_add(c / 10).unwrap_or(255)
}
fn gray_to_c256(c: &u8) -> u8 {
    8 + (c % 232) * 10
}
fn rgb_to_5bit(r: &u8, g: &u8, b: &u8) -> u8 {
    16 + 36 * (r / 51) + 6 * (g / 51) + (b / 51)
}
fn rgb_from_5bit(c: &u8) -> (u8, u8, u8) {
    ((c / 36) * 0xF, ((c / 6) % 6) * 0xF, (c / 4 & 0x1) * 0xF)
}
fn rgb_to_ansi(r: &u8, g: &u8, b: &u8) -> u8 {
    let c = rgb_to_4bit(r, g, b);
    let gray = c256_to_gray(&r);

    if (*r, *g, *b) == rgb_from_4bit(&c) {
        c
    } else if r == g && g == b {
        gray
    } else {
        rgb_to_5bit(r, g, b)
    }
}