lrstanley / bubblezone

helper utility for BubbleTea, allowing easy mouse event tracking
https://pkg.go.dev/github.com/lrstanley/bubblezone
MIT License
518 stars 15 forks source link

bug: Console freezes with certain lipgloss.place usage #17

Closed BigJk closed 1 year ago

BigJk commented 1 year ago

🌧 Describe the problem

I'm currently using bubbletea and bubblezone to develop a small console game pet-project. While developing I encountered a strange bug.

When I use lipgloss.place in a certain way with zone.mark the whole program and console freezes after responding correctly a few times. Even when I kill the process the console will be stuck without further input being possible. This happens with iterm2 and the default macos terminal. I have to create a new console session to get the console working again.

β›… Expected behavior

I expected the program to keep processing mouse inputs and not freeze.

πŸ”„ Minimal reproduction

I threw together a minimal example without any of my game logic that keeps freezing on my system:

package main

import (
    "fmt"
    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
    zone "github.com/lrstanley/bubblezone"
    "github.com/muesli/reflow/wordwrap"
    "os"
    "strings"
)

const TriggerBug = true

type TestModel struct {
    size         tea.WindowSizeMsg
    selectedCard int
    numberCards  int
}

func (m TestModel) Init() tea.Cmd {
    return nil
}

func (m TestModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        if msg.String() == "ctrl+c" {
            return m, tea.Quit
        }
    case tea.WindowSizeMsg:
        m.size = msg
    case tea.MouseMsg:
        if msg.Type == tea.MouseLeft || msg.Type == tea.MouseMotion {
            // Check zones to set selected card
            for i := 0; i < m.numberCards; i++ {
                if zone.Get(fmt.Sprintf("%s%d", "card_", i)).InBounds(msg) {
                    m.selectedCard = i
                }
            }
        }
    }

    return m, nil
}

func (m TestModel) View() string {
    cardStyle := lipgloss.NewStyle().Width(30).Padding(1, 2).Margin(0, 2)

    var cardBoxes []string
    for i := 0; i < m.numberCards; i++ {
        selected := i == m.selectedCard

        style := cardStyle.
            Border(lipgloss.NormalBorder(), selected, false, false, false).
            BorderBackground(lipgloss.Color("#cccccc")).
            Background(lipgloss.Color("#cccccc")).
            BorderForeground(lipgloss.Color("#ffffff")).
            Foreground(lipgloss.Color("#ffffff"))

        // If the card is selected we give it a bit more height
        if selected {
            cardBoxes = append(cardBoxes,
                style.
                    Height(min(m.size.Height-1, m.size.Height/2+5)).
                    Render(wordwrap.String(fmt.Sprintf("%s\n\n%s\n\n%s", strings.Repeat("β€’", 3), "Example Card", "Hello World... Hello World... Hello World.."), 20)),
            )
            continue
        }

        // Non-selected card style
        cardBoxes = append(cardBoxes,
            style.
                Height(m.size.Height/2).
                Render(wordwrap.String(fmt.Sprintf("%s\n\n%s\n\n%s", strings.Repeat("β€’", 3), "Example Card", "Hello World... Hello World... Hello World.."), 20)),
        )
    }

    for i := range cardBoxes {
        cardBoxes[i] = zone.Mark(fmt.Sprintf("%s%d", "card_", i), cardBoxes[i])
    }

    // EDIT: also breaks after a while
    if !TriggerBug {
        return zone.Scan(lipgloss.JoinHorizontal(lipgloss.Bottom, cardBoxes...))
    }

    // Freeze:
    return zone.Scan(lipgloss.Place(m.size.Width, m.size.Height, lipgloss.Center, lipgloss.Bottom, lipgloss.JoinHorizontal(lipgloss.Bottom, cardBoxes...)))
}

func main() {
    zone.NewGlobal()

    p := tea.NewProgram(TestModel{
        numberCards: 3,
    }, tea.WithAltScreen(), tea.WithMouseAllMotion())
    if _, err := p.Run(); err != nil {
        fmt.Printf("Alas, there's been an error: %v", err)
        os.Exit(1)
    }
}

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

πŸ’  Version: bubblezone

v0.0.0-20230303230241-08f906ff62a9

πŸ–₯ Version: Operating system

macos

βš™ Additional context

Video showing the working lipgloss.JoinHorizontal vs the freezing lipgloss.Place. When the second terminal tab stops responding it's completely frozen. ctrl+c does nothing and I manually need to restart it.

https://streamable.com/65dklc

EDIT: lipgloss.JoinHorizontal also breaks for me after a while

🀝 Requirements

lrstanley commented 1 year ago

From what I've found so far, I'm unable to replicate using powershell (windows) or WSL2 with Windows Terminal (linux) -- both work fine for at least a minute or two. I did a test with Alacritty (via WSL), and I do get an error:

The app opens like normal, however, after moving the mouse for a second or two, it exits with the above output.

The above may be the same issue you are receiving, but iTerm2 might not be handling the exit correctly. The above issue also doesn't look related to bubblezone, but hard to say for sure.

I also see something similar here: https://github.com/charmbracelet/bubbletea/discussions/664

I will keep poking the above error, though not 100% sure it's the same issue you're having. It's unlikely an issue with BubbleZone, however, as we don't inject any special characters that aren't already used by bubbletea/lipgloss itself.

Would you be able to provide your go.mod file? Would like to get the same versions of the dependencies to see if I didn't just pull in newer versions.

lrstanley commented 1 year ago

Confirmed my above thoughts. I removed BubbleZone completely from the example, and I am still experiencing the issue. Furthermore, Alacritty does seem to have the same issue sometimes as you are seeing in iTerm2, though it's not consistent. Sometimes it returns immediately, sometimes it will hang/freeze and do nothing, and sometimes it will work for about 5 seconds then exit.

Code that doesn't include BubbleZone, but sees what I think is the same issue:

package main

import (
    "fmt"
    "os"
    "strings"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
    "github.com/muesli/reflow/wordwrap"
)

type TestModel struct {
    size         tea.WindowSizeMsg
    selectedCard int
    numberCards  int

    mouseEvents int
}

func (m TestModel) Init() tea.Cmd {
    return nil
}

func (m TestModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        if msg.String() == "ctrl+c" {
            return m, tea.Quit
        }
    case tea.WindowSizeMsg:
        m.size = msg
    case tea.MouseMsg:
        m.mouseEvents++
    }

    return m, nil
}

func (m TestModel) View() string {
    cardStyle := lipgloss.NewStyle().Width(30).Padding(1, 2).Margin(0, 2)

    var cardBoxes []string
    for i := 0; i < m.numberCards; i++ {
        selected := i == m.selectedCard

        style := cardStyle.
            Border(lipgloss.NormalBorder(), selected, false, false, false).
            BorderBackground(lipgloss.Color("#cccccc")).
            Background(lipgloss.Color("#cccccc")).
            BorderForeground(lipgloss.Color("#ffffff")).
            Foreground(lipgloss.Color("#ffffff"))

        // If the card is selected we give it a bit more height
        if selected {
            cardBoxes = append(cardBoxes,
                style.
                    Height(min(m.size.Height-1, m.size.Height/2+5)).
                    Render(wordwrap.String(fmt.Sprintf(
                        "%s\n\nmouse events: %d\n%s\n\n%s",
                        strings.Repeat("β€’", 3),
                        m.mouseEvents,
                        "Example Card",
                        "Hello World... Hello World... Hello World..",
                    ), 19)),
            )
            continue
        }

        // Non-selected card style
        cardBoxes = append(cardBoxes,
            style.
                Height(m.size.Height/2).
                Render(wordwrap.String(fmt.Sprintf(
                    "%s\n\n%s\n\n%s",
                    strings.Repeat("β€’", 3),
                    "Example Card",
                    "Hello World... Hello World... Hello World..",
                ), 20)),
        )
    }

    return lipgloss.Place(m.size.Width, m.size.Height, lipgloss.Center, lipgloss.Bottom, lipgloss.JoinHorizontal(lipgloss.Bottom, cardBoxes...))
}

func main() {
    p := tea.NewProgram(TestModel{
        numberCards: 3,
    }, tea.WithAltScreen(), tea.WithMouseAllMotion())
    if _, err := p.Run(); err != nil {
        fmt.Printf("Alas, there's been an error: %v", err)
        os.Exit(1)
    }
}

func min(x, y int) int {
    if x < y {
        return x
    }
    return y
}

Above code should include the number of mouse events as a counter on one of the cards.

I can still test the same versions of the dependencies if given the go.mod, however.

BigJk commented 1 year ago

Thanks for the quick response and investigation!

From your findings I tried to rule out Bubblezone and you are probably right. It doesn't seem to be related to Bubblezone but to the usage of tea.WithMouseAllMotion(). With it enabled iterm freezes if I move my mouse often enough in the window. Alternatively after minimising and re-opening the running program iterm seems to freeze up consistently. Same happens with the basic macos terminal. tea.WithMouseCellMotion() doesn't trigger that problem.

I will head over to the Bubbltea issues now: https://github.com/charmbracelet/bubbletea/issues/668