diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | Makefile | 18 | ||||
-rw-r--r-- | README.rst | 87 | ||||
-rw-r--r-- | doc/img/phosphor.png | bin | 0 -> 16312 bytes | |||
-rw-r--r-- | doc/img/solarized.png | bin | 0 -> 20639 bytes | |||
-rw-r--r-- | doc/vtcol.rst | 180 | ||||
-rw-r--r-- | flake.lock | 94 | ||||
-rw-r--r-- | flake.nix | 54 | ||||
-rw-r--r-- | misc/nixos/pkgs/os-specific/linux/vtcol/default.nix | 57 | ||||
-rw-r--r-- | src/edit.rs | 600 | ||||
-rw-r--r-- | src/lib.rs | 592 | ||||
-rw-r--r-- | src/vtcol.rs | 1204 |
13 files changed, 2557 insertions, 338 deletions
@@ -12,6 +12,8 @@ todo # cargo junk Cargo.lock +misc/nixos/pkgs/os-specific/linux/vtcol/cargo-lock.patch # manpage doc/vtcol.1.gz + @@ -5,9 +5,9 @@ [package] name = "vtcol" description = "Set Linux console color scheme" -version = "0.42.5" +version = "0.42.7" authors = [ "Philipp Gesang <phg@phi-gamma.net>" ] -repository = "https://github.com/phi-gamma/vtcol" +repository = "https://gitlab.com/phgsng/vtcol" keywords = [ "linux", "virtual_terminal", "tty", "console", "system" ] readme = "README.rst" license = "GPL-3.0" @@ -15,12 +15,15 @@ edition = "2021" [dependencies] libc = "0.2" +indoc = "1.0" clap = { version = "2.33", optional = true } anyhow = { version = "1.0", optional = true } base64 = "0.13" +slint = { version = "0.2", optional = true } [features] vtcol-bin = [ "anyhow", "clap" ] +gui = [ "vtcol-bin", "slint" ] [[bin]] name = "vtcol" @@ -7,8 +7,10 @@ lib-src = src/lib.rs src = $(bin-src) $(lib-src) meta = Cargo.toml rustdoc-entry = target/doc/vtcol/index.html +cargo-lock = Cargo.lock +cargo-lock-patch= misc/nixos/pkgs/os-specific/linux/vtcol/cargo-lock.patch -all: bin lib doc +all: bin lib doc nix check: $(src) cargo test @@ -23,6 +25,18 @@ man: $(manpage) rustdoc: $(rustdoc-entry) +nix: lockpatch + +lockpatch: $(cargo-lock-patch) + +$(cargo-lock-patch): $(meta) + rm -f -- $(cargo-lock) + cargo update + cargo generate-lockfile + mkdir -p tmp + mv -f -- $(cargo-lock) tmp/ + diff -u /dev/null tmp/$(cargo-lock) >$(cargo-lock-patch) ; : + $(lib): $(lib-src) $(meta) cargo build --release @@ -37,6 +51,8 @@ $(rustdoc-entry): $(src) $(meta) clean: rm -f -- $(manpage) + rm -f -- $(cargo-lock) rm -rf -- $(cargo-target) + rm -rf -- tmp .PHONY: clean check @@ -2,27 +2,38 @@ VTCOL ############################################################################### -Change the color scheme of the virtual Linux console. Inspired by the -setcolors_ utility. +Change the color scheme (and more) of the virtual Linux console. -Usage ------ -**vtcol** knows two ways of loading a color scheme: From a set of predefined -palettes or by loading it from a definition file. The latter accepts input in -the format supported by setcolors_. (Not much effort has been put into ensuring -compliance so YMMV.) Check the subdirectory ``./schemes`` in the **vtcol** tree -for examples of definition files. +Color schemes +------------------------------------------------------------------------------- -Three color schemes are predefined: +Use ``vtcol colors`` to manipulate the console palette. + +.. image:: ./doc/img/solarized.png + +**vtcol** loads color schemes from various source: From a set of predefined +palettes, from a binary dump or by loading it from a definition file. The +latter supports input in the format used by setcolors_. (Not much effort has +been put into ensuring compliance so YMMV.) Check the subdirectory +``./schemes`` in the **vtcol** tree for examples of definition files. + +Four color schemes are predefined: * ``default`` the default color scheme of the Linux console. * ``solarized`` the Solarized_ color scheme, dark version. * ``solarized_light`` the Solarized_ color scheme, light version. + * ``phosphor`` monochrome green text on black background. Invoke **vtcol** with the ``set`` command specifying the scheme of your choice: :: - $ vtcol set solarized_light + $ vtcol colors set solarized_light + +Or for a more 80s look’n’feel: :: + + $ vtcol colors set phosphor + +.. image:: ./doc/img/phosphor.png In order to view the available schemes, use the ``list`` command. Should the scheme specified not resolve to one of the predefined ones, **vtcol** will fall @@ -30,39 +41,71 @@ back on interpreting the name as that of a file. Likewise, loading a scheme directly from a definition file is accomplished by specifying the ``--file`` argument to ``set``. :: - $ vtcol set --file ./schemes/solarized + $ vtcol colors set --file ./schemes/solarized Instead of an actual scheme or file name, these parameters accept ``-`` as an argument in order to read from ``stdin``. :: - $ vtcol set - + $ vtcol colors set - To show the current scheme of the active console use the ``get`` subcommand: :: - $ vtcol get + $ vtcol colors get solarized_dark +With the ``-base64`` switch ``vtcol`` outputs a binary representation of color +schemes: :: + + $ vtcol colors get --base64 + ACs23DIvhZkAtYkAJovS0zaCKqGY7ujVACs2y0sWWG51ZXuDg5SWbHHEk6Gh/fbj + +which can then be re-applied with ``vtcol set``:: + + $ vtcol colors set --base64 \ + ACs23DIvhZkAtYkAJovS0zaCKqGY7ujVACs2y0sWWG51ZXuDg5SWbHHEk6Gh/fbj + It is also possible to use vtcol to switch between two themes by means of the ``toggle`` subcommand. E. g. to cycle between “dark mode” and “light mode”: :: - $ vtcol toggle solarized solarized_light + $ vtcol colors toggle solarized solarized_light To view a scheme’s definition, for instance in order to verify that **vtcol** parses it correctly, use the ``dump`` command. :: - $ vtcol dump default - $ vtcol dump ./schemes/solarized + $ vtcol colors dump default + $ vtcol colors dump ./schemes/solarized This will print the color definitions contained in the scheme; if the specified name does not resolve to a pre-defined scheme it will be interpreted as a file name instead. +Keyboard LEDs and flags +------------------------------------------------------------------------------- + +Keyboard handling is grouped under the ``vtcol kb`` subcommand. + +Show the current state of the keyboard LEDs: :: + + $ vtcol kb leds get + caps: false, num: true, scroll: false + +Set the state of the Caps Lock LED (does *not* affect the Caps Lock state, just +the LED itself): :: + + $ vtcol leds set --caps on + +Likewise for the keyboad state (*not* the LEDs): :: + + $ vtcol kb flags get + [flags: caps: false, num: false, scroll: false; default: caps: false, num: false, scroll: false] + + Building --------- +------------------------------------------------------------------------------- Use Cargo to obtain a binary: :: - $ cargo build + $ cargo build --features=vtcol-bin To generate the manpage, run :: @@ -71,7 +114,8 @@ To generate the manpage, run :: *rst2man* from the *Docutils* suite needs to be installed for this. Background ----------- +------------------------------------------------------------------------------- + The default palette that comes with a Linux terminal was inherited from a long history of virtual console implementations. The colors assigned were chosen for pragmatic reasons but that palette may not harmonize with everybody’s taste. @@ -83,7 +127,8 @@ included as predefined palettes; the same is true of the Linux default palette, so they can be conveniently restored when experimenting. About ------ +------------------------------------------------------------------------------- + The **vtcol** source code is available from the `canonical repository`_. **vtcol** is redistributable under the terms of the `GNU General Public License`_ version 3 (exactly). Patches or suggestions welcome. diff --git a/doc/img/phosphor.png b/doc/img/phosphor.png Binary files differnew file mode 100644 index 0000000..5b4e29f --- /dev/null +++ b/doc/img/phosphor.png diff --git a/doc/img/solarized.png b/doc/img/solarized.png Binary files differnew file mode 100644 index 0000000..9305eb7 --- /dev/null +++ b/doc/img/solarized.png diff --git a/doc/vtcol.rst b/doc/vtcol.rst index 2e3c533..a6843da 100644 --- a/doc/vtcol.rst +++ b/doc/vtcol.rst @@ -3,18 +3,18 @@ =============================================================================== ******************************************************************************* - color schemes for the Linux™ console + color schemes for the Linux™ console ******************************************************************************* -:Date: 2021-11-10 -:Version: 0.42.5 +:Date: 2022-01-01 +:Version: 0.42.7 :Manual section: 1 :Manual group: console Synopsis ------------------------------------------------------------------------------- -**vtcol** [--help] [--version] [--verbose] <command> [<args>] +**vtcol** [--help] [--version] [--verbose] [--console <con>] <command> [<args>] Description ------------------------------------------------------------------------------- @@ -25,24 +25,35 @@ using ``ioctl_console(2)`` syscalls. vtcol commands ------------------------------------------------------------------------------- -``vtcol`` actions are provided by these these subcommands: +``vtcol`` actions are grouped into subcommands depending on the functionality +they operate on: **help** Prints this message or the help of the given subcommand(s) +**colors** + + Inspect and manipulate the console palette. + +**kb** + + Keyboard manipulation (LEds, modifiers). + +vtcol colors commands +##################### **set** Set the scheme for the current terminal with ``PIO_CMAP``. :: - $ vtcol set solarized + $ vtcol colors set solarized **list** List predefined schemes. :: - $ vtcol list + $ vtcol colors list 4 color schemes available: * solarized * solarized_light @@ -63,14 +74,14 @@ vtcol commands against the builtin schemes; if a scheme matches, only its preferred name is printed (e. g. ``solarized_light``). :: - $ vtcol get + $ vtcol colors get solarized Otherwise the palette is printed as with the ``dump`` subcommand. **toggle** - Like ``vtcol set`` but supports two ``SCHEME`` arguments. + Like ``vtcol colors set`` but supports two ``SCHEME`` arguments. First the active scheme is checked against *the first positional argument*; if there is a match, the scheme specified by the *second argument* will be @@ -78,22 +89,87 @@ vtcol commands of the same command the scheme is toggled between the two arguments. E. g. use: :: - $ vtcol toggle solarized solarized_light + $ vtcol colors toggle solarized solarized_light to cycle the console palette between “dark mode” and “light mode”. -set options +**fade** + + Transition between two color schemes with a fading effect. If no starting + color scheme is specified, the current one is used. :: + + $ vtcol colors fade --ms 1337 --from solarized --to solarized_light + +vtcol kb leds commands +###################### + +**get** + + Get the current LED state. Example: :: + + $ vtcol leds get + caps: false, num: false, scroll: false + +**set** + + Set the state of individual keyboard LEDs: :: + + $ vtcol leds set --caps on --num off + + Not that this command only affects the LEDs themselves. It will not change + the state of the corresponding modifier locks. Revert the LEDs to normal: + :: + + $ vtcol leds set --revert + +vtcol kb flags commands +###################### + +**get** + + Get the current keyboard flags. Example: :: + + $ vtcol -C /dev/tty6 kb flags get + [flags: caps: false, num: false, scroll: false; default: caps: false, num: false, scroll: false] + +**set** + + Set the state of individual keyboard LEDs: :: + + $ vtcol flags set --caps on --num off + + +global options +------------------------------------------------------------------------------- + +The options described in this section are not specific to a subcommand and can +be used everywhere on the command line. + +``-v, --verbose`` + + Enable some extra status messages. + +``-C, --console CON`` + + Operate on the console located at the path ``CON`` instead of the active + one. Usually this is one of the ``/dev/tty*`` devices. For example, to + get the scheme on a console some *getty*: :: + + $ vtcol colors get \ + --console $(readlink /proc/$(pgrep getty| head -1)/fd/0) + +colors set options ------------------------------------------------------------------------------- :: - vtcol set [--file FILE] + vtcol colors set [--file FILE] - vtcol set [SCHEME] + vtcol colors set [SCHEME] ``SCHEME`` Load the predefined color scheme *SCHEME*. For a list of options, use - ``vtcol list``. Alternatively, ``-`` can be used to specify *stdin*. + ``vtcol colors list``. Alternatively, ``-`` can be used to specify *stdin*. ``-f FILE, --file FILE`` @@ -102,10 +178,10 @@ set options Mutually exclusive with *SCHEME*. -toggle options +colors toggle options ------------------------------------------------------------------------------- -`vtcol toggle ONE TWO`` +`vtcol colors toggle ONE TWO`` ``ONE`` @@ -117,42 +193,98 @@ toggle options Second scheme to alternate between. Will be loaded if *ONE* is the active scheme. +colors fade options +------------------------------------------------------------------------------- +:: + + vtcol colors fade [--clear] [--frequency HZ] [--ms MS] + [--from START] --to END + +``END`` + + The scheme to transition to. After completion this will remain the active + scheme of the current console. + +``START`` + + Load the predefined color scheme *SCHEME* before starting the transition. + +``-f HZ, --frequency HZ`` + + Screen update rate in ``HZ / s``. + +``-m MS, --ms MS`` + + Duration of the transition in milliseconds. + +``-c, --clear`` + + Whether to clear the console after each update step. This causes the + background color to be applied to the entire screen and not just the parts + that changed for a more uniform transition. The downside of ``--clear`` is + that all text is erased as well. + Scheme file syntax overview ------------------------------------------------------------------------------- -.. TODO:: unimplemented!() +Scheme files use a trivial line-based grammar: whatever hexadecimal RGB color +expression is found first on a line is used as the next color until the whole +palette of 16 colors is reached. :: + + $ cat schemes/zebra + 00#000000 white + 01#ffffff black + 02#000000 white + … + 15#ffffff black + +Everything before and after the color expression will be ignored. If the end +of the scheme file is reached without having assigned all colors, the remainder +will be set to zero (RGB 0, 0, 0). Examples ------------------------------------------------------------------------------- 1. Get the current color scheme: :: - $ vtcol get + $ vtcol colors get 2. Turn your screen into an 80s style monochrome lookalike: :: - $ vtcol set phosphor + $ vtcol colors set phosphor 3. Reset console to default colors: :: - $ vtcol set default + $ vtcol colors set default 4. Set color scheme from stdin: :: - $ <./schemes/zebra vtcol set - + $ <./schemes/zebra vtcol colors set - 5. List available builtin schemes: :: - $ vtcol list + $ vtcol colors list 6. Cycle between night mode and day mode: :: - $ vtcol toggle solarized solarized_light + $ vtcol colors toggle solarized solarized_light + +7. Transition from dark to light scheme while dumping some data for effect: :: + + $ vtcol colors fade -m 1500 -f solarized -t solarized_light & dmesg + +8. Dump current scheme in binary form: :: + + $ vtcol colors get --base64 + +9. Get the color scheme of a specific console: :: + + $ vtcol colors get --console /dev/tty6 Copyright ------------------------------------------------------------------------------- -Copyright: 2015-2021 Philipp Gesang +Copyright: 2015-2022 Philipp Gesang This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f04c2d7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,94 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1670064435, + "narHash": "sha256-+ELoY30UN+Pl3Yn7RWRPabykwebsVK/kYE9JsIsUMxQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "61a8a98e6d557e6dd7ed0cdb54c3a3e3bbc5e25c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1665296151, + "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1670207212, + "narHash": "sha256-uuKbbv0L+QoXiqO7METP9BihY0F7hJqGdKn7xDVfyFw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "18823e511bc85ed27bfabe33cccecb389f9aa92d", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..75077af --- /dev/null +++ b/flake.nix @@ -0,0 +1,54 @@ +{ + description = "vtcol development shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + with pkgs; + { + devShells.default = mkShell { + LD_LIBRARY_PATH = lib.makeLibraryPath [ + libGL + libxkbcommon + wayland + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + ]; + + buildInputs = [ + cmake + fontconfig + libglvnd + openssl + pkg-config + pkgconfig + rust-bin.beta.latest.default + xorg.libX11 + xorg.libX11.dev + xorg.libXcursor + xorg.libXext + xorg.libXft + xorg.libXi + xorg.libXrandr + xorg.libXrender + xorg.libXt + xorg.xorgproto + xorg.xorgserver + ]; + }; + } + ); +} diff --git a/misc/nixos/pkgs/os-specific/linux/vtcol/default.nix b/misc/nixos/pkgs/os-specific/linux/vtcol/default.nix new file mode 100644 index 0000000..dec074b --- /dev/null +++ b/misc/nixos/pkgs/os-specific/linux/vtcol/default.nix @@ -0,0 +1,57 @@ +{ lib, stdenv +, cpio +, docutils +, rust +, installShellFiles +, fetchFromGitLab +, rustPlatform +}: + +rustPlatform.buildRustPackage rec { + pname = "vtcol"; + version = "0.42.5"; + + src = fetchFromGitLab { + domain = "gitlab.com"; + owner = "phgsng"; + repo = pname; + rev = "d6e8a37767644b50001e14ebbab594e799a94db7"; + sha256 = "sha256-fHWxiuCFO+N+ubPCS/Is64V2pLzh7xozDcRo7pQLMBg="; + }; + + cargoSha256 = "sha256-NSwn3vavrsPr+qn6oQY6kO5d9TKrxwcnqO7a3qpBphs=="; + + nativeBuildInputs = [ docutils cpio installShellFiles ]; + + buildFeatures = [ "vtcol-bin" ]; + + cargoPatches = [ ./cargo-lock.patch ]; + + target = rust.toRustTargetSpec stdenv.hostPlatform; + + # building the manpage requires docutils/rst2man + postInstall = '' + echo "create and install manpage" + make man + installManPage doc/vtcol.1.gz + + echo "create cpio archive for initrd" + binout="target/${target}/release/${pname}" + ls "$binout" + mkdir -p -- tmp/usr/bin + ls + ls tmp + cp -f -- "$binout" tmp/usr/bin + pushd tmp + <<<usr/bin/${pname} cpio -ov -H ustar >${pname}.cpio + + install -D -m644 ${pname}.cpio $out/share/vtcol/${pname}.cpio + ''; + + meta = with lib; { + description = "Color schemes for the Linux console"; + homepage = "https://gitlab.com/phgsng/vtcol"; + license = licenses.gpl3; + mainProgram = "vtcol"; + }; +} 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) + }) + } +} @@ -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<()> |