Some exciting developments in the area of babashka and creating TUIs. https://github.com/jline/jline3/issues/1566
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).This snake game is now playable (with some extra commits pushed to CI now) https://gist.github.com/borkdude/21f8e8996d5d20044f3f1c265ab7cc48
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
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)))))gist updated
Thanks to @timok for working with me on this
I enjoyed it
@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
I've done this before with static methods of some classes
I'll wait for a reply from the jline folks about how they see stability
I like that jline also covers Windows, which is tricky to figure out.
I'm going to test windows now on my windows PC
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
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
hmm, I added:
{
"parameterTypes": ["long", "long", "int"],
"returnType": "int"
}
but that doesn't seem to fix itWow, 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.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!
Made this issue: https://github.com/babashka/babashka/issues/1909
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.edngrml, I went to dinner and my Windows system (which I use rarely, only for cases like this) reboot ... twice!
how can anyone work with such a system
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.
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
but it seems jline is where things have gone, it's now the defacto TUI thing in Java, I think?
Windows works!
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 🙂
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.interesting, not sure what that means. perhaps localhost isn't set in /etc/hosts or so?
Our servers all have names that are not in DNS, e.g., s06520.
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. Three little Babashka scripts 🙂
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)you can avoid the dep on babashka.json if you use cheshire directly
then you could avoid downloading deps.clj as well I think
I've also done a production deployment with Babashka now.
I don't like Cheshire in the mix, due to Jackson etc. I worked very hard to excise Cheshire from everything at work.
I even have ring-data-json which is a fork of ring-json that uses clojure.data.json -- zero dependencies.
yeah sure, but using it in bb scripts won't affect your production app
I double-checked that babashka.json would use clojure.data.json before I went down that path 🙂
babashka.json still uses cheshire underneath since it's the only json built into bb
on the JVM babashka.json uses clojure.data.json
but not in bb itself
Ah... interesting...
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
That is not what I understood from the readme 🙂
And I did not realize bb has "Cheshire Inside" 😐
Where in the docs would be a good place for me to learn about exactly which libraries/namespaces are built-in?
I hope it's up to date, if not, PRs or issues always welcome.
bb also has transit which uses jackson anyway
it's a bit crazy that the JVM still doesn't have a JSON lib
built-in I mean
What does it mean by "cheshire.core aliased as json: dealing with JSON" -- "aliased as json"?
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 lineOh... but not in regular scripts... Got it!
correct, not outside of the user namespace
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
Ooh, Selmer is built-in as well!
What approach do you recommend for writing tests for bb scripts? What test runner do you use?
(since I now have some code that is bb-only due to load-pod)
cognitect test runner. https://github.com/TimoKramer/charm.clj/pull/1/changes#diff-62100e26b5ed7e930acb75d51f6f1eff9484b124a4fe8b260f77d33cfa78a55cR1
or you can write your own thing using clojure.test/run-tests
I wrote a blog about it once: https://blog.michielborkent.nl/babashka-test-runner.html
Thanks. I'll need to think about how (or whether) to integrate that into my Polylith External Test Runner setup...
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.
Hah, this might even be one of those great uses for with-test and just add the tests after the function body!
sure :)
(I hope that works in bb... I vaguely remember adding support for it once)
It seems to work just fine 🙂
did you ever write a test for a bash script? not at this level of convenience I bet :)
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).
Shell scripts have always been "just try running it" (and then fix the bugs)... 😱
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?)
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
Even without caching anything, it's nearly instant
especially when deps are already in the local mvn.
Interesting...
I'll take a look at that tomorrow. It would be nice to rewrite our update-libs ns to run via bb.
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.you can try the pod and I'm willing to fix bugs, but be warned that it's experimental
is t tools.build here?
this is a bb compatible version of tools.build that is driven by this pod: https://github.com/babashka/tools.bbuild
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
t is tools.deps in this context.
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"maybe find-edn-maps works too
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"This approach is better -- I should have used create-basis in the first place really.
ok nice
This will speed up our deployments -- and let me simplify a bunch of code (and scripts). You rock, dude! 🙂
Since we're adding jline3, finally we can have a good console REPL too perhaps :)