Repository navigation
Description
I've attached a minimal repro project to showcase what I'm running into.
The README from said project:
This project showcases (I think?) a bug with the FFM terminal representation when trying to call
terminal.reader().read(timeout)
Repro steps
This project uses JDK25 (via Gradle toolchain) and has a dependency on jline 4.1.0 (see gradle/libs.versions.toml).
Probably earlier JDKs might also work, but my understanding is JDK25 is the first version that FFM is really mature.
In case it matters, my machine is an M3 MacBook Pro. (uname -m β arm64)
This project has only a single code file in it: src/main/kotlin/main.kt
- Unzip the attached project into a location of your preference. It will pull out a folder called
jline-ffm-read-eofso you don't have to worry about files exploding all over your current directory like a mess. - In a terminal....
- Make sure you have JDK25 or newer installed (
java -version) cdinto this project's root./gradlew installDist./build/install/jline-ffm-read-eof/bin/jline-ffm-read-eof
The program should immediately print out the line: Terminal class: org.jline.terminal.impl.ffm.FfmUnixSysTerminal
Expected behavior
Then, as a user, you should just be able to press keys, e.g. press "q" and the terminal will say: "Read: 113"
Actual behavior
The program exits immediately before I get a chance to type, printing: "Got EOF (-1), exiting"
Additionally, if you change main.kt to look like this:
// Before
if (c >= 0) {
println("Read: $c")
}
if (c == -1) {
println("Got EOF (-1), exiting")
quit = true
}
// After
if (c >= -1) {
println("Read: $c")
}you will see that the FFM implementation does read keypresses, but it also spits out -1's at a regular cadence.
Here is the output I got when typing "Hello":
Terminal class: org.jline.terminal.impl.ffm.FfmUnixSysTerminal
Read: -1
Read: -1
Read: -1
Read: -1
Read: -1
Read: -1
Read: 104
Read: -1
Read: -1
Read: 101
Read: -1
Read: 108
Read: -1
Read: 108
Read: -1
Read: -1
Read: 111
Read: -1
Read: -1
Read: -1
Read: -1
Read: -1
My understanding
I'll write out my assumptions, just in case this bug is really just user error.
When calling reader().read(...), essentially, this value should either return:
- a real character code value (a user just pressed a key)
- -2 if the read call timed out (which is fine, just ignore it)
- -1 to indicate the input stream ended (e.g. user pressed ctrl+D), which means you should quit the loop
In other words:
var quit = false
while (!quit) {
val c = terminal.reader().read(16)
if (c >= 0) {
// Valid character received. Process it!
} else if (c == -1) {
quit = true
}
}I've been using Jline for years and this has worked fine for me using the Exec fallback.
But with FFM, it seems to sporadically give me -1's, which kills my while loop almost immediately.
I assume this is a bug. It may also be related to my OS, I'm not sure.
Hopefully the team will be able to reproduce this issue on their end.
Activity
bitspittle commentedon May 14, 2026
Also this might be interesting? I tried using versions 4.0.0 through 4.0.15, and in those cases, the FFM dependency didn't pick up at all, and I kept getting set to the system exec fallback (
org.jline.terminal.impl.PosixSysTerminal). So something major changed in 4.1.0 for me.Also also: I am not getting the jni dependency to work on this machine. I always end up falling back to posix sys terminal implementation in that case as well. You can go into
libs.versions.tomland comment out theffmdeclaration on line 22 which means you should now be trying to load thejniprovider. Check to see if you're actually getting that or, like me, are getting the fallback implementation instead.[-]FFM implementation returns -1 when using `reader().read(timeout)`[/-][+]FFM implementation returns -1 when using `reader().read(timeout)` (MacOS M3 related?)[/+]fix: NonBlockingInputStream keeps thread alive after EOF (fixes jlineβ¦
bitspittle commentedon May 23, 2026
Oh wow thanks for the quick fix. I'll try updating to 4.1.2 when I get a chance and confirm. It may be a few days.
fix: NonBlockingInputStream keeps thread alive after EOF (fixes #1879) (
fix: NonBlockingInputStream keeps thread alive after EOF (fixes #1879) (
fix: NonBlockingInputStream keeps thread alive after EOF (fixes #1879) (
fix: NonBlockingInputStream keeps thread alive after EOF (fixes #1879) (