pub mod lib; use vtcol::{Console, Fade, KbLedState, Palette, Scheme}; use anyhow::{anyhow, Result}; use std::{io::{self, BufWriter}, sync::atomic::{AtomicBool, Ordering}, time::Duration}; static VERBOSITY: AtomicBool = AtomicBool::new(false); const DEFAULT_FADE_DURATION_MS: u64 = 500; const DEFAULT_FADE_UPDATE_HZ: u8 = 25; macro_rules! vrb { ( $( $e:expr ),* ) => {( if VERBOSITY.load(Ordering::SeqCst) { println!( $( $e ),* ) } )} } #[derive(Debug)] enum LedJob { /** Get keyboard LED state. */ Get(bool), /** Set keyboard LED state. */ Set, } #[derive(Debug)] enum ColorJob { /** List available schemes. */ List, /** Dump a scheme. */ Dump(Scheme), /** Switch to color scheme. */ Set(Scheme), /** Get currently active scheme. */ Get(bool), /** Toggle between two schemes. */ Toggle(Scheme, Scheme), /** Fade from current scheme to another. */ Fade(Option, Scheme, Duration, u8, bool), } /** Subcommand and runtime parameters. */ #[derive(Debug)] enum Job { /** Console palette ops. */ Colors(ColorJob), /** Keyboard LED ops. */ Leds(LedJob), } 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!()) .arg( Arg::with_name("verbose") .short("v") .long("verbose") .help("enable extra diagnostics") .takes_value(false), ) .subcommand( SubCommand::with_name("colors") .about("operations on the console palette") .subcommand( SubCommand::with_name("dump") .about("dump a color scheme") .arg( Arg::with_name("scheme") .help("name of the scheme") .required(false) .value_name("NAME") .takes_value(true), ) .arg( Arg::with_name("base64") .short("b") .long("base64") .value_name("DATA") .help("base64 encoded binary input") .required(false) .takes_value(true), ), ) .subcommand( SubCommand::with_name("list") .about("list builtin 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), ) .arg( Arg::with_name("base64") .short("b") .long("base64") .help("base64 encoded binary input") .value_name("DATA") .required(false) .takes_value(true), ), ) .subcommand( SubCommand::with_name("get") .about("get current color scheme") .arg( Arg::with_name("base64") .short("b") .long("base64") .help("base64 encoded binary output") .required(false) .takes_value(false), ), ) .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), ), ) .subcommand( SubCommand::with_name("fade") .about("fade from one scheme to another") .arg( Arg::with_name("from") .short("f") .long("from") .value_name("NAME1") .help( "initial color scheme (default: \ current)", ) .takes_value(true), ) .arg( Arg::with_name("to") .short("t") .long("to") .value_name("NAME2") .help("final color scheme") .takes_value(true) .required(true), ) .arg( Arg::with_name("ms") .value_name("MS") .short("m") .long("ms") .help( "how long (in ms) the fade should take", ) .takes_value(true), ) .arg( Arg::with_name("clear") .short("c") .long("clear") .help("clear terminal on each fade step") .takes_value(false), ) .arg( Arg::with_name("frequency") .value_name("HZ") .short("h") .long("frequency") .help( "rate (HZ/s) of intermediate scheme \ changes", ) .takes_value(true), ), ), ) .subcommand( SubCommand::with_name("leds") .about("operations regarding keyboard LEDs") .subcommand( SubCommand::with_name("get") .about("get keyboard LED state") .arg( Arg::with_name("u8") .value_name("NAME") .short("8") .long("u8") .help("output raw state as integer") .takes_value(false), ), ), ); let matches = app.get_matches(); if matches.is_present("verbose") { VERBOSITY.store(true, Ordering::SeqCst); } match matches.subcommand() { ("colors", Some(subm)) => match subm.subcommand() { ("dump", Some(subm)) => { if let Some(b64) = subm.value_of("base64") { let scheme = Scheme::from_base64(b64)?; return Ok(Self::Colors(ColorJob::Dump(scheme))); } if let Some(name) = subm.value_of("scheme") { let scm = Scheme::from(name); return Ok(Self::Colors(ColorJob::Dump(scm))); } Err(anyhow!("dump requires an argument")) }, ("list", _) => Ok(Self::Colors(ColorJob::List)), ("set", Some(subm)) => { if let Some(b64) = subm.value_of("base64") { let scheme = Scheme::from_base64(&b64)?; return Ok(Self::Colors(ColorJob::Set(scheme))); } 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::Colors(ColorJob::Set(scheme))) }, ("get", Some(subm)) => Ok(Self::Colors(ColorJob::Get( subm.is_present("base64"), ))), ("toggle", Some(subm)) => { match (subm.value_of("one"), subm.value_of("two")) { (Some(one), Some(two)) => { vrb!("toggle schemes [{}] and [{}]", one, two); Ok(Self::Colors(ColorJob::Toggle( Scheme::from(one), Scheme::from(two), ))) }, _ => Err(anyhow!( "please supply two schemes to toggle \ between" )), } }, ("fade", Some(subm)) => { let dur: u64 = if let Some(ms) = subm.value_of("ms") { ms.parse()? } else { DEFAULT_FADE_DURATION_MS }; let hz: u8 = if let Some(ms) = subm.value_of("frequency") { ms.parse()? } else { DEFAULT_FADE_UPDATE_HZ }; let clear = subm.is_present("clear"); let dur = Duration::from_millis(dur); match (subm.value_of("from"), subm.value_of("to")) { (_, None) => Err(anyhow!( "please supply color scheme to fade to" )), (from, Some(to)) => Ok(Self::Colors(ColorJob::Fade( from.map(Scheme::from), Scheme::from(to), dur, hz, clear, ))), } }, (junk, _) => Err(anyhow!( "invalid sub-subcommand to colors: [{}]; try ``{} \ colors --help``", junk, clap::crate_name!() )), }, ("leds", Some(subm)) => match subm.subcommand() { ("get", Some(subm)) => { let raw = subm.is_present("u8"); Ok(Self::Leds(LedJob::Get(raw))) }, (junk, _) => Err(anyhow!( "invalid sub-subcommand to leds: [{}]; try ``{} \ leds --help``", junk, clap::crate_name!() )), }, (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::Colors(ColorJob::Dump(scm)) => Self::dump(scm)?, Self::Colors(ColorJob::List) => Self::list_schemes(), Self::Colors(ColorJob::Set(scm)) => Self::set_scheme(scm)?, Self::Colors(ColorJob::Get(b64)) => Self::get_scheme(b64)?, Self::Colors(ColorJob::Toggle(one, two)) => Self::toggle_scheme(one, two)?, Self::Colors(ColorJob::Fade(from, to, ms, hz, clear)) => Self::fade(from, to, ms, hz, clear)?, Self::Leds(LedJob::Get(raw)) => Self::get_leds(raw)?, Self::Leds(LedJob::Set) => unimplemented!(), } Ok(()) } fn set_scheme(scheme: Scheme) -> Result<()> { let con = Console::current()?; vrb!("console fd: {}", con); con.apply_scheme(&scheme)?; con.clear()?; vrb!("successfully enabled scheme {:?}", scheme); /* It’s fine to leak the fd, the kernel will clean up anyways. */ Ok(()) } fn get_scheme(b64: bool) -> Result<()> { let fd = Console::current()?; vrb!("console fd: {}", fd); let scm = fd.current_scheme()?; if b64 { print!("{}", scm.base64()?); } else { 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 = Console::current()?; vrb!("console fd: {}", fd); if fd.current_palette()? == Palette::try_from(&one)? { Self::set_scheme(two) } else { Self::set_scheme(one) } } /** Fade from one scheme to another. If ``from`` is ``None``, the current palette is used as starting point. */ fn fade( from: Option, to: Scheme, dur: Duration, hz: u8, clear: bool, ) -> Result<()> { let fd = Console::current()?; vrb!("console fd: {}", fd); let from = if let Some(from) = from { Palette::try_from(&from)? } else { fd.current_palette()? }; let to = Palette::try_from(&to)?; let fade = Fade::new(from, to, dur, hz, clear); fade.commence(&fd)?; Ok(()) } /** Get the keyboard led state. */ fn get_leds(raw: bool) -> Result<()> { let fd = Console::current()?; vrb!("console fd: {}", fd); let leds = KbLedState::get(&fd)?; if raw { println!("{}", u8::from(leds)); } else { println!("{}", leds); } Ok(()) } } /* [impl Job] */ fn main() -> Result<()> { let job = Job::from_argv()?; vrb!("job parms: {:?}", job); job.run() }