package tui import ( "fmt" "git.mlow.ca/mlow/lmcli/pkg/conversation" "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" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) 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 } 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 } 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...) } func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd { switch msg.String() { case "esc": if len(m.errs) > 0 { m.errs = m.errs[1:] return shared.KeyHandled(msg) } } view, cmd := m.views[m.activeView].Update(msg) m.views[m.activeView] = view if cmd != nil { return cmd } switch msg.String() { case "ctrl+c", "ctrl+q": return tea.Quit } 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 } 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) } var errors string if len(errBanners) > 0 { errors = lipgloss.JoinVertical(lipgloss.Left, errBanners...) } else { errors = "" } fixedUIHeight += tuiutil.Height(errors) content := m.views[m.activeView].Content(m.width, m.height-fixedUIHeight, errors) sections := make([]string, 0, 4) if header != "" { sections = append(sections, header) } if content != "" { sections = append(sections, content) } if footer != "" { sections = append(sections, footer) } return lipgloss.JoinVertical(lipgloss.Left, sections...) } 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 { return fmt.Errorf("Error running program: %v", err) } return nil }