bubbletea
bubbletea copied to clipboard
terminal must be dragged before viewport displays
For some unknown reasons to me, whenever i try to use the viewport, the terminal must be dragged for it to display. Could i be doing something wrong?
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.isReady {
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
m.viewport.SetContent(textStyle(m.response))
m.isReady = true
m.viewport.YPosition = headerHeight + 1
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
if useHighPerformanceRenderer {
cmds = append(cmds, viewport.Sync(m.viewport))
}
and in my view i have
case uiLoaded:
state = fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
https://user-images.githubusercontent.com/87912847/182598826-b60eab2b-0b8e-475b-9d0b-8d1620d3e978.mov
Hi! Which terminal emulator are you using?
Iterm 2 and also default mac terminal
can you share a full reproducible of the issue?
I can't reproduce with that due to missing api keys.
Could you provide an example program with just the minimum required to reproduce the issue? (i.e. dummy data, just the viewport, etc)
For a full reproduction
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/PullRequestInc/go-gpt3"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
const (
uiMainPage uiState = iota
uiIsLoading
uiLoaded
useHighPerformanceRenderer = false
)
var (
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("175")).Bold(true).Render
docStyle = lipgloss.NewStyle().Padding(3).Render
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
// border = lipgloss.NewStyle().
// BorderStyle(lipgloss.RoundedBorder()).
// BorderForeground(lipgloss.Color("228")).
// BorderTop(true).
// BorderLeft(true).
// BorderRight(true).BorderBottom(true).Render
color = termenv.EnvColorProfile().Color
MainRuler = lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), true, false).Render
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.Copy().BorderStyle(b)
}()
)
type model struct {
textInput textinput.Model
uiState uiState
response string
err error
spinner spinner.Model
isReady bool
quitting bool
viewport viewport.Model
}
type uiState int
func (m model) Init() tea.Cmd {
switch m.uiState {
case uiMainPage:
return textinput.Blink
case uiIsLoading:
return nil
case uiLoaded:
return nil
}
return nil
}
func initialModel() model {
ti := textinput.New()
ti.Placeholder = "Let me convert your English into code?"
ti.Focus()
ti.CharLimit = 156
ti.Width = 50
ti.Prompt = "🔍 "
return model{
uiState: uiMainPage,
textInput: ti,
err: nil,
}
}
const dummy = `
Today’s Menu
| Name | Price | Notes |
| --- | --- | --- |
| Tsukemono | $2 | Just an appetizer |
| Tomato Soup | $4 | Made with San Marzano tomatoes |
| Okonomiyaki | $4 | Takes a few minutes to make |
| Curry | $3 | We can add squash if you’d like |
Seasonal Dishes
| Name | Price | Notes |
| --- | --- | --- |
| Steamed bitter melon | $2 | Not so bitter |
| Takoyaki | $3 | Fun to eat |
| Winter squash | $3 | Today it's pumpkin |
Desserts
| Name | Price | Notes |
| --- | --- | --- |
| Dorayaki | $4 | Looks good on rabbits |
| Banana Split | $5 | A classic |
| Cream Puff | $3 | Pretty creamy! |
All our dishes are made in-house by Karen, our chef. Most of our ingredients
are from our garden or the fish market down the street.
Some famous people that have eaten here lately:
* [x] René Redzepi
* [x] David Chang
* [ ] Jiro Ono (maybe some day)
Bon appétit!
`
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.uiState {
case uiMainPage:
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
return m, tea.Quit
case "enter":
m.response = getCommand(m.textInput.Value())
m.uiState = uiLoaded
return m, cmd
}
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
case uiLoaded:
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.isReady {
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
m.viewport.SetContent(textStyle(dummy))
m.isReady = true
m.viewport.YPosition = headerHeight + 1
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
if useHighPerformanceRenderer {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case tea.KeyMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "ctrl+n":
m.isReady = false
m.response = ""
m.uiState = uiMainPage
m.textInput.Focus()
m.textInput.SetValue("")
case "esc":
m.isReady = false
m.response = ""
m.uiState = uiMainPage
m.textInput.Blink()
m.textInput.SetValue(m.textInput.Value())
}
}
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
return m, nil
}
func (m model) helpView() string {
return helpStyle("\n ↑/↓: Navigate • q: Quit\n")
}
func (m model) headerView() string {
title := titleStyle.Render(m.textInput.Value())
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func (m model) View() string {
var state string
switch m.uiState {
case uiMainPage:
state =
docStyle(fmt.Sprintf(
textStyle("Commander")+"\n\n%s\n\n\n%s",
m.textInput.View(),
helpStyle("enter: confirm exit • esc: exit\n"),
) + "\n")
case uiIsLoading:
state = fmt.Sprintf("\n %s%s%s\n\n", m.spinner.View(), " ", textStyle("Thinking..."))
case uiLoaded:
state = fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
return state
}
func main() {
p := tea.NewProgram(initialModel(), tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil {
log.Fatal(err)
}
}
https://user-images.githubusercontent.com/87912847/182616326-07ecea73-f045-431e-bbff-0289e24d244d.mov
that example does not run, and still has a whole lot more than the minimum needed...
my guess would be:
- as per docs, the window size msg is fired once when the program starts, and then on resizes
- your code is ignoring it unless
uiLoaded
is true - probably when the program starts,
uiLoaded
is false, so you never handle that first message
You'd probably need to rework your update to handle that msg regardless of uiLoaded
.
I absolutely understand what you mean and your guess is perhaps correct. I would like to know your thought on ways to resolve this and maybe steps to rework the update function.
PS this shouldn't be an issue anymore, i think its more suitable as a discussion
you could probably always handle that event, and show the viewport or not in the View
function...