Skip to content

A new Clojure Terminal REPL under development

A complete migration of all Practicalli websites to Zensical is underway, now all the plugins are available.

I was not that familiar with the TOML syntax (although I am now), so time was spend wrangling the right syntax for zensical.toml configuration. I occasionally used DuckDuckGo Search Assistant (via web browser) to find working examples (which it did about 75% of the time, sometimes after telling the assistant it had made things up).

A quick spike test with the Clojure CLI REPL was carried out using a simple Practicalli project, comparing the experience with my current preference of Rebel Readine. Potential is there, but a few more things to be added before I could use it regularly. A great start though and it is a new project.

Trying out the whalebird Mastodon desktop clients for Debian Linux, which was installed from a Debian package in the GitHub release (via DRA).

I needed to create a new Mastodon account for Practicalli: mastodon.social/@practicalli_johnny. Functional.cafe is closed to new accounts and clj.social was down (need to check if this domain is still active).

This week I am listening to an audio book version of "The Truth" by "Terry Pratchett". It is a satire on the evolution of the first printing press with many familiar Discworld characters and a few new ones too.

Ungovernd Produce channel has a few Terry Pratchett books that they read as their own Audiobook version. Curiously, I was thinking of creating my own audio books on my walk in the park yesterday.

Mastodon

I am trying out the whalebird desktop client for Mastodon social network.

A debian linux install script was created and added to the other post-install scripts in Practicalli Dotfiles.

#!/usr/bin/env bash

echo ""

echo "# ---------------------------------------"
echo "Whalebird Mastodon Desktop UI"

# install the nvim.appimage (automatic only installs nvim and not runtime)
# dra download --automatic --install --output ~/.local/bin h3poteto/whalebird-desktop
sudo dra download --select "*.deb" --install --output ~/.local/bin h3poteto/whalebird-desktop

echo "# ---------------------------------------"

echo ""

This installs Whalebird using the Debian package in the projects GitHub release artifacts.

The desktop launcher was used to start Whalebird.

Click the + icon to start adding an account.

Sign in to the server where the account was created by specifying the server domain, i.e. mastodon.social

The desktop app opens a browser page to confirm access to the domain and access account details. I assume a login is required if not already logged into the server.

A quick test post from Whalebird shows that its working okay.

Zensical

With the release of blog, rss, and social support I can complete the migration of all Practicalli sites from mkdocs to Zensical.

The speed of Zensical is excellent and the feedback from zensical serve alone has made my content quality greatly increase.

For simplicity I've used uvx to run zensical as a tool, both locally and via GitHub CI workflow (this also includes the zensical-catppuccin plugin for a really great theme).

TOML syntax

TOML consist of key-value pairs, tables, and arrays. Keys and values are separated by an equals sign.

Use double quotes for basic strings, single quotes for literal strings, and triple quotes for multiline strings.

basic = "A string with \"quotes\" and \\backslashes\\"
literal = 'No escape needed: \n stays as-is'
multiline = """This string
spans multiple lines."""

Boolean values are written as true or false.

enabled = true
disabled = false

TOML supports RFC 3339 format for date and time.

dob = 1987-07-01T13:45:00Z
date_only = 2025-06-05

A dotted key is a sequence of keys joined by dots, creating nested tables implicitly.

parent.child = "value"
site."example.com" = true
physical.color = "orange"
physical.shape = "round"

Arrays hold values of the same type (homogeneous). Separate elements with commas and use square brackets.

fruits = ["apple", "oranges", "peaches"]
fibbonacci = [1, 2, 3, 5, 8, 13]
complex = [[1, 2], [3, 4]]
mappings = [{key1 = "string"}, {key1 = "string"}]  # Zensical navigation

A table header defined as [table] starts a table and defines where subsequent key/value pairs are stored.

A dotted table header, [table.name.option], defines nested tables using dot-separated name segments.

A Table is used to add a plugin to the Zensical configuration. Each plugin can override the default plugin options by adding the respective keys and values.

Key names are case-sensitive.

Tables are better for Multiple related settings, providing greater readability for complex configurations

[table.name]
key1 = value
key2 = value

An array of tables uses [[name]] to append a new table element to an array under name.

[[array_table]]

The Social links in the footer of Zensical are an example of adding several icon links using an array of tables, all adding to the same project.extra.social table.

[[project.extra.social]]
icon = "fontawesome/brands/github"
link = "https://github.com/practicalli/journal"
[[project.extra.social]]
icon = "material/web"
link = "https://practical.li/"
[[project.extra.social]]
icon = "fontawesome/brands/linkedin"
link = "https://www.linkedin.com/in/jr0cket/"
[[project.extra.social]]
icon = "fontawesome/brands/slack"
link = "https://clojurians.slack.com/messages/practicalli"
[[project.extra.social]]
icon = "fontawesome/brands/mastodon"
link = "https://mastodon.social/@practicalli_johnny"
[[project.extra.social]]
icon = "fontawesome/brands/github"
link = "https://github.com/practicalli"
[[project.extra.social]]
icon = "fontawesome/brands/docker"
link = "https://hub.docker.com/u/practicalli"

NOTE: the array of tables syntax is used for the different pallette modes in the zensical-catppuccin theme config.

An Inline Table allows an option to have nested options.

Inline tables are used for simple one-off groupings when compactness is preferred

config_option = {  key1 = value, key2 = value }

TOML reserves characters and it is not allow to unescaped them in keys or strings:

  • . (dot) separates nested keys
  • # comment
  • " or ' define a string

Toml Configuration

zensical will build a site using an existing mkdocs.yml configuration and use the blog, rss and social configurations I already included.

I decided to migrate to the zensical.toml configuration as eventually there are features that are Zensical specific, e.g. using zensical-catppuccin theme (and future development of zensical plugins).

The three plugins are added as tables

[project.plugins.blog]
[project.plugins.rss]
[project.plugins.social]

Each plugin can be configured by adding keys and there respective values.

Practicalli Zensical Configuration for Blog site with RSS Feed and Social Cards

zensical.toml
# ----------------------------------------------------------------------------
# Blog Website Support (blog, rss, social)

[project.plugins.blog]
blog_dir = "."                      # root directory of blog posts (used if not blog/posts/)
post_url_format = "{date}/{slug}"   # url for post (slug is title of post)
draft = false                       # build drafts
draft_on_serve = true               # show drafts in local server (default true)
draft_if_future_date = true         # draft not published, but not marked as draft in local serve

# Only build with allowed categories
categories_allowed = [
    "100daysofcode",
    "clojure",
    "clojurists-together",
    "debian",
    "git",
    "github",
    "hardware",
    "hyprland",
    "journal",
    "megalinter",
    "neovim",
    "practicalli",
]

[project.plugins.rss]
image = "https://github.com/practicalli/graphic-design/blob/live/logos/practicalli-logo.png?raw=true"
match_path = "posts/.*"               # path to posts that should be included in the rss feed
categories = ["categories", "tags",]  # Included in feed summary
length = 100                          # articles to include (Default 20)

# Use date from the front matter of each post
# date_from_meta = {as_creation = "date"}  (simple format)
# date_from_meta = { as_creation = "date.created" } # date: created: format
# date_from_meta = { as_update = "date.updated" } # date: updated : format

# Set multiple values from the front matter (or use the expanded TOML below)
# date_from_meta = [
#   { as_creation = "date.created",  as_update = "date.updated" },
# ]

# Use date from the front matter of each post
[project.plugins.rss.date_from_meta]
as_creation = "date.created"
as_update = "date.updated"

# https://squidfunk.github.io/mkdocs-material/plugins/social/#social-cards
[project.plugins.social]
cards_layout_options = { font_family = "Ubuntu" }
# ----------------------------------------------------------------------------

Clojure CLI REPL

I had a quick play with the Clojure CLI REPL, a new project to provide a rich command line REPL support, using Java Jline.

Clojure CLI REPL has an interesting Inspector feature and the ability to evaluate part of an expression. A config file is required to define key mappings for these features (no key mappings by default).

There is an example config and docs to help and there is a lot more to the config examples I didnt have time to investigate.

There is a lot of promise to the Clojure CLI REPL and once tab completion of namespace and function names it ready its worth testing as a potential replacement or alternative for Rebel Readline.

NOTE: I had to set up auto-require configuration so I could use unqualified functions from clojure.core (it seems strange that clojure.core isnt there by default).

For day to day work I will continue using Rebel Readline as my Terminal REPL prompt

Practicalli Rebel Readline Terminal REPL prompt

Features comparable to Rebel Readlin include multi-line editing, structural editing, doc lookup, and Vim or Emacs editing modes 💖

Unique features include:

  • inline eval
  • a data inspector
  • configurable prompts
  • custom keybindings

I've added aliases to Practicalli Clojure CLI Config using my standard naming convention:

  • :repl/cli start nREPL server and rich REPL client
  • :repl/serve start nREPL server only
  • :repl/attach start a client and attach to an existing nREPL server

The nREPL server creates an .nrepl.port file containing the local port the nREPL server is listening too

Startup

I have Java v25.0.4 installed and Clojure CLI 1.12.4.1602, running on Debian Linux.

Aliases to run the tool were added to the Clojure user level configuration $XDG_CONFIG_HOME/clojure/deps.edn using Practicalli Clojure CLI Config.

The XDG_CONFIG_HOME environment variable was checked that it still points to the correct location.

❯ echo $XDG_CONFIG_HOME
/home/practicalli/.config

Run the nREPL server and client using the Clojure CLI tool.

clojure -M:repl/cli

The command proceeds to download the internet (I regularly clear the local Maven cache so this took a few seconds).

Warnings were shown at the end of the startup output regarding jline calling java.lang.System::load

WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jline.nativ.JLineNativeLoader in an unnamed module (file:/home/practicalli/.m2/repository/org/jline/jline/4.3.1/jline-4.3.1.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

The default prompt was showing at the end, just after the message stating the port nREPL was listening too. I believe this is a Java version 25 warning (I also get this with Rebel Readline).

connected to nREPL on 45963
user =>

The Clojure CLI REPL honours the Clojure CLI location. It did not try to create a separate $HOME/.clojure directory, which is good as I dont use that classic approach to dotfiles. I checked as the Clojure CLI REPL docs only mention ~/.clojure as the config directory.

The CLojure CLI REPL did not create a config file, but did create a history buried deep in the path $XDG_CONFIG_HOME/clojure/.cljconf/org.clojure/clojure-cli.repl/history

My assumption about the deep path is because there is a bigger tree for all the code configuration. Not sure I want to maintain a bunch of code in order to configure a REPL UI. Maybe there are bigger plans that would benefit from more customisation.

NOTE: it felt confusing to have a .cljconf dotfile within the parent Clojure CLI dotfile, especially when the Clojure CLI config is in $HOME/.config/clojure/.cljconf. I missed that .cljconf had been created at first glance.

The .cljconf name could easily be mistaken for something related to Clojure CLI rather than Clojure CLI REPL.

First use

I could evaluate expressions and multi-line typing worked.

(map
 inc
 [1 2 3 4 5])

History of expressions is accessed via the Up arrow Up or via Ctrl r and typing a pattern (just like the usual shell history search)

I did not get docs after pausing at the end of a function name. I didnt realise initially that a configuration file with key mappings is required to make some features work.

Because the key mappings were not at hand, I didnt know how to do structural editing, editing a multi-line expression, call docs or inspect a result). The docs seem clear enough though, just have to think about key mappings I would actually use.

Ctrl d to exit the REPL prompt back to the shell prompt. I first evaluated (quit), (exit), :repl/quit, :exit to see if they would exit the REPL, but without success.

A simple project

I created a simple project using Practicalli Project Templates

clojure -T:project/create

Then ran the Clojure REPL CLI from the root of that project, calling the -main function from the practicalli.playground namespace.

No support is provided for namespace of function name completion at present (at least not when I tried), an obvious feature missing compared to Rebel Readline.

UPDATE: this is actively being worked on.

 ~/temp/playground    v25.0.4  ❯ clojure -M:repl/cli
Downloading: org/clojure/clojure/1.12.3/clojure-1.12.3.pom from central
Downloading: com/brunobonacci/mulog/0.9.0/mulog-0.9.0.pom from clojars
Downloading: org/clojure/clojure/1.12.3/clojure-1.12.3.jar from central
Downloading: com/brunobonacci/mulog/0.9.0/mulog-0.9.0.jar from clojars
nREPL server listening on port 33423
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jline.nativ.JLineNativeLoader in an unnamed module (file:/home/practicalli/.m2/repository/org/jline/jline/4.3.1/jline-4.3.1.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

connected to nREPL on 33423
user => (require 'practicalli.playground)
nil
user => (practicalli.playground/-main)
practicalli playground service developed by the secret engineering team
nil
user => (practicalli.playground/-main {:team-name "clojure cli repl"})
practicalli playground service developed by the clojure cli repl team
nil
user => (in-ns 'practicalli.playground)
#object[clojure.lang.Namespace 0x52e8cc95 "practicalli.playground"]
practicalli.playground => (-main)
practicalli playground service developed by the secret engineering team
nil
practicalli.playground =>

Configuration file

A configuration file is required to define key mappings (as currently there seem to be no default mappings).

I copied the .edn file from the example configuration to the hidden path the project specifies. It seemed strange to add all the code too, but that seems to be were all the additional customisation can be done.

{:prompt dev.full-width-prompt/prompt :middleware [dev.timing/middleware dev.heap/middleware] :keybindings dev.keybindings/install :print-hook dev.hooks/pprint-values :bracket-pairs false :eval-form-at-cursor "M-e" :doc-at-cursor "M-d" :inspect "M-i"}

Staring the REPL failed when running with this configuration in the user level location.

 ~/temp/playground    v25.0.4  ❯ clojure -M:repl/cli
Execution error (FileNotFoundException) at clojure-cli.repl.hooks/resolve-hook (hooks.clj:24).
Could not locate dev/hooks__init.class, dev/hooks.clj or dev/hooks.cljc on classpath.

Full report at:
/tmp/clojure-5814065469117925004.edn

Looking at the full report my first assumption is that dev is not on the path when Clojure CLI REPL. As the example has several dev.,,, namespaces mentioned, then that is probably why it failed.

I used only a small part of the example config

I had limited time and just wanted to see what was out of the box. The example had a config and a separate namespace which I contained more customisations (in Clojure code).

I didnt have time to go through the code examples (and didnt initiall understand why they were neccessary).

Trying again after commenting the keywords that have dev.,,, namespaces as their values

{ :bracket-pairs false
 :eval-form-at-cursor "M-e"
 :doc-at-cursor "M-d"
 :inspect "M-i"}
 ~/temp/playground    v25.0.4  ❯ clojure -M:repl/cli
nREPL server listening on port 46857
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.jline.nativ.JLineNativeLoader in an unnamed module (file:/home/practicalli/.m2/repository/org/jline/jline/4.3.1/jline-4.3.1.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

connected to nREPL on 46857
user => (require
clojure.core/require
[& args]
Loads libs, skipping any that are already loaded. Each argument is
  either a libspec that identifies a lib, a prefix list that identifies
  multiple libs whose names share a common prefix, or a flag that modifies
  how all the identified libs are loaded. Use :require in the ns macro
  in preference to calling this directly.

  Libs

  A 'lib' is a named set of resources in classpath whose contents define a
  library of Clojure code. Lib names are symbols and each lib is associated
  with a Clojure namespace and a Java package that share its name. A lib's
  name also locates its root directory within classpath using Java's
  package name to classpath-relative path mapping. All resources in a lib
  should be contained in the directory structure under its root directory.
  All definitions a lib makes should be in its associated namespace.
... doc key again for the full doc

Auto-require namespaces

To make it easy to use some f the functions from clojure.repl when changing to any other namespace but user the config needs to add and auto-require.

Practicalli declarative config for Clojure CLI REPL (so far)

;; Practicalli Config for Clojure CLI REPL
{:bracket-pairs false
 :eval-form-at-cursor "M-e"
 :doc-at-cursor "M-d"
 :inspect "M-i"

 :auto-require
 [[clojure.repl :refer [apropos demunge dir doc find-doc pst root-cause source]]]}

Now I can use convenience functions from clojure.repl unqualified in any namespace

Inspector

I started by evaluating a basic expression

(map inc [1 2 3 4 5])

Then use M-i to inspect the result

*1  LazySeq · 5 items

0 · 2
1 · 3
2 · 4
3 · 5
4 · 6

1-5 of 5  ←↓↑→/hjkl navigate · d def · q quit

The inspector shows each index and value from the sequence that was returned as the result of the evaluation.

Results that return a nested map are much more interesting though.

Things that didn't work as expected

Not including resources directory when defined in the project deps.edn file

I have a file I slurp that resides in the root of the resources directory.

When evaluating code in the namespace that uses the file the code fails to evaluate as the file is not found.

deps.edn
{:paths ["src" "resources"]

 :deps
 {org.clojure/clojure {:mvn/version "1.12.5"}
  ;; Logic
  org.clojure/core.match {:mvn/version "1.1.1"}
  ;; Date and time
  clojure.java-time/clojure.java-time {:mvn/version "1.4.3"}
  ;; HTML generation
  hiccup/hiccup {:mvn/version "2.0.0"}}
 :aliases
 {};; Aliases from practicalli/clojure-deps-edn included via .dir-locals
 #_()}

I ran out of time to look into this further.

Debian Linux

After running sudo apt update I noticed a new version of Firefox and Chromium. These usually have security fixes, so good to keep up with new releases.

No obvious large or breaking changes in any of the packages, so should be safe to upgrade without causing any maintenance issues.

Apt package update - 30 September 2026

❯ sudo apt upgrade
The following package was automatically installed and is no longer required:
  chromium-sandbox
Use 'sudo apt autoremove' to remove it.

Upgrading:
  at-spi2-common        heif-thumbnailer        libgs10                       libvlc-bin
  at-spi2-core          imagemagick             libgs10-common                libvlc5
  base-files            imagemagick-7-common    libheif-plugin-aomenc         libvlccore9
  bash                  imagemagick-7.q16       libheif-plugin-dav1d          libwbclient0
  bind9-dnsutils        jq                      libheif-plugin-libde265       linux-image-amd64
  bind9-host            libasound2-data         libheif-plugin-x265           linux-libc-dev
  bind9-libs            libasound2-dev          libheif1                      locales
  busybox               libasound2t64           libimage-magick-perl          locales-all
  chromium              libatk-adaptor          libimage-magick-q16-perl      logsave
  chromium-common       libatk-bridge2.0-0t64   libjq1                        openssl
  chromium-sandbox      libatk1.0-0t64          libldb2                       openssl-provider-legacy
  code                  libatopology2t64        libmagickcore-7.q16-10        perl
  curl                  libatspi2.0-0t64        libmagickcore-7.q16-10-extra  perl-base
  dcmtk-data            libaudit-common         libmagickwand-7.q16-10        perl-modules-5.40
  dhcpcd-base           libaudit1               libmbedcrypto16               python3.13
  dirmngr               libc-bin                libmbedtls21                  python3.13-dev
  dnsmasq-base          libc-dev-bin            libmbedx509-7                 python3.13-minimal
  e2fsprogs             libc-l10n               libnfs14                      python3.13-venv
  exim4-base            libc6                   libodbc2                      samba-libs
  exim4-config          libc6-dev               libodbccr2                    socat
  exim4-daemon-light    libc6-i386              libodbcinst2                  tzdata
  firefox               libcap2                 libpcre2-16-0                 unixodbc-common
  firefox-l10n-en-gb    libcap2-bin             libpcre2-32-0                 vlc
  ghostscript           libcom-err2             libpcre2-8-0                  vlc-bin
  gimp                  libcurl3t64-gnutls      libpcre2-dev                  vlc-data
  gimp-data             libcurl4t64             libpcre2-posix3               vlc-l10n
  gir1.2-atk-1.0        libdcmtk19              libperl5.40                   vlc-plugin-access-extra
  gir1.2-atspi-2.0      libevent-2.1-7t64       libpython3.13                 vlc-plugin-base
  gir1.2-gimp-3.0       libevent-core-2.1-7t64  libpython3.13-dev             vlc-plugin-notify
  gir1.2-glib-2.0       libext2fs2t64           libpython3.13-minimal         vlc-plugin-qt
  girepository-tools    libfluidsynth3          libpython3.13-stdlib          vlc-plugin-samba
  gnupg-utils           libgimp-3.0-0           libraw23t64                   vlc-plugin-skins2
  google-chrome-stable  libgio-2.0-dev          libsmbclient0                 vlc-plugin-video-output
  gpg                   libgio-2.0-dev-bin      libsqlite3-0                  vlc-plugin-video-splitter
  gpg-agent             libgirepository-2.0-0   libss2                        vlc-plugin-visualization
  gpg-wks-client        libglib2.0-0t64         libssh2-1t64                  xdg-dbus-proxy
  gpgconf               libglib2.0-bin          libssl3t64                    xserver-common
  gpgsm                 libglib2.0-data         libtalloc2                    xserver-xephyr
  gpgv                  libglib2.0-dev          libtdb1                       xserver-xorg-core
  gzip                  libglib2.0-dev-bin      libtevent0t64                 xserver-xorg-legacy
  heif-gdk-pixbuf       libgs-common            libunbound8                   zsh

Installing dependencies:
  linux-image-6.12.111+deb13-amd64

Suggested packages:
  firmware-linux-free  linux-doc-6.12  debian-kernel-handbook

Summary:
  Upgrading: 164, Installing: 1, Removing: 0, Not Upgrading: 0
  Download size: 0 B / 845 MB
  Space needed: 125 MB / 933 MB available

There was a Linux Kernel package update, so a reboot is required at some point today to pick up the new version.

Linux Kernel image package update

Setting up linux-image-6.12.111+deb13-amd64 (6.12.111-1) ...
I: /vmlinuz.old is now a symlink to boot/vmlinuz-6.12.107+deb13-amd64
I: /initrd.img.old is now a symlink to boot/initrd.img-6.12.107+deb13-amd64
I: /vmlinuz is now a symlink to boot/vmlinuz-6.12.111+deb13-amd64
I: /initrd.img is now a symlink to boot/initrd.img-6.12.111+deb13-amd64
/etc/kernel/postinst.d/initramfs-tools:
update-initramfs: Generating /boot/initrd.img-6.12.111+deb13-amd64
/etc/kernel/postinst.d/zz-update-grub:
Generating grub configuration file ...
Found background image: /usr/share/images/desktop-base/desktop-grub.png
Found linux image: /boot/vmlinuz-6.12.111+deb13-amd64
Found initrd image: /boot/initrd.img-6.12.111+deb13-amd64
Found linux image: /boot/vmlinuz-6.12.107+deb13-amd64
Found initrd image: /boot/initrd.img-6.12.107+deb13-amd64
Found linux image: /boot/vmlinuz-6.12.101+deb13-amd64
Found initrd image: /boot/initrd.img-6.12.101+deb13-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
Adding boot menu entry for UEFI Firmware Settings ...
done

Thank you.

🌐 Practical.li Website

Practical.li GitHub Org practicalli-johnny profile

@practicalli@clj.social