<feed xmlns="http://www.w3.org/2005/Atom"> <id>https://batsov.com/</id><title>(think)</title><subtitle>Bozhidar Batsov's personal blog</subtitle> <updated>2026-06-11T18:17:54+03:00</updated> <author> <name>Bozhidar Batsov</name> <uri>https://batsov.com/</uri> </author><link rel="self" type="application/atom+xml" href="https://batsov.com/feed.xml"/><link rel="alternate" type="text/html" hreflang="en" href="https://batsov.com/"/> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <rights> © 2026 Bozhidar Batsov </rights> <icon>/assets/img/favicons/favicon.ico</icon> <logo>/assets/img/favicons/favicon-96x96.png</logo> <entry><title>Emacs loves AsciiDoc</title><link href="https://batsov.com/articles/2026/06/11/emacs-loves-asciidoc/" rel="alternate" type="text/html" title="Emacs loves AsciiDoc" /><published>2026-06-11T09:00:00+03:00</published> <updated>2026-06-11T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/06/11/emacs-loves-asciidoc/</id> <content type="html"><![CDATA[<p>Regular readers know I have a soft spot for <a href="https://asciidoc.org/">AsciiDoc</a> – I’ve written about it more than once, and it’s the markup behind the documentation of most of my bigger OSS projects (CIDER, nREPL, Projectile, RuboCop). It hits a sweet spot Markdown never quite reaches: rich enough for proper technical writing (admonitions, includes, real tables, cross-references), without dragging in the full ceremony of something like DocBook.</p><p>There was just one problem, and it nagged at me for years: editing AsciiDoc in Emacs was never much fun.</p><h2 id="background">Background</h2><p>For years the only real option was <code class="language-plaintext highlighter-rouge">adoc-mode</code>, and it was showing its age. The font-locking was uneven, it predated modern Asciidoctor syntax, and it lagged far behind what <code class="language-plaintext highlighter-rouge">markdown-mode</code> (and <code class="language-plaintext highlighter-rouge">org-mode</code>) offered. So I’d catch myself reaching for Markdown in situations where AsciiDoc was clearly the better tool – not because Markdown was better, but because <code class="language-plaintext highlighter-rouge">markdown-mode</code> was. That always bugged me.</p><p>Eventually the itch got bad enough that I did something about it. Back in 2023 I <a href="https://metaredux.com/posts/2023/03/12/adoc-mode-reborn.html">took over maintenance of adoc-mode</a>, which had been dormant for years, and set myself a clear goal: get it to feature parity with <code class="language-plaintext highlighter-rouge">markdown-mode</code>. More recently I also started a brand-new <a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a> built on top of tree-sitter. The target for both is the same – AsciiDoc support in Emacs that’s on par with Markdown, and ideally not far off from Org.</p><h2 id="less-is-more-unless-its-modes">Less is more, unless it’s modes</h2><p>Why two modes? They scratch slightly different itches:</p><ul><li><code class="language-plaintext highlighter-rouge">adoc-mode</code> is the classic, regexp-based mode. It works on any reasonably modern Emacs, has no external dependencies, and now ships a pile of interactive features (markup commands, list editing, image preview, tempo templates).<li><code class="language-plaintext highlighter-rouge">asciidoc-mode</code> is the newer, tree-sitter-based mode. It leans on a real grammar for robust, accurate highlighting and good performance in large documents. It’s leaner by design, and it needs Emacs 30.1+ with tree-sitter.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></ul><p>If you want the kitchen sink on any Emacs, reach for <code class="language-plaintext highlighter-rouge">adoc-mode</code>. If you’re on a recent Emacs and want highlighting backed by an actual parser, try <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>. Pick whichever fits your setup – both are actively maintained.</p><h2 id="recent-developments">Recent developments</h2><p>I’ve spent a good chunk of the past month bringing both modes to a place I’m genuinely happy with, and they’re now mostly feature-complete as far as I’m concerned. Some highlights:</p><ul><li><strong>Modern AsciiDoc, not legacy AsciiDoc.py.</strong> Both modes now track the modern Asciidoctor spec – something I’d <a href="/articles/2024/02/22/asciidoc-language-specification/">wanted to do for a while</a>. <code class="language-plaintext highlighter-rouge">+text+</code> and <code class="language-plaintext highlighter-rouge">++text++</code> are treated as passthroughs rather than monospace, curved-quote syntax (<code class="language-plaintext highlighter-rouge">"`text`"</code>) is recognized, the block ID shorthand <code class="language-plaintext highlighter-rouge">[#id]</code> is supported, checklists are highlighted, and the menus and templates dropped a bunch of deprecated AsciiDoc.py leftovers.<li><strong>More uniform font-locking.</strong> I went through the faces in both modes and aligned them with <code class="language-plaintext highlighter-rouge">markdown-mode</code> and <code class="language-plaintext highlighter-rouge">org-mode</code>: bold renders as plain bold, emphasis as plain italic, structural markup (delimiters, list markers) fades into the background, and dedicated semantic faces cover metadata, URLs, footnotes, highlights, and roles. Switching between Markdown and AsciiDoc no longer feels jarring.<li><strong>Navigation that actually works.</strong> Clickable cross-references and links, an <code class="language-plaintext highlighter-rouge">xref</code> backend over anchors, context-aware completion, and – for <code class="language-plaintext highlighter-rouge">adoc-mode</code> – cross-file Antora cross-reference resolution.<li><strong>Tooling integration.</strong> Asciidoctor-backed preview/export and a Flymake checker that reports parser errors inline.<li><strong>Native code-block fontification.</strong> <code class="language-plaintext highlighter-rouge">[source,LANG]</code> blocks in <code class="language-plaintext highlighter-rouge">asciidoc-mode</code> are highlighted using the language’s own major mode, just like in <code class="language-plaintext highlighter-rouge">markdown-mode</code> and <code class="language-plaintext highlighter-rouge">org-mode</code>. <code class="language-plaintext highlighter-rouge">adoc-mode</code> had added support for this a while back.</ul><p>Getting <code class="language-plaintext highlighter-rouge">asciidoc-mode</code> where I wanted it also meant sending a few fixes upstream to the <a href="https://github.com/cathaysia/tree-sitter-asciidoc">tree-sitter-asciidoc</a> grammar it builds on. A grammar-backed mode is only as good as its grammar, so some of the work happened a layer below Emacs. If you’re curious about that side of things, I wrote up the broader experience in <a href="/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learned/">building Emacs major modes with Tree-sitter</a>.</p><h2 id="give-it-a-try">Give it a try</h2><p>If you’ve been avoiding AsciiDoc in Emacs because the tooling wasn’t there – I get it, that was me too. But the situation is genuinely different now. Grab <a href="https://github.com/bbatsov/adoc-mode">adoc-mode</a> or <a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a>, point it at one of your <code class="language-plaintext highlighter-rouge">.adoc</code> files, and see how it feels.</p><p>And please share your feedback. Both projects are at the stage where real-world usage surfaces the rough edges I can’t see myself. Bug reports, ideas, and PRs are all very welcome on the respective issue trackers.</p><p>Together we can make AsciiDoc a first-class citizen of the Emacs ecosystem!<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p><p>That’s all from me, folks! Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>Also, I really wanted to name something <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Afterwards we’ll aim for world domination, of course. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>Regular readers know I have a soft spot for AsciiDoc – I’ve written about it more than once, and it’s the markup behind the documentation of most of my bigger OSS projects (CIDER, nREPL, Projectile, RuboCop). It hits a sweet spot Markdown never quite reaches: rich enough for proper technical writing (admonitions, includes, real tables, cross-references), without dragging in the full ceremony of...</summary> </entry> <entry><title>nREPL Forever</title><link href="https://batsov.com/articles/2026/05/20/nrepl-forever/" rel="alternate" type="text/html" title="nREPL Forever" /><published>2026-05-20T09:00:00+03:00</published> <updated>2026-05-20T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/20/nrepl-forever/</id> <content type="html"><![CDATA[<p>Last week I <a href="/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/">announced Port</a>, a small prepl client for Emacs. That post focused on Port itself, but writing it left me with the itch to do a follow-up on the bigger picture, because the socket REPL / prepl story is one I’ve been meaning to write up for years.</p><p>If you’ve been around Clojure long enough, you remember the chatter. Socket REPL landed in Clojure 1.8 (January 2016), prepl in Clojure 1.10 (December 2018), and for a couple of years there was a steady stream of posts, tweets, and Slack threads to the effect of “this is what we should be building tools on. nREPL is on the way out.” Some serious people put their weight behind that idea, and some of them went and built tools to prove it.</p><p>Now it’s 2026 and we can take stock.</p><h2 id="sky-is-the-limit">Sky is the limit</h2><p>The pitch was good. Socket REPL is just <em>the</em> Clojure REPL exposed on a TCP port. prepl wraps it with a structured printer so the bytes coming back are EDN-tagged maps (<code class="language-plaintext highlighter-rouge">:ret</code>, <code class="language-plaintext highlighter-rouge">:out</code>, <code class="language-plaintext highlighter-rouge">:err</code>, <code class="language-plaintext highlighter-rouge">:tap</code>) instead of a human-readable prompt. Both ship with Clojure itself. No external server library, no middleware, no third-party namespaces. You start a JVM, you bind a port, you’re done.</p><p>The intellectual case for moving off nREPL had been made by Rich Hickey himself, most clearly in a <a href="https://groups.google.com/g/clojure-dev/c/Dl3Stw5iRVA/m/IHoVWiJz5UIJ">March 2015 clojure-dev post</a> that’s worth reading in full. Rich didn’t actually attack nREPL by name in that message. What he did was argue carefully for what a REPL <em>is</em>: a thing that reads characters, evaluates forms, prints results, and loops, with those streams available to user code so that things like nested REPLs and debuggers compose naturally. The money line:</p><blockquote><p>While framing and RPC orientation might make things easier for someone who just wants to implement an eval window, it makes the resulting service strictly less powerful than a REPL.</p></blockquote><p>His proposal, in the same post, was that tools should open <em>multiple</em> connections to the running program: one for the human-facing stream, and dedicated channels for IDE operations. The socket REPL (which landed in 1.8 the following January) and prepl (which arrived in 1.10) were the official implementation of that worldview.</p><p>A handful of editor projects took the cue and built clients:</p><ul><li><a href="https://tutkain.flowthing.me/">Tutkain</a> for Sublime Text<li><a href="https://github.com/mauricioszabo/atom-chlorine">Chlorine</a> for Atom<li><a href="https://github.com/Olical/conjure">Conjure</a> for Neovim (in its early Rust incarnation)<li><a href="https://github.com/tonsky/Clojure-Sublimed">Clojure-Sublimed</a> by Nikita Tonsky<li>a steady drip of smaller experiments around <a href="https://github.com/Unrepl/unrepl"><code class="language-plaintext highlighter-rouge">unrepl</code></a>, <a href="https://github.com/Olical/propel"><code class="language-plaintext highlighter-rouge">propel</code></a>, and friends</ul><p>It was real momentum. If you were following Clojure tooling in 2018-2020, it genuinely felt like nREPL might be the past, and the future would be some combination of socket REPL plus a thin self-installing protocol on top of it. You can find a fair number of “RIP nREPL” hot takes from that period if you go looking.</p><h2 id="reality-check">Reality Check</h2><p>I went and surveyed each of those projects recently while working on Port. The pattern is depressingly consistent:</p><p><strong>Tutkain</strong> started on prepl. In November 2021, its v0.11 release explicitly stopped using prepl message framing and switched to a hand-rolled EDN-RPC protocol that Tutkain boots onto the raw socket REPL by sending it a base64-encoded blob. The new protocol has request ids, op dispatch (<code class="language-plaintext highlighter-rouge">:eval</code>, <code class="language-plaintext highlighter-rouge">:lookup</code>, <code class="language-plaintext highlighter-rouge">:completions</code>, <code class="language-plaintext highlighter-rouge">:test</code>, <code class="language-plaintext highlighter-rouge">:apropos</code>, <code class="language-plaintext highlighter-rouge">:interrupt</code>, …), and server-managed thread bindings. In other words: Tutkain grew into nREPL, just spelled differently.</p><p><strong>Chlorine</strong> never used prepl directly. It used socket REPL plus an <code class="language-plaintext highlighter-rouge">unrepl</code>-style upgrade blob. Its author’s successor project, <a href="https://github.com/mauricioszabo/lazuli">Lazuli</a>, abandoned the whole approach in favor of nREPL. The <a href="https://mauricio.szabo.link/blog/2024/11/09/goodbye-chlorine-and-welcome-lazuli/">post-mortem</a> is worth reading and is fairly blunt: tools that attempted prepl went back to nREPL because, honestly, it’s simply better.</p><p><strong>Conjure</strong> had a prepl client in its early Rust days. The current Lua/Fennel rewrite ships only an nREPL client. The author’s reasoning in the release notes was that nREPL “has complete ecosystem adoption and brilliant ClojureScript support.”</p><p><strong>Clojure-Sublimed</strong> technically still talks to a raw socket REPL, but only after sending it an EDN-printing prelude that upgrades the REPL to a structured protocol of tonsky’s own design. His <a href="https://tonsky.me/blog/clojure-sublimed-3/">post on the topic</a> is one of the most thoughtful pieces I’ve read on Clojure REPL design, and his conclusion is roughly: the bare socket REPL is <em>more</em> useful than prepl because you can install your own protocol on top of it. Which is true. But notice that everyone who reached that conclusion ended up reinventing the same wheel: ids, ops, request/response correlation, completion support, lookup, interrupts. You know, the things nREPL has had since 2010.</p><p>So the trajectory looks roughly like this:</p><ol><li>Editor decides nREPL is too heavy or an undesirable external dependency and starts on prepl.<li>Editor discovers prepl has no ids, no ops, no interrupts, no server-side completion, no namespace tracking, no test runner integration, etc.<li>Editor rolls a custom protocol on top of socket REPL, or…<li>Editor gives up and goes to nREPL.</ol><p>Pure prepl clients are nearly extinct in the wild. The one I found that qualifies is <a href="https://github.com/Olical/propel">propel</a> by Oliver Caldwell (of Conjure fame), which is delightful, about 70 lines of Clojure, and explicitly synchronous (one outstanding eval at a time). That works! But it’s not a foundation for the kind of feature set people expect from an editor.</p><h2 id="real-repl-vs-tool-friendly-repl">“Real” REPL vs. tool-friendly REPL</h2><p>Here’s where I land. Rich isn’t wrong that prepl is closer to a “real” REPL in the strict sense. prepl genuinely is a more faithful encoding of read-eval-print: each form goes in, each result comes out, and the semantics match what you’d get at the standard REPL prompt.</p><p>The thing is, “real REPL” is not the property you optimize for when you’re building editor tooling. The properties editor tooling actually needs are:</p><ul><li>A way to <strong>correlate</strong> a request with its response when output and results are interleaved.<li>A way to <strong>multiplex</strong> – one connection, several logical conversations.<li><strong>Server-side hooks</strong> for the operations every IDE expects: completion, lookup, go-to-definition, find-references, test running, stacktrace structuring, interrupt.<li>A <strong>protocol stable enough</strong> that ten different editors can target it without each one inventing its own dialect.</ul><p>nREPL was <em>explicitly designed</em> for those properties. The <a href="https://nrepl.org/nrepl/design/overview.html">ops, middleware, and transport</a> abstractions exist precisely because the people building it knew the consumers are not humans typing at a prompt, they’re programs negotiating a session.</p><p>Calling nREPL “not a real REPL” is technically defensible and practically beside the point. Nobody on the consuming end is confused about what nREPL is <em>for</em>.</p><h2 id="taking-stock-eight-years-later">Taking stock, eight years later</h2><p>I <a href="https://metaredux.com/posts/2018/10/29/nrepl-redux.html">wrote about nREPL’s revival in 2018</a>. At that point I had just finished migrating the project out of Clojure Contrib, and the goal was to give it a real home and a working development process. It was a lot of work, but in hindsight things played out pretty well.</p><p>Looking at where things ended up:</p><ul><li><strong>nREPL itself</strong> is healthier than it has ever been. Active maintainers, a proper <a href="https://nrepl.org">manual</a>, a steady release cadence, an actual ecosystem organization on GitHub.<li><strong>Most popular Clojure editors</strong> support it. <a href="https://github.com/clojure-emacs/cider">CIDER</a>, <a href="https://calva.io">Calva</a>, <a href="https://cursive-ide.com">Cursive</a> (via its own client), Conjure, <a href="https://github.com/liquidz/vim-iced">vim-iced</a>, you name it.<li><strong><a href="https://babashka.org">babashka</a></strong> ships with nREPL built in. You boot a <code class="language-plaintext highlighter-rouge">bb</code> and you get an nREPL server, no extra dependencies. That’s how a lot of people use nREPL in scripting contexts today, and it’s been a hit.<li><strong><a href="https://github.com/basilisp-lang/basilisp">basilisp</a></strong> (the Clojure dialect on Python) has <a href="https://github.com/basilisp-lang/basilisp-nrepl-async">nREPL support</a>. nREPL running on Python, talking to Emacs, evaluating Clojure. Nice.<li><strong><a href="https://github.com/clojure/clojure-clr">ClojureCLR</a></strong> has a working nREPL story now, and <strong><a href="https://jank-lang.org">jank</a></strong> (the C++ Clojure) has nREPL on its roadmap too.<li>The middleware ecosystem (<a href="https://github.com/clojure-emacs/cider-nrepl"><code class="language-plaintext highlighter-rouge">cider-nrepl</code></a>, <a href="https://github.com/clojure-emacs/refactor-nrepl"><code class="language-plaintext highlighter-rouge">refactor-nrepl</code></a>, <a href="https://github.com/nrepl/piggieback"><code class="language-plaintext highlighter-rouge">piggieback</code></a>, <a href="https://github.com/clojure-emacs/sayid"><code class="language-plaintext highlighter-rouge">sayid</code></a>, <a href="https://github.com/nrepl/drawbridge"><code class="language-plaintext highlighter-rouge">drawbridge</code></a>, …) is alive, well, and continues to add features.</ul><p>Meanwhile prepl is, as best as I can tell, mostly a curiosity. It got me a side project I had fun with. It did not displace nREPL.</p><h2 id="epilogue">Epilogue</h2><p>The history of tooling protocols is full of cases where “purer”, “simpler”, or “more elegant” lost to “shipped, documented, and battle-tested.” LSP beat fifteen ad-hoc language protocols. DAP beat the same fifteen debuggers. nREPL beat prepl in the (Clojure) editor space.</p><p>It’s not that the simpler thing is bad. prepl is a fine, elegant little protocol, and there’s a real case for embedding it in CI scripts, ops automation, deployment pipelines, or anywhere you want to drive a Clojure VM programmatically without pulling in a server library. Use it there.</p><p>But for editor tooling? The Clojure community made an enormous, multi-year, multi-tool investment in nREPL. We have the protocol, the middleware, the manual, the books, the conference talks. nREPL works, it’s actively maintained, it’s increasingly portable across Clojure dialects, and the design decisions that Rich called out as un-REPL-like are the exact ones that make it a good substrate for editors.</p><p>So I’ll say what I felt awkward saying back in 2018: nREPL forever. It’s the right abstraction for the job, and it’s not going anywhere.</p><p>One more thing. After finishing Port I got curious what a minimal nREPL client would look like by comparison, so I went and built one. As you can imagine, it turned out to be significantly simpler. If that sounds interesting, take a look at <a href="/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/">neat</a>, a small, language-agnostic nREPL client for Emacs.</p><p>Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>Last week I announced Port, a small prepl client for Emacs. That post focused on Port itself, but writing it left me with the itch to do a follow-up on the bigger picture, because the socket REPL / prepl story is one I’ve been meaning to write up for years. If you’ve been around Clojure long enough, you remember the chatter. Socket REPL landed in Clojure 1.8 (January 2016), prepl in Clojure 1....</summary> </entry> <entry><title>neat: a language-agnostic nREPL client for Emacs</title><link href="https://batsov.com/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/" rel="alternate" type="text/html" title="neat: a language-agnostic nREPL client for Emacs" /><published>2026-05-20T09:00:00+03:00</published> <updated>2026-05-20T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/</id> <content type="html"><![CDATA[<blockquote><p>I think I’ll take my REPL neat <br /> My parens black and my bed at three <br /> CIDER’s too sweet for me…</p><p>– Bozier</p></blockquote><p>Last week I <a href="/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/">announced Port</a>, a small prepl client for Emacs. Today I’m following it up with another small Emacs package. Meet <a href="https://github.com/nrepl/neat">neat</a>, a tiny, deliberately language-agnostic nREPL client.</p><h2 id="why">Why?</h2><p>For years I’ve been hearing some version of the same request: “could CIDER work with my non-Clojure nREPL server?”. Babashka, Basilisp, nREPL-CLR, even some homegrown servers people built on top of nREPL for languages I’d never heard of.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> The answer was always the same kind of squishy “sort of, in theory, with caveats”, because while bare nREPL is genuinely language-agnostic, CIDER is not. CIDER was built for Clojure and assumes Clojure pretty much everywhere.</p><p>I always thought the right answer was “let’s gradually make CIDER more language-agnostic.” That’s the kind of plan that sounds reasonable until you actually try it.</p><p>The thing that pushed me over the edge was, oddly enough, building Port. Port is small, focused, and doesn’t try to be CIDER. Working on it for a couple of weeks reminded me how (deceptively) productive it is to start from a clean slate when the new requirements don’t match the assumptions baked into a mature codebase. Trying to retrofit CIDER into a language-agnostic shape would have meant fighting with every helper that ever assumed <code class="language-plaintext highlighter-rouge">clojure.repl</code> exists, every middleware contract <code class="language-plaintext highlighter-rouge">cider-nrepl</code> defines, every project-type heuristic that knows about <code class="language-plaintext highlighter-rouge">deps.edn</code> and <code class="language-plaintext highlighter-rouge">project.clj</code> and nothing else. A whole lot of “is the server Clojure, or is it the other thing?” branches. The Port experience reaffirmed that the right move for a genuinely different client is a <em>new project</em>, not a thousand cuts to an existing one.</p><p>So <code class="language-plaintext highlighter-rouge">neat</code> was born. The name is short, says what it does (it’s neat, both in the small-and-tidy sense and in the “no deps, no special assumptions, just the protocol” sense), and conveniently leaves room for puns I haven’t fully committed to yet. I might land on a backronym one day. For now it’s just “neat”.</p><h2 id="what-neat-actually-is">What neat actually is</h2><p>neat is a small Emacs nREPL client. The code is split across four files:</p><ul><li><code class="language-plaintext highlighter-rouge">neat-bencode.el</code>: bencode encode/decode.<li><code class="language-plaintext highlighter-rouge">neat-client.el</code>: TCP connections, request dispatch, the standard nREPL ops.<li><code class="language-plaintext highlighter-rouge">neat-repl.el</code>: a comint-derived REPL buffer.<li><code class="language-plaintext highlighter-rouge">neat.el</code>: the entry point, customization group, and <code class="language-plaintext highlighter-rouge">neat-mode</code> minor mode for source buffers.</ul><p>It only uses Emacs builtins. There are no external runtime dependencies, not even on <code class="language-plaintext highlighter-rouge">clojure-mode</code>, because neat doesn’t assume Clojure on the other end. If you write <code class="language-plaintext highlighter-rouge">clojure-mode</code>, <code class="language-plaintext highlighter-rouge">fennel-mode</code>, <code class="language-plaintext highlighter-rouge">hy-mode</code>, or anything else that talks nREPL, you turn on <code class="language-plaintext highlighter-rouge">neat-mode</code> in that buffer and it just works.</p><p>The connection routing is also intentionally library-friendly. There’s a buffer-local <code class="language-plaintext highlighter-rouge">neat-current-connection</code> override so downstream packages can implement their own routing logic, plus a global default for the simple “one server at a time” case that most people will want.</p><p>Capability discovery is done at connect time via the nREPL <code class="language-plaintext highlighter-rouge">describe</code> op. neat doesn’t hardcode “this server has completions, this one doesn’t” assumptions. If the server reports a <code class="language-plaintext highlighter-rouge">completions</code> op, the CAPF backend lights up (with type annotations next to each candidate, when the server provides them). If it reports <code class="language-plaintext highlighter-rouge">lookup</code>, eldoc starts working and <code class="language-plaintext highlighter-rouge">M-.</code> jumps to definitions via an xref backend. If neither is there, you still get a perfectly serviceable raw REPL.</p><h2 id="basic-usage">Basic usage</h2><p>Start an nREPL server. Anything that speaks the protocol will do. For a Clojure server:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre><td class="rouge-code"><pre>clj -M:nrepl
# or
bb nrepl-server :port 7888
# or
lein repl :headless :port 7888
</pre></div></div><p>Then in Emacs:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>M-x neat RET localhost RET 7888 RET
</pre></div></div><p>A REPL buffer pops up, the prompt follows the server’s reported namespace, and you can type expressions at it. Multi-line input works because <code class="language-plaintext highlighter-rouge">RET</code> only submits when the form parses as balanced under <code class="language-plaintext highlighter-rouge">neat-repl-input-syntax-table</code> (Emacs Lisp syntax by default, which is close enough for any Lisp). Input history is persisted across sessions.</p><p>If there’s a <code class="language-plaintext highlighter-rouge">.nrepl-port</code> file in the project, the prompt defaults to its contents, so <code class="language-plaintext highlighter-rouge">M-x neat RET RET</code> is enough to connect.</p><p>To evaluate from a source buffer, turn on the minor mode:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>M-x neat-mode
</pre></div></div><p>The familiar bindings are there, intentionally compatible with what CIDER users expect:</p><table><thead><tr><th>Key<th>Command<tbody><tr><td><code class="language-plaintext highlighter-rouge">C-c C-e</code><td><code class="language-plaintext highlighter-rouge">neat-eval-last-sexp</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-c</code><td><code class="language-plaintext highlighter-rouge">neat-eval-defun</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-r</code><td><code class="language-plaintext highlighter-rouge">neat-eval-region</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-b</code><td><code class="language-plaintext highlighter-rouge">neat-eval-buffer</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-l</code><td><code class="language-plaintext highlighter-rouge">neat-load-buffer-file</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-z</code><td><code class="language-plaintext highlighter-rouge">neat-switch-to-repl</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-k</code><td><code class="language-plaintext highlighter-rouge">neat-interrupt-eval</code><tr><td><code class="language-plaintext highlighter-rouge">C-c M-n</code><td><code class="language-plaintext highlighter-rouge">neat-set-ns</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-d C-d</code><td><code class="language-plaintext highlighter-rouge">neat-show-doc-at-point</code><tr><td><code class="language-plaintext highlighter-rouge">M-.</code><td><code class="language-plaintext highlighter-rouge">xref-find-definitions</code><tr><td><code class="language-plaintext highlighter-rouge">M-,</code><td><code class="language-plaintext highlighter-rouge">xref-go-back</code></table><p><code class="language-plaintext highlighter-rouge">neat-eval-buffer</code> ships the buffer contents as an <code class="language-plaintext highlighter-rouge">eval</code> op; <code class="language-plaintext highlighter-rouge">neat-load-buffer-file</code> uses the standard <code class="language-plaintext highlighter-rouge">load-file</code> op instead, so the server can attribute file and line numbers to errors. Use the latter when you’re actually loading a file from disk and care about good diagnostics.</p><p><code class="language-plaintext highlighter-rouge">neat-set-ns</code> sets the buffer-local <code class="language-plaintext highlighter-rouge">neat-ns</code>, which gets sent as the <code class="language-plaintext highlighter-rouge">ns</code> field on every <code class="language-plaintext highlighter-rouge">eval</code> op from that buffer. For languages where the namespace is declared in the source (Clojure’s <code class="language-plaintext highlighter-rouge">(ns foo.bar)</code>, etc.), swap in a parser via <code class="language-plaintext highlighter-rouge">neat-buffer-ns-function</code>.</p><p>For juggling multiple connections, <code class="language-plaintext highlighter-rouge">M-x neat-list-connections</code> opens a tabulated-list buffer with one row per live connection, where you can set the default or disconnect interactively.</p><p>That’s roughly the whole user-facing surface today. There’s no jack-in command, no inspector, no debugger, no test runner. Likely there will never be, but if you need those you should probably be using CIDER anyways…</p><h2 id="should-you-use-it">Should you use it?</h2><p>If you write Clojure and CIDER works for you, keep using CIDER. It’s mature, full-featured, and supported, and I’m going to keep working on it for as long as people use it. Nothing about neat changes that.</p><p>But if you find yourself in one of these situations:</p><ul><li>you write a non-Clojure language whose runtime ships an nREPL server, and you’ve been muddling through with a half-supported CIDER setup,<li>you write Clojure but you value minimalism and don’t need the full CIDER feature set,<li>you’re building an Emacs package that needs to talk nREPL and you want a small, dependency-free library to build on,<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></ul><p>then neat might be a better fit. It’s small enough that you can read the whole thing in an afternoon, and the library/UI split (<code class="language-plaintext highlighter-rouge">neat-bencode</code> and <code class="language-plaintext highlighter-rouge">neat-client</code> are perfectly usable from other packages) is genuinely designed for downstream consumers.</p><h2 id="the-bigger-picture">The bigger picture</h2><p>neat is part of a broader push I’ve been chewing on for a while now: making nREPL a healthy multi-language ecosystem rather than a Clojure-only protocol. That push has three legs:</p><ol><li><strong>An actual nREPL specification.</strong> The <a href="https://github.com/nrepl/spec.nrepl.org">spec.nrepl.org</a> draft is (will be) the formal version of what today is “whatever nREPL the project does”.<li><strong>Reference clients.</strong> neat is one. The point of building a deliberately Clojure-free client is that it stress-tests the spec. Anywhere neat ends up needing to special-case the server, the spec has a gap.<li><strong>A compatibility test suite.</strong> The parameterised integration suite in neat already runs the same assertions against multiple servers and surfaces real divergences (Clojure batching <code class="language-plaintext highlighter-rouge">(println "hi")</code> into a single <code class="language-plaintext highlighter-rouge">out</code> message where Basilisp emits two, for example). I’d like to grow this into a portable suite that any nREPL server can self-check against.</ol><p>This is also why I keep teasing a “reference CLI client” in conversations. An editor client is one thing, but a small command-line nREPL client written in a non-Lisp language would be a much sharper test of how language-agnostic the protocol really is. neat is plausibly a precursor to that. Time will tell how far I push this; for now I just wanted to get the Emacs side moving.</p><h2 id="thanks">Thanks</h2><p>As always, big thanks to <a href="https://www.clojuriststogether.org/">Clojurists Together</a> and everyone supporting my open source work. You make it possible for me to keep tweaking and improving CIDER, nREPL, clj-refactor, and friends, and occasionally try something “neat” on the side. <code class="language-plaintext highlighter-rouge">neat</code> isn’t replacing any of the existing Clojure tooling for Emacs. It’s just another tool in the box for the people who want it.</p><p>Feedback, ideas, and contributions are most welcome over at the <a href="https://github.com/nrepl/neat/issues">issue tracker</a>.</p><p>Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p><a href="https://github.com/clojure-emacs/cider/issues/3905">https://github.com/clojure-emacs/cider/issues/3905</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>For a long time I planned to extract CIDER’s nREPL client code into a reusable package, but now that we have <code class="language-plaintext highlighter-rouge">neat</code> I probably will finally abandon this idea. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I think I’ll take my REPL neat My parens black and my bed at three CIDER’s too sweet for me… – Bozier Last week I announced Port, a small prepl client for Emacs. Today I’m following it up with another small Emacs package. Meet neat, a tiny, deliberately language-agnostic nREPL client. Why? For years I’ve been hearing some version of the same request: “could CIDER work with my non-Cloju...</summary> </entry> <entry><title>Port: a minimalist prepl client for Emacs</title><link href="https://batsov.com/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/" rel="alternate" type="text/html" title="Port: a minimalist prepl client for Emacs" /><published>2026-05-12T09:00:00+03:00</published> <updated>2026-05-12T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/</id> <content type="html"><![CDATA[<p>For ages I’ve had “add prepl support to CIDER” sitting somewhere in the back of my head. CIDER is built firmly around nREPL, but <a href="https://clojure.org/reference/repl_and_main#prepl">prepl</a> ships with Clojure itself, and the appeal of dropping the external REPL server requirement is obvious. Recently, as part of a broader internals cleanup “mini-project” in CIDER, I finally sat down and put a prototype together: <a href="https://github.com/clojure-emacs/cider/pull/3899">cider#3899</a>.</p><p>The good news is that the prototype sort of worked. The bad news is that the more I poked at it, the more I kept running into the same pattern. CIDER assumes ops, sessions, request ids, and a whole structured protocol that prepl simply doesn’t have. The amount of CIDER code that would need to grow “is this nREPL or prepl?” branches added up quickly, and I’d be papering over prepl’s limitations in dozens of subtle places. The exercise was fun, but it ended up reaffirming my long-standing belief that nREPL is a much better fit for editor tooling than prepl is.</p><p>The exercise did leave me thinking though. What if, instead of bolting prepl onto CIDER, I built a small standalone client in the spirit of <a href="https://github.com/clojure-emacs/inf-clojure">inf-clojure</a> and <a href="https://github.com/sanel/monroe">monroe</a>? Something tiny and focused that doesn’t have to pretend to be CIDER, and where prepl’s quirks would be the design rather than something to work around.</p><p>Conveniently, I was on vacation in Portugal at the time, where I spent a few days in Porto, and the name pretty much picked itself. <a href="https://github.com/clojure-emacs/port">Port</a> was born. It kept us firmly in the land of fun, drink-inspired Clojure-on-editor names: CIDER, <a href="https://calva.io">Calva</a> (after Calvados, the apple brandy), and now Port (the famous fortified wine). The protocol Port talks to is <code class="language-plaintext highlighter-rouge">prepl</code>, over a TCP <strong>port</strong>, so the pun was hard to pass up.</p><p>This time around I didn’t manage to land on a backronym I love (at least not yet). The contenders so far:</p><ul><li>prepl omnipotent repl toolkit (my favorite so far)<li>prepl-operated repl toolkit<li>peak optimized repl toolkit</ul><p>Naming is hard. I remain open to better suggestions. :D</p><h2 id="scope-or-what-port-isnt">Scope, or what Port isn’t</h2><p>Port is a side project. I don’t plan to invest serious time in it past the point I consider it feature-complete, which won’t be far beyond what’s already there. The deliberate goal is to keep it simple and focused, and its feature set will stay close to inf-clojure and monroe. <strong>Port is not competing with CIDER.</strong> If you want the full feature set (debugger, inspector, test runner, profiler, structured stacktraces, refactor support), CIDER + <code class="language-plaintext highlighter-rouge">cider-nrepl</code> is, and will remain, the way.</p><p>What Port gives you today is a small, dependable Clojure REPL that you can hook into Emacs without any external dependencies, just a stock Clojure JVM with a prepl listening on a port.</p><h2 id="a-few-architectural-notes">A few architectural notes</h2><p>If you’re up for the long version, <a href="https://github.com/clojure-emacs/port/blob/main/doc/design.md">doc/design.md</a> goes deep. Here’s the short version of what prepl gives you compared to nREPL:</p><ul><li><strong>No bencode</strong>. prepl emits EDN-tagged maps, one per line. This might be a feature or a problem, depending on your perspective.<li><strong>No middleware</strong>. Whatever the server prints is what you get. No interception, no extension surface.<li><strong>No sessions</strong>. There’s one thread per TCP connection.<li><strong>No ops</strong>. You send a Clojure form, the server evaluates it, and prints back tagged messages: <code class="language-plaintext highlighter-rouge">:ret</code>, <code class="language-plaintext highlighter-rouge">:out</code>, <code class="language-plaintext highlighter-rouge">:err</code>, <code class="language-plaintext highlighter-rouge">:tap</code>, plus an <code class="language-plaintext highlighter-rouge">:exception true</code> flag on errors.<li><strong>No request id</strong>. This is the main issue. Tags identify the <em>kind</em> of message, not which request produced it.</ul><p>That last point is the central design constraint. If you want to issue a request and reliably read back its result without accidentally consuming output from an unrelated background <code class="language-plaintext highlighter-rouge">future</code> or <code class="language-plaintext highlighter-rouge">tap&amp;gt;</code>, you need to layer correlation on top of the protocol. Port does this with two tricks:</p><ol><li><strong>Two sockets per session.</strong> A user socket drives the REPL buffer with raw streaming output, and a separate tool socket carries helper-command requests. Background prints from <code class="language-plaintext highlighter-rouge">future</code>/<code class="language-plaintext highlighter-rouge">agent</code> on the user thread don’t bleed into the tool channel.<li><strong>A bootstrap form.</strong> On connect, the tool socket evaluates a one-shot form that defines a <code class="language-plaintext highlighter-rouge">port.tooling/-eval</code> wrapper. Every subsequent helper call goes through it, which captures <code class="language-plaintext highlighter-rouge">*out*</code> / <code class="language-plaintext highlighter-rouge">*err*</code> and returns a tagged map containing the request id. The client matches the id against a pending-callback registry.</ol><p>This is what nREPL provides via sessions and ops, just reinvented at the TCP layer. It’s a fair amount of work for something nREPL gives you for free, which only strengthens my view that nREPL is the better protocol for editor tooling. Still, it was an interesting and educational exercise.</p><h2 id="zero-dependencies">Zero dependencies</h2><p>One thing I’m fairly proud of: Port has no hard dependencies. You’ll want either <code class="language-plaintext highlighter-rouge">clojure-mode</code> or <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> installed for the source-buffer side of things, but Port itself only soft-depends on them via runtime <code class="language-plaintext highlighter-rouge">fboundp</code> checks. Hook it onto whichever one(s) you actually use. I intend to keep it that way. Dependency creep is a real problem in the Emacs (every?) ecosystem, and a small package should stay small.</p><h2 id="whats-there-in-01">What’s there in 0.1</h2><p>I tagged <a href="https://github.com/clojure-emacs/port/releases/tag/v0.1.0">v0.1.0</a> yesterday. It’s small but already perfectly usable:</p><ul><li><code class="language-plaintext highlighter-rouge">M-x port</code> “jacks in” (bootstraps) (auto-detects <code class="language-plaintext highlighter-rouge">deps.edn</code> / <code class="language-plaintext highlighter-rouge">project.clj</code> / <code class="language-plaintext highlighter-rouge">bb.edn</code>, starts a server and connects to it)<li>single-buffer REPL with persistent input history, completion, and eldoc at the prompt<li>interactive evaluation from source buffers with pretty-printed results<li>structured stacktrace buffer with cause chain and navigable frames<li><code class="language-plaintext highlighter-rouge">M-.</code> find-definition that follows into jar sources<li>doc/source/apropos/macroexpand helpers</ul><p>MELPA submission is queued up next. After that, expect Port to be in <a href="/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenance/">burst-driven</a> maintenance mode like most of my smaller projects.</p><p>Feedback, ideas, and contributions are most welcome. The <a href="https://github.com/clojure-emacs/port/issues">issue tracker</a> is the right place.</p><h2 id="closing-thoughts">Closing thoughts</h2><p>Funny thing, I’d never actually written any code against prepl until I started this project. It was fun to spend some quality time with the “competition” of my beloved nREPL. Working with a different protocol always teaches you something about the one you’re used to, and I came away from this with a renewed appreciation for both: prepl is genuinely elegant for what it is, and nREPL is genuinely well-designed for what we use it for.</p><p>Big thanks to <a href="https://www.clojuriststogether.org/">Clojurists Together</a> and everyone else who supports my OSS Clojure work. You rock! Now if you’ll excuse me, I have new releases of CIDER, clj-refactor, and refactor-nrepl to get back to.</p><p>Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>For ages I’ve had “add prepl support to CIDER” sitting somewhere in the back of my head. CIDER is built firmly around nREPL, but prepl ships with Clojure itself, and the appeal of dropping the external REPL server requirement is obvious. Recently, as part of a broader internals cleanup “mini-project” in CIDER, I finally sat down and put a prototype together: cider#3899. The good news is that ...</summary> </entry> <entry><title>Batppuccin: My Take on Catppuccin for Emacs</title><link href="https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/" rel="alternate" type="text/html" title="Batppuccin: My Take on Catppuccin for Emacs" /><published>2026-03-29T10:00:00+03:00</published> <updated>2026-03-29T10:00:00+03:00</updated> <id>https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/</id> <content type="html"><![CDATA[<p>I promised I’d take a break from building Tree-sitter major modes, and I meant it. So what better way to relax than to build… color themes? Yeah, I know. My idea of chilling is weird, but I genuinely enjoy working on random Emacs packages. Most of the time at least…</p><h2 id="some-background">Some Background</h2><p>For a very long time my go-to Emacs themes were <a href="https://github.com/bbatsov/zenburn-emacs">Zenburn</a> and <a href="https://github.com/bbatsov/solarized-emacs">Solarized</a> – both of which I maintain popular Emacs ports for. Zenburn was actually one of my very first open source projects (created way back in 2010, when Emacs 24 was brand new). It served me well for years.</p><p>But at some point I got bored. You know the feeling – you’ve been staring at the same color palette for so long that you stop seeing it. My experiments with other editors (Helix, Zed, VS Code) introduced me to <a href="https://github.com/enkia/tokyo-night-vscode-theme">Tokyo Night</a> and <a href="https://github.com/catppuccin/catppuccin">Catppuccin</a>, and they’ve been my daily drivers since then.</p><p>Eventually, I ended up creating my own Emacs ports of both. I’ve already published <a href="https://github.com/bbatsov/tokyo-night-emacs">Tokyo Night for Emacs</a>, and I’ll write more about that one down the road. Today is all about Catppuccin. (and by this I totally mean Batppuccin!)</p><h2 id="why-another-catppuccin-port">Why Another Catppuccin Port?</h2><p>There’s already an <a href="https://github.com/catppuccin/emacs">official Catppuccin theme for Emacs</a>, and it works. So why build another one? A few reasons.</p><p>The official port registers a single <code class="language-plaintext highlighter-rouge">catppuccin</code> theme and switches between flavors (Mocha, Macchiato, Frappe, Latte) via a global variable and a reload function. This is unusual by Emacs standards and breaks the normal <code class="language-plaintext highlighter-rouge">load-theme</code> workflow – theme-switching packages like <code class="language-plaintext highlighter-rouge">circadian.el</code> need custom glue code to work with it. It also loads color definitions from an external file in a way that fails when Emacs hasn’t marked the theme as safe yet, which means some users can’t load the theme at all.</p><p>Beyond the architecture, there are style guide issues. <code class="language-plaintext highlighter-rouge">font-lock-variable-name-face</code> is set to the default text color, making variables invisible. All <code class="language-plaintext highlighter-rouge">outline-*</code> levels use the same blue, so org-mode headings are flat. <code class="language-plaintext highlighter-rouge">org-block</code> forces green on all unstyled code. Several faces still ship with <code class="language-plaintext highlighter-rouge">#ff00ff</code> magenta placeholder colors. And there’s no support for popular packages like vertico, marginalia, transient, flycheck, or cider.</p><p>I think some of this comes from the official port trying to match the structure of the Neovim version, which makes sense for their cross-editor tooling but doesn’t sit well with how Emacs does things.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p><h2 id="meet-batppuccin">Meet Batppuccin</h2><p><a href="https://github.com/bbatsov/batppuccin-emacs">Batppuccin</a> is my opinionated take on Catppuccin for Emacs. The name is a play on my last name (Batsov) + Catppuccin.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> I guess you can think of this as <code class="language-plaintext highlighter-rouge">@bbatsov</code>’s Catppuccin… or perhaps Batman’s Catppuccin?</p><p>The key differences from the official port:</p><p><strong>Four proper themes.</strong> <code class="language-plaintext highlighter-rouge">batppuccin-mocha</code>, <code class="language-plaintext highlighter-rouge">batppuccin-macchiato</code>, <code class="language-plaintext highlighter-rouge">batppuccin-frappe</code>, and <code class="language-plaintext highlighter-rouge">batppuccin-latte</code> are all separate themes that work with <code class="language-plaintext highlighter-rouge">load-theme</code> out of the box. No special reload dance needed.</p><p><strong>Faithful to the style guide.</strong> Mauve for keywords, green for strings, blue for functions, peach for constants, sky for operators, yellow for types, overlay2 for comments, rosewater for the cursor. The rainbow heading cycle (red, peach, yellow, green, sapphire, lavender) makes org-mode and outline headings actually distinguishable.</p><p><strong>Broad face coverage.</strong> Built-in Emacs faces plus magit, vertico, corfu, marginalia, embark, orderless, consult, transient, flycheck, cider, company, doom-modeline, treemacs, web-mode, and more. No placeholder colors.</p><p><strong>Clean architecture.</strong> Shared infrastructure in <code class="language-plaintext highlighter-rouge">batppuccin-themes.el</code>, thin wrapper files for each flavor, color override mechanism, configurable heading scaling. The same pattern I use in <a href="https://github.com/bbatsov/zenburn-emacs">Zenburn</a> and <a href="https://github.com/bbatsov/tokyo-night-emacs">Tokyo Night</a>.</p><p>I didn’t really re-invent anything here - I just created a theme in a way I’m comfortable with.</p><blockquote class="prompt-info"><p>I’m not going to bother with screenshots here – it looks like Catppuccin, because it <em>is</em> Catppuccin. There are small visual differences if you know where to look (headings, variables, a few face tweaks), but most people wouldn’t notice them side by side. If you’ve seen Catppuccin, you know what to expect.</p></blockquote><h2 id="installation">Installation</h2><p>The easiest way to install it right now:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">batppuccin</span>
  <span class="ss">:vc</span> <span class="p">(</span><span class="ss">:url</span> <span class="s">"https://github.com/bbatsov/batppuccin-emacs"</span> <span class="ss">:rev</span> <span class="ss">:newest</span><span class="p">)</span>
  <span class="ss">:config</span>
  <span class="p">(</span><span class="nv">load-theme</span> <span class="ss">'batppuccin-mocha</span> <span class="no">t</span><span class="p">))</span>
</pre></div></div><p>Replace <code class="language-plaintext highlighter-rouge">mocha</code> with <code class="language-plaintext highlighter-rouge">macchiato</code>, <code class="language-plaintext highlighter-rouge">frappe</code>, or <code class="language-plaintext highlighter-rouge">latte</code> for the other flavors. You can also switch interactively with <code class="language-plaintext highlighter-rouge">M-x batppuccin-select</code>.</p><h2 id="there-can-never-be-enough-theme-ports">There Can Never Be Enough Theme Ports</h2><p>I remember when Solarized was the hot new thing and there were something like five competing Emacs ports of it. People had strong opinions about which one got the colors right, which one had better org-mode support, which one worked with their favorite completion framework. And that was fine! Different ports serve different needs and different tastes.</p><p>The same applies here. The official Catppuccin port is perfectly usable for a lot of people. Batppuccin is for people who want something more idiomatic to Emacs, with broader face coverage and stricter adherence to the upstream style guide. Both can coexist happily.</p><p>I’ve said many times that for me the best aspect of Emacs is that you can tweak it infinitely to make it your own, so as far as I’m concerned having a theme that you’re the only user of is perfectly fine. That being said, I hope a few of you will appreciate my take on Catppuccin as well.</p><h2 id="wrapping-up">Wrapping Up</h2><p>This is an early release and there’s plenty of room for improvement. I’m sure there are faces I’ve missed, colors that could be tweaked, and packages that deserve better support. If you try it out and something looks off, please <a href="https://github.com/bbatsov/batppuccin-emacs/issues">open an issue</a> or send a PR.</p><p>I’m also curious – what are your favorite Emacs themes these days? Still rocking Zenburn? Converted to modus-themes? Something else entirely? I’d love to hear about it.</p><p>That’s all from me, folks! Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>The official port uses Catppuccin’s <a href="https://github.com/catppuccin/toolbox/tree/main/whiskers">Whiskers</a> template tool to generate the Elisp from a <code class="language-plaintext highlighter-rouge">.tera</code> template, which is cool for keeping ports in sync across editors but means the generated code doesn’t follow Emacs conventions. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Naming is hard, but it should also be fun! Also – I’m a huge fan of Batman. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I promised I’d take a break from building Tree-sitter major modes, and I meant it. So what better way to relax than to build… color themes? Yeah, I know. My idea of chilling is weird, but I genuinely enjoy working on random Emacs packages. Most of the time at least… Some Background For a very long time my go-to Emacs themes were Zenburn and Solarized – both of which I maintain popular Emacs p...</summary> </entry> </feed>
