-
Notifications
You must be signed in to change notification settings - Fork 842
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
Input is still read after program quits #24
Comments
Thanks for flagging this. If I'm reading correctly, this is something you've verified, correct? Internally Bubble Tea listens for SIGWINCH only. ^C is handled via putting the terminal into raw mode and reading stdin. |
I haven't taken a look at your implementation (yet). I was just assuming you are catching the things like Ctrl-C via Anyway, the effect I've tried to describe is that for every previous |
I also encountered a problem in my program, any keystroke takes a second time to respond. |
Good to know. I'll bump this one up in priority. Could you clarify what's happening with the keystrokes, exactly? |
When the TUI program is run twice in a row, the second time it runs, the key is pressed twice to respond; This is my test code: /~https://github.com/mritd/bubbles/blob/master/example/prompt/main.go#L12 When m1 is running, there will be no response to the first key press (the Update method is not called), and normal operation is restored after the second key press. Note: After the "Please input password:" prompt, I pressed the "a" key twice, but the terminal only received it once. |
Thanks for that. Can you also post the source code for your prompt program? |
The source code is in the mritd/bubbles repository. |
Wonderful, thank you. At first glance the prompt library looks fine (love the |
I just started learning bubbletea, bubbletea made me learn Elm Architecture, I am exploring some feasibility by writing a lot of code; sometimes I am not sure whether I am right, but I prefer to try. 😁😁😁 |
I have found the problem and fixed it: In fact, this is a goroutine leakage problem caused by io read blocking. I found a solution after reading a lot of articles. Description of the cause of the problem
I tried many ways to solve this problem (I have some stupid ideas):
The final fixChange the message channel into a global variable (the simplest fix, of course, I'm not sure if it is elegant and reasonable enough) /~https://github.com/charmbracelet/bubbletea/blob/v0.12.2/tea.go#L109 Some related articles: |
Fix first input loss when running multiple times fix charmbracelet#24 Signed-off-by: mritd <mritd@linux.com>
fix bubbletea see also charmbracelet/bubbletea#24 Signed-off-by: mritd <mritd@linux.com>
Nice find! I can see why your fix works, but I wonder if we couldn't close the channel and/or cancel the blocking subroutines, instead. |
@muesli Wait a minute, I am trying to add context to fix this problem. |
add context to prevent goroutine leakage see also charmbracelet#24 Signed-off-by: mritd <mritd@linux.com>
@muesli I tried to add a context to prevent goroutine from leaking. |
This is awesome, @mritd. Thanks for both the PR and all the details on the solution. Since this is slightly behind master, I'm going to merge another PR and then bring your branch up to date. I have some thoughts about how to clean this up a little bit, too. |
Fix first input loss when running multiple times fix #24 Signed-off-by: mritd <mritd@linux.com>
add context to prevent goroutine leakage see also #24 Signed-off-by: mritd <mritd@linux.com>
So it would be ideal not to have the |
This is true, go does not allow cancellation of blocking read IO. This means that after the TUI program has finished running, the input of stdin will always be captured by another goroutine; I tried to close stdin, but I cannot reopen it.... Perhaps it can be solved by some hacking methods, such as finding a way to kill this goroutine, but this is not an elegant solution; after reading some articles, most of the methods are to repackage this blocking IO. @meowgorithm I don't have a better idea yet, but the discussion in this issue of the go official may inspire you:: golang/go#20280 😊 |
Okay, well good to know it's not just us who wants to cancel a
As a side note, I'd personally implement the use cases I've seen in this issue as a single Bubble Tea program rather than multiple ones. For example, to prompt the user for a username and password I'd build those prompts as different states of one application. That would also allow your users to navigate back to a previous field to make corrections before submitting, too. example.mov |
So we just merged a PR that automatically opens a TTY for input if input's not TTY. In other words, if you pipe something into Bubble Tea it'll receive keypresses and so on as normal. If we either make that the default behavior or expose that behavior as something you opt into that should solve this one. |
I pulled the code from the tutorials section of the bubbletea repo and made it a modular function call for use in my terminal programs. See the code in the details below, but I run the bubbletea shopping list program, and after the user quits, I read from stdin -- where the first input is ignored. package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type listSelectorModel struct {
prompt string // string prompt that tells the user what they're doing
choices []string // items on the to-do list
cursor int // which to-do list item our cursor is pointing at
selected map[int]struct{} // which to-do items are selected
}
func main() {
ListSelector("Favorite Pokemon: \n", []string{"Cindaquil", "Todadile", "Chickorita"}).Execute()
x := Prompt("Enter something: ")
fmt.Printf("You entered: %s", x)
}
// Prompt is a simple prompt which returns stripped keyboard input as a string.
func Prompt(prompt string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Print(prompt)
text, _ := reader.ReadString('\n')
return strings.TrimSpace(text)
}
// ListSelector serves as a constructor for a listSelectorModel.
func ListSelector(prompt string, choices []string) listSelectorModel {
return listSelectorModel{
prompt: prompt,
choices: choices,
selected: make(map[int]struct{}),
}
}
// Run the listSelectorModel and return an updated instance of the model.
func (initialModel listSelectorModel) Execute() []interface{} {
tty, err := os.Open("/dev/tty")
if err != nil {
fmt.Println("could not open tty:", err)
os.Exit(1)
}
defer tty.Close()
p := tea.NewProgram(initialModel, tea.WithInput(tty))
// Run the ListSelector with the model.
// p := tea.NewProgram(initialModel)
if err := p.Start(); err != nil {
log.Fatal(err.Error())
}
result := make([]interface{}, 0)
for k := range initialModel.selected {
result = append(result, initialModel.choices[k])
}
return result
}
// Init is used by bubbletea and should be treated as private.
func (m listSelectorModel) Init() tea.Cmd {
return nil // Just return `nil`, which means "no I/O right now, please."
}
// Update is used by bubbletea and should be treated as private.
func (m listSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// Cool, what was the actual key pressed?
switch msg.String() {
// These keys should exit the program.
case "ctrl+c", "q":
return m, tea.Quit
// The "up" and "k" keys move the cursor up
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
// The "down" and "j" keys move the cursor down
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
// The "enter" key and the spacebar (a literal space) toggle
// the selected state for the item that the cursor is pointing at.
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
// Return the updated model to the Bubble Tea runtime for processing.
// Note that we're not returning a command.
return m, nil
}
// View is used by bubbletea and should be treated as private.
func (m listSelectorModel) View() string {
s := m.prompt
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\n(q to quit)\n"
return s // Send the UI for rendering
} Am I doing something wrong? Or is it possible this leaky channel is still affecting me? |
@strickolas Ick. I'm able to reproduce that though I'm not sure the cause yet. For the record, here's a stripped-down version that reproduces the issue. package main
import (
"bufio"
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
tty, err := os.Open("/dev/tty")
if err != nil {
fmt.Println("Could not open TTY:", err)
os.Exit(1)
}
if err := tea.NewProgram(model{}, tea.WithInput(tty)).Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
tty.Close()
reader := bufio.NewReader(os.Stdin)
fmt.Print("Now type something: ")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading input:", err)
os.Exit(1)
}
fmt.Printf("You entered: %s\n", strings.TrimSpace(text))
}
type model struct{}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if _, ok := msg.(tea.KeyMsg); ok {
return m, tea.Quit
}
return m, nil
}
func (m model) View() string {
return "Bubble Tea running. Press any key to exit...\n"
} |
Was there ever a fix for this bug? I think MR #41 solves this -- but I'm not quite sure. |
Unfortunately, not yet. #41 may solve the problem, but the implementation is less than ideal, so we've left the PR open. As mentioned earlier, this comes down to canceling a read, which is tricky in Go. This is not a case of a leaky channel. You're welcome to have a look at this and submit a PR if you'd like. I've been meaning to look into applying the the implementation in Elvish, but have yet to do so. |
I've got a rough POC that seems to work based on the |
This is now fixed in |
This is now available in |
If multiple
tea.Program
s are run in succession, each of them appears to set up a system signal handler (for catching SIGINT etc.), but does not clean it up after the program has quit without being interrupted this way. If you run n programs in succession that each want to catch"ctrl+c"
, for the n-th such program, you'll need to press Ctrl-C (n-1-m) times, where m is the number of previous programs that were interrupted. Of course, the interrupt-catching functionality should work independently of what has happened before.The text was updated successfully, but these errors were encountered: