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