chore: rework layout primitives

This commit is contained in:
adamdottv 2025-06-28 06:04:01 -05:00
parent d090c08ef0
commit 9f3ba03965
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
7 changed files with 298 additions and 589 deletions

View file

@ -22,7 +22,7 @@ import (
type EditorComponent interface {
tea.Model
tea.ViewModel
layout.Sizeable
SetSize(width, height int) tea.Cmd
Content() string
Lines() int
Value() string
@ -158,7 +158,15 @@ func (m *editorComponent) Content() string {
func (m *editorComponent) View() string {
if m.Lines() > 1 {
return ""
t := theme.CurrentTheme()
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
"",
styles.WhitespaceStyle(t.Background()),
)
}
return m.Content()
}

View file

@ -21,6 +21,7 @@ import (
type MessagesComponent interface {
tea.Model
tea.ViewModel
SetSize(width, height int) tea.Cmd
PageUp() (tea.Model, tea.Cmd)
PageDown() (tea.Model, tea.Cmd)
HalfPageUp() (tea.Model, tea.Cmd)
@ -311,6 +312,7 @@ func (m *messagesComponent) View() string {
if len(m.app.Messages) == 0 {
return m.home()
}
t := theme.CurrentTheme()
if m.rendering {
return lipgloss.Place(
m.width,
@ -318,19 +320,18 @@ func (m *messagesComponent) View() string {
lipgloss.Center,
lipgloss.Center,
"Loading session...",
styles.WhitespaceStyle(t.Background()),
)
}
t := theme.CurrentTheme()
return lipgloss.JoinVertical(
lipgloss.Left,
lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.header(),
styles.WhitespaceStyle(t.Background()),
),
m.viewport.View(),
header := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.header(),
styles.WhitespaceStyle(t.Background()),
)
return styles.NewStyle().
Background(t.Background()).
Render(header + "\n" + m.viewport.View())
}
func (m *messagesComponent) home() string {

View file

@ -9,7 +9,6 @@ import (
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@ -17,7 +16,7 @@ import (
type CommandsComponent interface {
tea.Model
tea.ViewModel
layout.Sizeable
SetSize(width, height int) tea.Cmd
SetBackgroundColor(color compat.AdaptiveColor)
}

View file

@ -1,292 +0,0 @@
package layout
import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type Container interface {
tea.Model
tea.ViewModel
Sizeable
Focusable
Alignable
}
type container struct {
width int
height int
x int
y int
content tea.ViewModel
paddingTop int
paddingRight int
paddingBottom int
paddingLeft int
borderTop bool
borderRight bool
borderBottom bool
borderLeft bool
borderStyle lipgloss.Border
maxWidth int
align lipgloss.Position
focused bool
}
func (c *container) Init() tea.Cmd {
if model, ok := c.content.(tea.Model); ok {
return model.Init()
}
return nil
}
func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if model, ok := c.content.(tea.Model); ok {
u, cmd := model.Update(msg)
c.content = u.(tea.ViewModel)
return c, cmd
}
return c, nil
}
func (c *container) View() string {
t := theme.CurrentTheme()
style := styles.NewStyle().Background(t.Background())
width := c.width
height := c.height
// Apply max width constraint if set
if c.maxWidth > 0 && width > c.maxWidth {
width = c.maxWidth
}
// Apply border if any side is enabled
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
// Adjust width and height for borders
if c.borderTop {
height--
}
if c.borderBottom {
height--
}
if c.borderLeft {
width--
}
if c.borderRight {
width--
}
style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
// Use primary color for border if focused
if c.focused {
style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
} else {
style = style.BorderBackground(t.Background()).BorderForeground(t.Border())
}
}
style = style.
Width(width).
Height(height).
PaddingTop(c.paddingTop).
PaddingRight(c.paddingRight).
PaddingBottom(c.paddingBottom).
PaddingLeft(c.paddingLeft)
return style.Render(c.content.View())
}
func (c *container) SetSize(width, height int) tea.Cmd {
c.width = width
c.height = height
// Apply max width constraint if set
effectiveWidth := width
if c.maxWidth > 0 && width > c.maxWidth {
effectiveWidth = c.maxWidth
}
// If the content implements Sizeable, adjust its size to account for padding and borders
if sizeable, ok := c.content.(Sizeable); ok {
// Calculate horizontal space taken by padding and borders
horizontalSpace := c.paddingLeft + c.paddingRight
if c.borderLeft {
horizontalSpace++
}
if c.borderRight {
horizontalSpace++
}
// Calculate vertical space taken by padding and borders
verticalSpace := c.paddingTop + c.paddingBottom
if c.borderTop {
verticalSpace++
}
if c.borderBottom {
verticalSpace++
}
// Set content size with adjusted dimensions
contentWidth := max(0, effectiveWidth-horizontalSpace)
contentHeight := max(0, height-verticalSpace)
return sizeable.SetSize(contentWidth, contentHeight)
}
return nil
}
func (c *container) GetSize() (int, int) {
return min(c.width, c.maxWidth), c.height
}
func (c *container) MaxWidth() int {
return c.maxWidth
}
func (c *container) Alignment() lipgloss.Position {
return c.align
}
// Focus sets the container as focused
func (c *container) Focus() tea.Cmd {
c.focused = true
if focusable, ok := c.content.(Focusable); ok {
return focusable.Focus()
}
return nil
}
// Blur removes focus from the container
func (c *container) Blur() tea.Cmd {
c.focused = false
if blurable, ok := c.content.(Focusable); ok {
return blurable.Blur()
}
return nil
}
func (c *container) IsFocused() bool {
if blurable, ok := c.content.(Focusable); ok {
return blurable.IsFocused()
}
return c.focused
}
// GetPosition returns the x, y coordinates of the container
func (c *container) GetPosition() (x, y int) {
return c.x, c.y
}
func (c *container) SetPosition(x, y int) {
c.x = x
c.y = y
}
type ContainerOption func(*container)
func NewContainer(content tea.ViewModel, options ...ContainerOption) Container {
c := &container{
content: content,
borderStyle: lipgloss.NormalBorder(),
}
for _, option := range options {
option(c)
}
return c
}
// Padding options
func WithPadding(top, right, bottom, left int) ContainerOption {
return func(c *container) {
c.paddingTop = top
c.paddingRight = right
c.paddingBottom = bottom
c.paddingLeft = left
}
}
func WithPaddingAll(padding int) ContainerOption {
return WithPadding(padding, padding, padding, padding)
}
func WithPaddingHorizontal(padding int) ContainerOption {
return func(c *container) {
c.paddingLeft = padding
c.paddingRight = padding
}
}
func WithPaddingVertical(padding int) ContainerOption {
return func(c *container) {
c.paddingTop = padding
c.paddingBottom = padding
}
}
func WithBorder(top, right, bottom, left bool) ContainerOption {
return func(c *container) {
c.borderTop = top
c.borderRight = right
c.borderBottom = bottom
c.borderLeft = left
}
}
func WithBorderAll() ContainerOption {
return WithBorder(true, true, true, true)
}
func WithBorderHorizontal() ContainerOption {
return WithBorder(true, false, true, false)
}
func WithBorderVertical() ContainerOption {
return WithBorder(false, true, false, true)
}
func WithBorderStyle(style lipgloss.Border) ContainerOption {
return func(c *container) {
c.borderStyle = style
}
}
func WithRoundedBorder() ContainerOption {
return WithBorderStyle(lipgloss.RoundedBorder())
}
func WithThickBorder() ContainerOption {
return WithBorderStyle(lipgloss.ThickBorder())
}
func WithDoubleBorder() ContainerOption {
return WithBorderStyle(lipgloss.DoubleBorder())
}
func WithMaxWidth(maxWidth int) ContainerOption {
return func(c *container) {
c.maxWidth = maxWidth
}
}
func WithAlign(align lipgloss.Position) ContainerOption {
return func(c *container) {
c.align = align
}
}
func WithAlignLeft() ContainerOption {
return WithAlign(lipgloss.Left)
}
func WithAlignCenter() ContainerOption {
return WithAlign(lipgloss.Center)
}
func WithAlignRight() ContainerOption {
return WithAlign(lipgloss.Right)
}

View file

@ -1,255 +1,260 @@
package layout
import (
tea "github.com/charmbracelet/bubbletea/v2"
"strings"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type FlexDirection int
type Direction int
const (
FlexDirectionHorizontal FlexDirection = iota
FlexDirectionVertical
Row Direction = iota
Column
)
type FlexChildSize struct {
Fixed bool
Size int
type Justify int
const (
JustifyStart Justify = iota
JustifyEnd
JustifyCenter
JustifySpaceBetween
JustifySpaceAround
)
type Align int
const (
AlignStart Align = iota
AlignEnd
AlignCenter
AlignStretch // Only applicable in the cross-axis
)
type FlexOptions struct {
Direction Direction
Justify Justify
Align Align
Width int
Height int
}
var FlexChildSizeGrow = FlexChildSize{Fixed: false}
func FlexChildSizeFixed(size int) FlexChildSize {
return FlexChildSize{Fixed: true, Size: size}
type FlexItem struct {
View string
FixedSize int // Fixed size in the main axis (width for Row, height for Column)
Grow bool // If true, the item will grow to fill available space
}
type FlexLayout interface {
tea.ViewModel
Sizeable
SetChildren(panes []tea.ViewModel) tea.Cmd
SetSizes(sizes []FlexChildSize) tea.Cmd
SetDirection(direction FlexDirection) tea.Cmd
}
type flexLayout struct {
width int
height int
direction FlexDirection
children []tea.ViewModel
sizes []FlexChildSize
}
type FlexLayoutOption func(*flexLayout)
func (f *flexLayout) View() string {
if len(f.children) == 0 {
// Render lays out a series of view strings based on flexbox-like rules.
func Render(opts FlexOptions, items ...FlexItem) string {
if len(items) == 0 {
return ""
}
t := theme.CurrentTheme()
views := make([]string, 0, len(f.children))
for i, child := range f.children {
if child == nil {
continue
}
// Calculate dimensions for each item
mainAxisSize := opts.Width
crossAxisSize := opts.Height
if opts.Direction == Column {
mainAxisSize = opts.Height
crossAxisSize = opts.Width
}
alignment := lipgloss.Center
if alignable, ok := child.(Alignable); ok {
alignment = alignable.Alignment()
// Calculate total fixed size and count grow items
totalFixedSize := 0
growCount := 0
for _, item := range items {
if item.FixedSize > 0 {
totalFixedSize += item.FixedSize
} else if item.Grow {
growCount++
}
var childWidth, childHeight int
if f.direction == FlexDirectionHorizontal {
childWidth, childHeight = f.calculateChildSize(i)
view := lipgloss.PlaceHorizontal(
childWidth,
alignment,
child.View(),
// TODO: make configurable WithBackgroundStyle
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
)
views = append(views, view)
}
// Calculate available space for grow items
availableSpace := mainAxisSize - totalFixedSize
if availableSpace < 0 {
availableSpace = 0
}
// Calculate size for each grow item
growItemSize := 0
if growCount > 0 && availableSpace > 0 {
growItemSize = availableSpace / growCount
}
// Prepare sized views
sizedViews := make([]string, len(items))
actualSizes := make([]int, len(items))
for i, item := range items {
view := item.View
// Determine the size for this item
itemSize := 0
if item.FixedSize > 0 {
itemSize = item.FixedSize
} else if item.Grow && growItemSize > 0 {
itemSize = growItemSize
} else {
childWidth, childHeight = f.calculateChildSize(i)
view := lipgloss.Place(
f.width,
childHeight,
lipgloss.Center,
alignment,
child.View(),
// TODO: make configurable WithBackgroundStyle
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
)
views = append(views, view)
}
}
if f.direction == FlexDirectionHorizontal {
return lipgloss.JoinHorizontal(lipgloss.Center, views...)
}
return lipgloss.JoinVertical(lipgloss.Center, views...)
}
func (f *flexLayout) calculateChildSize(index int) (width, height int) {
if index >= len(f.children) {
return 0, 0
}
totalFixed := 0
flexCount := 0
for i, child := range f.children {
if child == nil {
continue
}
if i < len(f.sizes) && f.sizes[i].Fixed {
if f.direction == FlexDirectionHorizontal {
totalFixed += f.sizes[i].Size
// No fixed size and not growing - use natural size
if opts.Direction == Row {
itemSize = lipgloss.Width(view)
} else {
totalFixed += f.sizes[i].Size
itemSize = lipgloss.Height(view)
}
}
// Apply size constraints
if opts.Direction == Row {
// For row direction, constrain width and handle height alignment
if itemSize > 0 {
view = styles.NewStyle().
Width(itemSize).
Height(crossAxisSize).
Render(view)
}
// Apply cross-axis alignment
switch opts.Align {
case AlignCenter:
view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Center, view)
case AlignEnd:
view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Bottom, view)
case AlignStart:
view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Top, view)
case AlignStretch:
// Already stretched by Height setting above
}
} else {
flexCount++
// For column direction, constrain height and handle width alignment
if itemSize > 0 {
view = styles.NewStyle().
Height(itemSize).
Width(crossAxisSize).
Render(view)
}
// Apply cross-axis alignment
switch opts.Align {
case AlignCenter:
view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Center, view)
case AlignEnd:
view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Right, view)
case AlignStart:
view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Left, view)
case AlignStretch:
// Already stretched by Width setting above
}
}
sizedViews[i] = view
if opts.Direction == Row {
actualSizes[i] = lipgloss.Width(view)
} else {
actualSizes[i] = lipgloss.Height(view)
}
}
if f.direction == FlexDirectionHorizontal {
height = f.height
if index < len(f.sizes) && f.sizes[index].Fixed {
width = f.sizes[index].Size
} else if flexCount > 0 {
remainingSpace := f.width - totalFixed
width = remainingSpace / flexCount
// Calculate total actual size
totalActualSize := 0
for _, size := range actualSizes {
totalActualSize += size
}
// Apply justification
remainingSpace := mainAxisSize - totalActualSize
if remainingSpace < 0 {
remainingSpace = 0
}
// Calculate spacing based on justification
var spaceBefore, spaceBetween, spaceAfter int
switch opts.Justify {
case JustifyStart:
spaceAfter = remainingSpace
case JustifyEnd:
spaceBefore = remainingSpace
case JustifyCenter:
spaceBefore = remainingSpace / 2
spaceAfter = remainingSpace - spaceBefore
case JustifySpaceBetween:
if len(items) > 1 {
spaceBetween = remainingSpace / (len(items) - 1)
} else {
spaceAfter = remainingSpace
}
case JustifySpaceAround:
if len(items) > 0 {
spaceAround := remainingSpace / (len(items) * 2)
spaceBefore = spaceAround
spaceAfter = spaceAround
spaceBetween = spaceAround * 2
}
}
// Build the final layout
var parts []string
// Add space before if needed
if spaceBefore > 0 {
if opts.Direction == Row {
parts = append(parts, strings.Repeat(" ", spaceBefore))
} else {
parts = append(parts, strings.Repeat("\n", spaceBefore))
}
}
// Add items with spacing
for i, view := range sizedViews {
parts = append(parts, view)
// Add space between items (not after the last one)
if i < len(sizedViews)-1 && spaceBetween > 0 {
if opts.Direction == Row {
parts = append(parts, strings.Repeat(" ", spaceBetween))
} else {
parts = append(parts, strings.Repeat("\n", spaceBetween))
}
}
}
// Add space after if needed
if spaceAfter > 0 {
if opts.Direction == Row {
parts = append(parts, strings.Repeat(" ", spaceAfter))
} else {
parts = append(parts, strings.Repeat("\n", spaceAfter))
}
}
// Join the parts
if opts.Direction == Row {
return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
} else {
width = f.width
if index < len(f.sizes) && f.sizes[index].Fixed {
height = f.sizes[index].Size
} else if flexCount > 0 {
remainingSpace := f.height - totalFixed
height = remainingSpace / flexCount
}
}
return width, height
}
func (f *flexLayout) SetSize(width, height int) tea.Cmd {
f.width = width
f.height = height
var cmds []tea.Cmd
currentX, currentY := 0, 0
for i, child := range f.children {
if child != nil {
paneWidth, paneHeight := f.calculateChildSize(i)
alignment := lipgloss.Center
if alignable, ok := child.(Alignable); ok {
alignment = alignable.Alignment()
}
// Calculate actual position based on alignment
actualX, actualY := currentX, currentY
if f.direction == FlexDirectionHorizontal {
// In horizontal layout, vertical alignment affects Y position
// (lipgloss.Center is used for vertical alignment in JoinHorizontal)
actualY = (f.height - paneHeight) / 2
} else {
// In vertical layout, horizontal alignment affects X position
contentWidth := paneWidth
if alignable, ok := child.(Alignable); ok {
if alignable.MaxWidth() > 0 && contentWidth > alignable.MaxWidth() {
contentWidth = alignable.MaxWidth()
}
}
switch alignment {
case lipgloss.Center:
actualX = (f.width - contentWidth) / 2
case lipgloss.Right:
actualX = f.width - contentWidth
case lipgloss.Left:
actualX = 0
}
}
// Set position if the pane is Alignable
if c, ok := child.(Alignable); ok {
c.SetPosition(actualX, actualY)
}
if sizeable, ok := child.(Sizeable); ok {
cmd := sizeable.SetSize(paneWidth, paneHeight)
cmds = append(cmds, cmd)
}
// Update position for next pane
if f.direction == FlexDirectionHorizontal {
currentX += paneWidth
} else {
currentY += paneHeight
}
}
}
return tea.Batch(cmds...)
}
func (f *flexLayout) GetSize() (int, int) {
return f.width, f.height
}
func (f *flexLayout) SetChildren(children []tea.ViewModel) tea.Cmd {
f.children = children
if f.width > 0 && f.height > 0 {
return f.SetSize(f.width, f.height)
}
return nil
}
func (f *flexLayout) SetSizes(sizes []FlexChildSize) tea.Cmd {
f.sizes = sizes
if f.width > 0 && f.height > 0 {
return f.SetSize(f.width, f.height)
}
return nil
}
func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
f.direction = direction
if f.width > 0 && f.height > 0 {
return f.SetSize(f.width, f.height)
}
return nil
}
func NewFlexLayout(children []tea.ViewModel, options ...FlexLayoutOption) FlexLayout {
layout := &flexLayout{
children: children,
direction: FlexDirectionHorizontal,
sizes: []FlexChildSize{},
}
for _, option := range options {
option(layout)
}
return layout
}
func WithDirection(direction FlexDirection) FlexLayoutOption {
return func(f *flexLayout) {
f.direction = direction
return lipgloss.JoinVertical(lipgloss.Left, parts...)
}
}
func WithChildren(children ...tea.ViewModel) FlexLayoutOption {
return func(f *flexLayout) {
f.children = children
}
// Helper function to create a simple vertical layout
func Vertical(width, height int, items ...FlexItem) string {
return Render(FlexOptions{
Direction: Column,
Width: width,
Height: height,
Justify: JustifyStart,
Align: AlignStretch,
}, items...)
}
func WithSizes(sizes ...FlexChildSize) FlexLayoutOption {
return func(f *flexLayout) {
f.sizes = sizes
}
// Helper function to create a simple horizontal layout
func Horizontal(width, height int, items ...FlexItem) string {
return Render(FlexOptions{
Direction: Row,
Width: width,
Height: height,
Justify: JustifyStart,
Align: AlignStretch,
}, items...)
}

View file

@ -1,11 +1,7 @@
package layout
import (
"reflect"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
)
var Current *LayoutInfo
@ -34,33 +30,3 @@ type Modal interface {
Render(background string) string
Close() tea.Cmd
}
type Focusable interface {
Focus() tea.Cmd
Blur() tea.Cmd
IsFocused() bool
}
type Sizeable interface {
SetSize(width, height int) tea.Cmd
GetSize() (int, int)
}
type Alignable interface {
MaxWidth() int
Alignment() lipgloss.Position
SetPosition(x, y int)
GetPosition() (x, y int)
}
func KeyMapToSlice(t any) (bindings []key.Binding) {
typ := reflect.TypeOf(t)
if typ.Kind() != reflect.Struct {
return nil
}
for i := range typ.NumField() {
v := reflect.ValueOf(t).Field(i)
bindings = append(bindings, v.Interface().(key.Binding))
}
return
}

View file

@ -47,8 +47,6 @@ type appModel struct {
status status.StatusComponent
editor chat.EditorComponent
messages chat.MessagesComponent
editorContainer layout.Container
layout layout.FlexLayout
completions dialog.CompletionDialog
completionManager *completions.CompletionManager
showCompletionDialog bool
@ -360,7 +358,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Width: min(a.width, 80),
},
}
a.layout.SetSize(a.width, a.height)
// Update child component sizes
messagesHeight := a.height - 6 // Leave room for editor and status bar
a.messages.SetSize(a.width, messagesHeight)
a.editor.SetSize(min(a.width, 80), 5)
case app.SessionSelectedMsg:
messages, err := a.app.ListMessages(context.Background(), msg.ID)
if err != nil {
@ -424,33 +425,69 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (a appModel) View() string {
layoutView := a.layout.View()
editorWidth, _ := a.editorContainer.GetSize()
editorX, editorY := a.editorContainer.GetPosition()
messagesView := a.messages.View()
editorView := a.editor.View()
editorHeight := lipgloss.Height(editorView)
if editorHeight < 5 {
editorHeight = 5
}
t := theme.CurrentTheme()
centeredEditorView := lipgloss.PlaceHorizontal(
a.width,
lipgloss.Center,
editorView,
styles.WhitespaceStyle(t.Background()),
)
mainLayout := layout.Render(
layout.FlexOptions{
Direction: layout.Column,
Width: a.width,
Height: a.height - 1, // Leave room for status bar
},
layout.FlexItem{
View: messagesView,
Grow: true,
},
layout.FlexItem{
View: centeredEditorView,
FixedSize: editorHeight,
},
)
if a.editor.Lines() > 1 {
editorY = editorY - a.editor.Lines() + 1
layoutView = layout.PlaceOverlay(
editorWidth := min(a.width, 80)
editorX := (a.width - editorWidth) / 2
editorY := a.height - editorHeight - 1 // Position from bottom, accounting for status bar
mainLayout = layout.PlaceOverlay(
editorX,
editorY,
a.editor.Content(),
layoutView,
mainLayout,
)
}
if a.showCompletionDialog {
editorWidth := min(a.width, 80)
editorX := (a.width - editorWidth) / 2
a.completions.SetWidth(editorWidth)
overlay := a.completions.View()
layoutView = layout.PlaceOverlay(
overlayHeight := lipgloss.Height(overlay)
editorY := a.height - editorHeight - 1
mainLayout = layout.PlaceOverlay(
editorX,
editorY-lipgloss.Height(overlay)+2,
editorY-overlayHeight,
overlay,
layoutView,
mainLayout,
)
}
components := []string{
layoutView,
mainLayout,
a.status.View(),
}
appView := strings.Join(components, "\n")
@ -464,6 +501,7 @@ func (a appModel) View() string {
if theme.CurrentThemeUsesAnsiColors() {
appView = util.ConvertRGBToAnsi16Colors(appView)
}
return appView
}
@ -653,13 +691,6 @@ func NewModel(app *app.App) tea.Model {
editor := chat.NewEditorComponent(app)
completions := dialog.NewCompletionDialogComponent(initialProvider)
editorContainer := layout.NewContainer(
editor,
layout.WithMaxWidth(layout.Current.Container.Width),
layout.WithAlignCenter(),
)
messagesContainer := layout.NewContainer(messages)
var leaderBinding *key.Binding
if app.Config.Keybinds.Leader != "" {
binding := key.NewBinding(key.WithKeys(app.Config.Keybinds.Leader))
@ -676,17 +707,8 @@ func NewModel(app *app.App) tea.Model {
leaderBinding: leaderBinding,
isLeaderSequence: false,
showCompletionDialog: false,
editorContainer: editorContainer,
toastManager: toast.NewToastManager(),
interruptKeyState: InterruptKeyIdle,
layout: layout.NewFlexLayout(
[]tea.ViewModel{messagesContainer, editorContainer},
layout.WithDirection(layout.FlexDirectionVertical),
layout.WithSizes(
layout.FlexChildSizeGrow,
layout.FlexChildSizeFixed(5),
),
),
}
return model