peterh / liner

Pure Go line editor with history, inspired by linenoise
MIT License
1.04k stars 132 forks source link

Inject output while in prompt #46

Closed slavikm closed 9 years ago

slavikm commented 9 years ago

Is there a way to output something to the user while in prompt (from a separate go routine) - so that the output will be printed on a separate line and the prompt will be refreshed on a line below?

I made an ugly hack to make it work by creating a public function InjectString that saves the string to be injected on the State and pushed a marker rune on State.next. Then, I handle it in State.Prompt by doing:

case inject:
  s.cursorPos(0)
  s.eraseLine()
  fmt.Println(s.injected)
  s.refresh(p, line, pos)

But, it is really an ugly hack. I needed to change State.next to a regular channel so that I can write to it and not only read from it... There must be a better way but I could not find it in a few minutes of looking in the code.

peterh commented 9 years ago

There is not currently a way to interrupt the user. Partly because it can be a confusing UX, but primarily because it can't possibly work when $TERM == "dumb".

For a less racy version of your hack, you probably want to use a chan string instead of a string, and add the new chan to the select{} block at the top of readNext in input.go instead of injecting a fake rune into the input chan. (Making either version work on Windows left as an exercise for the reader).

See also the comments in https://github.com/peterh/liner/pull/16

slavikm commented 9 years ago

Makes sense. Thanks.

zserge commented 9 years ago

I personally don't think this issue should be closed as a wontfix. A lot of apps (chats, interactive shells that run background tasks etc) need to print messages asynchronously. It's ok to buffer messages in the channel and print periodically at the app level. So maybe Prompt() could take a timeout argument somehow? So the app could handle some timeout error it would return and handle pending messages?

Anyway, the following workaround might work on some systems:

go func() {
    for {
        time.Sleep(time.Second)
        fmt.Printf("\r")
        fmt.Println("Hey", time.Now())
        // Sending WINCH causes the prompt to be refreshed keeping the cursor position etc
        syscall.Kill(syscall.Getpid(), syscall.SIGWINCH)
    }
}()

Still hoping that a proper solution would appear one day.

slavikm commented 9 years ago

I forked and added PrintAbovePrompt that does just that. Not 100% sure regarding my Windows implementation. It definitely works but handling Ctrl-C, Ctrl-D might be wrong. Check it at https://github.com/slavikm/liner

If Peter is interested I will issue a pull request.

peterh commented 9 years ago

@zserge You almost had me convinced, right up until you said "chats". If you're implementing IRC, you have multiple types of out-of-band data, and really want one of the ncurses wrappers (or reimplementations), not Liner. As to "interactive shells that run background tasks", I use bash all the time, and it happily queues up background task messages until I'm done editing. I prefer it this way, because the text that I am referencing above the prompt does not move while I'm editing my line.

@slavikm That fork looks interesting. I did notice two bugs, though.

  1. When $TERM=dumb, PrintAbovePrompt will hang forever.
  2. There is a race condition: If PrintAbovePrompt is called right as the user presses Enter (and before Prompt returns), the goroutine that called PrintAbovePrompt will hang until Prompt is re-entered (assuming Prompt is ever re-entered). That goroutine has no way to know if it's safe to call PrintAbovePrompt.
zserge commented 9 years ago

@peterh Ok, let me explain with a bit more details. 1) Chats - I didn't mean a full IRC client, or something that complex. However, a single-room chat like http://tools.suckless.org/sic/ is a good example. Single incoming channel of strings, single line editor to send data. 2) By interactive shells I didn't mean bash and friends. On my work I often write simple utilities to test hardware and communication protocols. For example, imagine a board that can either send messages when a button is pressed, or toggle LEDs when incoming command is received. I made simple apps in Go to print button press messages to stdout, and to send LED commands from stdin. It can be used in unix pipelines as well. But of course I would prefer my LED command input to not be interrupted when I press a button on the board.

So basically, I speak about two-directional communication over a single socket, or RS232, or RS485, or USB etc and a user-friendly interface to that that would support line editing.

slavikm commented 9 years ago

@peterh thanks for the feedback.

peterh commented 9 years ago

I don't know what the right solution is (aside from going full ncurses).

Perhaps you could add a function s.DrainLineAbove (I'm sure you can come up with a better name) that reads from s.lineAbovePrompt using select with default, that can be called after s.Prompt returns and

  1. after the user has taken a mutex (or whatever else the user needs to do to prevent other goroutines from calling s.PrintAboveLine), or
  2. periodically before calling s.Prompt again

This should handle both cases, although note that s.PrintAbovePrompt may block until s.DrainAboveLine is called, which might be awkward in some programs.

Alternatively, perhaps it's better to buffer a small number and export the s.LineAbovePrompt channel, so that senders can send in select with default if they don't want to block, and the main goroutine can drain the channel when s.Prompt returns.

slavikm commented 9 years ago

@peterh I added a small buffer and made PrintAbovePrompt return an error if buffer is full. Also, I drain the buffer before returning from Prompt and PromptPassword. See https://github.com/slavikm/liner/commit/b41ae2c3c53b90de25d7b7467e7daf7acb0d358a.

complyue commented 5 years ago

I submitted #119, anyone here interested to review?