#![allow(clippy::option_map_unit_fn)] pub mod lib; #[cfg(feature = "gui")] pub mod edit; use vtcol::{Console, Fade, KbLedFlags, 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 ),* ) } )} } /** Helper for choosing the console. Defaults to the current one if none is explicitly supplied. */ #[inline] fn open_console(path: Option<&str>) -> io::Result { path.map(Console::from_path).unwrap_or_else(Console::current) } /** Trait for subcommands to implement. */ trait Run { fn run(self, console: Option) -> Result<()>; } #[derive(Debug)] enum LedJob { /** Get keyboard LED state. */ Get(bool), /** Revert to normal. */ Revert, /** Set keyboard LED state of all leds at once. */ Set(KbLedState), /** Set keyboard LED state of individual LEDs. */ SetIndividual(Option, Option, Option), } impl Run for LedJob { fn run(self, console: Option) -> Result<()> { match self { Self::Get(raw) => Self::get(console, raw), Self::Revert => Self::revert(console), Self::Set(st) => Self::set(console, st), Self::SetIndividual(c, n, s) => Self::set_individual(console, c, n, s), } } } /* [impl Run for LedJob] */ impl LedJob { /** Get the keyboard LED state. */ fn get(con: Option, raw: bool) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); let leds = KbLedState::get(&fd)?; if raw { println!("{}", u8::from(leds)); } else { println!("{}", leds); } Ok(()) } /** Set the keyboard LED state. */ fn set(con: Option, st: KbLedState) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); st.set(&fd)?; vrb!("applied"); Ok(()) } /** Set the state of the given LEDs using the current state as base. */ fn set_individual( con: Option, cap: Option, num: Option, scr: Option, ) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); let mut st = KbLedState::get(&fd)?; cap.map(|b| st.set_cap(b)); num.map(|b| st.set_num(b)); scr.map(|b| st.set_scr(b)); st.set(&fd)?; vrb!("applied"); Ok(()) } /** Revert the keyboard LED state. */ fn revert(con: Option) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); KbLedState::revert(&fd)?; vrb!("reverted"); Ok(()) } } /* [impl LedJob] */ #[derive(Debug)] enum FlagTarget { Current, Default, Both, } #[derive(Debug)] enum FlagJob { /** Get keyboard flags. */ Get(bool), /** Set all keyboard flags at once. */ Set(KbLedFlags), /** Set keyboard LED state of individual LEDs. */ SetIndividual(FlagTarget, Option, Option, Option), } impl FlagJob { /** Get the keyboard flags (modifier locks). */ fn get(con: Option, raw: bool) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); let leds = KbLedFlags::get(&fd)?; if raw { println!("{}", u8::from(leds)); } else { println!("{}", leds); } Ok(()) } /** Set the keyboard flags. */ fn set(con: Option, st: KbLedFlags) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); st.set(&fd)?; vrb!("applied"); Ok(()) } /** Set / unset keyboard flags using the current as base. */ fn set_individual( con: Option, target: FlagTarget, cap: Option, num: Option, scr: Option, ) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); let mut st = KbLedFlags::get(&fd)?; if matches!(target, FlagTarget::Default | FlagTarget::Both) { cap.map(|b| st.set_default_cap(b)); num.map(|b| st.set_default_num(b)); scr.map(|b| st.set_default_scr(b)); } if matches!(target, FlagTarget::Current | FlagTarget::Both) { cap.map(|b| st.set_cap(b)); num.map(|b| st.set_num(b)); scr.map(|b| st.set_scr(b)); } st.set(&fd)?; vrb!("applied"); Ok(()) } } /*[impl FlagJob] */ impl Run for FlagJob { fn run(self, console: Option) -> Result<()> { match self { Self::Get(raw) => Self::get(console, raw), Self::Set(flags) => Self::set(console, flags), Self::SetIndividual(dflt, c, n, s) => Self::set_individual(console, dflt, c, n, s), } } } /* [impl Run for FlagJob] */ #[derive(Debug)] enum KbJob { /** LED state ops. */ Leds(LedJob), /** Flags. */ Flags(FlagJob), } impl Run for KbJob { fn run(self, console: Option) -> Result<()> { match self { Self::Leds(leds) => leds.run(console), Self::Flags(flags) => flags.run(console), } } } /* [impl Run for KbJob] */ #[derive(Debug)] enum ColorJob { /** List available schemes. */ List, /** Dump a scheme. */ Dump(Scheme), /** Launch scheme editor. */ #[cfg(feature = "gui")] Edit(Option, 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), } impl Run for ColorJob { fn run(self, console: Option) -> Result<()> { match self { Self::Dump(scm) => Self::dump(scm), #[cfg(feature = "gui")] Self::Edit(name, scm) => Self::edit(name, scm), Self::List => Self::list(), Self::Set(scm) => Self::set(console, scm), Self::Get(b64) => Self::get(console, b64), Self::Toggle(one, two) => Self::toggle(console, one, two), Self::Fade(from, to, ms, hz, clear) => Self::fade(console, from, to, ms, hz, clear), } } } /* [impl Run for ColorJob] */ impl ColorJob { fn list() -> Result<()> { println!("{} color schemes available:", vtcol::BUILTIN_SCHEMES.len()); for s in vtcol::BUILTIN_SCHEMES { println!(" * {}", s.name()); } Ok(()) } 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)), } } #[cfg(feature = "gui")] fn edit(name: Option, scm: Scheme) -> Result<()> { vrb!("Launching color scheme editor for scheme {}", scm); let editor = crate::edit::Edit::new(name, scm); editor.run() } fn set(con: Option, scheme: Scheme) -> Result<()> { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); fd.apply_scheme(&scheme)?; fd.clear()?; vrb!("successfully enabled scheme {:?}", scheme); /* It’s fine to leak the fd, the kernel will clean up anyways. */ Ok(()) } fn get(con: Option, b64: bool) -> Result<()> { let fd = open_console(con.as_deref())?; 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(con: Option, one: Scheme, two: Scheme) -> Result<()> { let pal = { let fd = open_console(con.as_deref())?; vrb!("console fd: {}", fd); fd.current_palette()? }; if pal == Palette::try_from(&one)? { Self::set(con, two) } else { Self::set(con, one) } } /** Fade from one scheme to another. If ``from`` is ``None``, the current palette is used as starting point. */ fn fade( con: Option, from: Option, to: Scheme, dur: Duration, hz: u8, clear: bool, ) -> Result<()> { let fd = open_console(con.as_deref())?; 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(()) } } /* [impl ColorJob] */ /** Subcommand and runtime parameters. */ #[derive(Debug)] enum Subcmd { /** Console palette ops. */ Colors(ColorJob), /** Keyboard LED ops. */ Kb(KbJob), } #[derive(Debug)] struct Job(Option, Subcmd); impl<'a> Job { pub fn from_argv() -> Result { use clap::{App, Arg, ArgSettings, 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") .set(ArgSettings::Global) .short("v") .long("verbose") .help("enable extra diagnostics") .takes_value(false), ) .arg( Arg::with_name("console") .set(ArgSettings::Global) .short("C") .long("console") .help( "path to the console device to operate on [default: \ current console]", ) .takes_value(true), ) .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("edit") .about("launch a graphical color scheme editor") .arg( Arg::with_name("scheme") .help("name of the scheme to edit") .required(false) .value_name("NAME") .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("kb") .about("keyboard controls") .subcommand( SubCommand::with_name("leds") .about("operations regarding keyboard LEDs") .subcommand( SubCommand::with_name("get") .about("get LED state") .arg( Arg::with_name("u8") .short("8") .long("u8") .help("output raw state as integer") .takes_value(false), ), ) .subcommand( SubCommand::with_name("set") .about("set LED state") .arg( Arg::with_name("u8") .short("8") .long("u8") .help( "provide desired state as \ integer", ) .takes_value(true) .required_unless_one(&[ "revert", "caps", "num", "scroll", ]) .conflicts_with_all(&[ "revert", "caps", "num", "scroll", ]) .value_name("STATE"), ) .arg( Arg::with_name("caps") .short("c") .long("caps") .help("[de]activate Caps Lock LED") .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with_all(&[ "revert", "u8", ]) .value_name("STATE"), ) .arg( Arg::with_name("num") .short("n") .long("num") .help("[de]activate Num Lock LED") .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with_all(&[ "revert", "u8", ]) .value_name("STATE"), ) .arg( Arg::with_name("scroll") .short("s") .long("scroll") .help( "[de]activate Scroll Lock LED", ) .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with_all(&[ "revert", "u8", ]) .value_name("STATE"), ) .arg( Arg::with_name("revert") .short("r") .long("revert") .help("revert LED state to normal") .takes_value(false) .conflicts_with_all(&[ "u8", "caps", "num", "scroll", ]), ), ), ) .subcommand( SubCommand::with_name("flags") .about( "operations regarding keyboard flags \ (modifier locks)", ) .subcommand( SubCommand::with_name("get") .about("get flags") .arg( Arg::with_name("u8") .short("8") .long("u8") .help("output raw flags as integer") .takes_value(false), ), ) .subcommand( SubCommand::with_name("set") .about("set flags") .arg( Arg::with_name("u8") .short("8") .long("u8") .help( "provide desired flags as integer", ) .takes_value(true) .required_unless_one(&[ "caps", "num", "scroll", ]) .conflicts_with_all(&[ "dflt", "caps", "num", "scroll", ]) .value_name("STATE"), ) .arg( Arg::with_name("dflt") .short("d") .long("default") .help( "operate on defaults, not the \ current state", ) .conflicts_with("both") .takes_value(false), ) .arg( Arg::with_name("both") .short("b") .long("both") .help( "operate on both defaults and \ current state", ) .takes_value(false), ) .arg( Arg::with_name("caps") .short("c") .long("caps") .help("[de]activate Caps Lock flag") .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with("u8") .value_name("STATE"), ) .arg( Arg::with_name("num") .short("n") .long("num") .help("[de]activate Num Lock flag") .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with("u8") .value_name("STATE"), ) .arg( Arg::with_name("scroll") .short("s") .long("scroll") .help( "[de]activate Scroll Lock flag", ) .takes_value(true) .possible_values(&["on", "off"]) .conflicts_with("u8") .value_name("STATE"), ), ), ), ); let matches = app.get_matches(); if matches.is_present("verbose") { VERBOSITY.store(true, Ordering::SeqCst); } let con = matches.value_of("console").map(String::from); 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( con, Subcmd::Colors(ColorJob::Dump(scheme)), )); } if let Some(name) = subm.value_of("scheme") { let scm = Scheme::from(name); return Ok(Self( con, Subcmd::Colors(ColorJob::Dump(scm)), )); } Err(anyhow!("dump requires an argument")) }, ("edit", Some(subm)) => { #[cfg(feature = "gui")] if let Some(name) = subm.value_of("scheme") { let scm = Scheme::from(name); Ok(Self( con, Subcmd::Colors(ColorJob::Edit( Some(name.to_string()), scm, )), )) } else { Ok(Self( con, Subcmd::Colors(ColorJob::Edit( None, Scheme::from("default"), )), )) } #[cfg(not(feature = "gui"))] { let _ = subm; /* silence warn(unused_variables) */ Err(anyhow!( "the ``edit'' subcommand requires vtcol to be \ built with the the ``gui'' feature" )) } }, ("list", _) => Ok(Self(con, Subcmd::Colors(ColorJob::List))), ("set", Some(subm)) => { if let Some(b64) = subm.value_of("base64") { let scheme = Scheme::from_base64(b64)?; return Ok(Self( con, Subcmd::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(con, Subcmd::Colors(ColorJob::Set(scheme)))) }, ("get", Some(subm)) => Ok(Self( con, Subcmd::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( con, Subcmd::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( con, Subcmd::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!() )), }, ("kb", Some(subm)) => match subm.subcommand() { ("leds", Some(subm)) => match subm.subcommand() { ("get", Some(subm)) => { let raw = subm.is_present("u8"); Ok(Self( con, Subcmd::Kb(KbJob::Leds(LedJob::Get(raw))), )) }, ("set", Some(subm)) => { if subm.is_present("revert") { return Ok(Self( con, Subcmd::Kb(KbJob::Leds(LedJob::Revert)), )); } if let Some(st) = subm.value_of("u8") { let st: u8 = st.parse()?; let st = KbLedState::try_from(st)?; return Ok(Self( con, Subcmd::Kb(KbJob::Leds(LedJob::Set( st, ))), )); } let cap = subm.value_of("caps").map(|a| a == "on"); let num = subm.value_of("num").map(|a| a == "on"); let scr = subm.value_of("scroll").map(|a| a == "on"); Ok(Self( con, Subcmd::Kb(KbJob::Leds( LedJob::SetIndividual(cap, num, scr), )), )) }, (leds_junk, _) => Err(anyhow!( "invalid sub-sub-subcommand to kb leds: \ [{}]; try ``{} kb leds --help``", leds_junk, clap::crate_name!() )), }, ("flags", Some(subm)) => match subm.subcommand() { ("get", Some(subm)) => { let raw = subm.is_present("u8"); Ok(Self( con, Subcmd::Kb(KbJob::Flags(FlagJob::Get(raw))), )) }, ("set", Some(subm)) => { if let Some(st) = subm.value_of("u8") { let st: u8 = st.parse()?; let st = KbLedFlags::try_from(st)?; return Ok(Self( con, Subcmd::Kb(KbJob::Flags(FlagJob::Set( st, ))), )); } let target = if subm.is_present("both") { FlagTarget::Both } else if subm.is_present("dflt") { FlagTarget::Default } else { FlagTarget::Current }; let cap = subm.value_of("caps").map(|a| a == "on"); let num = subm.value_of("num").map(|a| a == "on"); let scr = subm.value_of("scroll").map(|a| a == "on"); Ok(Self( con, Subcmd::Kb(KbJob::Flags( FlagJob::SetIndividual( target, cap, num, scr, ), )), )) }, (flags_junk, _) => Err(anyhow!( "invalid sub-sub-subcommand to kb flags: \ [{}]; try ``{} kb flags --help``", flags_junk, clap::crate_name!() )), }, (kb_junk, _) => Err(anyhow!( "invalid sub-subcommand to kb [{}]; try ``{} kb \ --help``", kb_junk, clap::crate_name!() )), }, (junk, _) => Err(anyhow!( "invalid subcommand [{}]; try ``{} --help``", junk, clap::crate_name!() )), } } fn read_scheme_from_stdin() -> Scheme { vrb!("Go ahead, type your color scheme …"); vrb!("vtcol>"); Scheme::from_stdin() } fn run(self) -> Result<()> { let Job(con, cmd) = self; match cmd { Subcmd::Colors(cols) => cols.run(con)?, Subcmd::Kb(kb) => kb.run(con)?, } Ok(()) } } /* [impl Job] */ fn main() -> Result<()> { let job = Job::from_argv()?; vrb!("job parms: {:?}", job); job.run() }