lmcli/pkg/tui/tui.go

165 lines
3.8 KiB
Go
Raw Normal View History

2024-03-12 01:10:54 -06:00
package tui
import (
"fmt"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
2024-03-12 01:10:54 -06:00
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
"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/conversations"
"git.mlow.ca/mlow/lmcli/pkg/tui/views/settings"
2024-03-12 01:10:54 -06:00
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
2024-03-12 01:10:54 -06:00
)
type Model struct {
App *model.AppModel
// window size
width int
height int
// errors to display
// TODO: allow dismissing errors
errs []error
activeView shared.View
views map[shared.View]shared.ViewModel
}
2024-03-12 01:10:54 -06:00
func initialModel(ctx *lmcli.Context, opts LaunchOptions) *Model {
app := model.NewAppModel(ctx, opts.InitialConversation)
m := Model{
App: app,
activeView: opts.InitialView,
views: map[shared.View]shared.ViewModel{
shared.ViewChat: chat.Chat(app),
shared.ViewConversations: conversations.Conversations(app),
shared.ViewSettings: settings.Settings(app),
},
}
return &m
2024-03-29 14:43:19 -06:00
}
2024-03-12 01:10:54 -06:00
func (m *Model) Init() tea.Cmd {
var cmds []tea.Cmd
for _, v := range m.views {
// Init views
cmds = append(cmds, v.Init())
}
cmds = append(cmds, func() tea.Msg {
// Initial view change
return shared.MsgViewChange(m.activeView)
})
return tea.Batch(cmds...)
2024-03-12 01:10:54 -06:00
}
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
view, cmd := m.views[m.activeView].Update(msg)
m.views[m.activeView] = view
if cmd != nil {
return cmd
}
2024-03-31 19:06:13 -06:00
switch msg.String() {
case "ctrl+c", "ctrl+q":
return tea.Quit
2024-03-31 19:06:13 -06:00
}
return nil
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyMsg:
cmd := m.handleGlobalInput(msg)
if cmd != nil {
return m, cmd
}
case shared.MsgViewChange:
currView := m.activeView
m.activeView = shared.View(msg)
return m, tea.Batch(tea.WindowSize(), shared.ViewEnter(currView))
case shared.MsgError:
m.errs = append(m.errs, msg.Err)
}
view, cmd := m.views[m.activeView].Update(msg)
m.views[m.activeView] = view
return m, cmd
2024-03-12 01:10:54 -06:00
}
func (m *Model) View() string {
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 footer != "" {
sections = append(sections, footer)
}
for _, errBanner := range errBanners {
sections = append(sections, errBanner)
}
return lipgloss.JoinVertical(lipgloss.Left, sections...)
2024-03-12 01:10:54 -06:00
}
type LaunchOptions struct {
InitialConversation *conversation.Conversation
InitialView shared.View
}
type LaunchOption func(*LaunchOptions)
func WithInitialConversation(conv *conversation.Conversation) LaunchOption {
return func(opts *LaunchOptions) {
opts.InitialConversation = conv
}
}
func WithInitialView(view shared.View) LaunchOption {
return func(opts *LaunchOptions) {
opts.InitialView = view
}
}
func Launch(ctx *lmcli.Context, options ...LaunchOption) error {
opts := &LaunchOptions{
InitialView: shared.ViewChat,
}
for _, opt := range options {
opt(opts)
}
program := tea.NewProgram(initialModel(ctx, *opts), tea.WithAltScreen())
if _, err := program.Run(); err != nil {
2024-03-12 01:10:54 -06:00
return fmt.Errorf("Error running program: %v", err)
}
return nil
}