Clean up tui View handling
This commit is contained in:
parent
c1792f27ff
commit
ed784bb1cf
@ -12,17 +12,14 @@ type Values struct {
|
||||
type State struct {
|
||||
Ctx *lmcli.Context
|
||||
Values *Values
|
||||
Views *Views
|
||||
Width int
|
||||
Height int
|
||||
Err error
|
||||
}
|
||||
|
||||
// this struct holds the final rendered content of various UI components, and
|
||||
// gets populated in the application's Update() method. View() simply composes
|
||||
// these elements into the final output
|
||||
// TODO: consider removing this, let each view be responsible
|
||||
type Views struct {
|
||||
// a convenience struct for holding rendered content for indiviudal UI
|
||||
// elements
|
||||
type Sections struct {
|
||||
Header string
|
||||
Content string
|
||||
Error string
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/views/chat"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/views/conversations"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Application model
|
||||
@ -31,7 +30,6 @@ func initialModel(ctx *lmcli.Context, values shared.Values) Model {
|
||||
State: shared.State{
|
||||
Ctx: ctx,
|
||||
Values: &values,
|
||||
Views: &shared.Views{},
|
||||
},
|
||||
}
|
||||
|
||||
@ -126,36 +124,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.State.Width == 0 {
|
||||
// this is the case upon initial startup, but it's also a safe bet that
|
||||
// we can just skip rendering if the terminal is really 0 width...
|
||||
// without this, the m.*View() functions may crash
|
||||
return ""
|
||||
}
|
||||
sections := make([]string, 0, 6)
|
||||
|
||||
if m.State.Views.Header != "" {
|
||||
sections = append(sections, m.State.Views.Header)
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case shared.StateConversations:
|
||||
sections = append(sections, m.State.Views.Content)
|
||||
if m.State.Views.Error != "" {
|
||||
sections = append(sections, m.State.Views.Error)
|
||||
}
|
||||
return m.conversations.View()
|
||||
case shared.StateChat:
|
||||
sections = append(sections, m.State.Views.Content)
|
||||
if m.State.Views.Error != "" {
|
||||
sections = append(sections, m.State.Views.Error)
|
||||
}
|
||||
sections = append(sections, m.State.Views.Input)
|
||||
return m.chat.View()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
if m.State.Views.Footer != "" {
|
||||
sections = append(sections, m.State.Views.Footer)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
|
||||
func Launch(ctx *lmcli.Context, convShortname string) error {
|
||||
|
@ -57,8 +57,7 @@ type (
|
||||
|
||||
type Model struct {
|
||||
shared.State
|
||||
width int
|
||||
height int
|
||||
shared.Sections
|
||||
|
||||
// app state
|
||||
conversation *models.Conversation
|
||||
@ -72,10 +71,6 @@ type Model struct {
|
||||
replyChunkChan chan string
|
||||
persistence bool // whether we will save new messages in the conversation
|
||||
|
||||
tokenCount uint
|
||||
startTime time.Time
|
||||
elapsed time.Duration
|
||||
|
||||
// ui state
|
||||
focus focusState
|
||||
wrap bool // whether message content is wrapped to viewport width
|
||||
@ -84,6 +79,10 @@ type Model struct {
|
||||
messageCache []string // cache of syntax highlighted and wrapped message content
|
||||
messageOffsets []int
|
||||
|
||||
tokenCount uint
|
||||
startTime time.Time
|
||||
elapsed time.Duration
|
||||
|
||||
// ui elements
|
||||
content viewport.Model
|
||||
input textarea.Model
|
||||
@ -228,7 +227,7 @@ func (m Model) Init() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *Model) HandleResize(width, height int) {
|
||||
m.width, m.height = width, height
|
||||
m.Width, m.Height = width, height
|
||||
m.content.Width = width
|
||||
m.input.SetWidth(width - m.input.FocusedStyle.Base.GetHorizontalFrameSize())
|
||||
if len(m.messages) > 0 {
|
||||
@ -390,21 +389,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// update views once window dimensions are known
|
||||
if m.width > 0 {
|
||||
m.State.Views.Header = m.headerView()
|
||||
m.State.Views.Footer = m.footerView()
|
||||
m.State.Views.Error = tuiutil.ErrorBanner(m.State.Err, m.width)
|
||||
fixedHeight := tuiutil.Height(m.State.Views.Header) + tuiutil.Height(m.State.Views.Error) + tuiutil.Height(m.State.Views.Footer)
|
||||
if m.Width > 0 {
|
||||
m.Header = m.headerView()
|
||||
m.Footer = m.footerView()
|
||||
m.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
||||
fixedHeight := tuiutil.Height(m.Header) + tuiutil.Height(m.Error) + tuiutil.Height(m.Footer)
|
||||
|
||||
// calculate clamped input height to accomodate input text
|
||||
// minimum 4 lines, maximum half of content area
|
||||
newHeight := max(4, min((m.height-fixedHeight-1)/2, m.input.LineCount()))
|
||||
newHeight := max(4, min((m.Height-fixedHeight-1)/2, m.input.LineCount()))
|
||||
m.input.SetHeight(newHeight)
|
||||
m.State.Views.Input = m.input.View()
|
||||
m.Input = m.input.View()
|
||||
|
||||
// remaining height towards content
|
||||
m.content.Height = m.height - fixedHeight - tuiutil.Height(m.State.Views.Input)
|
||||
m.State.Views.Content = m.content.View()
|
||||
m.content.Height = m.Height - fixedHeight - tuiutil.Height(m.Input)
|
||||
m.Content = m.content.View()
|
||||
}
|
||||
|
||||
// this is a pretty nasty hack to ensure the input area viewport doesn't
|
||||
@ -630,6 +629,29 @@ func (m *Model) HandleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.Width == 0 {
|
||||
return ""
|
||||
}
|
||||
sections := make([]string, 0, 6)
|
||||
|
||||
if m.Header != "" {
|
||||
sections = append(sections, m.Header)
|
||||
}
|
||||
|
||||
sections = append(sections, m.Content)
|
||||
if m.Error != "" {
|
||||
sections = append(sections, m.Error)
|
||||
}
|
||||
sections = append(sections, m.Input)
|
||||
|
||||
if m.Footer != "" {
|
||||
sections = append(sections, m.Footer)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
|
||||
func (m *Model) renderMessageHeading(i int, message *models.Message) string {
|
||||
icon := ""
|
||||
friendly := message.Role.FriendlyRole()
|
||||
@ -828,9 +850,9 @@ func (m *Model) headerView() string {
|
||||
} else {
|
||||
title = "Untitled"
|
||||
}
|
||||
title = tuiutil.TruncateToCellWidth(title, m.width-styles.Header.GetHorizontalPadding(), "...")
|
||||
title = tuiutil.TruncateToCellWidth(title, m.Width-styles.Header.GetHorizontalPadding(), "...")
|
||||
header := titleStyle.Render(title)
|
||||
return styles.Header.Width(m.width).Render(header)
|
||||
return styles.Header.Width(m.Width).Render(header)
|
||||
}
|
||||
|
||||
func (m *Model) footerView() string {
|
||||
@ -868,7 +890,7 @@ func (m *Model) footerView() string {
|
||||
right := strings.Join(rightSegments, segmentSeparator)
|
||||
|
||||
totalWidth := lipgloss.Width(left) + lipgloss.Width(right)
|
||||
remaining := m.width - totalWidth
|
||||
remaining := m.Width - totalWidth
|
||||
|
||||
var padding string
|
||||
if remaining > 0 {
|
||||
@ -877,9 +899,9 @@ func (m *Model) footerView() string {
|
||||
|
||||
footer := left + padding + right
|
||||
if remaining < 0 {
|
||||
footer = tuiutil.TruncateToCellWidth(footer, m.width, "...")
|
||||
footer = tuiutil.TruncateToCellWidth(footer, m.Width, "...")
|
||||
}
|
||||
return footerStyle.Width(m.width).Render(footer)
|
||||
return footerStyle.Width(m.Width).Render(footer)
|
||||
}
|
||||
|
||||
func (m *Model) setMessage(i int, msg models.Message) {
|
||||
|
@ -29,6 +29,7 @@ type (
|
||||
|
||||
type Model struct {
|
||||
shared.State
|
||||
shared.Sections
|
||||
|
||||
conversations []loadedConversation
|
||||
cursor int // index of the currently selected conversation
|
||||
@ -129,12 +130,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
if m.Width > 0 {
|
||||
m.Views.Header = m.headerView()
|
||||
m.Views.Footer = "" // TODO: show /something/
|
||||
m.Views.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
||||
fixedHeight := tuiutil.Height(m.Views.Header) + tuiutil.Height(m.Views.Error) + tuiutil.Height(m.Views.Footer)
|
||||
m.Header = m.headerView()
|
||||
m.Footer = "" // TODO: show /something/
|
||||
m.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
||||
fixedHeight := tuiutil.Height(m.Header) + tuiutil.Height(m.Error) + tuiutil.Height(m.Footer)
|
||||
m.content.Height = m.Height - fixedHeight
|
||||
m.Views.Content = m.content.View()
|
||||
m.Content = m.content.View()
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
@ -156,6 +157,28 @@ func (m *Model) loadConversations() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.Width == 0 {
|
||||
return ""
|
||||
}
|
||||
sections := make([]string, 0, 6)
|
||||
|
||||
if m.Header != "" {
|
||||
sections = append(sections, m.Header)
|
||||
}
|
||||
|
||||
sections = append(sections, m.Content)
|
||||
if m.Error != "" {
|
||||
sections = append(sections, m.Error)
|
||||
}
|
||||
|
||||
if m.Footer != "" {
|
||||
sections = append(sections, m.Footer)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
|
||||
func (m *Model) headerView() string {
|
||||
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||
header := titleStyle.Render("Conversations")
|
||||
|
Loading…
Reference in New Issue
Block a user