Refactor TUI rendering handling and general cleanup
Improves render handling by moving the responsibility of laying out the whole UI from each view and into the main `tui` model. Our `ViewModel` interface has now diverged from bubbletea's `Model` and introduces individual `Header`, `Content`, and `Footer` methods for rendering those UI elements. Also moved away from using value receivers on our Update and View functions (as is common across Bubbletea) to pointer receivers, which cleaned up some of the weirder aspects of the code (e.g. before we essentially had no choice but to do our rendering in `Update` in order to calculate and update the final height of the main content's `viewport`).
This commit is contained in:
parent
b7c89a4dd1
commit
676aa7b004
@ -9,15 +9,12 @@ import (
|
|||||||
type ViewModel interface {
|
type ViewModel interface {
|
||||||
Init() tea.Cmd
|
Init() tea.Cmd
|
||||||
Update(tea.Msg) (ViewModel, tea.Cmd)
|
Update(tea.Msg) (ViewModel, tea.Cmd)
|
||||||
View() string
|
|
||||||
Initialized() bool // Return whether this view is initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
type ViewState struct {
|
// View methods
|
||||||
Initialized bool
|
Header(width int) string
|
||||||
Width int
|
// Render the view's main content into a container of the given dimensions
|
||||||
Height int
|
Content(width, height int) string
|
||||||
Err error
|
Footer(width int) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type View int
|
type View int
|
||||||
@ -29,22 +26,12 @@ const (
|
|||||||
//StateHelp
|
//StateHelp
|
||||||
)
|
)
|
||||||
|
|
||||||
// a convenience struct for holding rendered content for indiviudal UI
|
|
||||||
// elements
|
|
||||||
type Sections struct {
|
|
||||||
Header string
|
|
||||||
Content string
|
|
||||||
Error string
|
|
||||||
Input string
|
|
||||||
Footer string
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// send to change the current state
|
// send to change the current state
|
||||||
MsgViewChange View
|
MsgViewChange View
|
||||||
// sent to a state when it is entered
|
// sent to a state when it is entered
|
||||||
MsgViewEnter struct{}
|
MsgViewEnter struct{}
|
||||||
// sent when an error occurs
|
// sent when a recoverable error occurs (displayed to user)
|
||||||
MsgError error
|
MsgError error
|
||||||
// sent when the view has handled a key input
|
// sent when the view has handled a key input
|
||||||
MsgKeyHandled tea.KeyMsg
|
MsgKeyHandled tea.KeyMsg
|
||||||
|
@ -7,9 +7,11 @@ import (
|
|||||||
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
|
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||||
|
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LaunchOptions struct {
|
type LaunchOptions struct {
|
||||||
@ -20,13 +22,18 @@ type LaunchOptions struct {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
App *model.AppModel
|
App *model.AppModel
|
||||||
|
|
||||||
|
// window size
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
// errors we will display to the user and allow them to dismiss
|
||||||
|
errs []error
|
||||||
|
|
||||||
activeView shared.View
|
activeView shared.View
|
||||||
views map[shared.View]shared.ViewModel
|
views map[shared.View]shared.ViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
func initialModel(ctx *lmcli.Context, opts LaunchOptions) *Model {
|
||||||
sharedData := shared.ViewState{}
|
|
||||||
|
|
||||||
app := &model.AppModel{
|
app := &model.AppModel{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Conversation: opts.InitialConversation,
|
Conversation: opts.InitialConversation,
|
||||||
@ -36,20 +43,24 @@ func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
|||||||
App: app,
|
App: app,
|
||||||
activeView: opts.InitialView,
|
activeView: opts.InitialView,
|
||||||
views: map[shared.View]shared.ViewModel{
|
views: map[shared.View]shared.ViewModel{
|
||||||
shared.ViewChat: chat.Chat(app, sharedData),
|
shared.ViewChat: chat.Chat(app),
|
||||||
shared.ViewConversations: conversations.Conversations(app, sharedData),
|
shared.ViewConversations: conversations.Conversations(app),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m *Model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
cmds := []tea.Cmd{
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
return shared.MsgViewChange(m.activeView)
|
return shared.MsgViewChange(m.activeView)
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
|
for _, v := range m.views {
|
||||||
|
cmds = append(cmds, v.Init())
|
||||||
|
}
|
||||||
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
@ -66,8 +77,10 @@ func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width, m.height = msg.Width, msg.Height
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
cmd := m.handleGlobalInput(msg)
|
cmd := m.handleGlobalInput(msg)
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
@ -75,15 +88,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case shared.MsgViewChange:
|
case shared.MsgViewChange:
|
||||||
m.activeView = shared.View(msg)
|
m.activeView = shared.View(msg)
|
||||||
view := m.views[m.activeView]
|
return m, tea.Batch(tea.WindowSize(), shared.ViewEnter())
|
||||||
|
case shared.MsgError:
|
||||||
var cmds []tea.Cmd
|
m.errs = append(m.errs, msg)
|
||||||
if !view.Initialized() {
|
|
||||||
cmds = append(cmds, view.Init())
|
|
||||||
}
|
|
||||||
cmds = append(cmds, tea.WindowSize(), shared.ViewEnter())
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view, cmd := m.views[m.activeView].Update(msg)
|
view, cmd := m.views[m.activeView].Update(msg)
|
||||||
@ -91,8 +98,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m *Model) View() string {
|
||||||
return m.views[m.activeView].View()
|
if m.width == 0 || m.height == 0 {
|
||||||
|
// we're dimensionless!
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
header := m.views[m.activeView].Header(m.width)
|
||||||
|
footer := m.views[m.activeView].Footer(m.width)
|
||||||
|
fixedUIHeight := tuiutil.Height(header) + tuiutil.Height(footer)
|
||||||
|
|
||||||
|
errBanners := make([]string, len(m.errs))
|
||||||
|
for idx, err := range m.errs {
|
||||||
|
errBanners[idx] = tuiutil.ErrorBanner(err, m.width)
|
||||||
|
fixedUIHeight += tuiutil.Height(errBanners[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
content := m.views[m.activeView].Content(m.width, m.height-fixedUIHeight)
|
||||||
|
|
||||||
|
sections := make([]string, 0, 4)
|
||||||
|
if header != "" {
|
||||||
|
sections = append(sections, header)
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
sections = append(sections, content)
|
||||||
|
}
|
||||||
|
if len(errBanners) > 0 {
|
||||||
|
sections = append(sections, lipgloss.JoinVertical(lipgloss.Left, errBanners...))
|
||||||
|
}
|
||||||
|
if footer != "" {
|
||||||
|
sections = append(sections, footer)
|
||||||
|
}
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LaunchOption func(*LaunchOptions)
|
type LaunchOption func(*LaunchOptions)
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
|
||||||
"github.com/charmbracelet/bubbles/cursor"
|
"github.com/charmbracelet/bubbles/cursor"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
@ -72,11 +71,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
*shared.ViewState
|
|
||||||
shared.Sections
|
|
||||||
|
|
||||||
// App state
|
// App state
|
||||||
App *model.AppModel
|
App *model.AppModel
|
||||||
|
Height int
|
||||||
|
Width int
|
||||||
|
|
||||||
// Chat view state
|
// Chat view state
|
||||||
state state // current overall status of the view
|
state state // current overall status of the view
|
||||||
@ -106,14 +104,9 @@ type Model struct {
|
|||||||
elapsed time.Duration
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Initialized() bool {
|
func Chat(app *model.AppModel) *Model {
|
||||||
return m.ViewState.Initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
func Chat(app *model.AppModel, shared shared.ViewState) shared.ViewModel {
|
|
||||||
m := Model{
|
m := Model{
|
||||||
App: app,
|
App: app,
|
||||||
ViewState: &shared,
|
|
||||||
|
|
||||||
state: idle,
|
state: idle,
|
||||||
persistence: true,
|
persistence: true,
|
||||||
@ -166,12 +159,10 @@ func Chat(app *model.AppModel, shared shared.ViewState) shared.ViewModel {
|
|||||||
m.input.FocusedStyle.CursorLine = lipgloss.NewStyle()
|
m.input.FocusedStyle.CursorLine = lipgloss.NewStyle()
|
||||||
m.input.FocusedStyle.Base = inputFocusedStyle
|
m.input.FocusedStyle.Base = inputFocusedStyle
|
||||||
m.input.BlurredStyle.Base = inputBlurredStyle
|
m.input.BlurredStyle.Base = inputBlurredStyle
|
||||||
|
return &m
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m *Model) Init() tea.Cmd {
|
||||||
m.ViewState.Initialized = true
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
m.waitForResponseChunk(),
|
m.waitForResponseChunk(),
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ func (m *Model) updateContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||||
inputHandled := false
|
inputHandled := false
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
@ -169,8 +169,8 @@ func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
m.updateContent()
|
m.updateContent()
|
||||||
case msgChatResponseError:
|
case msgChatResponseError:
|
||||||
m.state = idle
|
m.state = idle
|
||||||
m.ViewState.Err = error(msg)
|
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
|
return m, shared.WrapError(msg)
|
||||||
case msgToolResults:
|
case msgToolResults:
|
||||||
last := len(m.App.Messages) - 1
|
last := len(m.App.Messages) - 1
|
||||||
if last < 0 {
|
if last < 0 {
|
||||||
@ -249,24 +249,6 @@ func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update views once window dimensions are known
|
|
||||||
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()))
|
|
||||||
m.input.SetHeight(newHeight)
|
|
||||||
m.Input = m.input.View()
|
|
||||||
|
|
||||||
// remaining height towards content
|
|
||||||
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
|
// this is a pretty nasty hack to ensure the input area viewport doesn't
|
||||||
// scroll below its content, which can happen when the input viewport
|
// scroll below its content, which can happen when the input viewport
|
||||||
// height has grown, or previously entered lines have been deleted
|
// height has grown, or previously entered lines have been deleted
|
||||||
|
@ -39,29 +39,6 @@ var (
|
|||||||
footerStyle = lipgloss.NewStyle()
|
footerStyle = lipgloss.NewStyle()
|
||||||
)
|
)
|
||||||
|
|
||||||
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 *api.Message) string {
|
func (m *Model) renderMessageHeading(i int, message *api.Message) string {
|
||||||
icon := ""
|
icon := ""
|
||||||
friendly := message.Role.FriendlyRole()
|
friendly := message.Role.FriendlyRole()
|
||||||
@ -254,7 +231,20 @@ func (m *Model) conversationMessagesView() string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) headerView() string {
|
func (m *Model) Content(width, height int) string {
|
||||||
|
// calculate clamped input height to accomodate input text
|
||||||
|
// minimum 4 lines, maximum half of content area
|
||||||
|
inputHeight := max(4, min(height/2, m.input.LineCount()))
|
||||||
|
m.input.SetHeight(inputHeight)
|
||||||
|
input := m.input.View()
|
||||||
|
|
||||||
|
// remaining height towards content
|
||||||
|
m.content.Width, m.content.Height = width, height - tuiutil.Height(input)
|
||||||
|
content := m.content.View()
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, content, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Header(width int) string {
|
||||||
titleStyle := lipgloss.NewStyle().Bold(true)
|
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||||
var title string
|
var title string
|
||||||
if m.App.Conversation != nil && m.App.Conversation.Title != "" {
|
if m.App.Conversation != nil && m.App.Conversation.Title != "" {
|
||||||
@ -262,12 +252,12 @@ func (m *Model) headerView() string {
|
|||||||
} else {
|
} else {
|
||||||
title = "Untitled"
|
title = "Untitled"
|
||||||
}
|
}
|
||||||
title = tuiutil.TruncateToCellWidth(title, m.Width-styles.Header.GetHorizontalPadding(), "...")
|
title = tuiutil.TruncateToCellWidth(title, width-styles.Header.GetHorizontalPadding(), "...")
|
||||||
header := titleStyle.Render(title)
|
header := titleStyle.Render(title)
|
||||||
return styles.Header.Width(m.Width).Render(header)
|
return styles.Header.Width(width).Render(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) footerView() string {
|
func (m *Model) Footer(width int) string {
|
||||||
segmentStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Faint(true)
|
segmentStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Faint(true)
|
||||||
segmentSeparator := "|"
|
segmentSeparator := "|"
|
||||||
|
|
||||||
@ -305,7 +295,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 := width - totalWidth
|
||||||
|
|
||||||
var padding string
|
var padding string
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
@ -314,7 +304,7 @@ 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, width, "...")
|
||||||
}
|
}
|
||||||
return footerStyle.Width(m.Width).Render(footer)
|
return footerStyle.Width(width).Render(footer)
|
||||||
}
|
}
|
||||||
|
@ -27,29 +27,24 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
*shared.ViewState
|
App *model.AppModel
|
||||||
shared.Sections
|
width int
|
||||||
|
height int
|
||||||
App *model.AppModel
|
|
||||||
cursor int
|
|
||||||
|
|
||||||
|
cursor int
|
||||||
itemOffsets []int // conversation y offsets
|
itemOffsets []int // conversation y offsets
|
||||||
|
|
||||||
content viewport.Model
|
content viewport.Model
|
||||||
confirmPrompt bubbles.ConfirmPrompt
|
confirmPrompt bubbles.ConfirmPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
func Conversations(app *model.AppModel, shared shared.ViewState) Model {
|
func Conversations(app *model.AppModel) *Model {
|
||||||
|
viewport.New(0, 0)
|
||||||
m := Model{
|
m := Model{
|
||||||
App: app,
|
App: app,
|
||||||
ViewState: &shared,
|
content: viewport.New(0, 0),
|
||||||
content: viewport.New(0, 0),
|
|
||||||
}
|
}
|
||||||
return m
|
return &m
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Initialized() bool {
|
|
||||||
return m.ViewState.Initialized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
@ -129,12 +124,11 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m *Model) Init() tea.Cmd {
|
||||||
m.ViewState.Initialized = true
|
return nil
|
||||||
return m.loadConversations()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||||
isInput := false
|
isInput := false
|
||||||
inputHandled := false
|
inputHandled := false
|
||||||
|
|
||||||
@ -144,15 +138,14 @@ func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
isInput = true
|
isInput = true
|
||||||
cmd := m.handleInput(msg)
|
cmd := m.handleInput(msg)
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
inputHandled = true
|
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
inputHandled = true
|
||||||
}
|
}
|
||||||
case shared.MsgViewEnter:
|
case shared.MsgViewEnter:
|
||||||
cmds = append(cmds, m.loadConversations())
|
cmds = append(cmds, m.loadConversations())
|
||||||
m.content.SetContent(m.renderConversationList())
|
m.content.SetContent(m.renderConversationList())
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.Width, m.Height = msg.Width, msg.Height
|
m.width, m.height = msg.Width, msg.Height
|
||||||
m.content.Width = msg.Width
|
|
||||||
m.content.SetContent(m.renderConversationList())
|
m.content.SetContent(m.renderConversationList())
|
||||||
case msgConversationsLoaded:
|
case msgConversationsLoaded:
|
||||||
m.App.Conversations = msg
|
m.App.Conversations = msg
|
||||||
@ -171,26 +164,13 @@ func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isInput || !inputHandled {
|
if !isInput || !inputHandled {
|
||||||
var cmd tea.Cmd
|
content, cmd := m.content.Update(msg)
|
||||||
m.content, cmd = m.content.Update(msg)
|
m.content = content
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Width > 0 {
|
|
||||||
wrap := lipgloss.NewStyle().Width(m.Width)
|
|
||||||
m.Header = m.headerView()
|
|
||||||
m.Footer = "" // TODO: "Press ? for help"
|
|
||||||
if m.confirmPrompt.Focused() {
|
|
||||||
m.Footer = wrap.Render(m.confirmPrompt.View())
|
|
||||||
}
|
|
||||||
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.Content = m.content.View()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmds) > 0 {
|
if len(cmds) > 0 {
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
@ -218,32 +198,22 @@ func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m *Model) Header(width int) 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)
|
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||||
header := titleStyle.Render("Conversations")
|
header := titleStyle.Render("Conversations")
|
||||||
return styles.Header.Width(m.Width).Render(header)
|
return styles.Header.Width(width).Render(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Content(width int, height int) string {
|
||||||
|
m.content.Width, m.content.Height = width, height
|
||||||
|
return m.content.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Footer(width int) string {
|
||||||
|
if m.confirmPrompt.Focused() {
|
||||||
|
return lipgloss.NewStyle().Width(width).Render(m.confirmPrompt.View())
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderConversationList() string {
|
func (m *Model) renderConversationList() string {
|
||||||
@ -321,7 +291,7 @@ func (m *Model) renderConversationList() string {
|
|||||||
tStyle = tStyle.Inherit(selectedStyle)
|
tStyle = tStyle.Inherit(selectedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
title := tStyle.Width(m.Width - 3).PaddingLeft(2).Render(c.Conv.Title)
|
title := tStyle.Width(m.width - 3).PaddingLeft(2).Render(c.Conv.Title)
|
||||||
if i == m.cursor {
|
if i == m.cursor {
|
||||||
title = ">" + title[1:]
|
title = ">" + title[1:]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user