Emacs is unique

· :Note:

Take: Emacs is not a text editor, it's an extensible interface to work with computer. That old joke about Emacs being an operating system is about that. Well, and it comes with great text editor.

Example. Why using terminal when you can run a shell command from Emacs? async-shell-command, start-process, make-process and likes (e.g. ghostel-shell-command was trivial to implement).

In the core of that is several orthogonal concepts that blend together: buffer, point, region, keymap, completion. Plus Elisp programming language, being simple but stood the test of time, being Lisp 2 (separate namespaces for variables and functions), and having flat namespace make it easy to learn and adopt to your needs. Elisp-oriented Emacs concepts: packages, hooks, advices.

                      ╭─ buffer ── point ── region
                      │                     ╭─ C-c ── context
                      ├─ keymap ── prefixes ┼─ C-x ── always
          ╭─ concepts ┤                     ├─ C-h ── help
          │           │                     ╰─ M-g ── "go"
          │           ╰─ completion ┬─ minibuffer ── completing-read
          │                         ╰─ completion-at-point ── capf
          │        ╭─ Lisp-2 ┬─ variables namespace
          │        │         ╰─ functions namespace
          │        ├─ flat namespace
          │        ├─ macro
          │        ├─ function
          ├─ Elisp ┤                          ╭─ list
          │        │                          ├─ alist
          │        │           ╭─ collections ┼─ plist
          │        ╰─ variable ┤              ├─ hash-map
          │                    │              ╰─ cons cell
« Emacs » ┤                    ├─ defconst
          │                    ╰─ defvar
          │                                         ╭─ :init
          │                          ╭─ use-package ┼─ :config
          │              ╭─ packages ┤              ├─ :custom
          ├─ Emacs-Elisp ┤           │              ╰─ :bind
          │              │           ├─ package.el
          │              │           ╰─ autoload ── user-lisp-directory ── Emacs 31
          │              ├─ hooks
          │              ╰─ advices
          │                        ╭─ mailing list
          │         ╭─ emacs-devel ┴─ maintainers communicate there ┬─ Sean Whitton
          │         │                                               ╰─ Eli Zaretski
          │         ├─ reddit ── users ┬─ r/emacs
          ╰─ people ┤                  ╰─ r/orgmode
                    │              ╭─ Prot
                    ╰─ personalies ┼─ Sasha Chua
                                   ├─ Mike Zamansky
                                   ╰─ u/minad ── u/oanolin ── abo-abo ── alphapapa

(setq load-prefer-newer t)

My GNU Emacs configuration works from two machines I regularly use. Configuration is an Org file living within my Notes corpus. Then on save it's tangled into el file and init.el loads it. I keep custom packages in separate repository that configuration adds to load-path.

On <2026-08-22 Sat> I bumped into situation, when on one of machines, after fresh Emacs startup a symbol from lozenge.el package is not defined.

– prompt

defalias doesn't load via require and use-package why doesn't loading lozenge.el at emacs startup define an alias ◊git-status? I did (require 'lozenge), evaluated (use-package) block, added :demand t to it, still (◊git-status "~/.velppa/hotter.myaddr.dev") was failing with

eval: Symbol’s function definition is void: ◊git-status

I had to go to lozenge.el and do (eval-buffer) there.

Root cause was stale lozenge.elc file that didn't have alias defined. Solved by removing all elc files, and for the future setting this in Pavel's Emacs Configuration v3:

(setq load-prefer-newer t)

claude-pager

On <2026-06-03 Wed> I somehow bumped into idea to use emacsclient as EDITOR in Claude Code.

Turns out CC respects EDITOR env variable, either from environment or configured in ~/.claude/settings.json:

{"env": {"EDITOR": "emacsclient", "VISUAL": "emacsclient"}}

Even if [Feature Request] Allow configuring external editor for Ctrl-G prompt editing · Issue #18990 · anthropics/claude-code is still in open.

In that issue I spotted this project: gradigit/claude-pager. Fast C transcript pager for Claude Code: no blank Ctrl-G screen, clickable links/files, queued prompt composer. And its sister editor: gradigit/turbodraft.

Tried both. Turbodraft - I don't need another text editor rather than Emacs, so skipping it. But claude-pager looks useful, so I forked it.

First, I wired Emacs into claude-pager; so that it renders transcripts in both CC screen when it waits for the prompt + adds it to Emacs temporary buffer.

This mostly solves clunkiness of copying from claude terminal into Emacs and back - now I press {C-g} in claude and continue editing in Emacs.

Related to Agent SDK billing and parting ways with agent-shell.

This is how my env block looks like in ~/.claude/settings.json.

{
  "env": {
    "EDITOR": "/Users/pavel/.velppa/claude-pager/bin/claude-pager-open",
    "CLAUDE_PAGER_EDITOR": "/Users/pavel/.velppa/claude-pager/emacs/claude-emacs-prompt",
    "CLAUDE_PAGER_EDITOR_TYPE": "gui"
  }
}

{C-g} in claude opens claude-pager-open that generates transcript and pastes it into both terminal and to the temp file to edit in Emacs, then runs claude-emacs-prompt shell file that calls elisp file which loads into the buffer, assigns keybindings etc.

Setup involves adding hooks:

SessionStart hook:
     26:            "command": "/Users/pavel/.velppa/claude-pager/shim/save-session-transcript.sh"

Then I rewrote the binary from C to Zig. Rewrite used Superpowers to develop a spec first, then launch a series of agents. It took 4+ hours for CC to finish rewrite, it was ready in the morning. It used all extra usage (42 EUR) in fast mode in first 20 minutes.

7000 loc in C => ??? in Zig.

Now I understand the C well.

Another interesting project from the same author:

  • gradigit/confetti - Lightweight confetti animation for macOS — fire colorful confetti from your screen corners

Copy buffer-name with easy-kill

easy-kill Emacs package allows killing multiple things, switching between them on the fly. I use it to quickly grab the buffer-file-name (full path or file name only), using {M-w b}.

github
leoliu/easy-kill

Keybindings:

{M-w} without active region
copy current line, same as {x y} in meow-normal-mode
{M-w C-w} without active region
kill current line, same as {x s} in meow-normal-mode
{M-w b}
copy current full path to buffer-file-name
{M-w b 0}
copy current buffer-file-name
{M-w b -}
copy current directory

On <2026-06-04 Thu> I added {M-w B} to copy current buffer-name, useful to paste into Claude Code (running under ghostel for me).

Here's the configuration block:

(use-package easy-kill
  :config
  (keymap-global-set "<remap> <kill-ring-save>" #'easy-kill)
  (defun easy-kill-on-buffer-name (_n)
    "Get current `buffer-name'."
    (if (easy-kill-get mark)
        (easy-kill-echo "Not supported in `easy-mark'")
      (easy-kill-adjust-candidate 'buffer-name (buffer-name))))
  (add-to-list 'easy-kill-alist '(?B buffer-name)))
  1. Define function easy-kill-on-buffer-name.
  2. Add binding to 'easy-kill-alist.

kids-unicode-mode

Emacs supports Unicode:

(insert-char (char-from-name "T-REX")) ;; 🦖

To have fun with kids, I wrapped kids-unicode minor mode that brings keymap redefining every key to insert emoji. Enable with {M-x kids-unicode-mode} and have fun typing, encoding names with emojis and train memory.

(use-package kids-unicode-mode :ensure nil)

Source: kids-unicode-mode.el

Showcase: 260516--kids-unicode-mode__screenshot.png

terraform-ts-mode

At work I work with Terraform, so need to edit quite a lot of tf files in GNU Emacs. There's no built-in terraform-ts-mode Emacs package and I don't want to install regexp-based terraform-mode package

Existing terraform-ts-mode is this: kgrotel/terraform-ts-mode, which claims being experimental. So it's a good opportunity to learn how to create major modes using Tree Sitter and on <2026-05-05 Tue> I built my own with Claude Code.

Installation: put terraform-ts-mode.el to load-path.

Usage:

(use-package terraform-ts-mode :ensure nil
  :init
  (add-to-list 'major-mode-remap-alist '(terraform-mode . terraform-ts-mode)))

With this knowledge, the next step is [TK: try mickeynp/combobulate package].

Window configuration on m4pro

MacBook Pro M4 Pro has larger screen and windows in Emacs behave differently, often ending up with 6 windows at once. That's not what I want.

Time to learn Window management in Emacs.

On <2026-04-21 Tue> I switched to ace-window from other-window and keeping track of what has changed.

Keybindings:

Good:

Bad:

Things to try:

Advice to org-roam-dailies-goto-today

Currently, when calling org-roam-dailies-goto-today, {C-c n d} for me, Emacs switches to the daily note buffer, but moves point to (point-min). I want to advice to first check if the buffer with "YYYY-MM-DD.org" (for today) already exists, and if so, switch to this buffer. Related to Org-roam.

Here's an advice function to achieve that:

(defun my-org-roam-dailies-goto-today-advice (orig-fun &rest args)
  "Switch to today's daily note buffer/window if it exists, otherwise create it.
Preserves point position if buffer already exists."
  (let* ((today (format-time-string "%Y-%m-%d"))
         (daily-buffer-name (concat today ".org"))
         (existing-buffer (get-buffer daily-buffer-name))
         (existing-window (and existing-buffer
                               (get-buffer-window existing-buffer t))))
    (cond
     ;; Buffer is already displayed in a window - switch to that window
     (existing-window
      (select-window existing-window))
     ;; Buffer exists but not displayed - switch to it in current window
     (existing-buffer
      (switch-to-buffer existing-buffer))
     ;; Buffer doesn't exist - create it
     (t
      (apply orig-fun args)))))

(advice-add 'org-roam-dailies-goto-today
            :around #'my-org-roam-dailies-goto-today-advice)

This advice:

  1. Checks for buffer by name (not file path)
  2. If the buffer is displayed in any window (even in another frame), selects that window
  3. If the buffer exists but isn't displayed, switches to it in the current window
  4. Otherwise calls the original function

To remove the advice later:

(advice-remove 'org-roam-dailies-goto-today
               #'my-org-roam-dailies-goto-today-advice)

Improve public notes

Next round of improving Textpod on <2026-04-15 Wed>.

Deploy publicly –

Introduced --base-path flag, to serve notes under sub-path.

To start Textpod with sub-path, use:

textpod --token $TEXTPOD_TOKEN --base-path notes
Publish textpod.el to the repository

textpod.el is a companion Emacs package for quick publishing notes from Org Mode documents.

Install it like this:

(use-package textpod
  :config
  (setq textpod-token (cadr (auth-source-user-and-password "finita.myaddr.dev" "textpod")))
  (setq textpod-url "/notes"))

Then these helper functions do the heavy-lifting:

  • textpod-org-region-to-note: Convert the Org region between BEG and END to Markdown and send to Textpod.
  • textpod-org-heading-to-note: Send the current heading’s subtree to Textpod.
  • textpod-open-current-note: Open the current heading’s Textpod note in a browser.

When creating a note, textpod.el sets TEXTPOD_ID property that's equal to node'id.

Change separator in notes.md to ^L

Default separator \n\n---\n\n is fragile as it requires exactly two newlines before / after the note. I often messed up notes when editing notes.md file manually. To fix that, I replaced it with ^L character (insert in Emacs via C-q C-l) which usually represents new pages.

Fix attachments links

Attachement links need to respect --base-path flag.

Handling attachments

My current workflow:

  • Assets are stored next to Notes under assets/ directory

  • I can quickly search for them, then open in Emacs/qView:

    find ../assets -name "*prefix-in-emacs*"
    
    ../assets/m-g-prefix-in-emacs.png
    ../assets/m-s-prefix-in-emacs.png
    
  • I can reference an asset as an image: img

  • An image is a link to an image file that does not have a description part (Images (The Org Manual)).

  • Textpod already support images. What happens when I drag-n-drop an image file to the textarea? Textpod copies them under attachments directory.

What I want:

  • When creating a public note containing an image via API (using textpod.el), copy the asset to assets/ directory, mimicing the directory structure in the notes.
  • I need another API endpoint - POST /notes/assets/<name>, accepting binary as payload.

Okay, I implemented it, let's test.

textpod-org-heading-to-note

Prompt: Implement textpod-org-heading-to-note function, it should select current (top) heading - the one that has TEXTPOD_ID parameter, and call textpod-org-region-to-note on it.

The command {M-x textpod-org-heading-to-note} works!

Should the textpod id be just id?

Currently I use TEXTPOD_ID property. If using ID property, notes will automatically become Org-roam nodes. Currently IDs are generated as timestamps via (setq org-id-method 'ts) with nanoseconds precision. Then it will come with seconds precision. I will need to a prefix, to be able to quickly find them (e.g. with {M-x deadgrep}).

Wrap h3 into details on index page

To declutter the index page, h3 markdown headings (h2 Org headings) are wrapped into <details> tag. The note page renders normally, showing table of contents on the left sidebar.