summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/edit.rs600
-rw-r--r--src/lib.rs592
-rw-r--r--src/vtcol.rs1204
3 files changed, 2106 insertions, 290 deletions
diff --git a/src/edit.rs b/src/edit.rs
new file mode 100644
index 0000000..f21cf73
--- /dev/null
+++ b/src/edit.rs
@@ -0,0 +1,600 @@
+use vtcol::{Palette, Rgb, Scheme};
+
+use std::{fmt,
+ path::{Path, PathBuf},
+ rc::Rc};
+
+use anyhow::{anyhow, Result};
+
+use slint::{re_exports::KeyEvent, Color, VecModel};
+
+slint::slint! {
+ import { HorizontalBox, VerticalBox } from "std-widgets.slint";
+
+ export global Aux := {
+ property<int> selected: 0;
+ callback select(int);
+ callback set-palette-color(int, color);
+ callback format-rgb-hex(color) -> string;
+ callback format-rgb-component(int) -> string;
+ callback color-of-rgb-component(string, int) -> color;
+ callback component-of-rgb(string, color) -> int;
+ callback handle-command-buffer(KeyEvent, string, int) -> string;
+ property<string> mode: "normal"; /* normal | insert | command */
+ }
+
+ Colors := Rectangle {
+ width : 100%;
+ border-width : 2px;
+ background: @linear-gradient(90deg, #002b36 0%, #073642 100%);
+
+ property <[color]> colors: [
+ rgb( 0, 0, 0),
+ ];
+
+ property<int> base: 0;
+
+ squares := HorizontalBox {
+ width : 100%;
+ height : 20px;
+
+ for col[i] in colors : psquare := Rectangle {
+ property <color> current-color : col;
+ width : (squares.width / 8) - 12px;
+ height : (squares.width / 8) - 12px;
+ border-color : i == (Aux.selected - base) ? #cb4b16 : #839496;
+ border-width : 3px;
+ forward-focus: pval;
+
+ ptouch := TouchArea {
+ clicked => { Aux.select(base + i); }
+ }
+
+ prect := Rectangle {
+ y : 3px;
+ x : 3px;
+ width : psquare.width - 6px;
+ height : psquare.height - 6px;
+ background : current-color;
+
+ VerticalBox {
+ Rectangle {
+ pdesc := Text {
+ /* Text will be set through callback from Rust. */
+ text : i;
+ }
+ }
+ Rectangle {
+ background : ptouch.has-hover ? #ffffff77 : #cccccc33;
+ pval := Text {
+ text : Aux.format-rgb-hex(current-color);
+ font-family : "mono";
+ font-size : 9pt;
+ }
+ }
+ Rectangle {
+ background : green;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ComponentEdit := HorizontalBox {
+ height : 18pt;
+ alignment : start;
+
+ property <int> val;
+ property <string> comp : "";
+
+ Text {
+ width : 15%;
+ height : 14.4pt;
+ color : #a0a0a0;
+ font-size : 12pt;
+ font-weight : 700;
+ text : Aux.format-rgb-component (val);
+ }
+
+ Rectangle {
+ border-width : 2px;
+ height : 14.4pt;
+ width : 75%;
+ background : Aux.color-of-rgb-component (comp, val);
+ }
+ }
+
+ Edit := Rectangle {
+ width : 100%;
+ background : @linear-gradient(90deg, #002b36 0%, #073642 100%);
+ border-color : Aux.mode == "insert" ? #cb4b16 : #839496;
+ border-width : 3px;
+
+ property<color> rgb : Colors.white;
+
+ callback update (color);
+ update (col) => {
+ debug("edit > update");
+ red.val = Aux.component-of-rgb("r", col);
+ green.val = Aux.component-of-rgb("g", col);
+ blue.val = Aux.component-of-rgb("b", col);
+ }
+
+ VerticalBox {
+ alignment : start;
+
+ red := ComponentEdit { val : Aux.component-of-rgb("r", rgb); comp: "r"; }
+ green := ComponentEdit { val : Aux.component-of-rgb("g", rgb); comp: "g"; }
+ blue := ComponentEdit { val : Aux.component-of-rgb("b", rgb); comp: "b"; }
+ }
+ }
+
+ CommandBar := Rectangle {
+ width : 100%;
+ height : t.preferred-height + 3pt;
+ background : @linear-gradient(90deg, #002b36 0%, #073642 100%);
+
+ property <int> cursor : 0;
+ property text <=> t.text;
+ callback input (KeyEvent) -> bool;
+ callback clear;
+
+ clear () => { t.text = ""; }
+
+ input (k) => {
+ t.text = Aux.handle-command-buffer (k, t.text, cursor);
+
+ /* Buffer is empty if the user backspaced her way to the left. */
+ return t.text == "";
+ }
+
+ t := Text {
+ text: "";
+ color: #fdf6e3;
+ vertical-alignment: center;
+ }
+ }
+
+ GuiEdit := Window {
+ property scheme-name <=> name.text;
+
+ property <[color]> primary <=> primary-colors .colors;
+ property <[color]> secondary <=> secondary-colors.colors;
+
+ callback get-palette-color(int) -> color;
+ callback set-palette-color(int, color);
+ callback update-edit <=> edit.update;
+ callback user-quit();
+
+ key-inputs := FocusScope {
+ key-pressed(event) => {
+ debug("input: got", event.text, "shift?", event.modifiers.shift);
+ /* Escape always gets you back to normal mode. */
+ if (event.text == Keys.Escape) {
+ debug("enter normal");
+ master.enter-normal-mode ();
+ }
+ else if (Aux.mode == "command") {
+ if (command.input (event)) {
+ master.enter-normal-mode ();
+ }
+ }
+ else if (Aux.mode == "insert") {
+ /* In insert mode, most selection bindings are frozen. */
+ } /* Insert mode. */
+ else {
+ /* Normal mode bindings. */
+ if (event.text == "q") {
+ user-quit();
+ }
+ if (event.text == "h") {
+ debug("select prev");
+ master.select-prev();
+ }
+ else if (event.text == "l") {
+ debug("select next");
+ master.select-next();
+ }
+ else if (event.text == " ") {
+ if (event.modifiers.shift) {
+ debug("select prev");
+ master.select-prev();
+ } else {
+ debug("select next");
+ master.select-next();
+ }
+ }
+ else if (event.text == "j" || event.text == "k") {
+ debug("select other row");
+ master.select-other-row();
+ }
+ else if (event.text == Keys.Return || event.text == "i") {
+ debug("enter insert");
+ master.enter-insert-mode();
+ }
+ else if (event.text == ":") {
+ debug("enter command");
+ master.enter-command-mode();
+ }
+ if (event.modifiers.control) {
+ //debug("control was pressed during this event");
+ }
+ } /* Normal mode. */
+ edit.update(
+ Aux.selected < 8
+ ? primary-colors.colors [Aux.selected]
+ : secondary-colors.colors [Aux.selected - 8]
+ );
+ accept
+ }
+ }
+
+ get-palette-color (i) => {
+ i < 8 ? primary-colors.colors [i]
+ : secondary-colors.colors [i - 8]
+ }
+
+ set-palette-color (i, col) => {
+ if (i < 8) {
+ primary-colors.colors [i] = col;
+ } else {
+ secondary-colors.colors [i - 8] = col;
+ }
+ }
+
+ master := VerticalLayout {
+ height : 100%;
+
+ callback select-prev();
+ callback select-next();
+ callback select-other-row();
+
+ callback enter-command-mode();
+ callback enter-insert-mode();
+ callback enter-normal-mode();
+
+ select-prev () => {
+ Aux.select (Aux.selected == 0 ? 15 : Aux.selected - 1);
+ debug ("selected previous, now", Aux.selected);
+ }
+
+ select-next () => {
+ Aux.select (mod (Aux.selected + 1, 16));
+ debug ("selected next, now", Aux.selected);
+ }
+
+ select-other-row () => {
+ Aux.select (mod (Aux.selected + 8, 16));
+ debug ("selected row above/below, now", Aux.selected);
+ }
+
+ enter-command-mode () => {
+ Aux.mode = "command";
+ command.text = ":";
+ }
+
+ enter-normal-mode () => {
+ Aux.mode = "normal" ;
+ command.clear ();
+ }
+
+ enter-insert-mode () => { Aux.mode = "insert" ; }
+
+ status := HorizontalBox {
+ width : 100%;
+
+ name := Text {
+ text : "<unnamed>";
+ color : #a0a0a0;
+ font-weight : 700;
+ }
+ }
+
+ primary-colors := Colors {
+ base : 0;
+ }
+
+ secondary-colors := Colors {
+ base : 8;
+ }
+
+ edit := Edit {
+ }
+
+ /* invisible spacer */
+ Rectangle {
+ vertical-stretch : 2;
+ width : 100%;
+ min-height : 1pt;
+ //background : #ffFFffFF;
+ background : #aaaaaaaa;
+ }
+
+ command := CommandBar {
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+enum KeyInput
+{
+ Printable(String),
+ Escape,
+ Return,
+ Backspace,
+ Delete,
+ Tab,
+ Left,
+ Right,
+ Up,
+ Down,
+ /** Not representable. */
+ Junk,
+}
+
+impl fmt::Display for KeyInput
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
+ {
+ let k = match self {
+ Self::Printable(s) => return write!(f, "‘{}’", s),
+ Self::Escape => "ESC",
+ Self::Return => "\\r",
+ Self::Backspace => "\\b",
+ Self::Delete => "DEL",
+ Self::Tab => "\\t",
+ Self::Left => "←",
+ Self::Right => "→",
+ Self::Up => "↑",
+ Self::Down => "↓",
+ Self::Junk => return write!(f, "{}", "Ø"),
+ };
+
+ write!(f, "key:{}", k)
+ }
+}
+
+/** Crude filter for rejecting strings containing control chars. */
+fn is_printable(s: &str) -> bool
+{
+ s.chars().find(|c| c.is_control()).is_none()
+}
+
+impl From<&KeyEvent> for KeyInput
+{
+ fn from(ev: &KeyEvent) -> Self
+ {
+ match ev.text.as_str() {
+ "\u{0008}" => KeyInput::Backspace,
+ "\t" | "\u{000b}" => KeyInput::Tab,
+ "\n" | "\r" => KeyInput::Return,
+ "\u{000e}" => KeyInput::Left,
+ "\u{000f}" => KeyInput::Right,
+ "\u{0010}" => KeyInput::Up,
+ "\u{0011}" => KeyInput::Down,
+ "\u{001b}" => KeyInput::Escape,
+ "\u{007F}" => KeyInput::Delete,
+ any if is_printable(any) => KeyInput::Printable(any.into()),
+ _ => KeyInput::Junk,
+ }
+ }
+}
+
+pub struct Edit
+{
+ name: Option<String>,
+ scheme: Scheme,
+}
+
+impl Edit
+{
+ pub fn new(name: Option<String>, scheme: Scheme) -> Self
+ {
+ Self { name, scheme }
+ }
+
+ pub fn run(self) -> Result<()>
+ {
+ let Self { name, scheme } = self;
+
+ let pal = Palette::try_from(&scheme)?.iter().collect::<Vec<Rgb>>();
+
+ let primary = pal[0..8]
+ .iter()
+ .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b))
+ .collect::<Vec<Color>>();
+
+ let secondary = pal[8..]
+ .iter()
+ .map(|Rgb(r, g, b)| Color::from_rgb_u8(*r, *g, *b))
+ .collect::<Vec<Color>>();
+
+ let primary = Rc::new(VecModel::from(primary));
+ let secondary = Rc::new(VecModel::from(secondary));
+
+ let gui = GuiEdit::new();
+
+ gui.on_user_quit(move || {
+ std::process::exit(0);
+ });
+
+ {
+ let guiw = gui.as_weak();
+ let npal = pal.len();
+ gui.global::<Aux>().on_select(move |i: i32| {
+ let i = (i as usize % npal) as i32;
+ guiw.unwrap().global::<Aux>().set_selected(i);
+ let col = guiw.unwrap().invoke_get_palette_color(i);
+ guiw.unwrap().invoke_update_edit(col);
+ });
+ }
+
+ {
+ let guiw = gui.as_weak();
+ let npal = pal.len();
+ gui.global::<Aux>().on_set_palette_color(move |i, col| {
+ let i = (i as usize % npal) as i32;
+ guiw.unwrap().invoke_set_palette_color(i, col);
+ });
+ }
+
+ gui.global::<Aux>().on_format_rgb_hex(|col| {
+ let x = (col.red() as u32) << 2
+ | (col.green() as u32) << 1
+ | (col.blue() as u32);
+ format!("#{:06x}", x).into()
+ });
+
+ gui.global::<Aux>().on_format_rgb_component(|val| {
+ let val = 0xff & val;
+ format!("#{:02x} ({})", val, val).into()
+ });
+
+ gui.global::<Aux>().on_color_of_rgb_component(|comp, val| {
+ match comp.as_str() {
+ "r" => Color::from_rgb_u8(val as u8, 0, 0),
+ "g" => Color::from_rgb_u8(0, val as u8, 0),
+ "b" => Color::from_rgb_u8(0, 0, val as u8),
+ _ => Color::from_rgb_u8(0, 0, 0),
+ }
+ });
+
+ /* Why the hell is there no API for this in .60‽ */
+ gui.global::<Aux>().on_component_of_rgb(|comp, col| {
+ match comp.as_str() {
+ "r" => col.red() as i32,
+ "g" => col.green() as i32,
+ "b" => col.blue() as i32,
+ _ => 0,
+ }
+ });
+
+ {
+ let guiw = gui.as_weak();
+ gui.global::<Aux>().on_handle_command_buffer(move |ev, text, _pos| {
+ let text = match KeyInput::from(&ev) {
+ KeyInput::Printable(s) => {
+ let mut text = text.to_string();
+ text.push_str(&s);
+ text
+ },
+ KeyInput::Return => {
+ match Command::try_from(text.as_str()) {
+ Err(e) => {
+ eprintln!("bad command [{}]: {}", text, e);
+ String::new()
+ },
+ Ok(cmd) => {
+ use slint::Model;
+ let scheme_name =
+ guiw.unwrap().get_scheme_name().to_string();
+ let prim = guiw.unwrap().get_primary();
+ let secn = guiw.unwrap().get_secondary();
+ let pal = prim.iter().chain(secn.iter())
+ .collect::<Vec<Color>>();
+ let pal = Palette::from(pal.as_slice());
+ if let Err(e) = cmd
+ .exec(scheme_name.as_str(), pal)
+ {
+ eprintln!(
+ "error executing command [{}]: {}",
+ text, e
+ );
+ }
+ String::new()
+ },
+ }
+ /* The empty string signals to leave command mode. */
+ },
+ KeyInput::Backspace =>
+ match text.char_indices().next_back() {
+ Some((i, _)) => text[..i].into(),
+ None => text.to_string(),
+ },
+ other => {
+ eprintln!(
+ "»»» command mode input: “{}”",
+ other.to_string()
+ );
+ text.to_string()
+ },
+ };
+ eprintln!("»»» command buffer: “{}”", text);
+ text.into()
+ });
+ }
+
+ if let Some(name) = name {
+ gui.set_scheme_name(name.into());
+ }
+
+ gui.set_primary(primary.into());
+ gui.set_secondary(secondary.into());
+
+ gui.run();
+
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+enum Command
+{
+ Noop,
+ Save(Option<PathBuf>),
+}
+
+impl TryFrom<&str> for Command
+{
+ type Error = anyhow::Error;
+
+ fn try_from(text: &str) -> Result<Self>
+ {
+ let text = text.trim_start_matches(':').trim();
+ if text.len() == 0 {
+ return Ok(Self::Noop);
+ }
+ let (cmd, args) = if let Some(cmd) = text.get(..1) {
+ (cmd, &text[1..])
+ } else {
+ return Err(anyhow!("non-ascii command‽"));
+ };
+
+ match (cmd, args) {
+ ("w", "") => Ok(Self::Save(None)),
+ ("w", rest) => {
+ let rest = rest.trim();
+ let path = PathBuf::from(rest);
+ Ok(Self::Save(Some(path)))
+ },
+ _ => Err(anyhow!("only save (‘:w’) implemented for now")),
+ }
+ }
+}
+
+impl Command
+{
+ fn exec(self, name: &str, pal: Palette) -> Result<()>
+ {
+ match self {
+ Self::Noop => Ok(()),
+ Self::Save(None) => Self::save(&PathBuf::from(&name), pal),
+ Self::Save(Some(path)) => Self::save(&path, pal),
+ }
+ //todo!("got command: {:?}", self)
+ }
+
+ fn save(path: &Path, pal: Palette) -> Result<()>
+ {
+ use std::io::Write;
+
+ let mut f =
+ std::fs::OpenOptions::new().create(true).write(true).open(path)?;
+ let pal: String = pal.into();
+ f.write_all(pal.as_bytes()).map_err(|e| {
+ anyhow!("error saving Palette to file [{}]: {}", path.display(), e)
+ })
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index c652565..8ecc4a6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::new_without_default)]
+
use std::{convert::TryFrom,
fmt,
io::{self, BufWriter, Error, Write},
@@ -46,28 +48,25 @@ fn cvt_r<T: IsMinusOne>(f: &mut dyn FnMut() -> T) -> io::Result<T>
/** Wrappers for ``ioctl_console(2)`` functionality. */
pub mod ioctl
{
- use super::{cvt_r, Palette};
+ use super::{cvt_r, KbLedFlags, KbLedState, KbLeds, Palette};
use libc::ioctl;
- use std::{io, os::unix::io::AsRawFd};
+ use std::{io::Result, os::unix::io::AsRawFd};
/* XXX: can we get these into ``libc``? */
- pub const KDGKBTYPE: libc::c_ulong = 0x4b33; /* kd.h */
- pub const GIO_CMAP: libc::c_ulong = 0x00004B70; /* kd.h */
- pub const PIO_CMAP: libc::c_ulong = 0x00004B71; /* kd.h */
- pub const KB_101: libc::c_char = 0x0002; /* kd.h */
-
- pub fn kdgkbtype<F: AsRawFd>(fd: &F) -> io::Result<libc::c_char>
- {
- let mut kb: libc::c_char = 0;
-
- let _ = cvt_r(&mut || unsafe {
- ioctl(fd.as_raw_fd(), KDGKBTYPE, &mut kb as *mut _)
- })?;
-
- Ok(kb)
- }
-
- pub fn pio_cmap<F: AsRawFd>(fd: &F, pal: &Palette) -> io::Result<()>
+ /* kd.h */
+ pub const KDGKBTYPE: libc::c_ulong = 0x4b33;
+ pub const GIO_CMAP: libc::c_ulong = 0x00004B70;
+ pub const PIO_CMAP: libc::c_ulong = 0x00004B71;
+ pub const KB_101: libc::c_char = 0x0002;
+ pub const KDGETLED: libc::c_ulong = 0x4b31;
+ pub const KDSETLED: libc::c_ulong = 0x4b32;
+
+ pub const KDGKBLED: libc::c_ulong = 0x4B64;
+ pub const KDSKBLED: libc::c_ulong = 0x4B65;
+ pub const KD_KBLED_STATE_MASK: libc::c_ulong = 0x07;
+ pub const KD_KBLED_DEFAULT_MASK: libc::c_ulong = 0x70;
+
+ pub fn pio_cmap<F: AsRawFd>(fd: &F, pal: &Palette) -> Result<()>
{
/* cvt_r because technically it can’t be ruled out that we hit EINTR. */
cvt_r(&mut || {
@@ -75,14 +74,14 @@ pub mod ioctl
ioctl(
fd.as_raw_fd(),
PIO_CMAP,
- std::mem::transmute::<&Palette, *const libc::c_void>(&pal),
+ std::mem::transmute::<&Palette, *const libc::c_void>(pal),
)
}
})
.map(|_| ())
}
- pub fn gio_cmap<F: AsRawFd>(fd: &F) -> io::Result<Palette>
+ pub fn gio_cmap<F: AsRawFd>(fd: &F) -> Result<Palette>
{
let mut pal = Palette::new();
@@ -101,6 +100,426 @@ pub mod ioctl
.map(|_| ())?;
Ok(pal)
}
+
+ pub fn kdgetled<F: AsRawFd>(fd: &F) -> Result<KbLedState>
+ {
+ let mut leds: libc::c_char = 0;
+
+ cvt_r(&mut || {
+ unsafe {
+ ioctl(
+ fd.as_raw_fd(),
+ KDGETLED,
+ std::mem::transmute::<&mut libc::c_char, *mut libc::c_void>(
+ &mut leds,
+ ),
+ )
+ }
+ })
+ .map(|_| ())?;
+
+ Ok(KbLedState(KbLeds::from(leds)))
+ }
+
+ /** If ``state`` is ``None`` it is taken to mean “revert to normal” as per
+ the man page:
+
+ KDSETLED
+ Set the LEDs. The LEDs are set to correspond to the lower three
+ bits of the unsigned long integer in argp. However, if a higher
+ order bit is set, the LEDs revert to normal: displaying the state
+ of the keyboard functions of caps lock, num lock, and scroll lock.
+ */
+ pub fn kdsetled<F: AsRawFd>(fd: &F, state: Option<KbLedState>)
+ -> Result<()>
+ {
+ let leds: libc::c_ulong = if let Some(state) = state {
+ state.into()
+ } else {
+ libc::c_ulong::MAX
+ };
+
+ cvt_r(&mut || {
+ unsafe {
+ ioctl(
+ fd.as_raw_fd(),
+ KDSETLED,
+ std::mem::transmute::<libc::c_ulong, *const libc::c_void>(
+ leds,
+ ),
+ )
+ }
+ })
+ .map(|_| ())?;
+
+ Ok(())
+ }
+
+ pub fn kdgkbled<F: AsRawFd>(fd: &F) -> Result<KbLedFlags>
+ {
+ let mut flags: libc::c_char = 0;
+
+ cvt_r(&mut || {
+ unsafe {
+ ioctl(
+ fd.as_raw_fd(),
+ KDGKBLED,
+ std::mem::transmute::<&mut libc::c_char, *mut libc::c_void>(
+ &mut flags,
+ ),
+ )
+ }
+ })
+ .map(|_| ())?;
+
+ KbLedFlags::try_from(flags as u8)
+ }
+
+ pub fn kdskbled<F: AsRawFd>(fd: &F, flags: KbLedFlags) -> Result<()>
+ {
+ let default = libc::c_ulong::from(flags.default);
+ let flags = libc::c_ulong::from(flags.flags) | (default << 4);
+
+ cvt_r(&mut || {
+ unsafe {
+ ioctl(
+ fd.as_raw_fd(),
+ KDSKBLED,
+ std::mem::transmute::<libc::c_ulong, *const libc::c_void>(
+ flags,
+ ),
+ )
+ }
+ })
+ .map(|_| ())
+ }
+
+ /* This should perhaps return unit as the kernel supposedly always returns
+ the constant ``KB_101``. */
+ pub fn kdgkbtype<F: AsRawFd>(fd: &F) -> Result<libc::c_char>
+ {
+ let mut kb: libc::c_char = 0;
+
+ let _ = cvt_r(&mut || unsafe {
+ ioctl(fd.as_raw_fd(), KDGKBTYPE, &mut kb as *mut _)
+ })?;
+
+ //assert_eq(kb, KB_101); /* XXX */
+ Ok(kb)
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct KbLeds(u8);
+
+impl KbLeds
+{
+ fn new(cap: bool, num: bool, scr: bool) -> Self
+ {
+ let mut state = 0u8;
+
+ state |= (cap as u8) << 2;
+ state |= (num as u8) << 1;
+ state |= scr as u8;
+
+ Self(state)
+ }
+
+ fn off() -> Self { Self(0) }
+
+ #[inline]
+ fn cap(&self) -> bool { (self.0 & 0x4) != 0 }
+
+ #[inline]
+ fn num(&self) -> bool { (self.0 & 0x2) != 0 }
+
+ #[inline]
+ fn scr(&self) -> bool { (self.0 & 0x1) != 0 }
+
+ #[inline]
+ fn set_cap(&mut self, set: bool)
+ {
+ let bit = (set as u8) << 2;
+ self.0 = (self.0 & !bit) | bit;
+ }
+
+ #[inline]
+ fn set_num(&mut self, set: bool)
+ {
+ let bit = (set as u8) << 1;
+ self.0 = (self.0 & !bit) | bit;
+ }
+
+ #[inline]
+ fn set_scr(&mut self, set: bool)
+ {
+ let bit = set as u8;
+ self.0 = (self.0 & !bit) | bit;
+ }
+}
+
+impl fmt::Display for KbLeds
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
+ {
+ write!(
+ f,
+ "caps: {}, num: {}, scroll: {}",
+ self.cap(),
+ self.num(),
+ self.scr()
+ )
+ }
+}
+
+impl From<libc::c_char> for KbLeds
+{
+ fn from(leds: libc::c_char) -> Self
+ {
+ Self::new(leds & 0x4 != 0, leds & 0x2 != 0, leds & 0x1 != 0)
+ }
+}
+
+impl From<KbLeds> for libc::c_ulong
+{
+ fn from(state: KbLeds) -> Self { state.0 as libc::c_ulong }
+}
+
+impl From<KbLeds> for u8
+{
+ fn from(state: KbLeds) -> Self { state.0 }
+}
+
+impl TryFrom<u8> for KbLeds
+{
+ type Error = io::Error;
+
+ fn try_from(val: u8) -> io::Result<Self>
+ {
+ if val <= 0b111 {
+ Ok(Self(val))
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ format!(
+ "invalid raw led value: {:#b}; must not exceed 3 b",
+ val
+ ),
+ ))
+ }
+ }
+}
+
+#[cfg(test)]
+mod kb_led_state
+{
+ use super::KbLeds;
+
+ #[test]
+ fn create()
+ {
+ assert_eq!(0u8, KbLeds::new(false, false, false).into());
+ assert_eq!(1u8, KbLeds::new(false, false, true).into());
+ assert_eq!(2u8, KbLeds::new(false, true, false).into());
+ assert_eq!(4u8, KbLeds::new(true, false, false).into());
+ assert_eq!(6u8, KbLeds::new(true, true, false).into());
+
+ assert_eq!(0u8, KbLeds::from(0u8 as libc::c_char).into());
+ assert_eq!(1u8, KbLeds::from(1u8 as libc::c_char).into());
+ assert_eq!(2u8, KbLeds::from(2u8 as libc::c_char).into());
+ assert_eq!(4u8, KbLeds::from(4u8 as libc::c_char).into());
+ assert_eq!(6u8, KbLeds::from(6u8 as libc::c_char).into());
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct KbLedState(KbLeds);
+
+impl KbLedState
+{
+ pub fn new(cap: bool, num: bool, scr: bool) -> Self
+ {
+ Self(KbLeds::new(cap, num, scr))
+ }
+
+ #[inline]
+ pub fn get(con: &Console) -> io::Result<Self> { ioctl::kdgetled(con) }
+
+ #[inline]
+ pub fn set(&self, con: &Console) -> io::Result<()>
+ {
+ ioctl::kdsetled(con, Some(*self))
+ }
+
+ #[inline]
+ pub fn revert(con: &Console) -> io::Result<()>
+ {
+ ioctl::kdsetled(con, None)
+ }
+
+ #[inline]
+ pub fn cap(&self) -> bool { self.0.cap() }
+
+ #[inline]
+ pub fn num(&self) -> bool { self.0.num() }
+
+ #[inline]
+ pub fn scr(&self) -> bool { self.0.scr() }
+
+ #[inline]
+ pub fn set_cap(&mut self, set: bool) { self.0.set_cap(set) }
+
+ #[inline]
+ pub fn set_num(&mut self, set: bool) { self.0.set_num(set) }
+
+ #[inline]
+ pub fn set_scr(&mut self, set: bool) { self.0.set_scr(set) }
+}
+
+impl fmt::Display for KbLedState
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
+ {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl From<libc::c_char> for KbLedState
+{
+ fn from(leds: libc::c_char) -> Self { Self(KbLeds(leds as u8)) }
+}
+
+impl From<KbLedState> for libc::c_ulong
+{
+ fn from(state: KbLedState) -> Self { state.0 .0 as libc::c_ulong }
+}
+
+impl From<KbLedState> for u8
+{
+ fn from(state: KbLedState) -> Self { state.0 .0 }
+}
+
+impl TryFrom<u8> for KbLedState
+{
+ type Error = io::Error;
+
+ fn try_from(val: u8) -> io::Result<Self>
+ {
+ Ok(Self(KbLeds::try_from(val)?))
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct KbLedFlags
+{
+ flags: KbLeds,
+ default: KbLeds,
+}
+
+impl KbLedFlags
+{
+ pub fn new_flags(cap: bool, num: bool, scr: bool) -> Self
+ {
+ let flags = KbLeds::new(cap, num, scr);
+ let default = KbLeds::off();
+ Self { flags, default }
+ }
+
+ pub fn new(
+ fcap: bool,
+ fnum: bool,
+ fscr: bool,
+ dcap: bool,
+ dnum: bool,
+ dscr: bool,
+ ) -> Self
+ {
+ let flags = KbLeds::new(fcap, fnum, fscr);
+ let default = KbLeds::new(dcap, dnum, dscr);
+ Self { flags, default }
+ }
+
+ #[inline]
+ pub fn get(con: &Console) -> io::Result<Self> { ioctl::kdgkbled(con) }
+
+ #[inline]
+ pub fn set(&self, con: &Console) -> io::Result<()>
+ {
+ ioctl::kdskbled(con, *self)
+ }
+
+ #[inline]
+ pub fn cap(&self) -> bool { self.flags.cap() }
+
+ #[inline]
+ pub fn num(&self) -> bool { self.flags.num() }
+
+ #[inline]
+ pub fn scr(&self) -> bool { self.flags.scr() }
+
+ #[inline]
+ pub fn default_cap(&self) -> bool { self.default.cap() }
+
+ #[inline]
+ pub fn default_num(&self) -> bool { self.default.num() }
+
+ #[inline]
+ pub fn default_scr(&self) -> bool { self.default.scr() }
+
+ #[inline]
+ pub fn set_cap(&mut self, set: bool) { self.flags.set_cap(set) }
+
+ #[inline]
+ pub fn set_num(&mut self, set: bool) { self.flags.set_num(set) }
+
+ #[inline]
+ pub fn set_scr(&mut self, set: bool) { self.flags.set_scr(set) }
+
+ #[inline]
+ pub fn set_default_cap(&mut self, set: bool) { self.default.set_cap(set) }
+
+ #[inline]
+ pub fn set_default_num(&mut self, set: bool) { self.default.set_num(set) }
+
+ #[inline]
+ pub fn set_default_scr(&mut self, set: bool) { self.default.set_scr(set) }
+}
+
+impl From<KbLedFlags> for u8
+{
+ fn from(state: KbLedFlags) -> Self
+ {
+ state.flags.0 | (state.default.0 << 0x4)
+ }
+}
+
+impl TryFrom<u8> for KbLedFlags
+{
+ type Error = io::Error;
+
+ /** From the manpage:
+
+ The low order three bits (mask 0x7) get the current flag state,
+ and the low order bits of the next nibble (mask 0x70) get the
+ default flag state.
+ */
+ fn try_from(val: u8) -> io::Result<Self>
+ {
+ let flags = val & (ioctl::KD_KBLED_STATE_MASK as u8);
+ let default = val & (ioctl::KD_KBLED_DEFAULT_MASK as u8) >> 4;
+ let flags = KbLeds::try_from(flags)?;
+ let default = KbLeds::try_from(default)?;
+
+ Ok(Self { flags, default })
+ }
+}
+
+impl fmt::Display for KbLedFlags
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
+ {
+ write!(f, "[flags: {}; default: {}]", self.flags, self.default)
+ }
}
#[derive(Debug)]
@@ -427,14 +846,17 @@ macro_rules! byte_of_hex {
};
}
-struct Rgb(u8, u8, u8);
+pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb
{
+ #[inline]
fn r(&self) -> u8 { self.0 }
+ #[inline]
fn g(&self) -> u8 { self.1 }
+ #[inline]
fn b(&self) -> u8 { self.2 }
}
@@ -452,6 +874,18 @@ impl TryFrom<&[u8; 6]> for Rgb
}
}
+impl From<[u8; 3]> for Rgb
+{
+ fn from(bytes: [u8; 3]) -> Self
+ {
+ let r = bytes[0];
+ let g = bytes[1];
+ let b = bytes[2];
+
+ Self(r, g, b)
+ }
+}
+
impl From<u32> for Rgb
{
fn from(rgb: u32) -> Self
@@ -604,11 +1038,12 @@ impl Palette
}
let mut res = Self::new();
- res.0.copy_from_slice(&b);
+ res.0.copy_from_slice(b);
Ok(res)
}
+ pub fn iter(&self) -> PaletteIterator { PaletteIterator::new(&self) }
/* [Palette::from_stdin] */
} /* [impl Palette] */
@@ -690,6 +1125,99 @@ impl From<&RawPalette> for Palette
}
}
+#[cfg(feature = "gui")]
+impl From<&[slint::Color]> for Palette
+{
+ fn from(colors: &[slint::Color]) -> Self
+ {
+ let mut idx: usize = 0;
+ let mut pal: [u8; PALETTE_BYTES] = [0; PALETTE_BYTES];
+
+ for &col in colors.iter() {
+ pal[idx] = col.red();
+ pal[idx + 1] = col.green();
+ pal[idx + 2] = col.blue();
+ idx += 3;
+ }
+
+ Self(pal)
+ }
+}
+
+/** Convert palette to the default text format so it can be parsed as a scheme. */
+impl Into<String> for Palette
+{
+ fn into(self) -> String
+ {
+ let mut acc = String::with_capacity(16 * 10);
+ for i in 0..PALETTE_SIZE {
+ let idx = i * 3;
+ let (r, g, b) = (self.0[idx], self.0[idx + 1], self.0[idx + 2]);
+ acc.push_str(&format!("{}#{:02.x}{:02.x}{:02.x}\n", i, r, g, b));
+ }
+ acc
+ }
+}
+
+#[test]
+fn palette_dump_as_text()
+{
+ let pal = Palette::from(&SOLARIZED_COLORS_DARK);
+ let txt = indoc::indoc! { r#"
+ 0#002b36
+ 1#dc322f
+ 2#859900
+ 3#b58900
+ 4#268bd2
+ 5#d33682
+ 6#2aa198
+ 7#eee8d5
+ 8#002b36
+ 9#cb4b16
+ 10#586e75
+ 11#657b83
+ 12#839496
+ 13#6c71c4
+ 14#93a1a1
+ 15#fdf6e3
+ "#};
+
+ let pal: String = pal.into();
+ assert_eq!(pal, txt);
+}
+
+pub struct PaletteIterator
+{
+ pal: Palette,
+ cur: usize,
+}
+
+impl PaletteIterator
+{
+ fn new(pal: &Palette) -> Self { Self { pal: pal.clone(), cur: 0 } }
+}
+
+impl Iterator for PaletteIterator
+{
+ type Item = Rgb;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ if self.cur >= PALETTE_SIZE {
+ None
+ } else {
+ let off = self.cur * 3;
+ let rgb = Rgb::from([
+ self.pal.0[off],
+ self.pal.0[off + 1],
+ self.pal.0[off + 2],
+ ]);
+ self.cur += 1;
+ Some(rgb)
+ }
+ }
+}
+
const CONSOLE_PATHS: [&str; 6] = [
"/proc/self/fd/0",
"/dev/tty",
@@ -728,7 +1256,7 @@ impl Console
Ok(fd)
}
- fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self>
+ pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self>
{
let p =
std::ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap();
@@ -750,7 +1278,7 @@ impl Console
Err(io::Error::new(
io::ErrorKind::Other,
- format!("could not retrieve fd for any of the search paths"),
+ String::from("could not retrieve fd for any of the search paths"),
))
}
@@ -805,6 +1333,20 @@ impl Console
}
}
+impl Drop for Console
+{
+ fn drop(&mut self)
+ {
+ if unsafe { libc::close(self.0) } == -1 {
+ eprintln!(
+ "Console: error closing fd {}: {}",
+ self.0,
+ Error::last_os_error()
+ );
+ }
+ }
+}
+
impl AsRawFd for Console
{
fn as_raw_fd(&self) -> RawFd { self.0 }
diff --git a/src/vtcol.rs b/src/vtcol.rs
index 9a78d2f..baf5ae3 100644
--- a/src/vtcol.rs
+++ b/src/vtcol.rs
@@ -1,6 +1,10 @@
+#![allow(clippy::option_map_unit_fn)]
+
pub mod lib;
-use vtcol::{Console, Fade, Palette, Scheme};
+#[cfg(feature = "gui")] pub mod edit;
+
+use vtcol::{Console, Fade, KbLedFlags, KbLedState, Palette, Scheme};
use anyhow::{anyhow, Result};
use std::{io::{self, BufWriter},
@@ -18,15 +22,240 @@ macro_rules! vrb {
)}
}
-/* struct Job -- Runtime parameters.
- */
+/** Helper for choosing the console. Defaults to the current one if
+none is explicitly supplied. */
+#[inline]
+fn open_console(path: Option<&str>) -> io::Result<Console>
+{
+ path.map(Console::from_path).unwrap_or_else(Console::current)
+}
+
+/** Trait for subcommands to implement. */
+trait Run
+{
+ fn run(self, console: Option<String>) -> Result<()>;
+}
+
#[derive(Debug)]
-enum Job
+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<bool>, Option<bool>, Option<bool>),
+}
+
+impl Run for LedJob
+{
+ fn run(self, console: Option<String>) -> 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<String>, 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<String>, 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<String>,
+ cap: Option<bool>,
+ num: Option<bool>,
+ scr: Option<bool>,
+ ) -> 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<String>) -> 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<bool>, Option<bool>, Option<bool>),
+}
+
+impl FlagJob
+{
+ /** Get the keyboard flags (modifier locks). */
+ fn get(con: Option<String>, 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<String>, 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<String>,
+ target: FlagTarget,
+ cap: Option<bool>,
+ num: Option<bool>,
+ scr: Option<bool>,
+ ) -> 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<String>) -> 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<String>) -> 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<String>, Scheme),
/** Switch to color scheme. */
Set(Scheme),
/** Get currently active scheme. */
@@ -37,253 +266,34 @@ enum Job
Fade(Option<Scheme>, Scheme, Duration, u8, bool),
}
-impl<'a> Job
+impl Run for ColorJob
{
- pub fn from_argv() -> Result<Job>
+ fn run(self, console: Option<String>) -> 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(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 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),
- )
- .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),
- ),
- )
- .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(b64) = subm.value_of("base64") {
- let scheme = Scheme::from_base64(b64)?;
- return Ok(Self::Dump(scheme));
- }
- 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)) => {
- if let Some(b64) = subm.value_of("base64") {
- let scheme = Scheme::from_base64(&b64)?;
- return Ok(Self::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::Set(scheme))
- },
- ("get", Some(subm)) => Ok(Self::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::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::Fade(
- from.map(Scheme::from),
- Scheme::from(to),
- dur,
- hz,
- clear,
- )),
- }
- },
- (junk, _) =>
- Err(anyhow!(
- "invalid subcommand [{}]; try ``{} --help``",
- junk,
- clap::crate_name!()
- )),
+ 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] */
- fn list_schemes()
+impl ColorJob
+{
+ fn list() -> Result<()>
{
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()
+ Ok(())
}
fn dump(scm: Scheme) -> Result<()>
@@ -318,37 +328,31 @@ impl<'a> Job
}
}
- fn run(self) -> Result<()>
+ #[cfg(feature = "gui")]
+ fn edit(name: Option<String>, scm: Scheme) -> Result<()>
{
- match self {
- Self::Dump(scm) => Self::dump(scm)?,
- Self::List => Self::list_schemes(),
- Self::Set(scm) => Self::set_scheme(scm)?,
- Self::Get(b64) => Self::get_scheme(b64)?,
- Self::Toggle(one, two) => Self::toggle_scheme(one, two)?,
- Self::Fade(from, to, ms, hz, clear) =>
- Self::fade(from, to, ms, hz, clear)?,
- }
+ vrb!("Launching color scheme editor for scheme {}", scm);
+ let editor = crate::edit::Edit::new(name, scm);
- Ok(())
+ editor.run()
}
- fn set_scheme(scheme: Scheme) -> Result<()>
+ fn set(con: Option<String>, scheme: Scheme) -> Result<()>
{
- let con = Console::current()?;
- vrb!("console fd: {}", con);
+ let fd = open_console(con.as_deref())?;
+ vrb!("console fd: {}", fd);
- con.apply_scheme(&scheme)?;
- con.clear()?;
+ 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_scheme(b64: bool) -> Result<()>
+ fn get(con: Option<String>, b64: bool) -> Result<()>
{
- let fd = Console::current()?;
+ let fd = open_console(con.as_deref())?;
vrb!("console fd: {}", fd);
let scm = fd.current_scheme()?;
@@ -365,15 +369,18 @@ impl<'a> Job
/** Toggle between two schemes. Defaults to ``one`` in case neither scheme
is active.
*/
- fn toggle_scheme(one: Scheme, two: Scheme) -> Result<()>
+ fn toggle(con: Option<String>, one: Scheme, two: Scheme) -> Result<()>
{
- let fd = Console::current()?;
- vrb!("console fd: {}", fd);
+ let pal = {
+ let fd = open_console(con.as_deref())?;
+ vrb!("console fd: {}", fd);
+ fd.current_palette()?
+ };
- if fd.current_palette()? == Palette::try_from(&one)? {
- Self::set_scheme(two)
+ if pal == Palette::try_from(&one)? {
+ Self::set(con, two)
} else {
- Self::set_scheme(one)
+ Self::set(con, one)
}
}
@@ -381,6 +388,7 @@ impl<'a> Job
If ``from`` is ``None``, the current palette is used as starting point. */
fn fade(
+ con: Option<String>,
from: Option<Scheme>,
to: Scheme,
dur: Duration,
@@ -388,7 +396,7 @@ impl<'a> Job
clear: bool,
) -> Result<()>
{
- let fd = Console::current()?;
+ let fd = open_console(con.as_deref())?;
vrb!("console fd: {}", fd);
let from = if let Some(from) = from {
@@ -403,6 +411,672 @@ impl<'a> Job
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<String>, Subcmd);
+
+impl<'a> Job
+{
+ pub fn from_argv() -> Result<Job>
+ {
+ 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<()>