A few cases of literate configuration

A few cases of literate configuration

A post that arose from the discussion of literate configuration on the System Crafters Discord.

I am using the literate configuration strategy (based on Emacs’ Org Mode) to manage most of my configuration files. A piece of such a configuration can be as simple as an Org file, which is tangled to one or many plain-text configuration files, but it can be more.

In my opinion, a literate configuration can be more straightforward and concise than a “normal” one, thanks to Org Mode’s capabilities of working with source code. So here I present a few examples from my configuration where I think this is the case:

  • Managing system colors
  • Managing manifests for Guix profiles
  • Configuring modules in polybar

I hope you find something interesting here!

Colors

Let’s start with system colors.

My favorite color theme is Palenight (color codes), and I want to have one source of truth for these colors. Except for Emacs itself, which has doom-palenight (and in which I occasionally switch to doom-one-light, e.g. when reading a long text), it can be done rather nicely with Org Mode.

First, let’s define a table with all the color codes:

#+tblname: colors
| color         | key     | value   |
|---------------+---------+---------|
| black         | color0  | #292d3e |
| red           | color1  | #f07178 |
| green         | color2  | #c3e88d |
| yellow        | color3  | #ffcb6b |
| blue          | color4  | #82aaff |
| magenta       | color5  | #c792ea |
| cyan          | color6  | #89ddff |
| white         | color7  | #d0d0d0 |
| light-black   | color8  | #434758 |
| light-red     | color9  | #ff8b92 |
| light-green   | color10 | #ddffa7 |
| light-yellow  | color11 | #ffe585 |
| light-blue    | color12 | #9cc4ff |
| light-magenta | color13 | #e1acff |
| light-cyan    | color14 | #a3f7ff |
| light-white   | color15 | #ffffff |
| color-fg      |         | #000000 |

Contents of this table can then be accessed from a code block. Let’s define one to return the color code based on its name:

#+NAME: get-color
#+begin_src emacs-lisp :var table=colors name="black" quote=0
(let ((color (seq-some (lambda (e) (and (string= name (car e)) (nth 2 e))) table)))
  (if (> quote 0)
      (concat "\"" color "\"")
    color))
#+end_src

Evaluating this block of code should return color code, corresponding to “black”.

And the best part is that the results of evaluation of one code block can be included to others with noweb. For instance, here’s my zathura config:

#+begin_src conf-space :noweb yes :tangle .config/zathura/zathurarc
set abort-clear-search false
set guioptions cs
set selection-clipboard clipboard
set recolor true
map <C-r> set recolor false
map <C-R> set recolor true

set recolor-lightcolor <<get-color(name="black", quote=1)>>

set completion-bg <<get-color(name="black", quote=1)>>
set completion-fg <<get-color(name="white", quote=1)>>
set completion-group-bg <<get-color(name="light-black", quote=1)>>
set completion-group-fg <<get-color(name="white", quote=1)>>
set completion-highlight-bg <<get-color(name="magenta", quote=1)>>
set completion-highlight-fg <<get-color(name="black", quote=1)>>

set inputbar-bg <<get-color(name="black", quote=1)>>
set inputbar-fg <<get-color(name="light-magenta", quote=1)>>
set statusbar-bg <<get-color(name="black", quote=1)>>
set statusbar-fg <<get-color(name="light-magenta", quote=1)>>

set notification-error-bg <<get-color(name="red", quote=1)>>
set notification-error-fg <<get-color(name="color-fg", quote=1)>>
set notification-warning-bg <<get-color(name="yellow", quote=1)>>
set notification-warning-fg <<get-color(name="color-fg", quote=1)>>
#+end_src

Running M-x org-babel-expand-src-block (C-c C-v v) on this code block will open the code buffer with noweb expressions expanded, for instance the line with set recolor-lightcolor will look like:

set recolor-lightcolor "#292d3e"

M-x org-babel-tangle (C-c C-v t) will also produce zathurarc with the colors set (given that there’s :noweb yes somewhere in the code block configuration).

One note is that by default running these commands will require the user to confirm evaluation of each code block. To avoid that, you can set org-confirm-babel-evaluate to nil, for example:

(setq my/org-config-files
      '("/home/pavel/Emacs.org"
        "/home/pavel/Desktop.org"
        "/home/pavel/Console.org"
        "/home/pavel/Guix.org"
        "/home/pavel/Mail.org"))

(add-hook 'org-mode-hook
          (lambda ()
            (when (member (buffer-file-name) my/org-config-files)
              (setq-local org-confirm-babel-evaluate nil))))

And, to close the loop on colors, let’s generate .Xresources from that table:

#+NAME: get-xresources
#+begin_src emacs-lisp :var table=colors
(mapconcat
 (lambda (elem)
   (concat "*" (nth 1 elem) ": " (nth 2 elem)))
 (seq-filter
  (lambda (elem) (and (nth 1 elem)
                      (not (string-empty-p (nth 1 elem)))))
  table)
 "\n")
#+end_src

#+begin_src conf-xdefaults :noweb yes :tangle ~/.Xresources
<<get-xresources()>>

*background: <<get-color(name="black")>>
*foreground: <<get-color(name="white")>>
#+end_src

So, whenever a program is capable of reading .Xresources, it will get colors from there, otherwise, it will get colors from noweb expressions in the literate config. Thus, in both cases, the color is set in a single Org Mode table.

Guix dependencies

Another case I want to cover is using profiles in GNU Guix.

A “profile” in Guix is a way to group package installations. For instance, I have a “music” profile that has software like MPD, ncmpcpp that I’m still occasionally using because of its tag editor, etc. Corresponding to that profile, there’s a manifest named music.scm that looks like this:

(specifications->manifest
 '(
   "flac"
   "cuetools"
   "shntool"
   "mpd-mpc"
   "mpd-watcher"
   "picard"
   "ncmpcpp"
   "mpd"))

I could generate this file with org-babel as any other, but that is often not so convenient. For example, I have a polybar module that uses sunwait to show sunset and sunrise times, and ideally, I want to declare sunwait to be in the “desktop-polybar” profile in the same section that has the polybar module definition and the bash script.

So here’s an approach I came up with. The relevant section of the config looks like this:

*** sun
| Category        | Guix dependency |
|-----------------+-----------------|
| desktop-polybar | sunwait         |

Prints out the time of sunrise/sunset. Uses [[https://github.com/risacher/sunwait][sunwait]]

#+begin_src bash :tangle ./bin/polybar/sun.sh :noweb yes
...script...
#+end_src

#+begin_src conf-windows :noweb yes
...polybar module definition...
#+end_src

sunwait is declared in an Org table that looks like that:

Category Guix dependency
desktop-polybar sunwait

Such tables are spread through my Desktop.org, for instance, here is another one for a polybar module that depends on dateutils:

Category Guix dependency
desktop-polybar dateutils

Thus I made a function that extracts packages from all such tables from the current Org buffer. The rules are as follows:

  • If a column name matches [G|g]uix.*dep, its contents are added to the result.
  • If CATEGORY is passed, a column with name [C|c]ategory is used to filter results. That way, one Org file can be used to produce multiple manifests.
  • If CATEGORY is not passed, entries with the non-empty category are filtered out
  • If there is a [D|d]isabled column, entries that have a non-empty value in this column are filtered out.

And here is the implementation:

(defun my/extract-guix-dependencies (&optional category)
  (let ((dependencies '()))
    (org-table-map-tables
     (lambda ()
       (let* ((table
               (seq-filter
                (lambda (q) (not (eq q 'hline)))
                (org-table-to-lisp)))
              (dep-name-index
               (cl-position
                nil
                (mapcar #'substring-no-properties (nth 0 table))
                :test (lambda (_ elem)
                        (string-match-p "[G|g]uix.*dep" elem))))
              (category-name-index
               (cl-position
                nil
                (mapcar #'substring-no-properties (nth 0 table))
                :test (lambda (_ elem)
                        (string-match-p ".*[C|c]ategory.*" elem))))
              (disabled-name-index
               (cl-position
                nil
                (mapcar #'substring-no-properties (nth 0 table))
                :test (lambda (_ elem)
                        (string-match-p ".*[D|d]isabled.*" elem)))))
         (when dep-name-index
           (dolist (elem (cdr table))
             (when
                 (and
                  ;; Category
                  (or
                   ;; Category is not set and not present in the table
                   (and
                    (or (not category) (string-empty-p category))
                    (not category-name-index))
                   ;; Category is set and present in the table
                   (and
                    category-name-index
                    (not (string-empty-p category))
                    (string-match-p category (nth category-name-index elem))))
                  ;; Not disabled
                  (or
                   (not disabled-name-index)
                   (string-empty-p (nth disabled-name-index elem))))
               (add-to-list
                'dependencies
                (substring-no-properties (nth dep-name-index elem)))))))))
    dependencies))

Let’s execute this function in the current buffer:

(my/extract-guix-dependencies "desktop-polybar")
("dateutils" "sunwait")

As expected, it found both dateutils and sunwait. To make it work in the configuration, it is necessary to format the list so that Scheme could read it:

(defun my/format-guix-dependencies (&optional category)
  (mapconcat
   (lambda (e) (concat "\"" e "\""))
   (my/extract-guix-dependencies category)
   "\n"))

And we need an Org snippet such as this:

#+NAME: packages
#+begin_src emacs-lisp :tangle no :var category=""
(my/format-guix-dependencies category)
#+end_src

Now, creating a manifest for the desktop-polybar profile is as simple as:

#+begin_src scheme :tangle ~/.config/guix/manifests/desktop-polybar.scm :noweb yes
(specifications->manifest
 '(
   <<packages("desktop-polybar")>>))
#+end_src

There’s a newline symbol between “(” and <<packages("desktop-polybar")>> because whenever a noweb expression expands into multiple lines, for each new line noweb duplicates contents between the start of the line and the start of the expression.

One reason this is so is to support languages where indentation is a part of the syntax, for instance, Python:

class TestClass:
    <<class-contents>>

So every line of <<class-contents>> will be indented appropriately. In our case though, it is a minor inconvenience to be aware of.

Polybar

Now, the most crazy advanced case I’ve come up with so far.

Basically, here is how my polybar currently looks:

It has:

  • colors from the general color theme;
  • powerline-ish decorations between modules.

Colors

The “colors” part is straightforward enough. Polybar can use Xresources, so we just need to generate the appropriate bindings of Xresources to the polybar variables:

#+NAME: get-polybar-colors
#+begin_src emacs-lisp :var table=colors :tangle no
(mapconcat
 (lambda (elem)
   (format "%s = ${xrdb:%s}" (nth 0 elem) (nth 1 elem)))
 (seq-filter
  (lambda (elem) (when-let (name (nth 1 elem))
                   (not (string-empty-p name))))
  table)
 "\n")
#+end_src

#+begin_src conf-windows :noweb yes
[colors]
<<get-polybar-colors()>>

background = ${xrdb:background}
#+end_src

Module decorations

As for the module decorations though, I find it ironic that with all this fancy rendering around I have to resort to Unicode glyphs.

Anyhow, the approach is to put a glyph between two blocks like this:

block1  block2

And set the foreground and background colors like that:

block1 glyph block2
foreground F1 B2 F2
background B1 B1 B2

So, that’s a start. First, let’s define the glyph symbols in the polybar config:

[glyph]
gleft = 
gright = 

Defining modules

As we want to interweave polybar modules with these glyphs in the right order and with the right colors, it is reasonable to define a single source of truth:

#+NAME: polybar_modules
| Index | Module      | Color         | Glyph |
|-------+-------------+---------------+-------|
|     1 | pulseaudio  | light-magenta | +     |
|     2 | mpd         | magenta       | +     |
|     9 | battery     | light-cyan    | +     |
|     3 | cpu         | cyan          | +     |
|     4 | ram-memory  | light-green   | +     |
|     5 | swap-memory | green         | +     |
|     6 | network     | light-red     | +     |
|     7 | openvpn     | light-red     |       |
|     8 | xkeyboard   | red           | +     |
|    10 | weather     | light-yellow  | +     |
|    12 | sun         | yellow        | +     |
|    13 | aw-afk      | light-blue    | +     |
|    14 | date        | blue          | +     |

Also excluding some modules from certain monitors, which for now is about excluding battery from the monitors of my desktop PC:

#+NAME: polybar_modules_exclude
| Monitor  | Exclude |
|----------+---------|
| DVI-D-0  | battery |
| HDMI-A-0 | battery |

Generating glyphs

To generate the required set of glyphs, we need a glyph for every possible combination of adjacent colors that can occur in polybar.

Most of these combinations can be inferred from the polybar_modules table, the rest are defined in another table:

#+NAME: polybar_extra_colors
| Color 1    | Color 2       |
|------------+---------------|
| background | white         |
| background | light-magenta |
| blue       | background    |

There’s a definition of the source block with the required variables:

#+NAME: polybar-generate-glyphs
#+begin_src emacs-lisp :var table=polybar_modules exclude-table=polybar_modules_exclude extra=polybar_extra_colors
...source...
#+end_src

And there is the source block itself (because I want to have some syntax highlighting for this one in the post):

(let* ((monitors
        (thread-last
          exclude-table
          (seq-map (lambda (el) (nth 0 el)))
          (seq-uniq)))
       (exclude-combinations
        (seq-map
         (lambda (monitor)
           (seq-map
            (lambda (el) (nth 1 el))
            (seq-filter
             (lambda (el) (and (string-equal (nth 0 el) monitor)
                               (nth 1 el)))
             exclude-table)))
         `(,@monitors "")))
       (module-glyph-combinations
        (thread-last
          exclude-combinations
          (seq-map
           (lambda (exclude)
             (thread-last
               table
               (seq-filter
                (lambda (elt)
                  (not (or
                        (member (nth 1 elt) exclude)
                        (not (string-equal (nth 3 elt) "+")))))))))
          (seq-uniq)))
       (color-changes nil))
  (dolist (e extra)
    (add-to-list
     'color-changes
     (concat (nth 0 e) "--" (nth 1 e))))
  (dolist (comb module-glyph-combinations)
    (dotimes (i (1- (length comb)))
      (add-to-list
       'color-changes
       (concat (nth 2 (nth i comb))
               "--"
               (nth 2 (nth (1+ i) comb))))))
  (mapconcat
   (lambda (el)
     (let ((colors (split-string el "--")))
       (format "
[module/glyph-%s--%s]
type = custom/text
content-background = ${colors.%s}
content-foreground = ${colors.%s}
content = ${glyph.gright}
content-font = 5"
               (nth 0 colors)
               (nth 1 colors)
               (nth 0 colors)
               (nth 1 colors))))
   color-changes
   "\n"))

Here’s a rough outline of how the code works:

  • monitors is a list of unique monitors in exclude-table
  • exclude-combilnations is a list of lists of module names to be excluded for each monitor
  • module-glyphs-combinations is a list of lists of actual modules for each monitor
  • color-changes is a list of unique adjacent colors across modules in all monitors

Finally, color-changes is used to generate glyph modules that look like this:

[module/glyph-light-cyan--cyan]
type = custom/text
content-background = ${colors.light-cyan}
content-foreground = ${colors.cyan}
content = ${glyph.gright}
content-font = 5

As of now, 15 of such modules is generated.

And including this in the polybar config itself:

#+begin_src conf-windows :noweb yes
<<polybar-generate-glyphs()>>
#+end_src

Individual modules

Another thing we need to do is to set the color of modules in accordance with the polybar_modules table. The background can be determined from the Color column with the following code block:

#+NAME: get-polybar-bg
#+begin_src emacs-lisp :var table=polybar_modules module="pulseaudio"
(format
 "${colors.%s}"
 (nth
  2
  (seq-find
   (lambda (el) (string-equal (nth 1 el) module))
   table)))
#+end_src

And that block is meant to be invoked in each module definition, e.g. for the cpu module:

#+begin_src conf-windows :noweb yes
[module/cpu]
type = internal/cpu
format = " <label>"
label = %percentage%%
format-background = <<get-polybar-bg(module="cpu")>>
#+end_src

Global polybar configuration

To configure polybar itself, we first need to generate a set of modules for each monitor.

Here is the source block definition:

#+NAME: polybar-generate-modules
#+begin_src emacs-lisp :var table=polybar_modules exclude-table=polybar_modules_exclude monitor="DVI-D-0" first-color="background" last-color="background"
...
#+end_src

The parameters here, excluding the two required tables, are:

  • monitor - the current monitor on which to filter out the blocks by the polybar_modules_exclude table,
  • first-color - the first color of the first glyph,
  • last-color - the second color of the last glyph.

And here is the source:

(let* ((exclude-modules
        (thread-last
          exclude-table
          (seq-filter (lambda (el) (string-equal (nth 0 el) monitor)))
          (seq-map (lambda (el) (nth 1 el)))))
       (modules
        (thread-last
          table
          (seq-filter (lambda (el) (not (member (nth 1 el) exclude-modules))))))
       (prev-color first-color)
       (ret nil))
  (concat
   (mapconcat
    (lambda (el)
      (apply
       #'concat
       (list
        (when (string-equal (nth 3 el) "+")
          (setq ret (format "glyph-%s--%s " prev-color (nth 2 el)))
          (setq prev-color (nth 2 el))
          ret)
        (nth 1 el))))
    modules
    " ")
   (unless (string-empty-p last-color) (format " glyph-%s--%s " prev-color last-color))))

Here’s how it evaluates on my current monitor:

glyph-background--light-magenta pulseaudio glyph-light-magenta--magenta mpd glyph-magenta--cyan cpu glyph-cyan--light-green ram-memory glyph-light-green--green swap-memory glyph-green--light-red network openvpn glyph-light-red--red xkeyboard glyph-red--light-yellow weather glyph-light-yellow--yellow sun glyph-yellow--light-blue aw-afk glyph-light-blue--blue date glyph-blue--background

The polybar config doesn’t support conditional statements, but it does support environment variables, so we can pass the parameters from something like a bash script. Here’s an excerpt from mine:

...
declare -A BLOCKS=(
    ["eDP"]="<<polybar-generate-modules(monitor="eDP")>>"
    ["eDP-1"]="<<polybar-generate-modules(monitor="eDP-1")>>"
    ["DVI-D-0"]="<<polybar-generate-modules(monitor="DVI-D-0")>>"
    ["HDMI-A-0"]="<<polybar-generate-modules(monitor="HDMI-A-0")>>"
)
...
pkill polybar
for m in $(xrandr --query | grep " connected" | cut -d" " -f1); do
    ...
    export RIGHT_BLOCKS=${BLOCKS[$MONITOR]}
    polybar mybar &
done

(The full script has a lot of stuff that is not relevant to this post, but you can check here if you are interested.)

So, in the case of polybar, literate configuration allows for implementing a sort of logic that wouldn’t be available with the base configuration (also a promise of projects like Guix Home, by the way). Maintaining this configuration, e.g. changing the order of modules, is much easier this way than it would be if everything was hardcoded in the polybar config itself.