mirror of
https://github.com/SqrtMinusOne/sqrtminusone.github.io.git
synced 2025-12-10 15:53:03 +03:00
feat(literate): add
This commit is contained in:
parent
2d82debae7
commit
a60a6c51e9
5 changed files with 1210 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
632
content/posts/2022-02-12-literate.md
Normal file
632
content/posts/2022-02-12-literate.md
Normal file
|
|
@ -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 <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:
|
||||
|
||||
```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
|
||||
<<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 {#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
|
||||
'(
|
||||
<<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:
|
||||
|
||||
```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 {#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:
|
||||

|
||||
|
||||
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]
|
||||
<<get-polybar-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
|
||||
<<polybar-generate-glyphs()>>
|
||||
#+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 = " <label>"
|
||||
label = %percentage%%
|
||||
format-background = <<get-polybar-bg(module="cpu")>>
|
||||
#+end_src
|
||||
```
|
||||
|
||||
|
||||
#### Global polybar configuration {#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:
|
||||
|
||||
```text
|
||||
#+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:
|
||||
|
||||
```emacs-lisp
|
||||
(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:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```bash
|
||||
...
|
||||
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](https://github.com/SqrtMinusOne/dotfiles/blob/master/Desktop.org#launch-script-1) 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.
|
||||
575
org/2022-02-12-literate.org
Normal file
575
org/2022-02-12-literate.org
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
#+HUGO_SECTION: posts
|
||||
#+HUGO_BASE_DIR: ../
|
||||
#+TITLE: A few cases of literate configuration
|
||||
#+DATE: 2022-02-12
|
||||
#+HUGO_TAGS: emacs
|
||||
#+HUGO_TAGS: org-mode
|
||||
#+HUGO_DRAFT: false
|
||||
|
||||
A post that arose from the discussion of literate configuration on the [[https://systemcrafters.net/][System Crafters]] Discord.
|
||||
|
||||
I am using the [[https://leanpub.com/lit-config][literate configuration]] strategy (based on [[https://orgmode.org/][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 [[https://orgmode.org/manual/Working-with-Source-Code.html][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 ([[https://github.com/JonathanSpeek/palenight-iterm2][color codes]]), and I want to have one source of truth for these colors. Except for Emacs itself, which has [[https://github.com/doomemacs/themes#theme-list][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:
|
||||
#+begin_example
|
||||
#+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 |
|
||||
#+end_example
|
||||
|
||||
Contents of this table can then be [[https://orgmode.org/manual/Environment-of-a-Code-Block.html][accessed from a code block]]. Let's define one to return the color code based on its name:
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
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 [[https://orgmode.org/manual/Noweb-Reference-Syntax.html][noweb]]. For instance, here's my [[https://pwmt.org/projects/zathura/][zathura]] config:
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
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:
|
||||
#+begin_example
|
||||
set recolor-lightcolor "#292d3e"
|
||||
#+end_example
|
||||
|
||||
=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:
|
||||
#+begin_src 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))))
|
||||
#+end_src
|
||||
|
||||
And, to close the loop on colors, let's generate =.Xresources= from that table:
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
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 [[https://guix.gnu.org/en/cookbook/en/html_node/Advanced-package-management.html#Advanced-package-management][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 [[https://www.musicpd.org/][MPD]], [[https://github.com/ncmpcpp/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:
|
||||
#+begin_src scheme
|
||||
(specifications->manifest
|
||||
'(
|
||||
"flac"
|
||||
"cuetools"
|
||||
"shntool"
|
||||
"mpd-mpc"
|
||||
"mpd-watcher"
|
||||
"picard"
|
||||
"ncmpcpp"
|
||||
"mpd"))
|
||||
#+end_src
|
||||
|
||||
I could generate this file with =org-babel= as any other, but that is often not so convenient. For example, I have a [[https://github.com/polybar/polybar][polybar]] module that uses [[https://github.com/risacher/sunwait][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:
|
||||
#+begin_example
|
||||
,*** 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
|
||||
#+end_example
|
||||
|
||||
=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:
|
||||
#+begin_src emacs-lisp :noweb-ref guix-tables
|
||||
(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))
|
||||
#+end_src
|
||||
|
||||
Let's execute this function in the current buffer:
|
||||
#+begin_src emacs-lisp :results code :exports both :eval never-export
|
||||
(my/extract-guix-dependencies "desktop-polybar")
|
||||
#+end_src
|
||||
|
||||
#+RESULTS:
|
||||
#+begin_src emacs-lisp
|
||||
("dateutils" "sunwait")
|
||||
#+end_src
|
||||
|
||||
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:
|
||||
#+begin_src emacs-lisp :noweb-ref guix-tables
|
||||
(defun my/format-guix-dependencies (&optional category)
|
||||
(mapconcat
|
||||
(lambda (e) (concat "\"" e "\""))
|
||||
(my/extract-guix-dependencies category)
|
||||
"\n"))
|
||||
#+end_src
|
||||
|
||||
And we need an Org snippet such as this:
|
||||
#+begin_example
|
||||
#+NAME: packages
|
||||
#+begin_src emacs-lisp :tangle no :var category=""
|
||||
(my/format-guix-dependencies category)
|
||||
#+end_src
|
||||
#+end_example
|
||||
|
||||
Now, creating a manifest for the =desktop-polybar= profile is as simple as:
|
||||
#+begin_example
|
||||
#+begin_src scheme :tangle ~/.config/guix/manifests/desktop-polybar.scm :noweb yes
|
||||
(specifications->manifest
|
||||
'(
|
||||
<<packages("desktop-polybar")>>))
|
||||
#+end_src
|
||||
#+end_example
|
||||
|
||||
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:
|
||||
#+begin_src python
|
||||
class TestClass:
|
||||
<<class-contents>>
|
||||
#+end_src
|
||||
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 [[https://github.com/polybar/polybar][polybar]] currently looks:
|
||||
[[./images/literate--polybar.png]]
|
||||
|
||||
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:
|
||||
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
** 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:
|
||||
#+begin_example
|
||||
block1 block2
|
||||
#+end_example
|
||||
|
||||
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:
|
||||
#+begin_src ini
|
||||
[glyph]
|
||||
gleft =
|
||||
gright =
|
||||
#+end_src
|
||||
|
||||
*** 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:
|
||||
#+begin_example
|
||||
#+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 | + |
|
||||
#+end_example
|
||||
|
||||
Also excluding some modules from certain monitors, which for now is about excluding =battery= from the monitors of my desktop PC:
|
||||
#+begin_example
|
||||
#+NAME: polybar_modules_exclude
|
||||
| Monitor | Exclude |
|
||||
|----------+---------|
|
||||
| DVI-D-0 | battery |
|
||||
| HDMI-A-0 | battery |
|
||||
#+end_example
|
||||
|
||||
*** 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:
|
||||
#+begin_example
|
||||
#+NAME: polybar_extra_colors
|
||||
| Color 1 | Color 2 |
|
||||
|------------+---------------|
|
||||
| background | white |
|
||||
| background | light-magenta |
|
||||
| blue | background |
|
||||
#+end_example
|
||||
|
||||
There's a definition of the source block with the required variables:
|
||||
#+begin_example
|
||||
#+NAME: polybar-generate-glyphs
|
||||
#+begin_src emacs-lisp :var table=polybar_modules exclude-table=polybar_modules_exclude extra=polybar_extra_colors
|
||||
...source...
|
||||
#+end_src
|
||||
#+end_example
|
||||
|
||||
And there is the source block itself (because I want to have some syntax highlighting for this one in the post):
|
||||
#+begin_src 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"))
|
||||
#+end_src
|
||||
|
||||
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:
|
||||
#+begin_src ini
|
||||
[module/glyph-light-cyan--cyan]
|
||||
type = custom/text
|
||||
content-background = ${colors.light-cyan}
|
||||
content-foreground = ${colors.cyan}
|
||||
content = ${glyph.gright}
|
||||
content-font = 5
|
||||
#+end_src
|
||||
|
||||
As of now, 15 of such modules is generated.
|
||||
|
||||
And including this in the polybar config itself:
|
||||
#+begin_example
|
||||
#+begin_src conf-windows :noweb yes
|
||||
<<polybar-generate-glyphs()>>
|
||||
#+end_src
|
||||
#+end_example
|
||||
|
||||
*** 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:
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
And that block is meant to be invoked in each module definition, e.g. for the =cpu= module:
|
||||
#+begin_example
|
||||
#+begin_src conf-windows :noweb yes
|
||||
[module/cpu]
|
||||
type = internal/cpu
|
||||
format = " <label>"
|
||||
label = %percentage%%
|
||||
format-background = <<get-polybar-bg(module="cpu")>>
|
||||
#+end_src
|
||||
#+end_example
|
||||
|
||||
*** 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:
|
||||
#+begin_example
|
||||
#+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
|
||||
#+end_example
|
||||
|
||||
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:
|
||||
#+begin_src emacs-lisp
|
||||
(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))))
|
||||
#+end_src
|
||||
|
||||
Here's how it evaluates on my current monitor:
|
||||
#+begin_example
|
||||
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
|
||||
#+end_example
|
||||
|
||||
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:
|
||||
#+begin_src bash
|
||||
...
|
||||
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
|
||||
#+end_src
|
||||
|
||||
(The full script has a lot of stuff that is not relevant to this post, but you can [[https://github.com/SqrtMinusOne/dotfiles/blob/master/Desktop.org#launch-script-1][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.
|
||||
BIN
org/images/literate--polybar.png
Normal file
BIN
org/images/literate--polybar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
static/ox-hugo/literate--polybar.png
Normal file
BIN
static/ox-hugo/literate--polybar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Loading…
Add table
Reference in a new issue