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.