Split up tui code into packages (views/*, shared, util)
This commit is contained in:
171
pkg/tui/tui.go
171
pkg/tui/tui.go
@@ -10,99 +10,62 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||
"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"
|
||||
)
|
||||
|
||||
type state int
|
||||
// Application model
|
||||
type Model struct {
|
||||
shared.State
|
||||
|
||||
const (
|
||||
stateChat = iota
|
||||
stateConversations
|
||||
//stateModelSelect // stateOptions?
|
||||
//stateHelp
|
||||
)
|
||||
|
||||
// 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
|
||||
type views struct {
|
||||
header string
|
||||
content string
|
||||
error string
|
||||
input string
|
||||
footer string
|
||||
state shared.View
|
||||
chat chat.Model
|
||||
conversations conversations.Model
|
||||
}
|
||||
|
||||
type (
|
||||
// send to change the current state
|
||||
msgStateChange state
|
||||
// sent to a state when it is entered
|
||||
msgStateEnter struct{}
|
||||
// sent when an error occurs
|
||||
msgError error
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
convShortname string
|
||||
}
|
||||
|
||||
type basemodel struct {
|
||||
opts *Options
|
||||
ctx *lmcli.Context
|
||||
views *views
|
||||
err error
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
type model struct {
|
||||
basemodel
|
||||
|
||||
state state
|
||||
chat chatModel
|
||||
conversations conversationsModel
|
||||
}
|
||||
|
||||
func initialModel(ctx *lmcli.Context, opts Options) model {
|
||||
m := model{
|
||||
basemodel: basemodel{
|
||||
opts: &opts,
|
||||
ctx: ctx,
|
||||
views: &views{},
|
||||
func initialModel(ctx *lmcli.Context, values shared.Values) Model {
|
||||
m := Model{
|
||||
State: shared.State{
|
||||
Ctx: ctx,
|
||||
Values: &values,
|
||||
Views: &shared.Views{},
|
||||
},
|
||||
}
|
||||
m.state = stateChat
|
||||
m.chat = newChatModel(&m)
|
||||
m.conversations = newConversationsModel(&m)
|
||||
|
||||
m.state = shared.StateChat
|
||||
m.chat = chat.Chat(m.State)
|
||||
m.conversations = conversations.Conversations(m.State)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.conversations.Init(),
|
||||
m.chat.Init(),
|
||||
func() tea.Msg {
|
||||
return msgStateChange(m.state)
|
||||
return shared.MsgViewChange(m.state)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m *model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
func (m *Model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
// delegate input to the active child state first, only handling it at the
|
||||
// global level if the child state does not
|
||||
var cmds []tea.Cmd
|
||||
switch m.state {
|
||||
case stateChat:
|
||||
handled, cmd := m.chat.handleInput(msg)
|
||||
case shared.StateChat:
|
||||
handled, cmd := m.chat.HandleInput(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
if handled {
|
||||
m.chat, cmd = m.chat.Update(nil)
|
||||
cmds = append(cmds, cmd)
|
||||
return true, tea.Batch(cmds...)
|
||||
}
|
||||
case stateConversations:
|
||||
handled, cmd := m.conversations.handleInput(msg)
|
||||
case shared.StateConversations:
|
||||
handled, cmd := m.conversations.HandleInput(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
if handled {
|
||||
m.conversations, cmd = m.conversations.Update(nil)
|
||||
@@ -117,7 +80,7 @@ func (m *model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
@@ -126,32 +89,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if handled {
|
||||
return m, cmd
|
||||
}
|
||||
case msgStateChange:
|
||||
m.state = state(msg)
|
||||
case shared.MsgViewChange:
|
||||
m.state = shared.View(msg)
|
||||
switch m.state {
|
||||
case stateChat:
|
||||
m.chat.handleResize(m.width, m.height)
|
||||
case stateConversations:
|
||||
m.conversations.handleResize(m.width, m.height)
|
||||
case shared.StateChat:
|
||||
m.chat.HandleResize(m.State.Width, m.State.Height)
|
||||
case shared.StateConversations:
|
||||
m.conversations.HandleResize(m.State.Width, m.State.Height)
|
||||
}
|
||||
return m, func() tea.Msg { return msgStateEnter(struct{}{}) }
|
||||
case msgConversationSelected:
|
||||
return m, func() tea.Msg { return shared.MsgViewEnter(struct{}{}) }
|
||||
case conversations.MsgConversationSelected:
|
||||
// passed up through conversation list model
|
||||
m.opts.convShortname = msg.ShortName.String
|
||||
m.State.Values.ConvShortname = msg.ShortName.String
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
return msgStateChange(stateChat)
|
||||
return shared.MsgViewChange(shared.StateChat)
|
||||
})
|
||||
case tea.WindowSizeMsg:
|
||||
m.width, m.height = msg.Width, msg.Height
|
||||
case msgError:
|
||||
m.err = msg
|
||||
m.State.Width, m.State.Height = msg.Width, msg.Height
|
||||
case shared.MsgError:
|
||||
m.State.Err = msg
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
switch m.state {
|
||||
case stateConversations:
|
||||
case shared.StateConversations:
|
||||
m.conversations, cmd = m.conversations.Update(msg)
|
||||
case stateChat:
|
||||
case shared.StateChat:
|
||||
m.chat, cmd = m.chat.Update(msg)
|
||||
}
|
||||
if cmd != nil {
|
||||
@@ -161,8 +124,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.width == 0 {
|
||||
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
|
||||
@@ -170,51 +133,33 @@ func (m model) View() string {
|
||||
}
|
||||
sections := make([]string, 0, 6)
|
||||
|
||||
if m.views.header != "" {
|
||||
sections = append(sections, m.views.header)
|
||||
if m.State.Views.Header != "" {
|
||||
sections = append(sections, m.State.Views.Header)
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case stateConversations:
|
||||
sections = append(sections, m.views.content)
|
||||
if m.views.error != "" {
|
||||
sections = append(sections, m.views.error)
|
||||
case shared.StateConversations:
|
||||
sections = append(sections, m.State.Views.Content)
|
||||
if m.State.Views.Error != "" {
|
||||
sections = append(sections, m.State.Views.Error)
|
||||
}
|
||||
case stateChat:
|
||||
sections = append(sections, m.views.content)
|
||||
if m.views.error != "" {
|
||||
sections = append(sections, m.views.error)
|
||||
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.views.input)
|
||||
sections = append(sections, m.State.Views.Input)
|
||||
}
|
||||
|
||||
if m.views.footer != "" {
|
||||
sections = append(sections, m.views.footer)
|
||||
if m.State.Views.Footer != "" {
|
||||
sections = append(sections, m.State.Views.Footer)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
|
||||
func errorBanner(err error, width int) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("1")).
|
||||
Render(fmt.Sprintf("%s", err))
|
||||
}
|
||||
|
||||
func wrapError(err error) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return msgError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Launch(ctx *lmcli.Context, convShortname string) error {
|
||||
p := tea.NewProgram(initialModel(ctx, Options{convShortname}), tea.WithAltScreen())
|
||||
p := tea.NewProgram(initialModel(ctx, shared.Values{ConvShortname: convShortname}), tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
return fmt.Errorf("Error running program: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user