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)))
- Define function
easy-kill-on-buffer-name. - 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:
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:
- {M-o}: ace-window
- {§}: ace-window
- {C-x o}: other-window (built-in)
- {C-u C-u M-o}: delete window
- {C-x
}: winner-undo
Good:
- {C-u M-o}: swap windows, crux package no longer needed.
- window-extras package contains two functions – transpose-windows (same as {C-u M-o}) and toggle-window-split. Will keep it for now.
Bad:
- golden-ratio-mode is not triggered, because it adds after-advice (ad-Advice-other-window). The advice itself is called ‘golden-ratio-resize-window’. For now I disabled golden-ratio mode. Will use balance-window {C-x +} ad-hoc.
Things to try:
- toggle-window-dedicated {C-x w d}
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:
- Checks for buffer by name (not file path)
- If the buffer is displayed in any window (even in another frame), selects that window
- If the buffer exists but isn't displayed, switches to it in the current window
- Otherwise calls the original function
To remove the advice later:
(advice-remove 'org-roam-dailies-goto-today
#'my-org-roam-dailies-goto-today-advice)
- Edit <2026-04-17 Fri>: check existing buffer using buffer name, not the buffer vising a file, that depends on org-roam-dailies-directory (I have two different, but one set of dailies).
- Edit <2026-04-21 Tue>: check if the daily buffer is already opened in existing window, if so - switch to it.
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:

-
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.
Build HackerNews Legible Frontend
Let's build a legible frontend for HackerNews.
Goals:
- Bigger fonts
- Dark theme
- Better favourites
Results:
- https://hotter.surge.sh/hn/ – JS-based version
- https://hn.ptpf29qr6x.workers.dev – Cloudflare Workers version (so eww-friendly)
Project node is HackerNews legible frontend.
M-s and M-g prefixes in Emacs
Let's talk about two semantic and useful prefixes in GNU Emacs - M-s
and M-g. Semantics is there - M-s is for "search", M-g is for "go".
M-g is for Go
- M-g l
- go to line
- M-g c
- go to char
- M-g g
- go go go
- M-g M-g
- also "go go go", for convenience, hold Alt and press g g twice.
- M-g w
- go to word, asks for the first character of a word and jumps there.
- M-g C-h
- I ran out of remembering keybindings, so this one is list
all of them – add
C-hafter any prefix and Emacs will show you the help. No need to use which-key unless necessary, you're in control - you'll memorise things only looking first into memory, if missing - take action to learn (and thus remember). which-key is a false friend for new Emacser, beware!
Checking the keymap, we see many aliases like M-g M-g, so M-g M-c, M-g
M-l, M-g M-n and so on, they are for mnemonical convenience.
Most of movements run avy, which can do anything, it's an awesome way of arbitrary movements and beyond.
M-s is for Search
M-s is an entrypoint to search-related things. 95% of the time I use
M-s d (deadgrep) and M-s o (occur).
M-s d runs deadgrep, a frontend for ripgrep for searching in the
current project. I use it all the time, and nowadays with rise of
LLM, making your project greppable is must have to enable AI to do
sensible changes in the project.
M-s o is mighty M-x occur, but adjusted to "do what I mean"
semantics – if selection is active, it runs "occur" on it, otherwise
prompts for input.
(use-package emacs
:config
(defun occur-dwim (&optional _)
"Run occur with Active Region if any, otherwise regular occur."
(interactive)
(if (use-region-p)
(occur (buffer-substring-no-properties (region-beginning) (region-end)))
(call-interactively 'occur)))
(keymap-global-set "M-s o" #'occur-dwim))
M-s h is highlighing prefix, which deserves a separate post. So see
you later, cheers!