babashka

borkdude 2026-01-27T13:39:48.320639Z

Some exciting developments in the area of babashka and creating TUIs. https://github.com/jline/jline3/issues/1566

4
🆒 3
🔥 4
❤️ 3
🚀 3
borkdude 2026-01-28T11:51:40.306739Z

jline3 support merged. you can use the https://codeberg.org/timokramer/charm.clj library now with bb dev. Obtain bb dev with:

bash <(curl )  --dev-build --dir .
Binary grew with 3.5mb. Try it out, and let me know if it works properly for you (also Windows).

🎉 3
borkdude 2026-01-28T12:28:05.602009Z

This snake game is now playable (with some extra commits pushed to CI now) https://gist.github.com/borkdude/21f8e8996d5d20044f3f1c265ab7cc48

😁 2
🐍 2
🤩 2
borkdude 2026-01-28T13:10:21.924959Z

Windows MINGW64 (Git bash) and Powershell seem to work displaywise, but reading the arrow keys don't seem to work for me. The hjkl keys do work. Could be a problem in the program, or in jline+windows+native-image+bb

borkdude 2026-01-28T13:18:15.486989Z

Ah, this fixes it:

(defn read-key [reader]
  (let [c (.read reader 1)]
    (when (pos? c)
      (cond
        ;; ESC sequences
        (= c 27)
        (let [c2 (.read reader 50)]
          (when (pos? c2)
            (case c2
              ;; ESC [ A/B/C/D (Unix/ANSI)
              91 (let [c3 (.read reader 50)]
                   (case c3
                     65 :up
                     66 :down
                     67 :right
                     68 :left
                     nil))
              ;; ESC O A/B/C/D (Application mode / Windows)
              79 (let [c3 (.read reader 50)]
                   (case c3
                     65 :up
                     66 :down
                     67 :right
                     68 :left
                     nil))
              nil)))
        ;; Regular character
        :else (char c)))))

borkdude 2026-01-28T13:19:42.033279Z

gist updated

borkdude 2026-01-27T13:43:17.145549Z

Thanks to @timok for working with me on this

timo 2026-01-27T13:45:28.124149Z

I enjoyed it

borkdude 2026-01-27T14:06:03.769899Z

@timok What I could also do instead of the FFM impl thing we're relying on: hack SCI so you can stub out an alternative implementation of an instance method call

borkdude 2026-01-27T14:06:16.092859Z

I've done this before with static methods of some classes

borkdude 2026-01-27T14:06:44.940249Z

I'll wait for a reply from the jline folks about how they see stability

lread 2026-01-27T14:10:09.400029Z

I like that jline also covers Windows, which is tricky to figure out.

borkdude 2026-01-27T14:10:21.481129Z

I'm going to test windows now on my windows PC

timo 2026-01-27T14:11:20.696549Z

yeah, i would follow whatever keeps it most stable for babashka as well since i am seeing babashka as a natural way to use charm.clj now that it seems to be working

borkdude 2026-01-27T14:20:11.079689Z

Hmm, I'm getting this on Windows: Message: Cannot perform downcall with leaf type (long,long,int)int. To allow this operation, add the following to the 'foreign' section of 'reachability-metadata.json' and rebuild the native image: but I can probably fix that. Not sure why I'm getting that on windows and not on mac, but I'll just add that thing

borkdude 2026-01-27T14:27:02.750709Z

hmm, I added:

{
        "parameterTypes": ["long", "long", "int"],
        "returnType": "int"
      }
but that doesn't seem to fix it

borkdude 2026-01-27T14:29:23.080999Z

Wow, this is a great reply from the jline team:

Backward Compatibility between JLine 3 and 4
JLine 4.x does have some breaking changes compared to 3.x:

Java version requirement: JLine 4.x requires Java 11+, while JLine 3.x supports Java 8+
Removed providers: The JNA and Jansi terminal providers have been removed in JLine 4.x. The recommended alternatives are:
JNI provider (for maximum compatibility)
FFM provider (for best performance on Java 22+)
Full JPMS support: JLine 4.x provides comprehensive Java Platform Module System support
However, the core API you're using (Terminal, TerminalBuilder, LineReaderBuilder, Size, SignalHandler, etc.) remains largely unchanged and backward compatible.

borkdude 2026-01-27T14:33:34.588679Z

So I think I'll mock out the Terminal creation function, I'll figure that one out. That leaves us with that and Windows support. The rest seems ok!

🔥 2
borkdude 2026-01-27T16:30:03.651589Z

Made this issue: https://github.com/babashka/babashka/issues/1909

borkdude 2026-01-27T16:57:56.208479Z

Hmz, when running directly with clojure, I still get an error on Windows:

C:\Users\borkdude\dev\charm.clj\docs\examples> ..\..\..\..\Downloads\graalvm-jdk-25.0.2+10.1\bin\java.exe --enable-native-access=ALL-UNNAMED -cp "..\..\..\babashka\target\babashka-1.12.215-SNAPSHOT-standalone.jar;..\..\src;src"  clojure.main -m examples.counter
Execution error (NullPointerException) at org.jline.terminal.impl.ffm.NativeWinSysTerminal/createTerminal (NativeWinSysTerminal.java:63).
Cannot invoke "org.jline.terminal.spi.SystemStream.ordinal()" because "systemStream" is null

Full report at:
C:\Users\borkdude\AppData\Local\Temp\clojure-15276153254461651405.edn

borkdude 2026-01-27T18:26:46.145029Z

grml, I went to dinner and my Windows system (which I use rarely, only for cases like this) reboot ... twice!

borkdude 2026-01-27T18:26:53.272879Z

how can anyone work with such a system

💯 2
hlship 2026-01-27T18:34:12.287629Z

I'm excited about this, it was on my back shelf to see about accomplishing some of this with pods. It's the next step after creating command-line tools.

👍 1
borkdude 2026-01-27T18:35:26.492109Z

there's a lanterna pod currently with which you can do something like this: https://github.com/babashka/pod-registry/blob/master/examples/lanterna.clj

borkdude 2026-01-27T18:35:49.535829Z

but it seems jline is where things have gone, it's now the defacto TUI thing in Java, I think?

borkdude 2026-01-27T18:48:08.840159Z

Windows works!

😎 3
seancorfield 2026-01-27T19:56:41.988319Z

I finally "gave in" and installed bb on our QA and production servers, so I can start replacing some of our bash scripts with Babashka scripts 🙂

8
2
❤️ 3
🚀 2
seancorfield 2026-01-28T21:33:53.270049Z

As an FYI, I get this warning when running that code on one of our QA servers -- but the script still ran:

Downloading pod org.babashka/tools-deps-native (0.1.7)
Successfully installed pod org.babashka/tools-deps-native (0.1.7)
Jan 28, 2026 9:20:11 PM org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper getHostname
WARNING: Failed to get hostname, using 'localhost'
java.net.UnknownHostException: s06520: s06520: Name does not resolve
        at java.base@23/java.net.InetAddress.getLocalHost(InetAddress.java:1925)
        at org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper.getHostname(DiscriminatingNameMapper.java:100)
        at org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper.<init>(DiscriminatingNameMapper.java:83)
        at org.eclipse.aether.internal.impl.synccontext.named.SimpleNamedLockFactorySelector.<clinit>(SimpleNamedLockFactorySelector.java:60)
        at java.base@23/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:501)
        at java.base@23/java.lang.reflect.Constructor.newInstance(Constructor.java:485)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.newInstance(DefaultServiceLocator.java:176)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstances(DefaultServiceLocator.java:151)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstance(DefaultServiceLocator.java:137)
        at org.eclipse.aether.impl.DefaultServiceLocator.getService(DefaultServiceLocator.java:296)
        at org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory.initService(DefaultSyncContextFactory.java:70)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.newInstance(DefaultServiceLocator.java:181)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstances(DefaultServiceLocator.java:151)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstance(DefaultServiceLocator.java:137)
        at org.eclipse.aether.impl.DefaultServiceLocator.getService(DefaultServiceLocator.java:296)
        at org.eclipse.aether.internal.impl.DefaultMetadataResolver.initService(DefaultMetadataResolver.java:124)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.newInstance(DefaultServiceLocator.java:181)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstances(DefaultServiceLocator.java:151)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstance(DefaultServiceLocator.java:137)
        at org.eclipse.aether.impl.DefaultServiceLocator.getService(DefaultServiceLocator.java:296)
        at org.apache.maven.repository.internal.DefaultVersionResolver.initService(DefaultVersionResolver.java:108)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.newInstance(DefaultServiceLocator.java:181)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstances(DefaultServiceLocator.java:151)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstance(DefaultServiceLocator.java:137)
        at org.eclipse.aether.impl.DefaultServiceLocator.getService(DefaultServiceLocator.java:296)
        at org.eclipse.aether.internal.impl.DefaultRepositorySystem.initService(DefaultRepositorySystem.java:145)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.newInstance(DefaultServiceLocator.java:181)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstances(DefaultServiceLocator.java:151)
        at org.eclipse.aether.impl.DefaultServiceLocator$Entry.getInstance(DefaultServiceLocator.java:137)
        at org.eclipse.aether.impl.DefaultServiceLocator.getService(DefaultServiceLocator.java:296)
        at clojure.tools.deps.util.maven$make_system.invokeStatic(maven.clj:210)
        at clojure.tools.deps.util.maven$make_system.invoke(maven.clj:206)
        at clojure.tools.deps.util.maven$make_system.invokeStatic(maven.clj:208)
        at borkdude.tdn.main$_main.invokeStatic(main.clj:36)
        at borkdude.tdn.main$_main.doInvoke(main.clj:36)
        at clojure.lang.RestFn.invoke(RestFn.java:397)
        at clojure.lang.AFn.applyToHelper(AFn.java:152)
        at clojure.lang.RestFn.applyTo(RestFn.java:132)
        at borkdude.tdn.main.main(Unknown Source)
        at java.base@23/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)
Caused by: java.net.UnknownHostException: s06520: Name does not resolve
        at java.base@23/java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method)
        at java.base@23/java.net.Inet4AddressImpl.lookupAllHostAddr(Inet4AddressImpl.java:43)
        at java.base@23/java.net.InetAddress$PlatformResolver.lookupByName(InetAddress.java:1221)
        at java.base@23/java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1817)
        at java.base@23/java.net.InetAddress$NameServiceAddresses.get(InetAddress.java:1149)
        at java.base@23/java.net.InetAddress.getAllByName0(InetAddress.java:1807)
        at java.base@23/java.net.InetAddress.getLocalHost(InetAddress.java:1920)
        ... 39 more

Copying newrelic-agent-9.0.0.jar ...
New Relic updated.

borkdude 2026-01-28T21:36:55.651319Z

interesting, not sure what that means. perhaps localhost isn't set in /etc/hosts or so?

seancorfield 2026-01-28T21:45:19.471809Z

Our servers all have names that are not in DNS, e.g., s06520.

seancorfield 2026-01-28T22:37:14.765219Z

FWIW, I fixed it by editing /etc/hosts on each server to append the unqualified server name -- the output of hostname -- to the end of the 127.0.0.1 localhost ... line. This is all Babashka-powered in our automated deployment now:

Clojure tools not yet in expected location: /opt/tomcat/.deps.clj/1.12.4.1582/ClojureTools/clojure-tools-1.12.4.1582.jar
Downloading  to /opt/tomcat/.deps.clj/1.12.4.1582/ClojureTools/clojure-tools.zip
Unzipping /opt/tomcat/.deps.clj/1.12.4.1582/ClojureTools/clojure-tools.zip ...
Successfully installed clojure tools!
Downloading: org/babashka/json/0.1.6/json-0.1.6.pom from clojars
Downloading: org/babashka/json/0.1.6/json-0.1.6.jar from clojars
Downloading pod org.babashka/tools-deps-native (0.1.7)
Successfully installed pod org.babashka/tools-deps-native (0.1.7)
Copying newrelic-agent-9.0.0.jar ...
New Relic updated.
New Relic APM cache updated.
ws-api started, process ID 112361
Recorded deployment of WorldSingles Staging API version build-2026-01-28_22.25.18 successfully.

🤯 1
seancorfield 2026-01-28T22:38:11.627659Z

Three little Babashka scripts 🙂

seancorfield 2026-01-28T22:38:57.760619Z

Subsequent runs look like

Copying newrelic-agent-9.0.0.jar ...
New Relic updated.
New Relic APM cache updated.
ws-api started, process ID 124409
Recorded deployment of WorldSingles Staging API version build-2026-01-28_22.25.18 successfully.
(that's on a different QA server that I'd already run a deployment to)

borkdude 2026-01-28T22:39:22.671979Z

you can avoid the dep on babashka.json if you use cheshire directly

borkdude 2026-01-28T22:39:43.241919Z

then you could avoid downloading deps.clj as well I think

seancorfield 2026-01-28T22:39:43.527859Z

I've also done a production deployment with Babashka now.

👍 1
seancorfield 2026-01-28T22:40:14.503159Z

I don't like Cheshire in the mix, due to Jackson etc. I worked very hard to excise Cheshire from everything at work.

seancorfield 2026-01-28T22:40:43.043009Z

I even have ring-data-json which is a fork of ring-json that uses clojure.data.json -- zero dependencies.

borkdude 2026-01-28T22:41:24.811969Z

yeah sure, but using it in bb scripts won't affect your production app

seancorfield 2026-01-28T22:41:45.104899Z

I double-checked that babashka.json would use clojure.data.json before I went down that path 🙂

borkdude 2026-01-28T22:41:48.366229Z

babashka.json still uses cheshire underneath since it's the only json built into bb

borkdude 2026-01-28T22:42:10.456139Z

on the JVM babashka.json uses clojure.data.json

borkdude 2026-01-28T22:42:13.538919Z

but not in bb itself

seancorfield 2026-01-28T22:42:18.882749Z

Ah... interesting...

borkdude 2026-01-28T22:42:47.648229Z

yeah, I chose cheshire in 2019 for bb since that was most familiar to me (and enabled running programs that already used cheshire). clojure.data.json wasn't as optimized back then

seancorfield 2026-01-28T22:42:53.840539Z

That is not what I understood from the readme 🙂 And I did not realize bb has "Cheshire Inside" 😐

seancorfield 2026-01-28T22:43:47.371959Z

Where in the docs would be a good place for me to learn about exactly which libraries/namespaces are built-in?

borkdude 2026-01-28T22:44:11.724379Z

https://book.babashka.org/#libraries

borkdude 2026-01-28T22:45:24.274869Z

I hope it's up to date, if not, PRs or issues always welcome.

borkdude 2026-01-28T22:45:52.822749Z

bb also has transit which uses jackson anyway

borkdude 2026-01-28T22:47:11.025149Z

it's a bit crazy that the JVM still doesn't have a JSON lib

borkdude 2026-01-28T22:47:27.395849Z

built-in I mean

seancorfield 2026-01-28T22:47:47.531839Z

What does it mean by "cheshire.core aliased as json: dealing with JSON" -- "aliased as json"?

borkdude 2026-01-28T22:48:25.469569Z

it's aliased as that in the user namespace, so if you do:

bb -e '(json/parse-string "1")` 
it'll work as a one-liner on the command line

seancorfield 2026-01-28T22:48:47.474079Z

Oh... but not in regular scripts... Got it!

borkdude 2026-01-28T22:49:01.407599Z

correct, not outside of the user namespace

borkdude 2026-01-28T22:49:51.028229Z

not sure if I would do that again, but in the beginning (2019) bb was very much just a one-liner clojure-ish tool as follow up to jet

seancorfield 2026-01-28T22:51:27.326919Z

Ooh, Selmer is built-in as well!

👍 1
seancorfield 2026-01-28T22:53:51.412239Z

What approach do you recommend for writing tests for bb scripts? What test runner do you use?

seancorfield 2026-01-28T22:54:45.969439Z

(since I now have some code that is bb-only due to load-pod)

borkdude 2026-01-28T22:55:04.636269Z

or you can write your own thing using clojure.test/run-tests

borkdude 2026-01-28T22:55:43.252949Z

I wrote a blog about it once: https://blog.michielborkent.nl/babashka-test-runner.html

seancorfield 2026-01-28T22:56:13.960789Z

Thanks. I'll need to think about how (or whether) to integrate that into my Polylith External Test Runner setup...

seancorfield 2026-01-28T23:06:00.811039Z

For now I can just write tests and use my VS Code hot key to run tests in the bb REPL. Works well enough for dev-only scripts.

seancorfield 2026-01-28T23:12:07.254959Z

Hah, this might even be one of those great uses for with-test and just add the tests after the function body!

borkdude 2026-01-28T23:12:24.291389Z

sure :)

borkdude 2026-01-28T23:12:46.230199Z

(I hope that works in bb... I vaguely remember adding support for it once)

seancorfield 2026-01-28T23:16:49.428299Z

It seems to work just fine 🙂

borkdude 2026-01-28T23:19:19.512449Z

did you ever write a test for a bash script? not at this level of convenience I bet :)

seancorfield 2026-01-28T23:22:54.589139Z

Nope. For these scripts -- at least the original JVM Clojure scripts that I had -- there were just a few inline RCT forms (mjdowney's rich comment tests).

seancorfield 2026-01-28T23:23:19.161519Z

Shell scripts have always been "just try running it" (and then fix the bugs)... 😱

seancorfield 2026-01-27T22:16:50.087339Z

Back story to this: New Relic have let you use their regular agent JAR to record deployments for years. In their latest version, 9.0.0, they removed this functionality so our CI/CD pipeline broke. Now you have to use their newrelic CLI tool -- and to record a deployment, you have to first run a command to get all the APM metadata (as JSON), then go through that to find app name / 'guid' data (among everything else), and then you use that guid in another command to create the deployment marker. I could have done it as part of the tools.deps-based process we already use to update the New Relic agent JAR on our servers but it seemed like a good opportunity to have bb run the first command, cache the data, and then use bb to parse the JSON, and run the second command as needed. Slick, and lightning fast. Thank you! gratitude (I assume bb cannot run tools.deps due to all the Maven/Java code needed to resolve deps and download them?)

❤️ 1
borkdude 2026-01-27T22:21:46.336749Z

There is a tools-deps-native pod which is bloody fast. But also bloody experimental. https://github.com/babashka/pod-registry/blob/master/examples/tools-deps-native.clj It doesn't need a JVM to pull maven and git deps

borkdude 2026-01-27T22:24:00.711689Z

Even without caching anything, it's nearly instant

borkdude 2026-01-27T22:24:06.933199Z

especially when deps are already in the local mvn.

seancorfield 2026-01-27T22:24:48.074289Z

Interesting...

seancorfield 2026-01-27T22:27:25.015369Z

I'll take a look at that tomorrow. It would be nice to rewrite our update-libs ns to run via bb.

seancorfield 2026-01-27T22:28:45.043249Z

It's a very limited scope:

(defn- get-agent-jar-file
  "Returns a File pointing to the New Relic Java Agent JAR."
  ^java.io.File
  []
  (-> (t/find-edn-maps)
      :root-edn
      (assoc :deps {newrelic-agent {:mvn/version agent-version}})
      (t/calc-basis) ; download and tell us where it is
      ;; (doto tap>) ; how I debugged the basis
      (get-in [:libs newrelic-agent :paths])
      (first)
      (io/file)))
which I could certainly use create-basis for, with just the New Relic agent dependency.

borkdude 2026-01-27T22:30:50.549879Z

you can try the pod and I'm willing to fix bugs, but be warned that it's experimental

borkdude 2026-01-27T22:31:41.700419Z

is t tools.build here?

borkdude 2026-01-27T22:32:16.041799Z

this is a bb compatible version of tools.build that is driven by this pod: https://github.com/babashka/tools.bbuild

borkdude 2026-01-27T22:33:13.987219Z

I don't see any find-edn-maps function in there though, perhaps it was added "recently"? I didn't pull upstream code for a while

seancorfield 2026-01-27T22:34:10.336669Z

t is tools.deps in this context.

seancorfield 2026-01-27T22:35:05.941459Z

Sweet!!!!

(!2007)-> rlwrap bb
Babashka v1.12.214 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.

user=> (require '[babashka.pods :as pods])
nil
user=> (pods/load-pod 'org.babashka/tools-deps-native "0.1.7")
#:pod{:id "borkdude.tdn.pod"}
user=> (require '[clojure.tools.deps :as td])
nil
user=> (-> (td/create-basis {:project nil :user nil :extra '{:deps {com.newrelic.agent.java/newrelic-agent {:mvn/version "9.0.0"}}}}) (get-in [:libs 'com.newrelic.agent.java/newrelic-agent :paths]) (first))
"/home/sean/.m2/repository/com/newrelic/agent/java/newrelic-agent/9.0.0/newrelic-agent-9.0.0.jar"

borkdude 2026-01-27T22:35:43.996659Z

maybe find-edn-maps works too

seancorfield 2026-01-27T22:35:53.960619Z

Tested with an older version I didn't have locally:

user=> (-> (td/create-basis {:project nil :user nil :extra '{:deps {com.newrelic.agent.java/newrelic-agent {:mvn/version "8.25.1"}}}}) (get-in [:libs 'com.newrelic.agent.java/newrelic-agent :paths]) (first))
Downloading: com/newrelic/agent/java/newrelic-agent/8.25.1/newrelic-agent-8.25.1.pom from central
Downloading: com/newrelic/agent/java/newrelic-agent/8.25.1/newrelic-agent-8.25.1.jar from central
"/home/sean/.m2/repository/com/newrelic/agent/java/newrelic-agent/8.25.1/newrelic-agent-8.25.1.jar"

seancorfield 2026-01-27T22:36:10.758909Z

This approach is better -- I should have used create-basis in the first place really.

borkdude 2026-01-27T22:36:15.691379Z

ok nice

seancorfield 2026-01-27T22:38:47.450989Z

This will speed up our deployments -- and let me simplify a bunch of code (and scripts). You rock, dude! 🙂

4
borkdude 2026-01-27T20:09:21.268869Z

Since we're adding jline3, finally we can have a good console REPL too perhaps :)

🤞 3