use std::{fmt, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}}; type Fd = libc::c_int; static VERBOSITY: AtomicBool = AtomicBool::new(false); macro_rules! vrb { ( $( $e:expr ),* ) => {( if VERBOSITY.load(Ordering::SeqCst) { println!( $( $e ),* ) } )} } const PALETTE_SIZE: usize = 16; const PALETTE_BYTES: usize = PALETTE_SIZE * 3; // 16 * sizeof(int) const RAW_COLEXPR_SIZE: usize = 6; // e. g. 0xBADF00 type RawPalette<'a> = [&'a str; PALETTE_SIZE]; const KDGKBTYPE: libc::c_int = 0x4b33; /* kd.h */ const PIO_CMAP: libc::c_int = 0x00004B71; /* kd.h */ const KB_101: libc::c_char = 0x0002; /* kd.h */ const O_NOCTTY: libc::c_int = 0o0400; /* fcntl.h */ #[derive(Debug)] enum Color { Black(bool), Red(bool), Green(bool), Yellow(bool), Blue(bool), Magenta(bool), Cyan(bool), White(bool), } impl Color { fn of_value(val: u8) -> Color { match val { 0x00_u8 => Color::Black(false), 0x01_u8 => Color::Red(false), 0x02_u8 => Color::Green(false), 0x03_u8 => Color::Yellow(false), 0x04_u8 => Color::Blue(false), 0x05_u8 => Color::Magenta(false), 0x06_u8 => Color::Cyan(false), 0x07_u8 => Color::White(false), 0x08_u8 => Color::Black(true), 0x09_u8 => Color::Red(true), 0x0a_u8 => Color::Green(true), 0x0b_u8 => Color::Yellow(true), 0x0c_u8 => Color::Blue(true), 0x0d_u8 => Color::Magenta(true), 0x0e_u8 => Color::Cyan(true), 0x0f_u8 => Color::White(true), _ => panic!("invalid color value: {}", val), } } fn format_brightness(b: bool, s: &str) -> String { if b { "bright ".to_string() + s } else { s.to_string() } } } /* [impl Color] */ impl fmt::Display for Color { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let c = match *self { Color::Black(b) => Color::format_brightness(b, "black"), Color::Red(b) => Color::format_brightness(b, "red"), Color::Green(b) => Color::format_brightness(b, "green"), Color::Yellow(b) => Color::format_brightness(b, "yellow"), Color::Blue(b) => Color::format_brightness(b, "blue"), Color::Magenta(b) => Color::format_brightness(b, "magenta"), Color::Cyan(b) => Color::format_brightness(b, "cyan"), Color::White(b) => Color::format_brightness(b, "white"), }; write!(f, "{}", c) } } /* [impl fmt::Display for Color] */ #[derive(Debug)] enum Scheme { Default, SolarizedDark, SolarizedLight, Custom(Option), } impl<'a> fmt::Display for Scheme { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Scheme::Default => write!(f, "default"), Scheme::SolarizedDark => write!(f, "solarized_dark"), Scheme::SolarizedLight => write!(f, "solarized_light"), Scheme::Custom(None) => write!(f, ""), Scheme::Custom(Some(fname)) => write!(f, "{}", fname.display()), } } } /* [impl fmt::String for Scheme] */ /* struct Job -- Runtime parameters. */ #[derive(Debug)] struct Job { scheme: Scheme, /* The color scheme to switch to. */ } impl<'a> Job { pub fn from_argv() -> Job { use clap::{App, Arg}; let matches = App::new(clap::crate_name!()) .version(clap::crate_version!()) .author(clap::crate_authors!()) .about(clap::crate_description!()) .arg( Arg::with_name("scheme") .short("s") .long("scheme") .value_name("NAME") .help("predefined color scheme") .takes_value(true), ) .arg( Arg::with_name("dump") .short("d") .long("dump") .value_name("NAME") .help("dump predefined scheme") .takes_value(true), ) .arg( Arg::with_name("file") .short("f") .long("file") .value_name("PATH") .help("apply scheme from file") .takes_value(true), ) .arg( Arg::with_name("verbose") .short("v") .long("verbose") .help("enable extra diagnostics") .takes_value(false), ) .arg( Arg::with_name("list") .short("l") .long("list") .help("list available color schemes") .takes_value(false), ) .arg( Arg::with_name("help") .short("h") .long("help") .help("print this message") .takes_value(false), ) .get_matches(); if matches.is_present("v") { VERBOSITY.store(true, Ordering::SeqCst); } if matches.is_present("l") { Job::schemes(); std::process::exit(0); }; if let Some(name) = matches.value_of("dump") { let scm = Job::pick_scheme(name); Job::dump(scm); std::process::exit(0); } let scheme = match matches.value_of("file") { Some("-") => Job::scheme_from_stdin(), Some(fname) => Scheme::Custom(Some(PathBuf::from(fname))), None => match matches.value_of("scheme") { Some("-") | None => Job::scheme_from_stdin(), Some(name) => Job::pick_scheme(name), }, }; Job { scheme } } fn pick_scheme(name: &str) -> Scheme { match name { "solarized" | "solarized_dark" | "sd" => Scheme::SolarizedDark, "solarized_light" | "sl" => Scheme::SolarizedLight, "default" | "normal" => Scheme::Default, _any => Scheme::Custom(Some(PathBuf::from(name))), } } fn scheme_from_stdin() -> Scheme { Scheme::Custom(None) } fn schemes() { println!("Available color schemes:"); println!(" · solarized_dark"); println!(" · solarized_light"); println!(" · default"); } fn dump(scm: Scheme) { vrb!("Dumping color scheme {}", scm); match scm { Scheme::Default => Job::dump_scheme(&DEFAULT_COLORS), Scheme::SolarizedDark => Job::dump_scheme(&SOLARIZED_COLORS_DARK), Scheme::SolarizedLight => Job::dump_scheme(&SOLARIZED_COLORS_LIGHT), Scheme::Custom(None) => Job::dump_palette(Palette::from_stdin()), Scheme::Custom(Some(fname)) => Job::dump_palette(Palette::from_file(&fname)), } } fn dump_scheme(colors: &[&str; PALETTE_SIZE]) { let pal: Palette = Palette::new(colors); pal.dump() } fn dump_palette(pal: Palette) { pal.dump() } } /* [impl Job] */ /* Rust appears to come with two wrappers for ``ioctl(2)``, but neither can be utilized for our * purposes. The one in ``sys`` is part of a private (seriously‽) whereas the one in the * ``libc`` module is defined as taking variable arguments and therefore cannot be called from * Rust. Wrapping C is still a bit awkward, as it seems. */ extern "C" { pub fn ioctl( d: libc::c_int, request: libc::c_int, data: *mut libc::c_void, ) -> libc::c_int; } static CONSOLE_PATHS: [&str; 6] = [ "/proc/self/fd/0", "/dev/tty", "/dev/tty0", "/dev/vc/0", "/dev/systty", "/dev/console", ]; static DEFAULT_COLORS: RawPalette = [ "000000", "aa0000", "00aa00", "aa5500", "0000aa", "aa00aa", "00aaaa", "aaaaaa", "555555", "ff5555", "55ff55", "ffff55", "5555ff", "ff55ff", "55ffff", "ffffff", ]; static SOLARIZED_COLORS_DARK: RawPalette = [ "002b36", "dc322f", "859900", "b58900", "268bd2", "d33682", "2aa198", "eee8d5", "002b36", "cb4b16", "586e75", "657b83", "839496", "6c71c4", "93a1a1", "fdf6e3", ]; static SOLARIZED_COLORS_LIGHT: RawPalette = [ "eee8d5", "dc322f", "859900", "b58900", "268bd2", "d33682", "2aa198", "073642", "fdf6e3", "cb4b16", "93a1a1", "839496", "657b83", "6c71c4", "586e75", "002b36", ]; static DUMMY_COLORS: RawPalette = [ "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", "000000", "ffffff", ]; pub struct Palette { colors: [u8; PALETTE_BYTES], } impl Palette { fn dump(&self) { let mut buf: [u8; 3] = [0u8, 0u8, 0u8]; for (i, col) in self.colors.iter().enumerate() { let idx: usize = i % 3; buf[idx] = *col; if idx == 2 { println!( "{:>15} => 0x{:02.X}{:02.X}{:02.X}", Color::of_value((i / 3) as u8).to_string(), buf[0], buf[1], buf[2] ); } } } } /* [impl Palette] */ fn nibble_of_char(chr: u8) -> u8 { match chr { b'0'..=b'9' => chr - b'0', b'a'..=b'f' => chr - b'a' + 10, b'A'..=b'F' => chr - b'A' + 10, _ => 0, } } macro_rules! byte_of_hex { ($ar:ident, $off:expr) => { (nibble_of_char($ar[$off])) << 4 | nibble_of_char($ar[$off + 1]) as u8 }; } fn rgb_of_hex_triplet(def: &str) -> (u8, u8, u8) { let bytes = def.as_bytes(); let r: u8 = byte_of_hex!(bytes, 0); let g: u8 = byte_of_hex!(bytes, 2); let b: u8 = byte_of_hex!(bytes, 4); (r, g, b) } impl Palette { pub fn new(colors: &[&str; PALETTE_SIZE]) -> Palette { let mut idx: usize = 0; let mut pal: [u8; PALETTE_BYTES] = [0; PALETTE_BYTES]; for def in colors.iter() { let (r, g, b) = rgb_of_hex_triplet(*def); pal[idx] = r; pal[idx + 1] = g; pal[idx + 2] = b; //println!(">> {} -> {:X} {:X} {:X}", def, r, g, b); idx += 3; } Palette { colors: pal } } /* [Palette::new] */ pub fn dummy() -> Palette { Palette::new(&DUMMY_COLORS) } /* [Palette::dummy] */ pub fn from_buffered_reader(reader: &mut dyn std::io::BufRead) -> Palette { let mut pal_idx: usize = 0; let mut pal: [u8; PALETTE_BYTES] = [0; PALETTE_BYTES]; let mut line: String = String::new(); while reader.read_line(&mut line).is_ok() { let len = line.len(); if len == 0 { break; } else if len >= 8 { if let Some(off) = line.find('#') { if off != 0 { /* Palette index specified, number prepended */ let parse_res: Result = std::str::FromStr::from_str(&line[0..off]); if let Ok(new_idx) = parse_res { if new_idx < PALETTE_SIZE { pal_idx = new_idx * 3; } } } let off = off + 1; if off > len - 6 { /* no room left for color definition after '#' char */ panic!("invalid color definition: {}", line); } let col = &line[off..(off + RAW_COLEXPR_SIZE)]; let (r, g, b) = rgb_of_hex_triplet(col); pal[pal_idx] = r; pal[pal_idx + 1] = g; pal[pal_idx + 2] = b; pal_idx = (pal_idx + 3) % PALETTE_BYTES; } } line.truncate(0); } Palette { colors: pal } } /* [Palette::from_buffered_reader] */ pub fn from_file(fname: &Path) -> Palette { /* Check if file exists */ let file = match std::fs::File::open(&fname) { Err(e) => panic!("failed to open {} as file ({})", fname.display(), e), Ok(f) => f, }; let mut reader = std::io::BufReader::new(file); /* Parse scheme file */ Palette::from_buffered_reader(&mut reader) } /* [Palette::from_file] */ pub fn from_stdin() -> Palette { vrb!("Go ahead, type your color scheme …"); vrb!("vtcol>"); let mut reader = std::io::BufReader::new(std::io::stdin()); /* Parse scheme file */ Palette::from_buffered_reader(&mut reader) } /* [Palette::from_stdin] */ } /* [impl Palette] */ impl fmt::Display for Palette { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut i = 0; while i < PALETTE_BYTES { let _ = write!(f, "{}", if i == 0 { "(" } else { "\n " }); let r = self.colors[i]; let g = self.colors[i + 1]; let b = self.colors[i + 2]; let _ = write!( f, "((r 0x{:02.X}) (g 0x{:02.X}) (b 0x{:02.x}))", r, g, b ); i += 3; } writeln!(f, ")") } } /* [impl fmt::Display for Palette] */ impl fmt::Debug for Palette { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut i: u8 = 0_u8; while (i as usize) < PALETTE_BYTES { let r = self.colors[i as usize]; let g = self.colors[i as usize + 1]; let b = self.colors[i as usize + 2]; let _ = writeln!( f, "{} => 0x{:02.X}{:02.X}{:02.X}", Color::of_value(i), r, g, b ); i += 3_u8; } std::result::Result::Ok(()) } } /* [impl fmt::Debug for Palette] */ fn fd_of_path(path: &std::path::Path) -> Option { let p = std::ffi::CString::new(path.to_str().unwrap()).unwrap(); match unsafe { libc::open(p.as_ptr(), libc::O_RDWR | O_NOCTTY, 0) } { -1 => None, fd => { vrb!(" *> got fd"); if unsafe { libc::isatty(fd) } == 0 { vrb!(" *> not a tty"); return None; } let mut tty_type: libc::c_char = 0; let res = unsafe { ioctl( fd, KDGKBTYPE as libc::c_int, std::mem::transmute(&mut tty_type), ) }; if res < 0 { vrb!(" *> ioctl failed"); return None; } if tty_type != KB_101 { return None; } Some(fd) }, } } fn get_console_fd(path: Option<&str>) -> Option { match path { Some(path) => { let path = std::path::Path::new(path); match fd_of_path(path) { Some(fd) => Some(fd), None => panic!("cannot open {:?} as a tty", path), } }, None => { for path in CONSOLE_PATHS.iter() { vrb!("trying path: {:?}", path); let path = std::path::Path::new(path); if let Some(fd) = fd_of_path(path) { vrb!(" * Success!"); return Some(fd); } } vrb!("could not retrieve fd for any of the search paths"); None }, } } fn write_to_term(fd: Fd, buf: &str) { let len = buf.len() as libc::size_t; let raw = std::ffi::CString::new(buf.as_bytes()).unwrap(); unsafe { libc::write(fd, raw.as_ptr() as *const libc::c_void, len) }; } fn clear_term(fd: Fd) { let clear: &str = "\x1b[2J"; let cursor: &str = "\x1b[1;1H"; write_to_term(fd, clear); write_to_term(fd, cursor); } fn main() { let job = Job::from_argv(); vrb!("job parms: {:?}", job); let mut pal: Palette = match job.scheme { Scheme::Default => Palette::new(&DEFAULT_COLORS), Scheme::SolarizedDark => Palette::new(&SOLARIZED_COLORS_DARK), Scheme::SolarizedLight => Palette::new(&SOLARIZED_COLORS_LIGHT), Scheme::Custom(None) => Palette::from_stdin(), Scheme::Custom(Some(ref fname)) => Palette::from_file(fname), }; vrb!("Using palette:"); vrb!("{}", pal); let fd = get_console_fd(None).unwrap(); vrb!("fd: {}", fd); if unsafe { ioctl(fd, PIO_CMAP, std::mem::transmute(&mut pal)) } < 0 { panic!("PIO_CMAP, ioctl failed to insert new palette") } clear_term(fd); vrb!("terminated from job {:?}", job); }