fix(tui): layout issues

This commit is contained in:
adamdottv
2025-06-30 14:04:48 -05:00
parent dd5736fe5f
commit 5a107b275c
5 changed files with 92 additions and 138 deletions

View File

@@ -10,7 +10,6 @@ import (
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
@@ -39,7 +38,6 @@ type messagesComponent struct {
viewport viewport.Model viewport viewport.Model
spinner spinner.Model spinner spinner.Model
attachments viewport.Model attachments viewport.Model
commands commands.CommandsComponent
cache *MessageCache cache *MessageCache
rendering bool rendering bool
showToolDetails bool showToolDetails bool
@@ -49,7 +47,7 @@ type renderFinishedMsg struct{}
type ToggleToolDetailsMsg struct{} type ToggleToolDetailsMsg struct{}
func (m *messagesComponent) Init() tea.Cmd { func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init()) return tea.Batch(m.viewport.Init(), m.spinner.Tick)
} }
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -100,10 +98,6 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spinner = spinner m.spinner = spinner
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
updated, cmd := m.commands.Update(msg)
m.commands = updated.(commands.CommandsComponent)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
@@ -271,8 +265,8 @@ func (m *messagesComponent) renderView() {
)) ))
} }
m.viewport.SetHeight(m.height - lipgloss.Height(m.header())) m.viewport.SetHeight(m.height - lipgloss.Height(m.header()) + 1)
m.viewport.SetContent("\n" + strings.Join(centered, "\n") + "\n") m.viewport.SetContent("\n" + strings.Join(centered, "\n"))
} }
func (m *messagesComponent) header() string { func (m *messagesComponent) header() string {
@@ -309,9 +303,6 @@ func (m *messagesComponent) header() string {
} }
func (m *messagesComponent) View() string { func (m *messagesComponent) View() string {
if len(m.app.Messages) == 0 {
return m.home()
}
t := theme.CurrentTheme() t := theme.CurrentTheme()
if m.rendering { if m.rendering {
return lipgloss.Place( return lipgloss.Place(
@@ -334,70 +325,6 @@ func (m *messagesComponent) View() string {
Render(header + "\n" + m.viewport.View()) Render(header + "\n" + m.viewport.View())
} }
func (m *messagesComponent) home() string {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.Background())
base := baseStyle.Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
open := `
█▀▀█ █▀▀█ █▀▀ █▀▀▄
█░░█ █░░█ █▀▀ █░░█
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `
code := `
█▀▀ █▀▀█ █▀▀▄ █▀▀
█░░ █░░█ █░░█ █▀▀
▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`
logo := lipgloss.JoinHorizontal(
lipgloss.Top,
muted(open),
base(code),
)
// cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config
versionStyle := styles.NewStyle().
Foreground(t.TextMuted()).
Background(t.Background()).
Width(lipgloss.Width(logo)).
Align(lipgloss.Right)
version := versionStyle.Render(m.app.Version)
logoAndVersion := strings.Join([]string{logo, version}, "\n")
logoAndVersion = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
logoAndVersion,
styles.WhitespaceStyle(t.Background()),
)
m.commands.SetBackgroundColor(t.Background())
commands := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.commands.View(),
styles.WhitespaceStyle(t.Background()),
)
lines := []string{}
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config))
// lines = append(lines, "")
lines = append(lines, commands)
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
baseStyle.Render(strings.Join(lines, "\n")),
styles.WhitespaceStyle(t.Background()),
)
}
func (m *messagesComponent) SetSize(width, height int) tea.Cmd { func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
if m.width == width && m.height == height { if m.width == width && m.height == height {
return nil return nil
@@ -412,7 +339,6 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
m.viewport.SetHeight(height - lipgloss.Height(m.header())) m.viewport.SetHeight(height - lipgloss.Height(m.header()))
m.attachments.SetWidth(width + 40) m.attachments.SetWidth(width + 40)
m.attachments.SetHeight(3) m.attachments.SetHeight(3)
m.commands.SetSize(width, height)
m.renderView() m.renderView()
return nil return nil
} }
@@ -476,19 +402,11 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
attachments := viewport.New() attachments := viewport.New()
vp.KeyMap = viewport.KeyMap{} vp.KeyMap = viewport.KeyMap{}
t := theme.CurrentTheme()
commandsView := commands.New(
app,
commands.WithBackground(t.Background()),
commands.WithLimit(6),
)
return &messagesComponent{ return &messagesComponent{
app: app, app: app,
viewport: vp, viewport: vp,
spinner: s, spinner: s,
attachments: attachments, attachments: attachments,
commands: commandsView,
showToolDetails: true, showToolDetails: true,
cache: NewMessageCache(), cache: NewMessageCache(),
tail: true, tail: true,

View File

@@ -14,7 +14,6 @@ import (
) )
type CommandsComponent interface { type CommandsComponent interface {
tea.Model
tea.ViewModel tea.ViewModel
SetSize(width, height int) tea.Cmd SetSize(width, height int) tea.Cmd
SetBackgroundColor(color compat.AdaptiveColor) SetBackgroundColor(color compat.AdaptiveColor)
@@ -43,19 +42,6 @@ func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
c.background = &color c.background = &color
} }
func (c *commandsComponent) Init() tea.Cmd {
return nil
}
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
c.width = msg.Width
c.height = msg.Height
}
return c, nil
}
func (c *commandsComponent) View() string { func (c *commandsComponent) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()

View File

@@ -20,10 +20,7 @@ type helpDialog struct {
} }
func (h *helpDialog) Init() tea.Cmd { func (h *helpDialog) Init() tea.Cmd {
return tea.Batch( return h.viewport.Init()
h.commandsComponent.Init(),
h.viewport.Init(),
)
} }
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -38,10 +35,6 @@ func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
h.commandsComponent.SetSize(msg.Width-4, msg.Height-6) h.commandsComponent.SetSize(msg.Width-4, msg.Height-6)
} }
// Update commands component first to get the latest content
_, cmdCmd := h.commandsComponent.Update(msg)
cmds = append(cmds, cmdCmd)
// Update viewport content // Update viewport content
h.viewport.SetContent(h.commandsComponent.View()) h.viewport.SetContent(h.commandsComponent.View())

View File

@@ -73,10 +73,7 @@ func Render(opts FlexOptions, items ...FlexItem) string {
} }
// Calculate available space for grow items // Calculate available space for grow items
availableSpace := mainAxisSize - totalFixedSize availableSpace := max(mainAxisSize-totalFixedSize, 0)
if availableSpace < 0 {
availableSpace = 0
}
// Calculate size for each grow item // Calculate size for each grow item
growItemSize := 0 growItemSize := 0
@@ -164,10 +161,7 @@ func Render(opts FlexOptions, items ...FlexItem) string {
} }
// Apply justification // Apply justification
remainingSpace := mainAxisSize - totalActualSize remainingSpace := max(mainAxisSize-totalActualSize, 0)
if remainingSpace < 0 {
remainingSpace = 0
}
// Calculate spacing based on justification // Calculate spacing based on justification
var spaceBefore, spaceBetween, spaceAfter int var spaceBefore, spaceBetween, spaceAfter int

View File

@@ -17,6 +17,7 @@ import (
"github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/completions" "github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/chat" "github.com/sst/opencode/internal/components/chat"
cmdcomp "github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/components/status" "github.com/sst/opencode/internal/components/status"
@@ -425,13 +426,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (a appModel) View() string { func (a appModel) View() string {
messagesView := a.messages.View()
editorView := a.editor.View() editorView := a.editor.View()
lines := a.editor.Lines()
editorHeight := lipgloss.Height(editorView) messagesView := a.messages.View()
if editorHeight < 5 { if a.app.Session.ID == "" {
editorHeight = 5 messagesView = a.home()
} }
editorHeight := max(lines, 5)
t := theme.CurrentTheme() t := theme.CurrentTheme()
centeredEditorView := lipgloss.PlaceHorizontal( centeredEditorView := lipgloss.PlaceHorizontal(
@@ -445,7 +446,7 @@ func (a appModel) View() string {
layout.FlexOptions{ layout.FlexOptions{
Direction: layout.Column, Direction: layout.Column,
Width: a.width, Width: a.width,
Height: a.height - 1, // Leave room for status bar Height: a.height,
}, },
layout.FlexItem{ layout.FlexItem{
View: messagesView, View: messagesView,
@@ -453,15 +454,18 @@ func (a appModel) View() string {
}, },
layout.FlexItem{ layout.FlexItem{
View: centeredEditorView, View: centeredEditorView,
FixedSize: editorHeight, FixedSize: 5,
}, },
// layout.FlexItem{
// View: a.status.View(),
// FixedSize: 1,
// },
) )
if a.editor.Lines() > 1 { if lines > 1 {
editorWidth := min(a.width, 80) editorWidth := min(a.width, 80)
editorX := (a.width - editorWidth) / 2 editorX := (a.width - editorWidth) / 2
editorY := a.height - editorHeight - 1 // Position from bottom, accounting for status bar editorY := a.height - editorHeight
mainLayout = layout.PlaceOverlay( mainLayout = layout.PlaceOverlay(
editorX, editorX,
editorY, editorY,
@@ -476,7 +480,7 @@ func (a appModel) View() string {
a.completions.SetWidth(editorWidth) a.completions.SetWidth(editorWidth)
overlay := a.completions.View() overlay := a.completions.View()
overlayHeight := lipgloss.Height(overlay) overlayHeight := lipgloss.Height(overlay)
editorY := a.height - editorHeight - 1 editorY := a.height - editorHeight + 1
mainLayout = layout.PlaceOverlay( mainLayout = layout.PlaceOverlay(
editorX, editorX,
@@ -486,23 +490,82 @@ func (a appModel) View() string {
) )
} }
components := []string{
mainLayout,
a.status.View(),
}
appView := strings.Join(components, "\n")
if a.modal != nil { if a.modal != nil {
appView = a.modal.Render(appView) mainLayout = a.modal.Render(mainLayout)
} }
mainLayout = a.toastManager.RenderOverlay(mainLayout)
appView = a.toastManager.RenderOverlay(appView)
if theme.CurrentThemeUsesAnsiColors() { if theme.CurrentThemeUsesAnsiColors() {
appView = util.ConvertRGBToAnsi16Colors(appView) mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
}
return mainLayout + "\n" + a.status.View()
} }
return appView func (a appModel) home() string {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.Background())
base := baseStyle.Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
open := `
█▀▀█ █▀▀█ █▀▀ █▀▀▄
█░░█ █░░█ █▀▀ █░░█
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `
code := `
█▀▀ █▀▀█ █▀▀▄ █▀▀
█░░ █░░█ █░░█ █▀▀
▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`
logo := lipgloss.JoinHorizontal(
lipgloss.Top,
muted(open),
base(code),
)
// cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config
versionStyle := styles.NewStyle().
Foreground(t.TextMuted()).
Background(t.Background()).
Width(lipgloss.Width(logo)).
Align(lipgloss.Right)
version := versionStyle.Render(a.app.Version)
logoAndVersion := strings.Join([]string{logo, version}, "\n")
logoAndVersion = lipgloss.PlaceHorizontal(
a.width,
lipgloss.Center,
logoAndVersion,
styles.WhitespaceStyle(t.Background()),
)
commandsView := cmdcomp.New(
a.app,
cmdcomp.WithBackground(t.Background()),
cmdcomp.WithLimit(6),
)
cmds := lipgloss.PlaceHorizontal(
a.width,
lipgloss.Center,
commandsView.View(),
styles.WhitespaceStyle(t.Background()),
)
lines := []string{}
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config))
// lines = append(lines, "")
lines = append(lines, cmds)
return lipgloss.Place(
a.width,
a.height-5,
lipgloss.Center,
lipgloss.Center,
baseStyle.Render(strings.Join(lines, "\n")),
styles.WhitespaceStyle(t.Background()),
)
} }
func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {