Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various screen redraw fixes for wide characters, narrow screens etc. #202

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

tpodowd
Copy link
Contributor

@tpodowd tpodowd commented May 19, 2022

  • Don't overwrite existing text on same line as the prompt
  • Don't refresh screen when simply appending characters to buffer
  • Don't refresh screen unnessarily when pressing enter key
  • Handle prompts longer than screen width.
  • Fix wide characters in prompt
  • Fix screen edge issue when next character is wide.
  • Fix screen edge issue for masked characters
  • Fix narrow masked characteter, masking wide input
  • Fix wide masked character, masking narrow input
  • Reworked backspacesequence for index to use same algorithm as used
    for lineedge and reduce the control sequences to 2.
  • Reworked cleanup to incorporate initial cursor column position
    and avoid overwriting existing text as well as simplifying the
    control sequences used.
  • Fixed double width character detection and updated unit tests
  • Handle emoji in text or prompts.
  • Implement windows ANSI absolute horizonal position ansi code.
  • Get windows cursor position directly and don't send ansi DSR code
  • Don't write out empty mask runes
  • Cleanup - removed unused hadCLean variable

@tpodowd
Copy link
Contributor Author

tpodowd commented May 19, 2022

Hi @chzyer,

I noticed you became active again. Please have a look at this and see if you disagree with anything. I have tested the fixes on linux and windows 11 (used MS developer VM). I ran all the demo programs to check. I tried them with narrow and wide window sizes (although windows doesn't have code for window resize detection). Looking at the list of existing pull requests, I believe this pull request probably fixes some of the reported issues also. In particular, this PR replaces PR 171 (which I used to use but it was flawed).

Thanks!

wader added a commit to wader/fq that referenced this pull request May 19, 2022
Rebase on master
Cherry-pick chzyer/readline#202
@wader
Copy link
Collaborator

wader commented May 19, 2022

@tpodowd Hey! great stuff, took your changes for a spin and experience lots of less redraws 👍 I try to maintain a fork of readline at https://github.com/wader/readline/tree/fq with various cherry-picked stuff and my own changes. Will let you know if i run into something strange.

Would be great if various forks of chzyer/readline could be merged into one somewhere for less conflicts and work. Best of course would be to get things into @chzyer repo if he agrees.

@tpodowd
Copy link
Contributor Author

tpodowd commented May 20, 2022

Thanks for trying out the PR @wader . Good to hear that it works for you so far. Do let me know if you find any issues. I'll also push back anything I find here but so far it is looking solid. @chzyer did a great job on this readline library so yes, it would be good to keep it all here.

@chzyer
Copy link
Owner

chzyer commented May 20, 2022

Thanks @tpodowd , So many fixes in your PR, I will try them one by one.

@wader
Copy link
Collaborator

wader commented May 20, 2022

@chzyer Great to see you active again and thanks a lot for the readline package, works very well and have been a joy to use

@tpodowd
Copy link
Contributor Author

tpodowd commented May 31, 2022

Hi @chzyer - sorry the PR is a bit large. I tried to keep it as focused as I could. I've been using the changes on linux for a while now and haven't noticed any issues so far. I've been talking to @wader separately and it seems they are using it successfully also on Windows which is promising :-)

@wader
Copy link
Collaborator

wader commented Jun 1, 2022

Yeap i've been using fq daily with the patches on macOS for 2 weeks or so and haven't noticed any issues

@tpodowd
Copy link
Contributor Author

tpodowd commented Jun 28, 2022

Note: I rebased the screenredraw branch against the latest master and did a force push. No other changes.

@slingamn
Copy link
Collaborator

This change appears to introduce a number of data races: https://gist.github.com/slingamn/fb0ed14d6a5471b83fce276d11f72856

@tpodowd
Copy link
Contributor Author

tpodowd commented Feb 12, 2023

Hi @slingamn - Thanks for reporting that. I'm a little swamped right now, but I'll try look at that this week. I was able to replicate by running.

go run -race readline-demo.go
» ==================
WARNING: DATA RACE
Read at 0x00c000090258 by goroutine 13:
  github.com/chzyer/readline.(*RuneBuffer).getSplitByLine()
      /home/tpodowd/work/readline/runebuf.go:449 +0x3c4
  github.com/chzyer/readline.(*RuneBuffer).isInLineEdge()
      /home/tpodowd/work/readline/runebuf.go:439 +0x8c
  github.com/chzyer/readline.(*RuneBuffer).append()
      /home/tpodowd/work/readline/runebuf.go:558 +0x20f
  github.com/chzyer/readline.(*RuneBuffer).WriteRunes()
      /home/tpodowd/work/readline/runebuf.go:175 +0x276
  github.com/chzyer/readline.(*RuneBuffer).WriteRune()
      /home/tpodowd/work/readline/runebuf.go:162 +0xa5a
  github.com/chzyer/readline.(*Operation).ioloop()
      /home/tpodowd/work/readline/operation.go:331 +0x9d1

Previous write at 0x00c000090258 by goroutine 15:
  github.com/chzyer/readline.(*RuneBuffer).setOffset()
      /home/tpodowd/work/readline/runebuf.go:525 +0x117
  github.com/chzyer/readline.(*RuneBuffer).setOffset-fm()
      /home/tpodowd/work/readline/runebuf.go:522 +0x5e
  github.com/chzyer/readline.(*Terminal).GetOffset.func1()
      /home/tpodowd/work/readline/terminal.go:81 +0x83

Goroutine 13 (running) created at:
  github.com/chzyer/readline.NewOperation()
      /home/tpodowd/work/readline/operation.go:88 +0x81a
  github.com/chzyer/readline.(*Terminal).Readline()
      /home/tpodowd/work/readline/terminal.go:95 +0x84
  github.com/chzyer/readline.NewEx()
      /home/tpodowd/work/readline/readline.go:169 +0x5f
  main.main()
      /home/tpodowd/work/readline/example/readline-demo/readline-demo.go:74 +0x270

Goroutine 15 (finished) created at:
  github.com/chzyer/readline.(*Terminal).GetOffset()
      /home/tpodowd/work/readline/terminal.go:80 +0x5a
  github.com/chzyer/readline.(*RuneBuffer).getAndSetOffset()
      /home/tpodowd/work/readline/runebuf.go:513 +0xe4
  github.com/chzyer/readline.(*Operation).Runes()
      /home/tpodowd/work/readline/operation.go:393 +0x1f6
  github.com/chzyer/readline.(*Operation).String()
      /home/tpodowd/work/readline/operation.go:376 +0x56
  github.com/chzyer/readline.(*Instance).Readline()
      /home/tpodowd/work/readline/readline.go:257 +0x2f
  main.main()
      /home/tpodowd/work/readline/example/readline-demo/readline-demo.go:99 +0x8af
==================

@slingamn
Copy link
Collaborator

Thanks! Sorry, the full test case I was using is in the issue description of #217, in case that helps.

@slingamn
Copy link
Collaborator

Given #198 and #219, I'm not sure it makes sense to "spot-fix" race conditions in this library. A large-scale concurrency redesign might be necessary (for example, putting most operations on internal datastructures behind a single broad mutex, releasing it for I/O operations).

@slingamn
Copy link
Collaborator

@tpodowd @wader also interested in your thoughts on #220 if you have time

@tpodowd
Copy link
Contributor Author

tpodowd commented Feb 22, 2023

Hi @slingamn - Have not had a lot of time yet. Still high on my list. I did do some review though. the race condition above is a simple fix. There are a few more though so I think I'll just spend a little more time and address them also. It will be next week though as I'm off for a few days.

- Don't overwrite existing text on same line as the prompt
- Don't refresh screen when simply appending characters to buffer
- Don't refresh screen unnessarily when pressing enter key
- Handle prompts longer than screen width.
- Fix wide characters in prompt
- Fix screen edge issue when next character is wide.
- Fix screen edge issue for masked characters
- Fix narrow masked characteter, masking wide input
- Fix wide masked character, masking narrow input
- Reworked backspacesequence for index to use same algorithm as used
  for lineedge and reduce the control sequences to 2.
- Reworked cleanup to incorporate initial cursor column position
  and avoid overwriting existing text as well as simplifying the
  control sequences used.
- Fixed double width character detection and updated unit tests
- Handle emoji in text or prompts.
- Implement windows ANSI absolute horizonal position ansi code.
- Get windows cursor position directly and don't send ansi DSR code
- Don't write out empty mask runes
- Cleanup - removed unused hadCLean variable
@tpodowd
Copy link
Contributor Author

tpodowd commented Feb 28, 2023

Note: I rebased this PR on the latest master and also added a small commit that fixes the race condition mentioned above that this PR introduced.

I am also going to rebase my other PR208 on top of this branch to include this race condition fix.

@slingamn
Copy link
Collaborator

Thanks!

I tested this branch against the test case from #217. I could not reproduce the data race anymore. On the other hand:

  1. The issue from TOCTOU race in prompt redraw #220 is still present
  2. Combining the patch from TOCTOU race in prompt redraw #220 (7bb0e05674) with this branch results in a different redraw glitch, where the prompt can be drawn twice.
> a
received a
> b
received b
> c
received c
> > d
received d
> > e
received e
> f
received f
> g
received g

I'm uncertain about the cause, but maybe it's the operation.go changes in this branch?

@tpodowd
Copy link
Contributor Author

tpodowd commented Feb 28, 2023

Hi @slingamn. Just had a look at this. I think the patch to remove the IsReading check is probably not the right fix particularly combined with this PR.

So what is happening is that you have two go routines a and b. a) is calling readline and passing the string to b) before looping again and calling readline again. b) is looping printing the text a) sends it.

Everything works correctly if a) writes the prompt, reads the input and sends the input to b) which then prints the input and then a) writes the prompt again. However, if a) writes the prompt, reads input and sends input to b) and writes the prompt again before b) has time to write the input it received, you get the following:

> a
> received a
NEXTINPUTHERE

ie, the prompt "> " followed by "received a\n"
If you simply type the "NEXTINPUTHERE" it goes at the start of the line as the prompt was already output and is now on the previous line.

If you press ctrl-a or something like that, the prompt will redraw as follows

> a
> received a
> NEXTINPUTHERE

The reason this happens is that this PR tries to avoid redrawing the whole line every time a key is pressed and simply appended to the current position.

I don't know if the library can/should fix "> received a" issue as it's simply a race writing to stdout? I can look into my fix for redrawing the prompt every append though which would at least mean that you will always get the prompt in the right place as you type without using ctrl-a or something to initiate the redraw. This would slow down every key press though (but I guess that is the way it is in the current mainline anyway). I'll play with it tomorrow if I have time. Let me know what you think.

@slingamn
Copy link
Collaborator

slingamn commented Mar 1, 2023

Everything works correctly if a) writes the prompt, reads the input and sends the input to b) which then prints the input and then a) writes the prompt again.

As I understand the API here, it explicitly allows asynchronously writes via (*Instance).Write while the prompt is displayed and input is being read. (Hence why (*wrapWriter).Write explicitly supports redrawing the prompt, by wrapping the write in (*RuneBuffer).Refresh.) The case for 7bb0e05674, i.e. removing the fast path that skips the refresh, is that this inherently involves a TOCTOU race, because IsReading() can become true at any time.

Asynchronous writes are part of my core use case for this library (a rudimentary IRC client).

I don't know if the library can/should fix "> received a" issue as it's simply a race writing to stdout?

It seems to me that it's one of the core functions of this library to draw the prompt correctly. Drawing it incorrectly is not pleasant at a UX level (it makes the program feel inconsistent or unreliable). I don't know what's considered normal in TUI applications (this is my first time getting this close to terminal emulation) but it seems to me that any optimization that sacrifices correctness here isn't worth it. The performance penalty (which, as you point out, is present in the current master branch) doesn't seem to be affecting current consumers of this library (although I have noticed a weird "flicker" on Windows, possibly because of Windows idiosyncrasies).

@tpodowd
Copy link
Contributor Author

tpodowd commented Mar 1, 2023

Cool. Thanks for the feedback. I'll see what I can do.

Introduce a new operation.isPrompting field which is true from when a
prompt has been written until data is returned to the caller.
When Write is called on the wrapWriter to write to stdout or stderr,
check if we are currently prompting the user for input and if so
clean up the prompt and write the data before redrawing the
prompt at its new location after the written data.

Previously terminal.IsReading() was used for this, but this had various
race conditions and it was not correct to check this field to make
prompt and buffer redrawing decisions. In turn, I removed all the
isReading code also. The old isReading() check was actually checking
if the terminal goroutine was actively waiting for more input.
@tpodowd
Copy link
Contributor Author

tpodowd commented Mar 4, 2023

Hi @slingamn

I pushed another commit to this PR. I believe that this addresses your issues. I removed the race condition with the prompt redraw. I removed the terminal isReading bool field as it is not reliable. Instead I introduced an operation.isPrompting bool with some locking around it so that it is reliable. This also conflicts with your patch to remove the shortcut #220 and fixes the prompt rewrite also such that you can now write to rl reliably and the prompt will not race. I also did not have to remove my append redraw optimisation I mentioned earlier as part of this fix so that was nice.

I ran though all the examples in the example directory also and insured that they worked (on linux).

Please try this out and let me know if things are behaving better for you. I'll try to get a windows VM up also to test it but this may take a little time. I also have not tested non-interactive mode yet.

Anyway. give it a go and let me know.

@tpodowd
Copy link
Contributor Author

tpodowd commented Mar 5, 2023

Hi @slingamn - I tested this branch also on a windows VM this morning. Looks good to me. I tested all the examples as well as your test program in #217, so I believe I haven't broken anything on windows.

@slingamn
Copy link
Collaborator

slingamn commented Mar 5, 2023

@tpodowd thanks so much! I'm still testing, but I rebased my patchset https://github.com/slingamn/readline/commits/ircdog on your branch, and:

  1. The prompt redraw race is gone
  2. The flicker on Windows is considerably mitigated (probably because of your redraw optimization for the append case)
  3. data race in history search #219 appears to be fixed, probably by the additional locking you added in 62ab2cf

Comment on lines +412 to +416
// Query cursor position before printing the prompt as there
// maybe existing text on the same line that ideally we don't
// want to overwrite and cause prompt to jump left. Note that
// this is not perfect but works the majority of the time.
o.buf.getAndSetOffset(o.t)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This aspect of the change seems to cause a race condition in my application during asynchronous Close(), where sometimes the response to this DSR query is not read correctly by the library and ends up printed on the screen. It looks like ^[[70;1R or similar.

I'll look further into mitigations from my side, just wanted to note this so I don't forget about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This terminal will be asked for the current offset everytime you call Readline() to ask for text. Normally, since you call readline, input will be taking place so bytes will be read and readline will swallow this offset information from the terminal. However, if you call readline() and then close() in other goroutine (or exit the program abruptly) for example, it can happen that readline won't have time to read it and instead the program will exit and the next thing to read it will be your shell which will just output it. It doesn't happen in my use case because I'm not making exit decisions when readline is waiting for text. However, I can see in your example test case, it might have that issue.

It might be possible for the library to handle this better. Maybe in another PR if this is merged 😉

@Lekensteyn
Copy link

I ran into an issue with v1.5.1 where navigating the cursor over a long word-wrapped input line "moves the screen up". As if the library is trying to make sure that the prompt stays on the same line as the current cursor. Reproduced on macOS 14.0 with iTerm 3.4.21, TERM=xterm-256color.

This PR seems to fix that, so unless there are any other issues, I would recommend merging this PR.

In meantime, I am changing my application to point to this PR:

go mod edit -replace github.com/chzyer/[email protected]=github.com/cloudian/readline@screenredraw
go mod tidy

@wader
Copy link
Collaborator

wader commented Oct 18, 2023

@Lekensteyn you might want to look at https://github.com/ergochat/readline which i think has these changes plus a bunch of more things

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants