bumped bubbletea, lipgloss, bubbles, wish, and huh to v2. Mostly mechanical chur, new import paths, vewport getter/setter api, view() returns tea.view now, huh.theme became an interface so the theme package needed a small reshape. Render plumbing is gone (no more lipgloss.Render field on the Model or MakeRenderer in main.go). All pages still work over ssh. setting up for the refund composer next
234 lines
5.7 KiB
Go
234 lines
5.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/viewport"
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
|
|
"terminalShop/pkg/models"
|
|
)
|
|
|
|
func (m Model) calculateShopWidths() (int, int) {
|
|
menuWidth := 14
|
|
if len(m.Coffees) == 0 {
|
|
if m.size < large {
|
|
return m.widthContent, m.widthContent
|
|
}
|
|
return menuWidth, m.widthContent - menuWidth
|
|
}
|
|
for _, c := range m.Coffees {
|
|
if w := lipgloss.Width(c.Name); w > menuWidth {
|
|
menuWidth = w
|
|
}
|
|
}
|
|
if menuWidth > 0 {
|
|
menuWidth += 4
|
|
}
|
|
if m.size < large {
|
|
menuWidth = m.widthContent
|
|
}
|
|
detailWidth := m.widthContent - menuWidth
|
|
if m.size < large {
|
|
detailWidth = m.widthContent
|
|
}
|
|
return menuWidth, detailWidth
|
|
}
|
|
|
|
func (m Model) updateShopViewports() Model {
|
|
headerHeight := lipgloss.Height(m.BuildHeader())
|
|
footerHeight := lipgloss.Height(m.BuildFooter())
|
|
availableHeight := m.heightContainer - headerHeight - footerHeight
|
|
|
|
if len(m.Coffees) == 0 {
|
|
return m
|
|
}
|
|
|
|
menuWidth, detailWidth := m.calculateShopWidths()
|
|
|
|
if !m.shop.viewportsReady {
|
|
m.shop.menuViewport = newViewport(menuWidth, availableHeight)
|
|
m.shop.menuViewport.KeyMap = viewport.KeyMap{} // menu nav is manual
|
|
m.shop.detailViewport = newViewport(detailWidth, availableHeight)
|
|
m.shop.detailViewport.KeyMap = modifiedKeyMap
|
|
m.shop.viewportsReady = true
|
|
} else {
|
|
m.shop.menuViewport.SetWidth(menuWidth)
|
|
m.shop.menuViewport.SetHeight(availableHeight)
|
|
m.shop.detailViewport.SetWidth(detailWidth)
|
|
m.shop.detailViewport.SetHeight(availableHeight)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m Model) getShopMenuContent() string {
|
|
menuWidth, _ := m.calculateShopWidths()
|
|
|
|
selectedColor := m.theme.Highlight()
|
|
if m.shop.selected >= 0 && m.shop.selected < len(m.Coffees) {
|
|
if c := m.Coffees[m.shop.selected].Color; c != "" {
|
|
selectedColor = lipgloss.Color(c)
|
|
}
|
|
}
|
|
|
|
var normal, highlighted lipgloss.Style
|
|
if m.size < large {
|
|
menuWidth = m.widthContent
|
|
normal = m.theme.TextBody().
|
|
Width(menuWidth - 1).Align(lipgloss.Center)
|
|
highlighted = m.theme.TextAccent().
|
|
Width(menuWidth - 1).Align(lipgloss.Center).
|
|
Background(selectedColor).Bold(true)
|
|
} else {
|
|
normal = m.theme.TextBody().
|
|
Width(menuWidth+2).Padding(0, 1)
|
|
highlighted = m.theme.TextAccent().
|
|
Width(menuWidth+2).Padding(0, 1).
|
|
Background(selectedColor).Bold(true)
|
|
}
|
|
|
|
var b strings.Builder
|
|
for i, c := range m.Coffees {
|
|
if i == m.shop.selected {
|
|
b.WriteString(highlighted.Render(c.Name) + "\n")
|
|
} else {
|
|
b.WriteString(normal.Render(c.Name) + "\n")
|
|
}
|
|
}
|
|
return lipgloss.NewStyle().Padding(0, 1).Render(b.String())
|
|
}
|
|
|
|
func (m Model) getShopDetailContent() string {
|
|
if len(m.Coffees) == 0 {
|
|
return ""
|
|
}
|
|
coffee := m.Coffees[m.shop.selected]
|
|
_, detailWidth := m.calculateShopWidths()
|
|
if m.size >= large {
|
|
detailWidth -= 2
|
|
}
|
|
|
|
quantity := 0
|
|
if item, exists := m.Cart[coffee.ID]; exists {
|
|
quantity = item.Quantity
|
|
}
|
|
|
|
detail := lipgloss.JoinVertical(lipgloss.Left,
|
|
m.theme.TextAccent().Bold(true).Render(coffee.Name),
|
|
m.theme.TextBody().Render(
|
|
fmt.Sprintf("%s | %doz | %s", coffee.RoastType, coffee.Ounces, coffee.BeanType),
|
|
),
|
|
"",
|
|
lipgloss.NewStyle().Foreground(lipgloss.Color(coffee.Color)).Bold(true).Render(
|
|
fmt.Sprintf("$%.2f", float64(coffee.Price)/100),
|
|
),
|
|
"",
|
|
m.theme.TextAccent().Width(detailWidth).Render(coffee.Description),
|
|
"",
|
|
fmt.Sprintf("- %d +", quantity),
|
|
)
|
|
return lipgloss.NewStyle().Width(detailWidth).Render(detail)
|
|
}
|
|
|
|
func (m Model) ShopView() string {
|
|
if !m.shop.viewportsReady {
|
|
m = m.updateShopViewports()
|
|
}
|
|
if len(m.Coffees) == 0 {
|
|
return lipgloss.Place(m.widthContent, m.heightContainer,
|
|
lipgloss.Center, lipgloss.Center, "No products available.")
|
|
}
|
|
|
|
m.shop.menuViewport.SetContent(m.getShopMenuContent())
|
|
m.shop.detailViewport.SetContent(m.getShopDetailContent())
|
|
|
|
if m.size < large {
|
|
return lipgloss.JoinVertical(lipgloss.Top,
|
|
m.shop.menuViewport.View(),
|
|
m.shop.detailViewport.View(),
|
|
)
|
|
}
|
|
return lipgloss.JoinHorizontal(lipgloss.Top,
|
|
m.shop.menuViewport.View(),
|
|
" ",
|
|
m.shop.detailViewport.View(),
|
|
)
|
|
}
|
|
|
|
func (m Model) ShopUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
if len(m.Coffees) == 0 {
|
|
return m, nil
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
coffee := m.Coffees[m.shop.selected]
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
m = m.shopMoveSelected(true)
|
|
case "down", "j":
|
|
m = m.shopMoveSelected(false)
|
|
case "+", "=":
|
|
coffeeID := coffee.ID
|
|
if item, exists := m.Cart[coffeeID]; exists {
|
|
item.Quantity++
|
|
} else {
|
|
m.Cart[coffeeID] = &models.CartItem{CoffeeID: coffeeID, Coffee: coffee, Quantity: 1}
|
|
}
|
|
m, cmd := m.syncCartItemCmd(coffeeID, m.Cart[coffeeID].Quantity)
|
|
return m, cmd
|
|
case "-", "_":
|
|
coffeeID := coffee.ID
|
|
if item, exists := m.Cart[coffeeID]; exists {
|
|
item.Quantity--
|
|
newQty := item.Quantity
|
|
if item.Quantity <= 0 {
|
|
delete(m.Cart, coffeeID)
|
|
newQty = 0
|
|
}
|
|
m, cmd := m.syncCartItemCmd(coffeeID, newQty)
|
|
return m, cmd
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass messages to the detail viewport so pgup/pgdn scroll works
|
|
m.shop.detailViewport, cmd = m.shop.detailViewport.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
|
|
if m.shop.viewportsReady {
|
|
m.shop.menuViewport.SetContent(m.getShopMenuContent())
|
|
m.shop.detailViewport.SetContent(m.getShopDetailContent())
|
|
}
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m Model) shopMoveSelected(previous bool) Model {
|
|
next := m.shop.selected
|
|
if previous {
|
|
next--
|
|
} else {
|
|
next++
|
|
}
|
|
if next < 0 {
|
|
next = 0
|
|
}
|
|
if next >= len(m.Coffees) {
|
|
next = len(m.Coffees) - 1
|
|
}
|
|
m.shop.selected = next
|
|
|
|
if m.shop.viewportsReady {
|
|
targetY := (m.shop.selected + 2) * 1
|
|
m.shop.menuViewport.SetYOffset(targetY - m.shop.menuViewport.Height()/2)
|
|
m.shop.detailViewport.GotoTop()
|
|
}
|
|
return m
|
|
}
|