#+TITLE: Álvaro Ramírez #+AUTHOR: Álvaro Ramírez #+OPTIONS: toc:nil num:nil ^:nil * [2026-01-29 Thu] Film/TV bookmarks (chaos resolved) :PROPERTIES: :CUSTOM_ID: film-tv-bookmarks-chaos-resolved :END: I've been somewhat chaotically "bookmarking" things to watch for some time. The info typically goes straight to an Emacs org file primarily via [[https://journelly.com/][Journelly]]. I'm fairly forgiving in my input form. I often link to [[https://www.reddit.com][Reddit]] discussions, [[https://www.imdb.com][IMDB]]/[[https://letterboxd.com][Letterboxd]] links, or at times simply write movie or director names. The only real requirement is to sprinkle some hashtags (#film or #series or #watch) for retrieval. While this flexibility is important (makes it more likely for me to actually write), it's not super practical to browse through this mixed structure when looking for something to watch. Yesterday, I finally did something about that. Here's what my =save-to-Journelly= flow may look like for a Reddit link: #+ATTR_HTML: :width 95% [[file:images/film-tv-bookmarks-chaos-resolved/sharing.png]] All entries are saved to Journelly.org, a plain text org file ([[https://xenodium.com/journelly-1-3-released][Markdown is supported too]]), so I first extracted all relevant entries (containing #film or #series or #watch). A matching entry looks a little something like this: #+begin_src org ,* [2026-01-29 Thu 09:52] @ Home :PROPERTIES: :LATITUDE: 51.5007 :LONGITUDE: -0.1246 :WEATHER_TEMPERATURE: 4.2°C :WEATHER_CONDITION: Cloudy :WEATHER_SYMBOL: cloud :END: #film #hongkong #watch https://www.reddit.com/r/movies/comments/1j85b52/just_rewatched_kung_fu_hustle_still_felt_great #+end_src Having extracted non-personal items from =Journelly.org=, I created a git repo with just one file: =watchlist.org= including entries like the one above. With personal/private details out of the way, I decided to let the LLM robots loose. That is, hand =watchlist.org= over to a [[https://claude.com/product/claude-code][Claude Code agent]], via Emacs [[https://github.com/xenodium/agent-shell][agent-sell]] to organize my chaos. First I asked to normalize data by extracting all film/tv info from each entry, visiting Reddit/IMDB/Letterboxd links if needed, which yielded =normalized.org=: #+begin_src text Kung Fu Hustle https://www.imdb.com/title/tt0373074 #+end_src Next I asked to generate metadata for me (using the fields I wanted from =IMDB=), stored in org drawers, saved to =db.org=: #+begin_src org ,* Kung Fu Hustle :PROPERTIES: :TYPE: film :YEAR: 2004 :IMDB: https://www.imdb.com/title/tt0373074/ :IMDB_RATING: 7.7 :COUNTRY: Hong Kong :DIRECTOR: Stephen Chow :GENRE: action, comedy, fantasy :RUNTIME: 99 :ADDED: [2026-01-28] :THUMBNAIL: file:thumbnails/tt0373074.png :IMDB_THUMBNAIL: https://m.media-amazon.com/images/M/MV5BNGU2OWVlM2ItZGZlOC00Yzk2LWE1NzEtMDYwMzE4YTE5MzQ2XkEyXkFqcGc@._V1_FMjpg_UY1024_.jpg :END: #+end_src Finally, I asked to generate HTML from =db.org= so I can render and easily browse. #+begin_src html
#+end_src For each transformation, =Claude Code= created a python script. I've yet to decide what to do with all the generated source. Do I clean it up? Rewrite it? For now, I'm happy with the experiment result. I managed to organize some of my chaos! In some ways, this may not be too different from me writing a hacky/ugly script, when it's acceptable to do so. At the center of all this is my beloved org syntax. Thanks to plain text formats, we can easily peek at them, query them, poke at them, and bend til our heart's content. It's just so versatile and now we can throw them at LLMs too. Oh, and if you're curious about that watch list, here you go: #+BEGIN_EXPORT html
#+HTML:
#+HTML:
** Emacs can trim your videos too
Emacs is just about the perfect porcelain for command-line utilities. With little ceremony, you can integrate almost any CLI tool. [[https://magit.vc/][Magit]] remains the gold standard for CLI integration.
While trimming videos doesn't typically spring to mind as an Emacs use case, I was pleasantly [[https://xenodium.com/emacs-as-your-video-trimming-tool][surprised by the possibilities]].
#+ATTR_HTML: :width 95%
[[file:images/emacs-as-your-video-trimming-tool/trim.gif]]
** Landing Emacs patches upstream
While I've built my [[https://github.com/sponsors/xenodium][fair share of Emacs packages]], I'm still fairly new at submitting Emacs features upstream. This year, I landed my [[https://xenodium.com/emacs-send-to-aka-macos-sharing-merged-upstream][send-to (aka sharing on macOS) patch]]. While the proposal [[https://debbugs.gnu.org/cgi/bugreport.cgi?bug=76120][did spark quite the discussion]], I'm glad I stuck with it. Both Eli and Stefan were amazingly helpful.
#+ATTR_HTML: :width 95%
[[file:images/emacs-send-to-aka-macos-sharing-merged-upstream/context-menu.png]]
#+ATTR_HTML: :width 95%
[[file:images/emacs-send-to-aka-macos-sharing-merged-upstream/send-to.png]]
This year, I also wanted to experiment with dictating into my Emacs text buffers, but unfortunately [[https://lists.gnu.org/archive/html/bug-gnu-emacs/2025-03/msg00585.html][dictation had regressed in Emacs 30]].
Bummer. But hey, it gave me a new opportunity to submit [[https://xenodium.com/macos-dictation-returns-to-emacs-fix-merged][another patch upstream]].
#+ATTR_HTML: :width 95%
[[file:images/macos-dictation-returns-to-emacs-fix-merged/dictation.webp]]
** Ready Player improvements
[[https://github.com/xenodium/ready-player][Ready Player]], my Emacs media-playing package [[https://github.com/xenodium/ready-player/commits/main/?since=2025-01-01][received further improvements]] like starring media (via Emacs bookmarks), enabling further customizations, and other bug fixes. Also showcased a [[https://xenodium.com/a-tour-of-ready-player-mode][tour of its features]].
#+ATTR_HTML: :width 60%
[[file:images/a-tour-of-ready-player-mode/open-file.gif]]
** GitHub activity
- Commits: 1,095
- Issues created: 37
- PRs reviewed: 106
- Average commits per day: ~3
** New GitHub projects
- [[https://github.com/xenodium/EverTime][EverTime]] - An ever present clock for macOS
- [[https://github.com/xenodium/acp.el][acp.el]] - An ACP implementation in Emacs lisp
- [[https://github.com/xenodium/agent-shell][agent-shell]] - A native Emacs buffer to interact with LLM agents powered by ACP
- [[https://github.com/xenodium/diverted][diverted]] - Identify temporary Emacs diversions and return to original location
- [[https://github.com/xenodium/emacs-materialized-theme][emacs-materialized-theme]] - An Emacs theme derived from Material
- [[https://github.com/xenodium/homebrew-evertime][homebrew-evertime]] - EverTime formula for the Homebrew package manager
- [[https://github.com/xenodium/homebrew-one][homebrew-one]] - Homebrew recipe for one
- [[https://github.com/xenodium/homebrew-rinku][homebrew-rinku]] - Homebrew recipe for rinku
- [[https://github.com/xenodium/one][one]] - Transform images into character art using text from your codebase
- [[https://github.com/xenodium/rinku][rinku]] - Generate link previews from the command line (macOS)
- [[https://github.com/xenodium/time-zones][time-zones]] - View time at any city across the world in Emacs
- [[https://github.com/xenodium/video-trimmer][video-trimmer]] - A video-trimming utility for Emacs
- [[https://github.com/xenodium/wasabi][wasabi]] - A WhatsApp Emacs client powered by wuzapi and whatsmeow
** Blog posts
- [[https://xenodium.com/journelly-1-3-released][Journelly 1.3 released: Hello Markdown!]]
- [[https://xenodium.com/agent-shell-0-25-updates][agent-shell 0.25 updates]]
- [[https://xenodium.com/bending-emacs-episode-8-completing-read][Bending Emacs - Episode 8: completing-read]]
- [[https://xenodium.com/at-one-with-your-code][At one with your code]]
- [[https://xenodium.com/bending-emacs-episode-7-eshell-built-in-commands][Bending Emacs - Episode 7: Eshell built-in commands]]
- [[https://xenodium.com/rinku-cli-link-previews][Rinku: CLI link previews]]
- [[https://xenodium.com/bending-emacs-episode-6-overlays][Bending Emacs - Episode 6: Overlays]]
- [[https://xenodium.com/whatsapp-from-you-know-where][WhatsApp from you know where]]
- [[https://xenodium.com/want-a-whatsapp-emacs-client][Want a WhatsApp Emacs client? Will you fund it?]]
- [[https://xenodium.com/bending-emacs-episode-5-ready-player-mode][Bending Emacs - Episode 5: Ready Player Mode]]
- [[https://xenodium.com/agent-shell-016-improvements-melpa][agent-shell 0.17 improvements + MELPA]]
- [[https://xenodium.com/time-zones-now-on-melpa][time-zones now on MELPA. Do I have your support?]]
- [[https://xenodium.com/bending-emacs-episode-4-batch-renaming-files][Bending Emacs - Episode 4: Batch renaming files]]
- [[https://xenodium.com/emacs-time-zones-mode][Emacs time-zones]]
- [[https://xenodium.com/bending-emacs-episode-3-git-clone-the-lazy-way][Bending Emacs - Episode 3: Git clone (the lazy way)]]
- [[https://xenodium.com/agent-shell-0-5-improvements][agent-shell 0.5 improvements]]
- [[https://xenodium.com/bending-emacs-episode-2][Bending Emacs - Episode 2: From vanilla to your flavor]]
- [[https://xenodium.com/bending-emacs-episode-1][Bending Emacs - Episode 1: Applying CLI utils]]
- [[https://xenodium.com/introducing-agent-shell][Introducing Emacs agent-shell (powered by ACP)]]
- [[https://xenodium.com/introducing-acpel][Introducing acp.el]]
- [[https://xenodium.com/so-you-want-acp-for-emacs][So you want ACP (Agent Client Protocol) for Emacs?]]
- [[https://xenodium.com/diverted-mode][Diverted mode]]
- [[https://xenodium.com/who-moved-my-text][Who moved my text?]]
- [[https://xenodium.com/dired-buffers-with-media-overlays][Dired buffers with media overlays]]
- [[https://xenodium.com/brisket-recipe][Brisket recipe]]
- [[https://xenodium.com/a-tiny-upgrade-to-the-llm-model-picker][A tiny upgrade to the LLM model picker]]
- [[https://xenodium.com/emacs-elevator-pitch][Emacs elevator pitch]]
- [[https://xenodium.com/emacs-as-your-video-trimming-tool][Emacs as your video-trimming tool]]
- [[https://xenodium.com/macos-dictation-returns-to-emacs-fix-merged][macOS dictation returns to Emacs (fix merged)]]
- [[https://xenodium.com/writing-experience-my-decade-with-org][Writing experience: My decade with Org]]
- [[https://xenodium.com/interactive-ordering-of-dired-items][Interactive ordering of dired items]]
- [[https://xenodium.com/patching-your-homebrews-emacs-plus-macos][Patching your Homebrew's Emacs Plus (macOS)]]
- [[https://xenodium.com/emacs-send-to-aka-macos-sharing-merged-upstream][Emacs send-to (aka macOS sharing) merged upstream]]
- [[https://xenodium.com/mochi-invaders-now-on-the-app-store][Mochi Invaders now on the App Store]]
- [[https://xenodium.com/markdown-is-coming-to-journelly][Markdown is coming to Journelly]]
- [[https://xenodium.com/evertime-available-via-homebrew][EverTime available via Homebrew]]
- [[https://xenodium.com/journelly-1-2-released][Journelly 1.2 released]]
- [[https://xenodium.com/ranking-officer-now-on-the-app-store][Ranking Officer now on the App Store]]
- [[https://xenodium.com/awesome-emacs-on-macos][Awesome Emacs on macOS]]
- [[https://xenodium.com/journelly-1-1-released][Journelly 1.1 released]]
- [[https://xenodium.com/llm-text-chat-is-everywhere-whos-optimizing-ux][LLM text chat is everywhere. Who's optimizing its UX?]]
- [[https://xenodium.com/a-richer-journelly-org-capture-template][A richer Journelly org capture template]]
- [[https://xenodium.com/journelly-like-tweeting-but-for-your-eyes-only][Journelly: like tweeting but for your eyes only (in plain text)]]
- [[https://xenodium.com/journelly-vs-emacs-why-not-both][Journelly vs Emacs: Why Not Both?]]
- [[https://xenodium.com/the-mac-observer-showcases-journelly][The Mac Observer showcases Journelly]]
- [[https://xenodium.com/journelly-open-for-beta][Journelly open for beta]]
- [[https://xenodium.com/deepseek-open-router-kagi-and-perplexity-join-the-chat][DeepSeek, Open Router, Kagi, and Perplexity join the chat]]
- [[https://xenodium.com/keychron-k3-pro-f1-f12-as-default-macos-keys][Keychron K3 Pro: F1-F12 as default macOS keys]]
- [[https://xenodium.com/e-ink-bookmarks][E-ink bookmarks]]
- [[https://xenodium.com/sourdough-bookmarks][Sourdough bookmarks]]
- [[https://xenodium.com/cardamom-buns-a-yak-shaving-story-plus-recipe][Cardamom Buns recipe]]
- [[https://xenodium.com/a-tour-of-ready-player-mode][A tour of Ready Player Mode]]
- [[https://xenodium.com/a-platform-that-moulds-to-your-needs][A platform that moulds to your needs]]
- [[https://xenodium.com/blogging-minus-the-yucky-bits-of-modern-web][Blogging minus the yucky bits of the modern web]]
Hope you enjoyed my 2025 contributions. [[https://github.com/sponsors/xenodium][Sponsor]] the work.
* [2025-12-21 Sun] Journelly 1.3 released: Hello Markdown!
:PROPERTIES:
:CUSTOM_ID: journelly-1-3-released
:END:
#+ATTR_HTML: :width 98%
[[file:images/journelly-1-3-released/markdown.png]]
#+BEGIN_EXPORT html

Journelly 1.3 available on the App Store
Just a bit of fun. That's all there is to it.
While I've only run it on macOS, =one='s written in Go, so should be fairly portable. I'd love to know if you get it running on Linux. The code's on [[https://github.com/xenodium/one][GitHub]].
If you're on macOS, I've added a [[https://brew.sh][Homebrew]] on [[https://github.com/xenodium/homebrew-one][GitHub]], so you should just be able to install with:
#+begin_src sh
brew install --HEAD xenodium/one/one
#+end_src
** Make it all sustainable
Having fun with =one=? Enjoying this [[https://xenodium.com/][blog]] or [[https://github.com/xenodium][my projects]]? I am an 👉 indie dev 👈. Help make it sustainable by ✨[[https://github.com/sponsors/xenodium][sponsoring]]✨
Need a blog? [[https://lmno.lol][I can help with that]]. Maybe buy my [[https://apps.apple.com/us/developer/xenodium-ltd/id304568690][iOS apps]] too ;)
* [2025-11-30 Sun] Bending Emacs - Episode 7: Eshell built-in commands
:PROPERTIES:
:CUSTOM_ID: bending-emacs-episode-7-eshell-built-in-commands
:END:
With my recent [[https://xenodium.com/rinku-cli-link-previews][rinku post]] and [[https://xenodium.com/bending-emacs-episode-6-overlays][Bending Emacs episode 6]] both fresh in mind, I figured I may as well make another Bending Emacs episode, so here we are:
[[https://youtu.be/M6o1N2kfmuc][Bending Emacs Episode 7: Eshell built-in commands]]
[[https://youtu.be/M6o1N2kfmuc][Video]]
Check out the [[https://xenodium.com/rinku-cli-link-previews][rinku post]] for a rundown of things covered in the video.
** Want more videos?
Liked the video? Please let me know. Got feedback? Leave [[https://youtu.be/93wWCroTKnM][me some comments]].
Please go [[https://youtu.be/M6o1N2kfmuc][like my video]], share with others, and [[https://www.youtube.com/@xenodium][subscribe to my channel]].
If there's enough interest, I'll continue making more videos!
** Make it all sustainable
Enjoying this content or [[https://github.com/xenodium][my projects]]? I am an indie dev. Help make it sustainable by ✨[[https://github.com/sponsors/xenodium][sponsoring]]✨
Need a blog? [[https://lmno.lol][I can help with that]]. Maybe buy my [[https://apps.apple.com/us/developer/xenodium-ltd/id304568690][iOS apps]] too ;)
* [2025-11-29 Sat] Rinku: CLI link previews
:PROPERTIES:
:CUSTOM_ID: rinku-cli-link-previews
:END:
In my last [[https://www.youtube.com/watch?v=77NtPfgr4x0&list=PLudVBwrl_ir84jCQtAzDVtBc_oSIBvdCO&pp=gAQB][Bending Emacs]] episode, I [[https://xenodium.com/bending-emacs-episode-6-overlays][talked about overlays]] and used them to render link previews in an Emacs buffer.
While the overlays merely render an image, the actual link preview image is generated by [[https://github.com/xenodium/rinku][rinku]], a tiny command line utility I built recently.
=Rinku= leverages macOS APIs to do the actual heavy lifting, rendering/capturing a view off screen, and saving to disk. Similarly, it can fetch preview metadata, also saving the related thumbnail to disk. In both cases, =rinku= outputs to JSON.
By default, =rinku= fetches metadata for you.
#+begin_src sh
rinku https://soundcloud.com/shehackedyou
#+end_src
Returns:
#+begin_src json
{
"title": "she hacked you",
"url": "https://soundcloud.com/shehackedyou",
"image": "path/to/preview.png"
}
#+end_src
In this instance, the image looks a little something like this:
#+HTML:
** Eshell superpowers
While overlays is one way to integrate =rinku= anywhere in Emacs, I had been meaning to look into what I can do for eshell in particular. [[https://xenodium.com/yasnippet-in-emacs-eshell][Eshell is just another buffer]], and while overlays could do the job, I wanted a shell-like experience. After all, I already knew we can [[https://xenodium.com/wizard-zines-comics-eshell-util][echo images into an eshell buffer]].
Before getting to =rinku= on =eshell=, there's a related hack I'd been meaning to get to for some time... While we're all likely familiar with the [[https://www.man7.org/linux/man-pages/man1/cat.1.html][cat]] command, I remember being a little surprised to find that =eshell= offers an alternative =cat= elisp implementation. Surprised too? Go check it!
#+begin_src sh
$ which cat
eshell/cat is a native-comp-function in ‘em-unix.el’.
#+end_src
Where am I going with this? Well, if eshell's =cat= command is an elisp implementation, we know its internals are [[https://xenodium.com/its-all-up-for-grabs-and-it-compounds][up for grabs]], so we can technically
extend it to display images too. =eshell/cat= is just another function, so we can [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html][advice it]] to add image superpowers.
I was pleasantly surprised at how little code was needed. It basically scans for image arguments to handle within advice and otherwise delegates to =eshell='s original =cat= implementation.
#+begin_src emacs-lisp
(defun adviced:eshell/cat (orig-fun &rest args)
"Like `eshell/cat' but with image support."
(if (seq-every-p (lambda (arg)
(and (stringp arg)
(file-exists-p arg)
(image-supported-file-p arg)))
args)
(with-temp-buffer
(insert "\n")
(dolist (path args)
(let ((spec (create-image
(expand-file-name path)
(image-type-from-file-name path)
nil :max-width 350
:conversion (lambda (data) data))))
(image-flush spec)
(insert-image spec))
(insert "\n"))
(insert "\n")
(buffer-string))
(apply orig-fun args)))
(advice-add #'eshell/cat :around #'adviced:eshell/cat)
#+end_src
And with that, we can see our freshly powered-up =cat= command in action:
#+ATTR_HTML: :width 95%
[[file:images/rinku-cli-link-previews/cat.png]]
By now, you may wonder why the =cat= detour when the post was really about =rinku=? You see, this is Emacs, and everything compounds! We can now leverage our revamped =cat= command to give similar =eshell= superpowers to =rinku=, by merely adding an =eshell/rinku= function.
As we now know, =rinku= outputs things to JSON, so we can use =json-read-from-string= to parse the process output and subsequently feed the image path to =eshell/cat=. =rinku= can also output link titles, so we can show that too whenever possible.
#+begin_src emacs-lisp :lexical no
(defun eshell/rinku (&rest args)
"Fetch link preview with rinku and display image inline.
Usage: rinku https://soundcloud.com/shehackedyou
rinku --render https://soundcloud.com/shehackedyou"
(unless args
(error "rinku: no arguments provided"))
(let* ((output (with-temp-buffer
(apply #'call-process "rinku" nil t nil args)
(buffer-string)))
(metadata (condition-case nil
(json-read-from-string output)
(error nil))))
(if metadata
(concat
(if (map-elt metadata 'image)
(eshell/cat (map-elt metadata 'image))
"\n")
(when (map-elt metadata 'title)
(concat (map-elt metadata 'title)
"\n\n")))
output)))
#+end_src
With that, we can see the lot in action:
#+ATTR_HTML: :width 95%
[[file:images/rinku-cli-link-previews/rinku.gif]]
While non-Emacs users are often puzzled by how frequently we bring user flows and integrations on to our beloved editor, once you learn a little elisp, you start realising how relatively easily things can integrate with one another and pretty much [[https://xenodium.com/its-all-up-for-grabs-and-it-compounds][everything is up for grabs]].
** Make it all sustainable
Reckon =rinku= and these tips will be useful to you? Enjoying this [[https://xenodium.com/][blog]] or [[https://github.com/xenodium][my projects]]? I am an 👉 indie dev 👈. Help make it sustainable by ✨[[https://github.com/sponsors/xenodium][sponsoring]]✨
Need a blog? [[https://lmno.lol][I can help with that]]. Maybe buy my [[https://apps.apple.com/us/developer/xenodium-ltd/id304568690][iOS apps]] too ;)
* [2025-11-27 Thu] Bending Emacs - Episode 6: Overlays
:PROPERTIES:
:CUSTOM_ID: bending-emacs-episode-6-overlays
:END:
The Bending Emacs series continues with a new a new episode.
[[https://youtu.be/93wWCroTKnM][Bending Emacs Episode 6: Overlays]]
[[https://youtu.be/93wWCroTKnM][Video]]
Today we had a quick intro to overlays. Here's the snippet I used for adding snippets:
#+begin_src emacs-lisp
(save-excursion
(goto-char (point-min))
(when (search-forward "Hello World" nil t)
(let* ((start (match-beginning 0))
(end (match-end 0))
(ov (make-overlay start end)))
(overlay-put ov 'face '(:box (:line-width 1 :color "yellow")))
;; (overlay-put ov 'face 'underline)
;; (overlay-put ov 'face 'highlight)
;; (overlay-put ov 'before-string "🔥 ")
;; (overlay-put ov 'after-string " 🚀")
;; (overlay-put ov 'display "Howdy Planet")
;; (overlay-put ov 'invisible t)
;; (overlay-put ov 'help-echo "Yay overlay!")
;; (overlay-put ov 'mouse-face 'success)
(overlay-put ov 'category 'overlays)
(overlay-put ov 'evaporate t)
ov)))
#+end_src
Similarly, this is what we used for removing the overlay.
#+begin_src emacs-lisp
(remove-overlays (point-min) (point-max)
'category 'overlays)
#+end_src
Of the experiments, you can find:
- Redaction snippet [[https://xenodium.com/redact-that-buffer][at the related blog post]].
- Dired media metadata at Ready Player's [[https://github.com/xenodium/ready-player/blob/main/ready-player-dired.el][ready-player-dired.el]].
- Link previews: While I don't have elisp to share for link previews just yet, I did release a tiny thumbnail utility named [[https://github.com/xenodium/rinku][rinku]] ;)
Hope you enjoyed the video!
** Want more videos?
Liked the video? Please let me know. Got feedback? Leave [[https://youtu.be/93wWCroTKnM][me some comments]].
Please go [[https://youtu.be/93wWCroTKnM][like my video]], share with others, and [[https://www.youtube.com/@xenodium][subscribe to my channel]].
If there's enough interest, I'll continue making more videos!
** Make it all sustainable
Enjoying this content or [[https://github.com/xenodium][my projects]]? I am an indie dev. Help make it sustainable by ✨[[https://github.com/sponsors/xenodium][sponsoring]]✨
Need a blog? [[https://lmno.lol][I can help with that]]. Maybe buy my [[https://apps.apple.com/us/developer/xenodium-ltd/id304568690][iOS apps]] too ;)
* [2025-11-24 Mon] WhatsApp from you know where
:PROPERTIES:
:CUSTOM_ID: whatsapp-from-you-know-where
:END:
While there are plenty of messaging alternatives out there, for better or worse, [[https://www.whatsapp.com/][WhatsApp]] remains a necessity for some of us.
With that in mind, I looked for ways to bring WhatsApp messaging to the comfort of my beloved text editor.
As mentioned in [[https://xenodium.com/want-a-whatsapp-emacs-client][my initial findings]], WhatsApp on Emacs is totally doable with the help of [[https://github.com/asternic/wuzapi][wuzapi]] and [[https://github.com/tulir/whatsmeow][whatsmeow]], which offer a huge leg up.
** Wasabi joins the chat
Today, I introduce a super early version of [[https://github.com/xenodium/wasabi/][Wasabi]], a native Emacs interface for WhatsApp messaging.
#+HTML:
#+HTML:
#+HTML:
#+HTML:
** With diverted-mode
Notice how point is left where it was prior to selecting the function and pressing TAB to indent region.
#+HTML:
That's it for today. If you want to give =diverted.el= a try, head over to [[https://github.com/xenodium/diverted][GitHub]].
** Make it all sustainable
Reckon =diverted-mode= will be useful to you? Enjoying this [[https://xenodium.com/][blog]] or [[https://github.com/xenodium][my projects]]? I am an 👉 indie dev 👈. Help make it sustainable by ✨[[https://github.com/sponsors/xenodium][sponsoring]]✨
Need a blog? [[https://lmno.lol][I can help with that]]. Maybe buy my [[https://apps.apple.com/us/developer/xenodium-ltd/id304568690][iOS apps]] too ;)
* [2025-08-31 Sun] Who moved my text?
:PROPERTIES:
:CUSTOM_ID: who-moved-my-text
:END:
I had an annoying [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]] bug where sometimes the compose buffer's svg header would disappear while text was streaming into the Emacs buffer. There are a number of things that could have gone rogue when streaming and post-processing buffer text, so I wasn't quite sure where to start narrowing things down.
I had a feeling I could maybe use something like =after-change-functions= hook to monitor changes, but that only tells me about the text modified. I wanted to know which of my functions triggered the change, so maybe I could print a few frames from the stack? So that's what I did...
#+begin_src emacs-lisp :lexical no
(defun my-change-tracker (beg end len)
(let ((stack (mapcar (lambda (frame)
(car (cdr frame)))
(backtrace-frames))))
(message "Buffer %s changed: %d-%d | Stack: %s"
(buffer-name) beg end
(take 15 stack)))) ;; Adjust as needed
(add-hook 'after-change-functions 'my-change-tracker nil t)
#+end_src
Here's an extract from the logs:
#+begin_quote
Buffer *claude llm (sonnet-4/Programming)> compose* changed: 496-498 | Stack: (backtrace-frames mapcar let my-change-tracker insert save-excursion let save-current-buffer progn if #[(output) ((setq output (or output )) (if (buffer-live-p buffer) (progn (save-current-buffer (set-buffer buffer) (let ((inhibit-read-only t)) (save-excursion (if orig-region-active (progn (delete-region region-beginning region-end) (setq orig-region-active nil))) (goto-char marker) (insert output) (set-marker marker (+ (length output) (marker-position marker))))))))) ((region-end) (region-beginning) (orig-region-active) (marker . #

Journelly 1.2 available on the App Store
#+HTML: 
Journelly 1.1 available on the App Store
"It’s the perfect mix of simplicity and low-tech plain text wizardry"
"It takes a very particular set of features for a new app to impress me enough to hit the purchase button as fast as I did with Journelly."
"Journelly is the first Org-powered app I’ve seen that lays out the welcome mat for people who don’t even know what Org is, never mind how to use it."
Ellane / ellanew.com
** Changing course
In the past, I envisioned a different path for multi-model support. By isolating shell logic into a new package ([[https://github.com/xenodium/shell-maker][shell-maker]]), folks could use it as a [[https://lmno.lol/alvaro/a-shell-maker][building block to create new shells]] (adding support for their favourite LLM).
While each shell-maker-based shell currently shares a basic common experience, I did not foresee the minor differences affecting the general Emacs user experience. Learning the quirks of each new shell felt like unnecessary friction in developing muscle memory. I also became dependent on =chatgpt-shell= features, which I often missed when using other shells.
Along with slightly different shell experiences, we currently require multiple package installations (and setups). Depending on which camp you're on (batteries included vs fine-grained control) this may or may not be a downside.
With every new =chatgpt-shell= feature I showcased, I was often asked if they could be used with other LLM providers. I typically answered with "I've been meaning to work on this..." or "I heard you can do multi-model =chatgpt-shell= using a bridge like [[https://docs.litellm.ai/docs/][liteLLM]]". Neither of these where great answers, resulting in me just postponing the chunky work.
Eventually, I bit the bullet, changed course, and got to work on multi-model support. With my initial plan to spin multiple shells via =shell-maker=, =chatgpt-shell='s implementation didn't exactly lend itself to support multiple clients. Long story short, =chatgpt-shell= multi-model support required quite a bit of work. This where I divert to ask you to [[https://github.com/sponsors/xenodium][help make this project sustainable by sponsoring the work]].
** Make this project sustainable
Maintaining, experimenting, implementing feature requests, and supporting open-source packages takes work. Today, chatgpt-shell has over [[https://melpa.org/#/chatgpt-shell][20.5K downloads on MELPA]] and many untracked others elsewhere. If you're one of the happy users, [[https://github.com/sponsors/xenodium][consider sponsoring the project]]. If you see potential, help [[https://github.com/sponsors/xenodium][fuel development by sponsoring]] too.
Perhaps you enjoy some of the content I write about? Find my Emacs posts/tips useful?
- [[https://xenodium.com/][Blog (xenodium.com)]] (Web)
- [[https://lmno.lol/alvaro][Blog (lmno.lol/alvaro)]] (Web)
Alternatively, you want a blogging platform that skips the yucky side effects of the modern web?
- I'm building [[https://lmno.lol][lmno.lol]] (my blog is [[https://lmno.lol/alvaro][there]]).
Maybe you enjoy one of my other projects?
- [[https://plainorg.com][Plain Org]] (org mode / iOS)
- [[https://flathabits.com][Flat Habits]] (org mode / iOS)
- [[https://apps.apple.com/us/app/scratch/id1671420139][Scratch]] (org mode / iOS)
- [[https://github.com/xenodium/macosrec][macosrec]] (macOS)
- [[https://apps.apple.com/us/app/fresh-eyes/id6480411697?mt=12][Fresh Eyes]] (macOS)
- [[https://github.com/xenodium/dwim-shell-command][dwim-shell-command]] (Emacs)
- [[https://github.com/xenodium/company-org-block][company-org-block]] (Emacs)
- [[https://github.com/xenodium/org-block-capf][org-block-capf]] (Emacs)
- [[https://github.com/xenodium/ob-swiftui][ob-swiftui]] (Emacs)
- [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]] (Emacs)
- [[https://github.com/xenodium/ready-player][ready-player]] (Emacs)
- [[https://github.com/xenodium/sqlite-mode-extras][sqlite-mode-extras]]
- [[https://github.com/xenodium/ob-chatgpt-shell][ob-chatgpt-shell]] (Emacs)
- [[https://github.com/xenodium/dall-e-shell][dall-e-shell]] (Emacs)
- [[https://github.com/xenodium/ob-dall-e-shell][ob-dall-e-shell]] (Emacs)
- [[https://github.com/xenodium/shell-maker][shell-maker]] (Emacs)
So, umm... I'll just leave my GitHub sponsor page [[https://github.com/sponsors/xenodium][here]].
** chatgpt-shell, more than a shell
With chatgpt-shell being a [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Prompts.html][comint]] shell, you can bring your favourite Emacs flows along.
#+HTML:
As I used =chatgpt-shell= myself, I kept experimenting with different integrations and improvements. Read on for some of my favourites...
*** A shell hybrid
=chatgpt-shell= includes a compose buffer experience. This is my favourite and most frequently used mechanism to interact with LLMs.
For example, select a region and invoke =M-x chatgpt-shell-prompt-compose= (=C-c C-e= is my preferred binding), and an editable buffer automatically copies the region and enables crafting a more thorough query. When ready, submit with the familiar =C-c C-c= binding. The buffer automatically becomes read-only and enables single-character bindings.
#+HTML:
*** Navigation: n/p (or TAB/shift-TAB)
Navigate through source blocks (including previous submissions in history). Source blocks are automatically selected.
*** Reply: r
Reply with with follow-up requests using the =r= binding.
*** Give me more: m
Want to ask for more of the same data? Press =m= to request more of it. This is handy to follow up on any kind of list (suggestion, candidates, results, etc).
*** Request entire snippets: e
LLM being lazy and returning partial code? Press =e= to request entire snippet.
*** Quick quick: q
I'm a big fan of quickly disposing of Emacs buffers with the =q= binding. chatgpt-shell compose buffers are no exception.
** Confirm inline mods (via diffs)
Request inline modifications, with explicit confirmation before accepting.
#+HTML:
** Execute snippets (a la [[https://orgmode.org/worg/org-contrib/babel/intro.html][org babel]])
Both the shell and the compose buffers enable users to execute source blocks via =C-c C-c=, leveraging [[https://orgmode.org/worg/org-contrib/babel/intro.html][org babel]].
#+HTML:
** Vision experiments
I've been experimenting with image queries (currently ChatGPT only, please [[https://github.com/sponsors/xenodium][sponsor]] to help bring support for others).
Below is a handy integration to extract Japanese vocabulary. There's also a generic image descriptor available via =M-x chatgpt-shell-describe-image= that works on any Emacs image (via dired, image buffer, point on image, or selecting a desktop region).
#+HTML:
** Supporting new models
Your favourite model not yet supported? File a [[https://github.com/xenodium/chatgpt-shell/issues/new][feature request]]. You also know how to [[https://github.com/sponsors/xenodium][fuel the project]]. Want to contribute new models? [[https://github.com/xenodium/chatgpt-shell/issues/new][Reach out]].
** Local models
While the two new implementations rely on cloud APIs, local services are now possible. I've yet to use a local LLM, but please [[https://github.com/xenodium/chatgpt-shell/issues/new][reach out]], so we can make these happen too. Want to contribute?
** Should chatgpt-shell rename?
With =chatgpt-shell= going multi-model, it's not unreasonable to ask if this package should be renamed. Maybe it should. But that's additional work we can likely postpone for the time being (and avoid pushing users to migrate). For now, I'd prefer focusing on polishing the multi-model experience and work on ironing out any issues. For that, I'll need your help.
** Take Gemini and Claude for a spin
Multi-model support required chunky structural changes. While I've been using it myself, I'll need wider usage to uncover issues. Please take it for a spin and [[https://github.com/xenodium/chatgpt-shell/issues/new][file bugs or give feedback]]. Or if you just want to ping me, I'd love to hear about your experience ([[https://indieweb.social/@xenodium][Mastodon]] / [[https://twitter.com/xenodium][Twitter]] / [[https://www.reddit.com/user/xenodium][Reddit]] / [[mailto:me__AT__xenodium.com][Email]]).
- Be sure to update to =chatgpt-shell= v2.0.1 and =shell-maker= v0.68.1 as a minimum.
- Set =chatgpt-shell-anthropic-key= or =chatgpt-shell-google-key=.
- Swap models with existing =M-x chatgpt-shell-swap-model-version= or set a default with =(setq chatgpt-shell-model-version "claude-3-5-sonnet-20240620")= or =(setq chatgpt-shell-model-version "claude-gemini-1.5-pro-latest")=.
- Everything else should just work 🤞😅
Happy Emacsing!
* [2024-11-13 Wed] chatgpt-shell splits up
:PROPERTIES:
:CUSTOM_ID: chatgpt-shell-repo-splits-up
:END:
The [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]] package started as an experiment [[https://lmno.lol/alvaro/a-chatgpt-emacs-shell][glueing the ChatGPT API to an Emacs comint buffer]]. Over time, it grew into several packages within the same repository: shell-maker, ob-chatgpt-shell, dall-e-shell, ob-dall-e-shell, and of course chatgpt-shell itself.
I'm splitting the repository as a first step in reworking =chatgpt-shell= to enable multi-model support (i.e. Gemini, Claude, and others), a popular feature request.
** Want multi-model support?
Go 👍 the [[https://github.com/xenodium/chatgpt-shell/issues/244][feature request]] and ✨[[https://github.com/sponsors/xenodium][sponsor]]✨ the work.
If keen on having a multi-modal =chatgpt-shell= at your fingertips, please consider [[https://github.com/sponsors/xenodium][sponsoring]] to make the project sustainable. Improvements like this, integrations, and keeping up with the AI space takes quite a bit of work and effort.
** New package repositories
*** chatgpt-shell
No repo location changes. Remains at https://github.com/xenodium/chatgpt-shell
=chatgpt-shell= carries the ChatGPT shell itself, but also convenience integrations.
My hope is to make this a multi-model package.
#+ATTR_HTML: :width 100%
[[file:images/chatgpt-shell-repo-splits-up/swiftui.webp]]
#+ATTR_HTML: :width 100%
[[file:images/chatgpt-shell-repo-splits-up/japanese.webp]]
#+ATTR_HTML: :width 80%
[[file:images/chatgpt-shell-repo-splits-up/fix.webp]]
*** ob-chatgpt-shell
Moves to https://github.com/xenodium/ob-chatgpts-shell
An extension of =chatgpt-shell= to execute org babel blocks as ChatGPT prompts.
#+ATTR_HTML: :width 80%
[[file:images/chatgpt-shell-repo-splits-up/ob-chatgpt-shell.png]]
*** dall-e-shell
Moves to https://github.com/xenodium/dall-e-shell
A dedicated shell for DALL-E image generation.
#+ATTR_HTML: :width 80%
[[file:images/chatgpt-shell-repo-splits-up/dall-e-shell.png]]
*** ob-dall-e-shell
Moves to https://github.com/xenodium/ob-dall-e-shell
An extension of =dall-e-shell= to execute org babel blocks as ChatGPT prompts.
#+ATTR_HTML: :width 80%
[[file:images/chatgpt-shell-repo-splits-up/ob-dall-e-shell.png]]
*** shell-maker
Moves to https://github.com/xenodium/shell-maker
=shell-maker= a convenience wrapper around [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Prompts.html][comint mode]] to build shells. Both [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]] and [[https://github.com/xenodium/dall-e-shell][dall-e-shell]] are built on top of shell-maker.
#+ATTR_HTML: :width 70%
[[file:images/chatgpt-shell-repo-splits-up/sofia.gif]]
** Enjoying this content? Using one of my Emacs packages?
Help make the work sustainable. Consider [[https://github.com/sponsors/xenodium][sponsoring]]. I'm also building [[https://lmno.lol/][lmno.lol]]. A platform to [[https://indieweb.social/@xenodium/112265481282475542][drag and drop]] your blog to the web.
* [2024-10-21 Mon] Hide another detail
:PROPERTIES:
:CUSTOM_ID: hide-another-detail
:END:
It's been 5 years since I talked about [[https://xenodium.com/showhide-emacs-dired-details-in-style/][showing/hiding Emacs dired details in style]], a short post showcasing [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Misc-Dired-Features.html][hide-details-mode]] (built-in) and [[https://github.com/purcell/diredfl][diredfl]] (third-party).
While my dired usage increased over the years, my dired config remained largely unchanged. Today, I'll show a new dired tweak.
As you likely suspect by now, I'm a big fan of [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Misc-Dired-Features.html][hide-details-mode]]. It gives me super clean and minimalistic view of my files.
#+ATTR_HTML: :width 95%
[[file:images/hide-another-detail/before.png]]
If I need more details, it's one toggle away using my trusty C-( binding.
#+ATTR_HTML: :width 95%
[[file:images/hide-another-detail/toggle-before.gif]]
Now this is a super minor thing, but for a little while, I wished I could also hide the current directory's absolute path as part of =hide-details-mode='s toggling. In the same spirit as other hidden dired details, I rarely need to see the absolute path. And if I did, it'd only be a toggle away.
With that in mind, I set out to bend dired my way. I looked at the =dired-hide-details-mode= built-in code (dired.el) and came across invisibility specs, which I hadn't used before. Dired uses =add-to-invisibility-spec= and =remove-from-invisibility-spec= to show and hide details using the =invisible= property set to =dired-hide-details-information=.
Now that we know what property to set, we need to find the text to apply it to. Dired offers that via =dired-subdir-regexp=. All we need to do is match the regular expression and apply our invisible property to the relevant bounds.
#+begin_src emacs-lisp :lexical no
(defun hide-dired-details-include-all-subdir-paths ()
(save-excursion
(goto-char (point-min))
(while (re-search-forward dired-subdir-regexp nil t)
(let* ((match-bounds (cons (match-beginning 1) (match-end 1)))
(path (file-name-directory (buffer-substring (car match-bounds)
(cdr match-bounds))))
(path-start (car match-bounds))
(path-end (+ (car match-bounds) (length path)))
(inhibit-read-only t))
(put-text-property path-start path-end
'invisible 'dired-hide-details-information)))))
#+end_src
All that's left is for us to add our new function to a hook, and we're good to go.
#+begin_src emacs-lisp :lexical no
(use-package dired
:hook ((dired-mode . dired-hide-details-mode)
(dired-after-readin . hide-dired-details-include-all-subdir-paths)))
#+end_src
My Dired window is even cleaner now. The current directory's absolute path is now hidden.
#+ATTR_HTML: :width 95%
[[file:images/hide-another-detail/after.png]]
There may be times we need to peek at the absolute path. We can now toggle hiding this detail just like the others.
#+ATTR_HTML: :width 95%
[[file:images/hide-another-detail/toggle-after.gif]]
** My first Emacs patch
While this is a rather small change, I figured I could use it to get my toes dipped as a first Emacs contribution. I've since reworked the patch to fit into dired.el's code and submitted for review.
I'm happy to report the tiny feature's now merged to master as of [[https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=7cbca90569472af5643905fca5b7ab2dea67f876][a couple of days ago]]. Yay! 🎉
It'll be sometime until the feature makes it to a release, but if you're living on the Emacs master edge, it should be available there. While the feature is disabled by default, it can enabled with:
#+begin_src emacs-lisp :lexical no
(setq dired-hide-details-hide-absolute-location t)
#+end_src
Happy hiding!
** Enjoying this content? Using one of my Emacs packages?
Help make the work sustainable. Consider [[https://github.com/sponsors/xenodium][sponsoring]]. I'm also building [[https://lmno.lol/][lmno.lol]]. A platform to [[https://indieweb.social/@xenodium/112265481282475542][drag and drop]] your blog to the web.
* [2024-09-21 Sat] How I batch apply and save one-liners
:PROPERTIES:
:CUSTOM_ID: how-i-batch-apply-and-save-one-liners
:END:
My significant other needed to share proof of address by providing a number of bank statements for a period of time. That's easy enough to download as pdfs from the bank, but statements typically provide more personal information than the recipient requires. For a proof of address, the first page is more than enough.
macOS's Preview app can easily delete pages from a pdf by selecting undesired pages and hitting the delete key. This is fine for one pdf but for a handful of them, I figured there's a command line incantation I could use out there, and indeed there is:
#+begin_src sh
qpdf my.pdf --pages . 1 -- my-one-page.pdf
#+end_src
With command in mind, I resorted to my now my typical approach of:
- Convert to a generic [[https://github.com/xenodium/dwim-shell-command][dwim-shell-command]].
- Batch apply.
I could be done at this point, but since I now have the command fresh in mind...
- Save command for future usage.
So let's get on with it.
** Converting to dwim-shell-command
#+begin_src sh
qpdf '<
| As an Org mode fan, so I wrote Plain Org for iOS. It's on the App Store. | |
| Inspired by Atomic Habits, I wrote Flat Habits for iOS. Also on the App Store. | |
| I needed an Emacs-inspired scratch buffer on iOS (who doesn't?), so I built one. |