A question to linux users. I'm adding support for jline to bb that depends on the new Java FFM feature. But, this only works in the dynamic linux executable, not in the static one (compiled against musl on CI). The static binary has been the default when you install bb with the install script or on brew. I could flip this back to dynamic. The question is: what would be preferable? The jline stuff still works in the static binary but falls back to the "Exec" provider which basically shells out many times to inspect your shell size and then shells out to write something to your terminal. So, TUIs still work, but maybe not in the most performant way. More info: https://github.com/jline/jline3/issues/1566#issuecomment-3819914297 So the choices here are: • we install the dynamic executable again by default, better jline support with the caveat that you might have an incompatible (usually too old?) libc version on your system • we keep installing the static executable, users can install the dynamic one with a flag (easy to do with installer script, also possible in brew? don't know). by default you get a somewhat slower jline experience.
any way to know how many people dont set the --static flag when installing but expect static always? like alpine users? i for one dont bother about its static or not. as for the choices, im favouring the first one, potential bad perf by default is something i would not expect.
on linux the static executable is always chosen, regardless of the static flag. it needs a new --no-static flag to override that
or --dynamic or whatever
the static executable has been the default now for years
via the normal installation methods you don't get to choose a different one
yeah im saying we could make dynamic the default now but not sure how much breakage is that.
tbh I don't see a difference between the execprovider and the ffm provider in practice. the snake game is playable in both
(on mac)
yeah maybe we release it, get some feedback or if there is a way to benchmark
release as it is, static default
right. even the file browser in charm.clj is instant (to me) with both versions
so indeed, let's just release it as is
ghostty has a way to measure drawing perf, not sure if that helps, can tinker around later
cool. if you want to test the current musl build on linux:
bash <(curl ) --dev-build --dir . I have ghostty on mac as well.
how can I test this?
terminal inspector?
yep
but not sure what exactly it counts
which number in that screen is relevant?
need to have a look, i vaguely remember something about drawing speeds/refresh rates
As a nixos user (don't judge - I know I have control issues), dynamic executables are a problem. So my vote would definitely be for option b -- stick with static by default, and opt-in to dynamic....
but it could just be measuring ghostty's rendering speeds, not how fast the app produces things
... though thinking it through, it's probably relatively academic for people installing via their distributions package manager -- if making dynamic the default for performance reasons for the majority, just need to make sure the nixpkgs build is updated to use the appropriate flags? Probably good to keep the github binaries static though for distro-agnostic use, or provide static binaries in addition to dynamic ...
is there the possibility to build statically with glibc?
dont think graalvm supports that
indeed, not possible
what would work, yould make another alternative to the jline FFM provider that is based on the GraalVM c API. org.graalvm.nativeimage.c.CContext etc. That does work with musl.
this is no alternative?
native-image --static-nolibcthat would make the binary static, but still depend on external libc
which is the major stumbling block
e.g. on nixos as @cormacc said above
alright
that option is used for the aarch64 linux binary
Maybe static as I am completely used to bb just working. I might appreciate some hint to use the dynamic build if I use something like jline. I don't see static/dynamic in bb describe- it's not technically a feature but it would be useful if it were visible somewhere.
you can see this with ldd
$ ldd /home/james/.asdf/shims/bb
not a dynamic executable
Good to know, thanks.For Arch, it would need to be a new package to support dynamic, as presently it just pulls down the static versions of the packages.
However, nothing to say that Arch can't flip to pull down the dynamic versions, instead of the static versions.
The package depends upon gcc-libs, so it'll get whatever the latest is
it seems the jline exec provider is actually pretty good for everything it seems (benchmark written by claude):
JLine Provider Performance Benchmark
=====================================
Platform: Mac OS X
Java: 25.0.2
Operation Exec (ms) FFM (ms) Speedup
---------------------------------------------------------------
TTY detection 1,9329 0,0058 335,9x
getSize 0,0123 0,0021 6,0x
getAttributes 0,0184 0,0106 1,7x
enterRawMode 0,1158 0,0483 2,4x
write+flush 1,4958 1,4709 1,0x
Display.update (full) 0,7969 0,5698 1,4x
Display.update (diff) 0,0144 0,0047 3,0xFFM is overall faster, but the small numbers that exec already has probably don't make a real difference in practice.
@timok related to the primeagen video:
⏺ At 60fps, frame budget is ~16.67ms:
┌───────────────────┬────────────────────────┬─────────────────┐
│ Operation │ Exec │ FFM │
├───────────────────┼────────────────────────┼─────────────────┤
│ Full frame render │ 0.8ms = 4.8% of budget │ 0.57ms = 3.4% │
├───────────────────┼────────────────────────┼─────────────────┤
│ Diff render │ 0.014ms = 0.08% │ 0.005ms = 0.03% │
└───────────────────┴────────────────────────┴─────────────────┘
Still very comfortable. Even with exec, you're using less than 5% of your frame budget on JLine operations, leaving 95%+ for game logic.
The real bottleneck at 60fps would likely be the terminal emulator's ability to render that fast, not JLine.haha, yeah, should be fine i guess
Given that jline works with static, I'd stick with static. Folks can switch to dynamic if they see a need.
I have convinced the LLM that we basically don't even need the FFM module since exec is always fast enough
no wait, better numbers:
Operation Exec (ms) FFM (ms) Speedup
---------------------------------------------------------------
TTY detection 1,9446 0,2455 7,9x
getSize 1,8008 0,0070 257,2x
getAttributes 2,1393 0,0128 166,6x
enterRawMode 9,7044 0,1068 90,9x
write+flush 1,4269 1,4599 1,0x
Display.update (full) 0,6984 0,4805 1,5x
Display.update (diff) 0,0265 0,0046 5,8x
so FFM does have an edge over some operations, but those operations usually happen once or are still fast enough for 60pfs
but FFM has other use cases apart from jline right? maybe the benchmark does not show the whole picture?!
That crossed my mind too, but couldn't come up with any, do you have one in mind?
this is now the benchmark with compiled bb in native-image (previous was running in JVM still):
Exec provider: exec
FFM provider: ffm
Operation Exec (ms) FFM (ms) Speedup
---------------------------------------------------------------
TTY detection 1.3296 0.0141 94.1x
getSize 1.1657 0.0056 207.1x
getAttributes 1.6432 0.0054 303.8x
enterRawMode 7.2588 0.0303 239.8x
write+flush 0.0185 0.0176 1.1x
Display.update (full) 0.5380 0.5375 1.0x
Display.update (diff) 0.0286 0.0290 1.0xso the writing/updating seems to be equally fast... weird
ah it's equally fast because it doesn't need to shell out for that, it works the same in exec / ffm
FFM vs Exec. Claude's opinion: For your decision: Case for exec-only: - Works everywhere (musl, glibc, macOS, Windows) - Simpler codebase - one path to maintain - Startup cost (~10ms for raw mode) is imperceptible - Resize cost (~2ms) happens rarely - Rendering performance is identical Case for keeping FFM: - Faster startup (7ms → 0.03ms) - but users won't notice 7ms - Faster resize - but resizes are rare - "Correctness" - direct syscalls vs subprocess - Future-proofing if JLine adds features that need frequent provider calls Honestly, if exec works on Windows too, and the static musl build (your default) can't use FFM anyway... going exec-only simplifies things with no practical downside for users. The only scenario where FFM matters is high-frequency terminal state queries, which well-designed TUI apps (using Display) don't do.
Aha! Windows does need the FFM provider since exec only works in cygwin/git bash
So maybe only include the FFM stuff for windows...?
aaah wait, this is the reason why claude code doesn't work in powershell/cmd.exe either and they force you to use git bash for that too
which could be something that I prescribe to windows TUI users
is this the only reason jline provides FFM interface? there must be more to it
I have asked this to the maintainer, but he doesn't reply
it is pretty cool that bb TUIs just work in powershell / cmd.exe too
Yes, working in any Windows shell would be cool.
ok... I guess .. just the FFM stuff then for everything, but exec for linux (which shouldn't be much of a difference)
damn, all those trade-offs ;). the trade-off for including FFM is also binary size, 2mb
which is not that big of a deal
Cross-platform scripting, and soon... cross-platform TUI!
yeah I guess Anthropic could have adopted bb instead of bun for their stuff, if I was a bit earlier with this :P
❯ ah fuck it, cross platform TUI everywhere, with good fallback on linux
⏺ That's the spirit! Best of all worlds:
- macOS: FFM (native, fast)
- Linux glibc: FFM (native, fast)
- Linux musl/static: exec fallback (works fine, Display caches anyway)
- Windows native: FFM (proper console API support)
- Windows Git Bash: exec works too
Users get optimal experience everywhere, with solid fallback on static Linux builds. The benchmark showed the practical difference is minimal anyway since Display is smart about caching.
Ship it!- Linux musl/static: exec fallback (works fine, Display caches anyway)
could caching be the source of your ambiguous test results?how so?
no idea, was just wondering because it's mentioned there
writing/flushing isn't cached
but this all doesn't matter since exec doesn't shell out for this, it works the same way as FFM, you don't really need FFM for this, it's just writing to a stream in both cases
ok, cool
it's just for determining size, raw mode and tty detection that you shell out
and you usually need to determine the size of the window on each render, since the user could have re-sized. but there is a terminal signal for this, so until you receive that, you can cache the size
all this cleverness is in jline
A bit late to this discussion but I just want to express concern that bb might get updated to something that doesn't work on older Linux flavors (Ubuntu 20, CentOS 7 -- what we're stuck with at work). As long as bb continues to work on those older versions by default, I have no opinion on TUI stuff because it would not be of any interest to us.
To me, jline/TUI stuff isn't useful for a lot of scripting that bb is used for. It's a nice to have for some users. Am I misunderstanding?
@seancorfield The TUI stuff is something that bb is already used for, but needs external programs installed like gum or others. Adding jline just makes things easier (and more performant) to create these (and will e.g. also improve the console REPL experience: no more rlwrap needed). We chose the static binary to make it work on virtually every x64 linux since it doesn't depend on any external glibc and this static binary will stay around as long as possible. The discussion was just about what should be the default installed version (which has been static for years now). During this conversation I discovered that the fallback Jline provider (exec) works almost equally well on linux as the more optimal one (FFM which doesn't work in a static binary). So for linux static there isn't a real problem and no reason to change anything. Bottom line: it'll stay the same way as before, just with more features to build TUIs.
And even if bb TUI (via jline) did not work on some terminal/OS, it is opt-in by actually calling it from your code.
^ update: my statement isn't true for REPL, which will have rlwrap behaviour built-in https://clojurians.slack.com/archives/CLX41ASCS/p1769792315711259
I've re-implemented the babashka console REPL using jline, so you don't need rlwrap anymore.
Q: the history file with rlwrap used to be at ~/.bb_history. Maybe it would be nicer if we stored the history by project, maybe? Does nREPL does this? rebel-readline?
rebel-readline creates a .rebel_readline_history file
looks to be: <millis since epoch>:<command>
i.e.,
1762770286750:(:a foo)
1762770291068:(:b foo)
1762770292839:(:b foo [])
1762770381515:(:b foo {:til "w00t"})that's jline's history thing which I'm also using:
1769792484250:1
1769792484622:2is rebel readline always just doing this in the cwd and would this be desirable for bb?
I often use history for going back to commands in jrebel, like ctrl+r
that's a shell feature, not a rebel readline feature right
or maybe it is a jline feature in this case
it does work, but it doesn't suggest anything not from this history
It looks like clj (which uses rlwrap) puts the history in ~/.clojure_history?
rlwrap does that
<cmd>_history
I'm on the fence. Sometimes I'd prefer per-project history, sometimes I'd prefer global history. /unhelpful 🙂
I could just continue doing what bb and clj always did for now
I'll save it to HOME/.bb_repl_history
to not conflict with the bb_history file
(which already happened to me and jline then decided to reformat it)
I'm in the per-project camp 🙂
I lean slightly toward per-project history. Perhaps because I have a ~/clojure folder I use as a general scratch area and don't want that "project" history to interfere with my work project history. But it's a very mild preference.
One concern is that bb can be invoked from non-project directories and what makes a project (e.g. bb.edn in the current dir?) can be up for interpretation. Also you can have this ambiguous situation:
bb --config other-dir/bb.edn replThat ambiguity alone would make me consider the path of the edn file itself as the definition of a "project" over anything else.
yeah I was thinking the same
I always kinda hate tools that write local files to my projects, that's one argument against
Re: what to do with projectless, what would stop one from having a default history?
I'll stay on the current behavior and park this for a later time, but at least now it's under bb's control :)
Interesting. That is a pretty compelling argument. My "gut" says that if bb.edn is used, the history should go in the directory with bb.edn, wherever that is, else in $HOME. But that's a potentially complex/confusing approach, whereas always using $HOME is both simple and easy to understand.
Apologies for the late-ish and not overly helpful comment, but I very much agree with the local files dislike (especially for things like history) - really irks me. And I kind of feel like I'd rather be able to get history across projects - filtering history down is probably easier than aggregating dispersed history files.
Is there a possibility of jline being problematic on some terminals? Would there need to be an option to turn off jline-rlwrap support?
Without rlwrap the console REPL is barely usable (left/right arrow won't work). rlwrap works. the jline stuff makes rlwrap obsolete. It should work everywhere.
I'm going to merge the REPL improvements and people can test the dev build
The next step will be adding auto-completions etc to the console REPL
If people report errors, I'll make a flag to revert to the old console REPL (which is still there, it's used when you pipe input to bb's console REPL)
I tested this on Windows already, works well in pwsh, cmd.exe, cygwin, etc. Mac also tested
Very exciting, @borkdude!
Looking forward to testing this on Ubuntu 20.04 🙂
> Ubuntu 20.04 ... and I thought my machine was on an old version 😏
@seancorfield you're already using bb on that system right? I'll notify you when you can test the new REPL
I have 22 and 24 installed (WSL on Windows) but haven't gotten around to fully setting them up and switching my big ol' work project to either of those newer versions.
@borkdude Yup. bb is used by "most" of the projects I run on that old Ubuntu at this point.
Once latest updates to the new version of bb, I can easily test it on CentOS 7 on our QA servers too.
I remember it was not that long ago that you weren't so sure about this bb thing, Sean! It's so nice to see that you are enjoying bb as much as I am!
A year or so back, we'd talked about it at work, but the consensus then was that we didn't want to introduce a new dependency on our QA/production systems. So I'd also been hesitant to introduce it as a requirement in my OSS projects. But now:
> find oss workspace -name bb.edn
oss/next-jdbc/bb.edn
oss/durable-queue/bb.edn
oss/polylith/bb.edn
oss/slingshot/bb.edn
oss/honeysql/bb.edn
workspace/wsmain/bb.edn
That last one is work. And the tipping point there was New Relic breaking our old deployment recording machinery and requiring a new CLI tool to be installed, at which point I figured "Hey, if I need the newrelic CLI installed, why don't I also install bb?" 🙂The new console REPL is merged. You can test it with:
bash <(curl ) --dev-build --dir .
(this download also works in Git bash on Windows)
This will download the dev bb to your current directory.
Try ./bb and see if you can type expression, up arrow, left arrow, etc should work, without rlwrap.
Play around and see if anything obvious is broken.
Next I will add:
• multiline expression history and editing
• completions
Those aren't difficult to add, but maybe good to test this first versionBasic stuff seems to work on Ubuntu 20.04.
And on MX Linux 25!
And on CentOS 7 on one of our QA servers (I did a manual install into a new folder to test).
And on Windows 10 Powershell and CMD prompt. (Had to figure out how to turn off VirtualBox's grabbing of Ctrl-C first!) On my Windows VM, I hear an error chime when I try to up arrow past start of history or down arrow past end of history.
Looks fine on latest Fedora with latest kernel 6.18.8 too
Looking good on macOS Tahoe 26.2
Works on Arch Linux.
the basics work on Ubuntu(ish) 24.04 - a thing that (unsurprisingly) doesn't work is opening the current command in an editor 🙂
what's that?
I mean: current command?
Something to consider: if you enter a partial expression in Rebel Readline and press enter, you get this (`|` is the cursor):
[Rebel readline] Type :repl/help for online help info
user=> (* 1 2 3
#_=> |
but if you do the same thing in the new bb, you get this:
user=> (* 1 2 3
|
so it is a lot less obvious that it is waiting for input.I'm working on that. It's called multi-line input
it will also work with the up-arrow
The "current command in editor" thing is a readline-ish mechanism where you can do something like <C-x><C-e>, and it opens your edtior with a temp file containg the current content of whatever's in the line. You can edit the line as desired, and then save and exit your editor, and the thing that uses readline (e.g. bash) treats the file content as though it was input at the prompt. It's not unlike if you do a git commit at the CLI without a message and it opens the editor. JLine has the <C-x><C-e> binding (it would appear), but throws when it's invoked. It probably shouldn't be a priority at all, but I was just toying around; sometimes multi-line editing is nicer to work with in an editor.
would you like issues/discussions for things that are like "nice to have" or "beyond the basics", so they're written down but they don't need to be at the forefront of anyone's mind?
@highpressurecarsalesm I just tried that in Rebel Readline and it is bound to "eval current expr", i.e., at cursor.
C-x C-e is an emacs thing for evaluate
Ah, that explains why Rebel went that way then...
yeah sure, nice to haves a are good to write down in a Github discussion
ahh... so there's a bind conflict there to be worked out
C-x C-d (doc) is the thing I use most in Rebel, followed by C-x C-s (source) but mostly I just navigate around and edit. C-x C-e for eval is kinda cool tho' TIL!
I hadn't really dug into the key bindings in Rebel... lots of cool stuff there... but unclear how much is Rebel vs plain JLine?
Ooh, the incremental backward search works in the new bb... nice!
rebel uses jline
so in theory I could turn the bb console repl into something like rebel
I merged multi-line support. Wait 10 minutes from now and then re-install the dev build. You should be able to enter multi-line expressions and press up arrow to retrieve it back, also you can navigate in the lines to edit the expression. Intentionally I did not print
#_=>
before each multi-line like rebel does, because I kinda dislike those when copy-pasting expressions. I could add it and do it the same as rebel though. Let me know what you want. I'm AFK now getting some needed sleep!Heh, that prompt is exactly what I like about Rebel because it makes clear it is waiting for more input 🙂
ok, I can add it tomorrow, looking forward to what you all have said when I return
it should be there now
I experimented with adding classes to see if it'd be possible to run rebel using bb, and I ended up running up against proxy-super not being found - presumably a difference in how proxy works (can work?) in the native-image context.
rebel-readline's code is a bit hacky, it also contains references to .impl classes which are not public API
I changed the promptto #_=> now like rebel-readline for multiline stuff
I think showing docs would be nice and completions. Not sure about colored output and indentation, two other things rebel-readline provides. I'll focus on completions first
Oh I guess we're on par with leiningen now also REPL-wise:
user=> (+
#_=> 1
#_=> 2
#_=> 3)
6I guess lein is also using jline
wow nice
That is really nice @borkdude! Just updated my local dev copy to play with. Love it!
(press tab to get completions, for anyone not sure how that works)
My personal priority for features would be C-x C-d for docs, C-x C-e for eval at cursor, C-x C-s for source, indentation, code-coloring 🙂 But it is super-usable already at this point!
a builtin keybind that I like for building stuff up is alt-a, which accepts the current line and keeps it as the next line
TIL! Wow, that's neat!
on my mac alt-a is å.... how do I turn that off? ;)
pushed a couple more improvements for class completions, e.g.:
String -> String/.length, String/newI don't know macos very well, but the accented-a thing is (I think) an OS-level keyboard setting. I think that is default behavior on an "international" keyboard layout. It should be possible to turn on something like "option sends meta" in either your terminal emulator settings and/or OS-level keyboard settings. I think it's also possible to change the behavior of only the left or right, so if you use the mechanism to enter other accents/diacritics, you might be able to have access to both capabilities.
... with that said, emacs-y keybinds might make that obsolete in short order, so 🤷
ok, I now configured left option as esc+ and when I do:
user=> (+ 1 2 3)<alt-a>
6
user=> (+ 1 2 3)
I see this, does that make sense?yeah - since I sometimes end up wanting to build things up (like tacking add'l fns on the end of a threading macro), that's just a tiny bit nicer than "enter, up"
ah you mean with multi line stuff?
Console REPL improvement inspired by Node.js: • Input + ctrl-c ignores the input and yields next prompt • ctrl-c on empty input yields warning: next ctrl-c quits the REPL
$ clojure -M:babashka/dev
Babashka v1.12.215-SNAPSHOT REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.
user=>
(To exit, press Ctrl+C again or Ctrl+D or type :repl/quit)
user=>