pub mod lib; use vtcol::{Palette, Scheme}; use anyhow::{anyhow, Result}; use std::{io::{self, BufWriter, Error}, sync::atomic::{AtomicBool, Ordering}}; static VERBOSITY: AtomicBool = AtomicBool::new(false); macro_rules! vrb { ( $( $e:expr ),* ) => {( if VERBOSITY.load(Ordering::SeqCst) { println!( $( $e ),* ) } )} } /* struct Job -- Runtime parameters. */ #[derive(Debug)] enum Job { /** List available schemes. */ List, /** Dump a scheme. */ Dump(Scheme), /** Switch to color scheme. */ Set(Scheme), /** Get currently active scheme. */ Get, /** Toggle between two schemes. */ Toggle(Scheme, Scheme), } impl<'a> Job { pub fn from_argv() -> Result { use clap::{App, Arg, SubCommand}; let app = App::new(clap::crate_name!()) .version(clap::crate_version!()) .author(clap::crate_authors!()) .about(clap::crate_description!()) .subcommand( SubCommand::with_name("dump").about("dump a color scheme").arg( Arg::with_name("scheme") .help("name of the scheme") .required(true) .value_name("NAME") .takes_value(true), ), ) .subcommand( SubCommand::with_name("list").about("list available schemes"), ) .subcommand( SubCommand::with_name("set") .about("apply color scheme to current terminal") .arg( Arg::with_name("scheme") .value_name("NAME") .help("predefined color scheme") .takes_value(true) .conflicts_with("file"), ) .arg( Arg::with_name("file") .short("f") .long("file") .value_name("PATH") .help("apply scheme from file") .takes_value(true), ), ) .subcommand( SubCommand::with_name("get").about("get current color scheme"), ) .subcommand( SubCommand::with_name("toggle") .about("toggle between two schemes") .arg( Arg::with_name("one") .value_name("NAME1") .help("predefined color scheme") .takes_value(true), ) .arg( Arg::with_name("two") .value_name("NAME2") .help("predefined color scheme") .takes_value(true), ), ) .arg( Arg::with_name("verbose") .short("v") .long("verbose") .help("enable extra diagnostics") .takes_value(false), ); let matches = app.get_matches(); if matches.is_present("verbose") { VERBOSITY.store(true, Ordering::SeqCst); } match matches.subcommand() { ("dump", Some(subm)) => { if let Some(name) = subm.value_of("scheme") { let scm = Scheme::from(name); return Ok(Self::Dump(scm)); } Err(anyhow!("dump requires an argument")) }, ("list", _) => Ok(Self::List), ("set", Some(subm)) => { let scheme = match subm.value_of("scheme") { Some("-") => Self::read_scheme_from_stdin(), Some(name) => { vrb!("pick predefined scheme [{}]", name); Scheme::from(name) }, None => match subm.value_of("file") { None | Some("-") => Self::read_scheme_from_stdin(), Some(fname) => { vrb!( "read custom scheme from file [{}]", fname ); Scheme::from_path(fname) }, }, }; Ok(Self::Set(scheme)) }, ("get", _) => Ok(Self::Get), ("toggle", Some(subm)) => { match (subm.value_of("one"), subm.value_of("two")) { (Some(one), Some(two)) => { vrb!("toggle schemes [{}] and [{}]", one, two); Ok(Self::Toggle(Scheme::from(one), Scheme::from(two))) }, _ => Err(anyhow!( "please supply two schemes to toggle between" )), } }, (junk, _) => Err(anyhow!( "invalid subcommand [{}]; try ``{} --help``", junk, clap::crate_name!() )), } } fn list_schemes() { println!("{} color schemes available:", vtcol::BUILTIN_SCHEMES.len()); for s in vtcol::BUILTIN_SCHEMES { println!(" * {}", s.name()); } } fn read_scheme_from_stdin() -> Scheme { vrb!("Go ahead, type your color scheme …"); vrb!("vtcol>"); Scheme::from_stdin() } fn dump(scm: Scheme) -> Result<()> { vrb!("Dumping color scheme {}", scm); let mut out = BufWriter::new(io::stdout()); match scm { Scheme::Builtin(bltn) => Palette::from(bltn.palette()).dump(&mut out).map_err(|e| { anyhow!( "error loading builtin scheme {}: {}", bltn.name(), e ) }), Scheme::Custom(None) => Palette::from_stdin().dump(&mut out).map_err(|e| { anyhow!("error loading palette from stdin: {}", e) }), Scheme::Custom(Some(fname)) => Palette::from_file(&fname).dump(&mut out).map_err(|e| { anyhow!( "error loading palette from file [{}]: {}", fname.display(), e ) }), Scheme::Palette(pal) => pal.dump(&mut out) .map_err(|e| anyhow!("error dumping palette: {}", e)), } } fn run(self) -> Result<()> { match self { Self::Dump(scm) => Self::dump(scm)?, Self::List => Self::list_schemes(), Self::Set(scm) => Self::set_scheme(scm)?, Self::Get => Self::get_scheme()?, Self::Toggle(one, two) => Self::toggle_scheme(one, two)?, } Ok(()) } fn set_scheme(scheme: Scheme) -> Result<()> { let pal = Palette::from(&scheme); vrb!("Using palette:"); vrb!("{}", pal); let fd = get_console_fd()?; vrb!("console fd: {}", fd); vtcol::ioctl_pio_cmap(fd, &pal)?; clear_term(fd)?; vrb!("successfully enabled scheme {:?}", scheme); /* It’s fine to leak the fd, the kernel will clean up anyways. */ Ok(()) } fn get_scheme() -> Result<()> { let fd = get_console_fd()?; vrb!("console fd: {}", fd); let pal = vtcol::ioctl_gio_cmap(fd)?; let scm = Scheme::from(pal); vrb!("active scheme:"); println!("{}", scm); Ok(()) } /** Toggle between two schemes. Defaults to ``one`` in case neither scheme is active. */ fn toggle_scheme(one: Scheme, two: Scheme) -> Result<()> { let fd = get_console_fd()?; vrb!("console fd: {}", fd); let pal = vtcol::ioctl_gio_cmap(fd)?; if pal == Palette::from(&one) { Self::set_scheme(two) } else { Self::set_scheme(one) } } } /* [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; } const CONSOLE_PATHS: [&str; 6] = [ "/proc/self/fd/0", "/dev/tty", "/dev/tty0", "/dev/vc/0", "/dev/systty", "/dev/console", ]; 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 | libc::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, vtcol::KDGKBTYPE as libc::c_int, std::mem::transmute(&mut tty_type), ) }; if res < 0 { vrb!(" *> ioctl failed"); return None; } if tty_type != vtcol::KB_101 { return None; } Some(fd) }, } } fn get_console_fd() -> Result { 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 Ok(fd); } } Err(anyhow!("could not retrieve fd for any of the search paths")) } fn write_to_term(fd: vtcol::Fd, buf: &str) -> Result<()> { let len = buf.len() as libc::size_t; if unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, len) } != len as isize { Err(anyhow!( "failed to write {} B to fd {}: {}", len, fd, Error::last_os_error() )) } else { Ok(()) } } fn clear_term(fd: vtcol::Fd) -> Result<()> { let clear = "\x1b[2J"; let cursor = "\x1b[1;1H"; write_to_term(fd, clear)?; write_to_term(fd, cursor)?; Ok(()) } fn main() -> Result<()> { let job = Job::from_argv()?; vrb!("job parms: {:?}", job); job.run() }