bubbletea icon indicating copy to clipboard operation
bubbletea copied to clipboard

terminal must be dragged before viewport displays

Open ahmedsaheed opened this issue 2 years ago • 9 comments

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

ahmedsaheed avatar Aug 03 '22 11:08 ahmedsaheed

Hi! Which terminal emulator are you using?

caarlos0 avatar Aug 03 '22 12:08 caarlos0

Iterm 2 and also default mac terminal

ahmedsaheed avatar Aug 03 '22 12:08 ahmedsaheed

can you share a full reproducible of the issue?

caarlos0 avatar Aug 03 '22 12:08 caarlos0

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)

caarlos0 avatar Aug 03 '22 12:08 caarlos0

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

ahmedsaheed avatar Aug 03 '22 13:08 ahmedsaheed

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.

caarlos0 avatar Aug 03 '22 14:08 caarlos0

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

ahmedsaheed avatar Aug 03 '22 16:08 ahmedsaheed

you could probably always handle that event, and show the viewport or not in the View function...

caarlos0 avatar Aug 03 '22 17:08 caarlos0