(Doom) Emacs learning journal

..

Hey! Reading this document from start to end might not be useful. So why put it here if the intent isn’t consumption?

Keep reading if you’d like. Or stop here. 🐉

Prelude

My learning process works something like this:

  1. Identify something I value that I’d like to explore
  2. In the unexplored territory, would exploring enable me to do something new and interesting?
  3. If yes - try do it, and write down the process.

This document is the written record of the process of me exploring Emacs, and what learning Emacs can do for me.

I index on date, then topic. That means adding information is easy. “But is that nice to read?” Perhaps. This journal is optimized for writing, not reading. Other documents can be read-optimized.

So … why even keep this public?

For my ease of reading, and for easily sharing with others. The web is accessible :)

2022-06-12

’Morning.

Improving my Clerk workflow with hooks

Problem statement. I’ve been trying out Jack Rusher’s recommendation to bind a key to save document and refresh with Clerk. That lead to really fast refreshes, which I enjoyed. However, I coudn’t get past my muscle memory of pressing the wrong key. On my Norwegian keyboard, pressing C-ø is really convenient. On a US ANSI layout, that would be C-;. I’ve bound this to save-buffer, to avoid the sequence of having to press ESC SPC f s i to exit to normal mode, save and re-enter insert mode (with Evil and Doom Emacs).

Interlude – Writing this down feels really good. I think it will help me avoid getting distracted. Back to Emacs.

Let’s give this a shot!

I had a look at the Emacs Lisp, and it simply saves and messages Clerk to reload!

(defun clerk-show ()
  (interactive)
  (save-buffer)
  (let ((filename (buffer-file-name)))
    (when filename
      (cider-interactive-eval
       (concat "(nextjournal.clerk/show! \"" filename "\")")))))

(define-key clojure-mode-map (kbd "<M-return>") 'clerk-show)

Source: Clerk README.

See? It saves the buffer, then runs nextjournal.clerk/show! in the current Cider REPL.

Let’s extract a function for the save hook.

(defun teod/clerk-show ()
  (let ((filename (buffer-file-name)))
    (when filename
      (cider-interactive-eval
       (concat "(nextjournal.clerk/show! \"" filename "\")")))))

But … how do we run this?

My original ideas:

But I didn’t know where I should start. I asked on the Clojurians Slack - in the #doom-emacs channel, and got some much appreciated pointers.

In essence:

Signature for add-hook:

(add-hook HOOK FUNCTION &optional DEPTH LOCAL)

And there’s an after-save-hook that I can use.

In my case, I think I can use the following:

(add-hook after-save-hook teod/clerk-show 0 't)

Though I admit I don’t really understand when I should use different function reference styles.

teod/clerk-show ; like this?
'teod/clerk-show ; or this?
#'teod/clerk-show ; or this?

Let’s try the simplest.

(defun teod/clerk-autoshow ()
  (interactive)
  (add-hook after-save-hook teod/clerk-show 0 't))

Nope! That didn’t work. I got the following in the *Messages* buffer:

clerk-autoshow: Symbol’s value as variable is void: teod/clerk-show apply

Let’s try #'.

Didn’t work. Bah!

OK, found the manual: https://www.gnu.org/software/emacs/manual/html_node/elisp/Setting-Hooks.html

Example from manual:

(add-hook 'lisp-interaction-mode-hook 'auto-fill-mode)

So .. both should be qoted. OK! (interlude: I’m not super comfortable with when to use symbols. Clojure has some different idioms here.)

Let’s try the following:

(defun teod/clerk-autoshow ()
  (interactive)
  (add-hook 'after-save-hook 'teod/clerk-show 0 't))

Yaaay!

Reflection. Really enjoyable. I was able to achieve what I wanted. I got stuck first, asking got me unstuck. And asking also allowed me to nail a way simpler solution than just pushing ahead.

Future work.

  1. I want to add some ;; ... run the clerk autosave thing ... on top of Clerk Clojure files
  2. Still think a minor mode could be useful - but this really solves what I needed now.

Actionables now.

  1. Commit and push this doc.
  2. Report back to the kind person i Clojurians
  3. Perhaps post in #clerk on Clojurians.

Also need to eat breakfast and not miss my plans.

More clerk - startup and files.

Idea:

  1. Ensure Clerk has been required in user.clj
  2. Set the clerk auto stuff on opening files with file-local variables.

Outcome

Writing this at 21:22. Wanting to summarize a bit.

Deciding to dig into Clerk workflow rather than simply working was a decision that didn’t come natrually to me. Is this right? Should I be working on this?

I keep saying to myself that half of what I do should make long term sense, and half should make short term sense. This makes long term sense. And it’s a blocker for getting some reasonable value out short term. But … why does it feel like cheating? Imposter syndrome perhaps, I’m not worthy to make tooling? Not sure.

Anyway - I never came to the “more clerk - startup and files” part. I’ll probably scrap that work. I tried getting a minor mode up, but when I set mode: clerk-auto in my file, I got just clerk-auto-mode and not clojure-mode. So back to the drawing board.

In summary:

  1. Prioritizing long term tooling investments felt … weird at first, but I’m happy I did.
  2. Asking on Clojurians was great.
  3. I stopped exploring when I didn’t have more time – which was good in the end. So … tooling work makes sense as short spikes that can land in a sitting.

Now, I’m going to try use clerk a bit. See you later!

Teodor

2022-06-21

’Afternoon

Fast font configuration

Use case: need to use a big screen, want to change font size without an Emacs restart.

First, re-evaluate the form setting the font.

(setq doom-font (font-spec :family "monospace" :size 16))

Then, reload the font.

M-x doom/reload-font

Done!

2022-07-01

Guten Morgen. Dobre utro.

Weird freezes when saving Org-mode files

I just tried saving my journal, which is quite big. Emacs froze. Not sure why. Happens both when I use my own C-ø binding, and with the built-in SPC f s.

2022-07-12

Vacation is whatever I decide it is.

Creating small user interfaces in Emacs

Value prop: for small, specific use cases, Emacs can be a better choice than a CLI.

Sneak peak — this is what we’re going to create:

TODO gif

When are small Emacs UIs a nice fit?

When I create something for myself, I usually either make a small CLI or something in Emacs.

CLIs are:

  1. Easy to reuse for anyone
  2. Easy to script against
  3. Somewhat easy to use for humans.

So, what can specific Emacs snippets provide?

  1. Not for everyone.
  2. Not made to be scripted against. Emacs stuff can be scripted against, but what we’ll be making right now will not be extensible.
  3. Very easy to use for humans.

Let’s get to it!

Feedback loop for working with Emacs lisp

I typically write a small interactive Emacs Lisp function that I can redefine and test out. I don’t attempt to solve my problem at once – first I want to see what I’m doing. For that, I usually print stuff: (message "my output")

Prerequisites understanding how completing-read and read-string works.

read-string small example:

(let ((name (read-string "Please enter your name: ")))
  (message (s-concat "Hello, " name "!")))

read-string big example:

(let ((page-id (read-string "Page id: "))
      (title (read-string "Page title: "))
      (default-directory "~/dev/teodorlu/play.teod.eu"))
  (shell-command-to-string (s-concat "./play.clj create-page " page-id " :title \"" title "\""))
  (switch-to-buffer (find-file-noselect page-id)))

What about completing read? Doing it the emacsy way, I tried the built-in docs first. That worked out really well! Google not required.

(defun teod/lol ()
  (interactive)
  ;; read-string's argument is the text prompt
  (let ((name (read-string "What is your name? ")))
    (message (s-concat "Your name is: " name))
    ;; completing-read's first two arguments are the text prompt and a list of options
    (let ((dish (completing-read "What is your favourite dish? " '(:pizza :pasta :red-hot-chili-peppers))))
      (message (s-concat name " likes: " dish))
      (cond ((equal dish ":pizza") (message "I also like pizza!"))
            ((equal dish ":pasta") (message "Pasta is good."))
            (:else (message (s-concat dish "? Doesn't sound familiar")))))))
;; run with M-x teod/lol
;;
;; depending on your input, it may print:
;;
;;   Your name is: Teodor
;;   Teodor likes: :pizza
;;   I also like pizza!

Sidenote - equality in Emacs

I want to compare a string input with my of options. What kind of comparison should I use? I started to write eq, then saw both eq and equal show up. I read docs for both and tried them out:

(eq :pizza :pizza)
;; => t
(eq "pizza" "pizza")
;; => nil

(equal :pizza :pizza)
;; => t
(equal "pizza" "pizza")
;; => t

I wanted string equality, so equal was the right choice.

End note - struggle.

… aaaand … this is where I discover that I’m stuck. My ./play.clj create-page CLI entrypoint currently only suports setting title. BUT! Perhaps it should not set title. Perhaps that’s best handled externally.

Other option:

  1. first create new page
  2. Then invoke babashka to change a play edn directly.

Here’s what a CLI invocation could look like:

$ cat ./clojure-lightbulb-moments/play.edn | bb '(assoc *input* :thing :thong)'
{:title "Clojure Lightbulb Moments", :readiness :forever-incomplete, :author-url "https://teod.eu", :form :rambling, :lang :en, :thing :thong}

I’m stopping at a point of uncertainty. I really want one way to edit tags effectively. Not slap stuff on the CLI on the way in. Yet, I just can’t make up my mind.

Well, I did learn to use completing-read. That was easy!

let and let*

This works:

(let* ((x "Hello!")
       (_ (message x))
       (_ (message "123"))))

This crashes:

(let ((x "Hello!")
      (_ (message x))
      (_ (message "123"))))

Why?

let scope example
let in (let ((x 1) ) (message x))
let* in or down (let* ((x 1) (y (* x 10)) (message y))

2022-07-13

I really don’t like UPS.

My first Emacs Lisp module

I’m using Doom Emacs. I enjoy using Doom Emacs. So far, I’ve leaned heavily into workflows — how can I get this done? I haven’t put too much effort into structure. That’s about to change.

Why? This page. play.teod.eu. I’ve created quite a few Emacs lisp commands that play nice with the play.teod.eu structure. Up until now, all of these have gone into my Doom Emacs config.el.

How to create your own modules in Doom Emacs

Three things

Do want: BB EDN PLAYGROUND

2022-07-14

TIL: Org-roam needs an Emacs restart (or something) when moving files around

Org-mode is amazing.

From anywhere, M-x org-store-link. That link goes into the org-stored-links variable. Next time a link is added with org-insert-link, you can see all the links you’ve stored.

2022-07-18

Inline battery information

M-x battery Returns / shows battery info
M-x battery-display-mode Puts a nice battery indicator in the modeline

Bind function keys

I think <f8> is the way to bind stuff.

LOL I already have <f8> bound to open an org-roam node, haha NOPE, that inserts a link to the node. Remove.

Wtf, now it works. I have a wrong model for the map! macro somehow. Moving stuff up fixed my problems.

2022-07-29

Deleting branches with Magit (again)

“Again?” Yes. Again. I’ve learned this before.

In vanilla Emacs:

  1. M-x magit-show-refs
  2. Mark lines for deletion with normal marking method (C-SPC)
  3. Press k to delete

In Doom Emacs:

  1. M-x magit-show-refs
  2. Visually select branches with whatever Vim magit you want
  3. Press x to delete.

Beware Dired differences!

In Dired, a single d press marks a file for deletion. In Magit, d sends you off to magit-diff. So it’s different! But “select lines then press x” does the same, both for Doom Emacs Dired and Doom Emacs Magit.