mabe02 / lanterna

Java library for creating text-based GUIs
GNU Lesser General Public License v3.0
2.3k stars 243 forks source link

Rendering on OSX broke between 3.0.4 and 3.1.0 #591

Closed morgen-peschke closed 10 months ago

morgen-peschke commented 10 months ago

This came up while working through the tutorials.

Here's the code for rendering a very simple gut check:

package cli

import com.googlecode.lanterna.terminal.DefaultTerminalFactory

object Bug {
  def main(args: Seq[String]): Unit = {
    val term = new DefaultTerminalFactory().setForceTextTerminal(true).createTerminal()
    term.enterPrivateMode()
    val graphics = term.newTextGraphics()
    var pos = term.getCursorPosition
    (1 to 5).foreach { _ =>
      graphics.putString(pos, "Test!")
      pos = pos.withColumn(0).withRelativeRow(1)
    }
    term.flush()
    term.readInput()
    ()
  }
}

On 3.0.4, it immediately renders correctly in terminal:

Test!
Test!
Test!
Test!
Test!

On 3.1.x, it renders like this:

^[[55;245R

If you wait close to 10 seconds, it'll update to this:

Test!;245R^[[1;11R
Test!
Test!
Test!
Test!
avl42 commented 10 months ago

Are you running this from OS X's terminal app?

During setting up the default terminal object, it sends a couple escape sequences to the terminal to switch the terminal to private mode, hide the cursor, send it to far right down, then query the location to determine screen-size. Then it sends the cursor to home position. For the querying of location, it sends an escape which normally causes the terminal to respond with an escape-sequence on its own (as if typed by the user), thereby telling your application the current cursor position.

From your symptoms it seems like the app doesn't ever receive that reply, and eventually aborts after a few seconds of not receiving the screen size. Reasons are likely that the call to "stty" misbehaves, thus fails to set the terminal to raw mode.

I don't recognize the language (it aint java), and I don't know if it could be made to show some call stack dump on failure. Or maybe it has some debugger that would allow you to single-step through it, while the application under inspection is connected to the terminal - it wouldn't work with the usual "stdin/stdout/stderr"-panels of typical IDEs.

On Wed, Jan 17, 2024 at 9:10 PM Morgen Peschke @.***> wrote:

This came up while working through the tutorials.

Here's the code for rendering a very simple gut check:

— Reply to this email directly, view it on GitHub https://github.com/mabe02/lanterna/issues/591, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABIDBMT54ZLZ6YXPUDKTTQLYPAV3RAVCNFSM6AAAAABB7EFLGCVHI2DSMVQWIX3LMV43ASLTON2WKOZSGA4DMOJQGA4DENY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

morgen-peschke commented 10 months ago

Are you running this from OS X's terminal app?

I tried it in Terminal and iTerm2, and didn't notice any behavior differences between the two.

I don't recognize the language (it aint java)

Fair 🤷🏻 , it's Scala

and I don't know if it could be made to show some call stack dump on failure.

That shouldn't be a problem (it's the default behavior), however I'm not sure if it's relevant here as (other than rendering weirdly) there aren't any exceptions thrown.

It'll wait for until the next input and then shut down.

Reasons are likely that the call to "stty" misbehaves, thus fails to set the terminal to raw mode.

Not sure where it looks for stty, but if it helps, calling it directly seems ok:

$ /bin/stty
speed 38400 baud;
lflags: echoe echok echoke echoctl pendin
iflags: iutf8
oflags: -oxtabs
cflags: cs8 -parenb
morgen-peschke commented 10 months ago

I was playing around with Tutorial3, to see if the higher-level constructs worked (trying to reduce the chance that this is a problem with my code), and found something kind of interesting.

Every couple of times, it'll work as expected on 3.1.1, and when it doesn't work I noticed two differences:

  1. The screen size is only 80x24, rather than 245x55
  2. TerminalScreen#readInput() only returns when I press [Enter], rather than when any non-modifier key is pressed
avl42 commented 10 months ago

The symptoms you describe (waits for [Enter] key, reports 80x24, ...) all point towards stty failing.

Either stty fails silently, or failures of stty are suppressed. Could you add some code to obtain return-code from stty call, or any stderr-output?

iirc, stty gets called three times in total, once with option "-g" to report current state of terminal in some platform-specific syntax. Then it gets called to set terminal to raw mode -- that one appears to be failing. Finally, at the end of the program, it'll use the result of "stty -g" to restore terminal to original mode.

Try to identify these calls. Eventually, you could write a shell script by name of "stty" that will call the original stty and do some extra logging of output, errors and return code, and make sure your script is found in PATH before the real stty when you start the lanterna app.

I don't have access to any OS X machine myself, so I cannot do the experiments there. On Linux it always worked for me.

avl42 commented 10 months ago

(if you need help with the stty script, just ask...)

avl42 commented 10 months ago

checking into the git, I noticed a late change, where lanterna now calls "/usr/bin/env stty ..." rather than previously "/bin/stty ..."

could it be, that /usr/bin/env is flaky or missing on OS X ? It is supposed to find stty on the PATH and call it.

avl42 commented 10 months ago

... or could it be, that you have some other tool named stty and lanterna maybe picks up the wrong stty from PATH ?

morgen-peschke commented 10 months ago

Found the issue, looks like it's due to how the build tooling was forking during run 🤦🏻

The version I used to test (3.1.1) was still using the direct call to /bin/stty, but there was support for using com.googlecode.lanterna.terminal.UnixTerminal.sttyCommand to override this (thanks for adding that btw, saved a bunch of time 😄).

I used this script to capture the output of stty and stuck it in a directory on the path named local-bin:

#!/usr/bin/env bash

# Filter path to avoid /usr/bin/env finding this wrapper again
export PATH=$(tr ':' '\n' <<< "$PATH" | ag -v local-bin | tr '\n' ':' | sed 's/:$//')

{
  printf '=================================================\n'
  set -x
  which stty
  hash stty
  /usr/bin/env -v stty "$@"
} &>> stty.log.txt

/usr/bin/env stty "$@"

stty was called 7 times, so I'll omit the full log, this is a representative portion:

+ /usr/bin/env -v stty min 1
#env executing: stty
#env    arg[0]= 'stty'
#env    arg[1]= 'min'
#env    arg[2]= '1'
stty: tcsetattr: Input/output error

This turned out to have been because of the way the build tooling (mill) was forking to run the process.

This way does not work:

mill scratch.runMain --mainClass cli.Bug

This way works:

mill scratch.publishLocal &&
coursier launch \
  --scala 2.13.8 \
  -D com.googlecode.lanterna.terminal.UnixTerminal.sttyCommand=stty \
  com.example.morgen::scratch::0.0.1 \
  --main cli.Bug

The assumption that, because input worked, there was a functional tty turns out to have been a bad one.

Thanks for your help getting me pointed the right direction :)

mabe02 commented 10 months ago

I've updated UnixTerminal so the STTY path can be specified directly in the code, see https://github.com/mabe02/lanterna/commit/2f4e3b7f37d8645ef001c0dcc05d7ef2ad749044