Adventures In Computing: RGB Keyboard Control From Emacs
One of the problems with doing machine learning and artificial intelligence at home for fun is that you need hardware. Specifically, you need storage space for data, and memory for computation. At the moment, the easiest way to get a system that has plenty of storage and memory is to purchase a laptop designed for gaming. When I was looking for a new laptop last year, I settled on an ASUS TUF gaming laptop with 2TB of storage (1TB SSD, 1TB HDD), and 32 GB of RAM. Plenty! Here’s the neofetch:
Buying a gaming laptop had an additional side benefit: an RGB keyboard. The native control software is Windows-only (so it’s nice for when I’m playing Halo or Skyrim), but the OpenRGB project is an excellent open-source software suite for controlling many types of RGB hardware. It’s available for Linux, and it works with my TUF keyboard.
Then I wondered if I could sync the keyboard to what I’m doing on the computer. Specifically, I wanted to control the keyboard color from Emacs, and make the color match whatever language I’m using.
Major mode detection
Emacs editing modes come in two flavors:
major
and
minor. While
both major and minor modes alter the behavior of Emacs, major modes
are typically more wide-ranging, and importantly, only one major mode
can be active at a time. That means you can’t have python-mode
and
markdown-mode
active at the same time. Emacs being emacs, there is a
variable that holds the name of the current major mode. Appropriately,
it’s called major-mode
. In IELM:
*** Welcome to IELM *** Type (describe-mode) for help.
ELISP> major-mode
inferior-emacs-lisp-mode
ELISP>
Excellent! This mode is set per-buffer, so it will be correct for whatever you’re currently doing.
Connecting to OpenRGB
To use the OpenRGB software, I decided it would be easiest to use the
command-line interface (CLI) for the tool. It’s easy to locate the
executable on the PATH
:
(defcustom rgb-executable (executable-find "openrgb")
"Executable for controlling RGB hardware."
:group 'rgb
:type '(string))
OpenRGB requires a few arguments. It needs the device ID and the color/pattern to apply. This can be defined in a package:
(defcustom rgb-default-device-id nil
"Default device ID to control."
:group 'rgb
:type '(string))
To define the colors and patterns to use, I designed a data structure that’s a mapping of major mode to arguments passed to OpenRGB:
(defcustom rgb-argmap (cl-pairlis '() '())
"Mapping of major modes to associated RGB setting parameters."
:group 'rgb
:type '(alist
:key-type (symbol :tag "Major mode")
:value-type (repeat
:tag "Items"
(alist :tag "Parameters"
:key-type (symbol :tag "Parameter")
:value-type (string :tag "Value")))))
The use-package
configuration is straightforward:
(use-package rgb
:functions
rgb-mode
:commands
rgb-mode
:init
(setq rgb-device-ids (list "0"))
(setq rgb-argmap
(cl-pairlis
'(org-mode perl-mode python-mode emacs-lisp-mode rust-mode markdown-mode c-mode c++-mode)
'(((color . "#800080") (mode . "Static")) ;; org-mode
((color . "#FF0000") (mode . "Strobe")) ;; perl-mode
((color . "#FFFF00") (mode . "Static")) ;; python-mode
((color . "#800080") (mode . "Static")) ;; emacs-lisp-mode
((color . "#FFA500") (mode . "Static")) ;; rust-mode
((color . "#FFC0CB") (mode . "Static")) ;; markdown-mode
((color . "#0000FF") (mode . "Static")) ;; c-mode
((color . "#0000FF") (mode . "Static")) ;; c++-mode
)))
:config
(rgb-mode))
This sets up a few nice colors and patterns to trigger for various
mode. org-mode
, for example, will turn the keyboard a nice Emacs
purple. perl-mode
, on the other hand, will set the keyboard to flash
bright red (danger, Will Robinson, danger).
Triggering OpenRGB
To actually connect with OpenRGB, I wrote a function that takes four
optional arguments: device
, color
, light-mode
, and zone
(some
hardware supports multiple colors/patterns at once). For each non-nil
optional argument, it appends the correct flag and value to the
command string. Finally, the rgb-executable
(which defaults to
openrgb
), is invoked with those arguments.
(cl-defun rgb-set-openrgb (&key device color light-mode zone)
"Execute RGB set command on DEVICE with arguments for COLOR, LIGHT-MODE, and ZONE."
(let ((args (list)))
(if device (set 'args (append args (list "-d" (format "%s" device)))))
(if color (set 'args (append args (list "-c" (format "%s" color)))))
(if light-mode (set 'args (append args (list "-m" (format "%s" light-mode)))))
(if zone (set 'args (append args (list "-z" (format "%s" zone)))))
(apply #'start-process
(append (list rgb-executable (format "*%s*" rgb-executable) rgb-executable)
args))))
Detecting major mode changes
Finally, we need to detect when the major mode changes, so we can know
when to update the lighting. Emacs offers a hook for that (because of
course it does): 'window-state-change-functions
can be hooked to
trigger behavior whenever the window state changes (which it does when
the buffer changes).
First, we write the actual function to call rgb-set-openrgb
:
(defun rgb-backend-openrgb (current-major-mode)
"Set RGB color based on CURRENT-MAJOR-MODE."
(let ((params-list (assoc-default current-major-mode rgb-argmap)))
(if params-list
(mapc (lambda (params-item)
(rgb-set-openrgb
:device (assoc-default 'device params-item 'equal rgb-default-device-id)
:color (substring (assoc-default 'color params-item) 1)
:light-mode (assoc-default 'mode params-item)
:zone (assoc-default 'zone params-item)))
params-list))))
This function checks for the current-major-mode
value in the
rgb-argmap
dictionary. If it finds a parameter list, it extracts the
arguments and passes them to the RGB control function.
Next we write the hook function that is invoked on the switch:
(defun rgb-change-hook (_window)
"Hook for handling buffer/window change."
(rgb-set-rgb-from-mode major-mode))
And the minor-mode hook that turns everything on:
(define-minor-mode rgb-mode
"Global RGB mode."
:global t
:lighter " RGB"
:group 'rgb
(if rgb-mode
(add-hook 'window-state-change-functions #'rgb-change-hook)
(remove-hook 'window-state-change-functions #'rgb-change-hook)))
Installation
You can download the code from MELPA, MELPA Stable, or get it directly off of GitLab.