diff --git a/content/_index.md b/content/_index.md index 5c58065..4dd1f52 100644 --- a/content/_index.md +++ b/content/_index.md @@ -13,5 +13,8 @@ Master's student of Saint-Petersurg State Electrotechical University, Russia at * [e-mail](mailto:thexcloud@gmail.com) * [GitHub](https://github.com/SqrtMinusOne) * [VK](https://vk.com/sqrtminusone) +* [Twitter](https://twitter.com/SqrtMinusTwo) * [Reddit](https://www.reddit.com/user/XCapitan_1) + +### Also * [My PGP key](/0x914472A1FD6775C166F96EBEED739ADF81C78160.asc) diff --git a/content/posts/2022-02-12-literate.md b/content/posts/2022-02-12-literate.md new file mode 100644 index 0000000..46731c6 --- /dev/null +++ b/content/posts/2022-02-12-literate.md @@ -0,0 +1,632 @@ ++++ +title = "A few cases of literate configuration" +author = ["Pavel Korytov"] +date = 2022-02-12 +tags = ["emacs", "org-mode"] +draft = false ++++ + +A post that arose from the discussion of literate configuration on the [System Crafters](https://systemcrafters.net/) Discord. + +I am using the [literate configuration](https://leanpub.com/lit-config) strategy (based on [Emacs' Org Mode](https://orgmode.org/)) 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](https://orgmode.org/manual/Working-with-Source-Code.html). 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 {#colors} + +Let's start with system colors. + +My favorite color theme is Palenight ([color codes](https://github.com/JonathanSpeek/palenight-iterm2)), and I want to have one source of truth for these colors. Except for Emacs itself, which has [doom-palenight](https://github.com/doomemacs/themes#theme-list) (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: + +```text +#+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](https://orgmode.org/manual/Environment-of-a-Code-Block.html). Let's define one to return the color code based on its name: + +```text +#+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](https://orgmode.org/manual/Noweb-Reference-Syntax.html). For instance, here's my [zathura](https://pwmt.org/projects/zathura/) config: + +```text +#+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 set recolor false +map set recolor true + +set recolor-lightcolor <> + +set completion-bg <> +set completion-fg <> +set completion-group-bg <> +set completion-group-fg <> +set completion-highlight-bg <> +set completion-highlight-fg <> + +set inputbar-bg <> +set inputbar-fg <> +set statusbar-bg <> +set statusbar-fg <> + +set notification-error-bg <> +set notification-error-fg <> +set notification-warning-bg <> +set notification-warning-fg <> +#+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: + +```text +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: + +```emacs-lisp +(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: + +```text +#+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 +<> + +*background: <> +*foreground: <> +#+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 {#guix-dependencies} + +Another case I want to cover is [using profiles in GNU Guix](https://guix.gnu.org/en/cookbook/en/html_node/Advanced-package-management.html#Advanced-package-management). + +A "profile" in Guix is a way to group package installations. For instance, I have a "music" profile that has software like [MPD](https://www.musicpd.org/), [ncmpcpp](https://github.com/ncmpcpp/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: + +```scheme +(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](https://github.com/polybar/polybar) module that uses [sunwait](https://github.com/risacher/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: + +```text +*** 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: + +```emacs-lisp +(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: + +```emacs-lisp +(my/extract-guix-dependencies "desktop-polybar") +``` + +```emacs-lisp +("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: + +```emacs-lisp +(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: + +```text +#+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: + +```text +#+begin_src scheme :tangle ~/.config/guix/manifests/desktop-polybar.scm :noweb yes +(specifications->manifest + '( + <>)) +#+end_src +``` + +There's a newline symbol between "(" and `<>` 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: + +```python +class TestClass: + <> +``` + +So every line of `<>` will be indented appropriately. In our case though, it is a minor inconvenience to be aware of. + + +## Polybar {#polybar} + +Now, the most ~~crazy~~ advanced case I've come up with so far. + +Basically, here is how my [polybar](https://github.com/polybar/polybar) currently looks: +![](/ox-hugo/literate--polybar.png) + +It has: + +- colors from the general color theme; +- powerline-ish decorations between modules. + + +### Colors {#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: + +```text +#+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] +<> + +background = ${xrdb:background} +#+end_src +``` + + +### Module decorations {#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: + +```text +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: + +```ini +[glyph] +gleft =  +gright =  +``` + + +#### Defining modules {#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: + +```text +#+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: + +```text +#+NAME: polybar_modules_exclude +| Monitor | Exclude | +|----------+---------| +| DVI-D-0 | battery | +| HDMI-A-0 | battery | +``` + + +#### Generating glyphs {#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: + +```text +#+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: + +```text +#+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): + +```emacs-lisp +(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: + +```ini +[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: + +```text +#+begin_src conf-windows :noweb yes +<> +#+end_src +``` + + +#### Individual modules {#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: + +```text +#+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: + +```text +#+begin_src conf-windows :noweb yes +[module/cpu] +type = internal/cpu +format = "