summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml7
-rw-r--r--Makefile18
-rw-r--r--README.rst87
-rw-r--r--doc/img/phosphor.pngbin0 -> 16312 bytes
-rw-r--r--doc/img/solarized.pngbin0 -> 20639 bytes
-rw-r--r--doc/vtcol.rst180
-rw-r--r--flake.lock94
-rw-r--r--flake.nix54
-rw-r--r--misc/nixos/pkgs/os-specific/linux/vtcol/default.nix57
-rw-r--r--src/edit.rs600
-rw-r--r--src/lib.rs592
-rw-r--r--src/vtcol.rs1204
13 files changed, 2557 insertions, 338 deletions
diff --git a/.gitignore b/.gitignore
index ae0df3a..88c4ac6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ todo
# cargo junk
Cargo.lock
+misc/nixos/pkgs/os-specific/linux/vtcol/cargo-lock.patch
# manpage
doc/vtcol.1.gz
+
diff --git a/Cargo.toml b/Cargo.toml
index d743b4f..acc3138 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/Makefile b/Makefile
index a8db336..50dd1b2 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.rst b/README.rst
index 21b80b2..2631ac8 100644
--- a/README.rst
+++ b/README.rst
@@ -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
new file mode 100644
index 0000000..5b4e29f
--- /dev/null
+++ b/doc/img/phosphor.png
Binary files differ
diff --git a/doc/img/solarized.png b/doc/img/solarized.png
new file mode 100644
index 0000000..9305eb7
--- /dev/null
+++ b/doc/img/solarized.png
Binary files differ
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)
+ })
+ }
+}
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<()>