Literate Configuration Management with vanilla Emacs
Table of Contents
Update: :
This is all completely unnecessary. It turns out that you can practically
achieve the same functionality with imenu-list
, without it being so slow.
Introduction
I manage my dotfiles using GNU Stow. This means that I have a git directory dotfiles, which contains the configuration files for the applications I have bothered tailoring to my needs. Maintaining it is not as simple as it sounds, though.
In this article I aim to describe how this got just a little bit easier for me in the past couple days
Building the script
The following folder hierarchy shows part of the general dotfiles problem:
- dotfiles
- bspwm
- .config
- bspwm
- bspwm.org
- bspwmrc
- sxhkd
- .config
- sxhkd
- sxhkd.org
- sxhkdrc
- zsh
- zsh.org
- .zshrc
- emacs
- .emacs.d
- config.org
- …
The directories above highlight the fact that there is not a single place to look.
Almost every package has something different in the way it structures its
dotfiles, that makes the use of regular expressions necessary. Obviously, we
want to recursively search our directory for .org
files.
(setq chatziiola/dot-dir "~/dotfiles/emacs") (directory-files-recursively chatziiola/dot-dir "org$")
Dealing with too many org files
A problem arises: Too many files within the emacs directory, almost all of which
from the straight
subdirectory (this is what hapenned to me, it may be different
for you). To solve that we need to filter out non-important to our configs org files.
The way I see it there are two possible scenarios, neither of which do I like:
- Play with the output of
directory-files-recursively
, filtering out whatever contains an element of our blacklist - Redefine the
directory-files-recursively
function with one more parameterDIR-REGEXP
. Since it is a rather small function, I’m pretty sure we can work it out.
Alright, alright, alright. I found the third option:
Instead of using files in this format
<app>.org
, we are to use files only named as config.org and then differentiate them in the selection panel using their “TITLE” property(setq chatziiola/dot-dir "~/dotfiles/") (directory-files-recursively chatziiola/dot-dir "^config.org$")
Getting the titles
Make sure that we get the title of our configuration files:
(setq chatziiola/dot-dir "~/dotfiles/") (dolist (file (directory-files-recursively chatziiola/dot-dir "^config.org$")) (unless (stringp (chatziiola/get-keyword-value "TITLE" file)) (print file)))
- Complementary functions
These functions were found online (albeit the second one has been changed) when I was building my lecture notetaking scripts
; This and the next func ( even though slightly modified ): ; src: https://stackoverflow.com/questions/66574715/how-to-get-org-mode-file-title-and-other-file-level-properties-from-an-arbitra (defun ndk/get-keyword-key-value (kwd) "This function was found on stackoverflow - google or see config.org for more. \n It should only be used by ~chatziiola/get-keyword-value~" (let ((data (cadr kwd))) (list (plist-get data :key) (plist-get data :value)))) (defun chatziiola/get-keyword-value (key &optional file) "Return the actual value associated with key 'myString' of the current org buffer\n For example: (chatziiola/get-keyword-value \"TITLE\") should return the title of that org file." ; Return the nth element of the list ;; TODO better documentation (let ((file (or file ""))) (cond ((not (string-blank-p file)) (with-current-buffer (find-file-noselect file) (chatziiola/get-keyword-value key))) ;; In that case key is a list of keys actually ((proper-list-p key) (let ((keyVals '())) (cl-loop for title in key do (add-to-list 'keyVals (chatziiola/get-keyword-value title) t)) keyVals) ) (t (nth 1 (assoc key (org-element-map (org-element-parse-buffer 'greater-element) '(keyword) #'ndk/get-keyword-key-value)))))))
Presenting Them To The User
Create a dictionary in the form of "TITLE":"FILE"
:
We do not call it a directory but rather a prompt-list
(setq chatziiola/dot-dir "~/dotfiles/") (let* ((config-files (directory-files-recursively chatziiola/dot-dir "^config.org$")) (prompt-list (seq-map (lambda (e) (list (format "%-20s %-25s" (chatziiola/get-keyword-value "TITLE" e) e) e)) config-files))) prompt-list)
Prompt the user to select and get the file to be edited
(setq chatziiola/dot-dir "~/dotfiles/") (let* ((config-files (directory-files-recursively chatziiola/dot-dir "^config.org$")) (prompt-list (seq-map (lambda (e) (list (format "%-20s %-25s" (chatziiola/get-keyword-value "TITLE" e) e) e)) config-files)) (prompt-answer (completing-read "Select configuration file: " prompt-list))) ; Prompt an (car (last (assoc prompt-answer prompt-list))))
Script building is over: Just open the file
(setq chatziiola/dot-dir "~/dotfiles/") (find-file (let* ((config-files (directory-files-recursively chatziiola/dot-dir "^config.org$")) (prompt-list (seq-map (lambda (e) (list (format "%-20s %-25s" (chatziiola/get-keyword-value "TITLE" e) e) e)) config-files)) (prompt-answer (completing-read "Select configuration file: " prompt-list))) ; Prompt an (car (last (assoc prompt-answer prompt-list)))))
Integrating rougier’s scripts
With the steps above with have achieved a basic level of ease when dealing with dotfiles. What is missing?
- Declaring it as an interactive function
- Binding it to a key
- Integrating Rougier
@rougier functions
rougier
implemented these wonderful functions ( that inspired me to work on
this ):
(defun my/config () "Create a new for editing configuration" (interactive) (select-frame (make-frame '((name . "my/config") (width . 150) (height . 45)))) (find-file "~/.emacs.d/config.org") (my/org-sidebar-toggle))
(require 'imenu) (require 'imenu-list) (defun my/org-tree-to-indirect-buffer () "Create indirect buffer, narrow it to current subtree and unfold blocks" (org-tree-to-indirect-buffer) (org-show-block-all) (setq-local my/org-blocks-hidden nil)) (defun my/org-sidebar () "Open an imenu list on the left that allow navigation." (interactive) (setq imenu-list-after-jump-hook #'my/org-tree-to-indirect-buffer imenu-list-position 'left imenu-list-size 36 imenu-list-focus-after-activation t) (let ((heading (substring-no-properties (or (org-get-heading t t t t) "")))) (when (buffer-base-buffer) (switch-to-buffer (buffer-base-buffer))) (imenu-list-minor-mode) (imenu-list-stop-timer) (hl-line-mode) (face-remap-add-relative 'hl-line :inherit 'nano-subtle) (setq header-line-format '(:eval (nano-modeline-render nil (buffer-name imenu-list--displayed-buffer) "(outline)" ""))) (setq-local cursor-type nil) (when (> (length heading) 0) (goto-char (point-min)) (search-forward heading) (imenu-list-display-dwim))))
(defun my/org-sidebar-toggle () "Toggle the org-sidebar" (interactive) (if (get-buffer-window "*Ilist*") (progn (quit-window nil (get-buffer-window "*Ilist*")) (switch-to-buffer (buffer-base-buffer))) (my/org-sidebar)))
One does not really need to understand all of them. They just have to look at the first one. The reason why this is the path of least effort is obvious. By changing it just a little bit, we have the perfect end product:
Merging them with our funcs
(setq chatziiola/dot-dir "~/dotfiles/") (defun chatziiola/open-conf () "Finds all ~config.org~ files within chatziiola/dot-dir and opens them up for you" (interactive) (let* ((config-files (directory-files-recursively chatziiola/dot-dir "^config.org$")) (prompt-list (seq-map (lambda (e) (list (format "%-20s %-25s" (chatziiola/get-keyword-value "TITLE" e) e) e)) config-files)) (prompt-answer (completing-read "Select configuration file: " prompt-list)) (cur-conf-file (car (last (assoc prompt-answer prompt-list))))) (select-frame (make-frame '((name . "my/config") (width . 150) (height . 45)))) (find-file cur-conf-file)) (my/org-sidebar-toggle) )
Making it usable
Autoload the configuration function ( so that it is always available ), stolen
from rougier
, once again.
(autoload 'chatziiola/open-conf (expand-file-name "init.el" user-emacs-directory) "Autoloaded my/config command." t)
;; For those of you that don't use evil, you better start :P (evil-define-key 'normal 'global (kbd "<leader>fp") 'chatziiola/open-conf)
Summing Up ( some extra details )
This article took for granted that one has some prior experience with org mode,
though I understand that this is not always the case. To gain advantage of the
scripts outlined above you only need to add the following lines at the beginning
of your config.org
files:
#+TITLE: <yourtitle> #+PROPERTY: header-args :tangle <path-to-your-file>
And enclose your code in source blocks such as these:
Lastly: Do not forget to add config.org
to .stow-local-ignore to avoid clutter