tui.go

  1package tui
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	"github.com/kujtimiihoxha/termai/internal/app"
  8	"github.com/kujtimiihoxha/termai/internal/tui/components/core"
  9	"github.com/kujtimiihoxha/termai/internal/tui/components/dialog"
 10	"github.com/kujtimiihoxha/termai/internal/tui/layout"
 11	"github.com/kujtimiihoxha/termai/internal/tui/page"
 12	"github.com/kujtimiihoxha/termai/internal/tui/util"
 13	"github.com/kujtimiihoxha/vimtea"
 14)
 15
 16type keyMap struct {
 17	Logs   key.Binding
 18	Return key.Binding
 19	Back   key.Binding
 20	Quit   key.Binding
 21	Help   key.Binding
 22}
 23
 24var keys = keyMap{
 25	Logs: key.NewBinding(
 26		key.WithKeys("L"),
 27		key.WithHelp("L", "logs"),
 28	),
 29	Return: key.NewBinding(
 30		key.WithKeys("esc"),
 31		key.WithHelp("esc", "close"),
 32	),
 33	Back: key.NewBinding(
 34		key.WithKeys("backspace"),
 35		key.WithHelp("backspace", "back"),
 36	),
 37	Quit: key.NewBinding(
 38		key.WithKeys("ctrl+c", "q"),
 39		key.WithHelp("ctrl+c/q", "quit"),
 40	),
 41	Help: key.NewBinding(
 42		key.WithKeys("?"),
 43		key.WithHelp("?", "toggle help"),
 44	),
 45}
 46
 47type appModel struct {
 48	width, height int
 49	currentPage   page.PageID
 50	previousPage  page.PageID
 51	pages         map[page.PageID]tea.Model
 52	loadedPages   map[page.PageID]bool
 53	status        tea.Model
 54	help          core.HelpCmp
 55	dialog        core.DialogCmp
 56	dialogVisible bool
 57	editorMode    vimtea.EditorMode
 58	showHelp      bool
 59}
 60
 61func (a appModel) Init() tea.Cmd {
 62	cmd := a.pages[a.currentPage].Init()
 63	a.loadedPages[a.currentPage] = true
 64	return cmd
 65}
 66
 67func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 68	switch msg := msg.(type) {
 69	case vimtea.EditorModeMsg:
 70		a.editorMode = msg.Mode
 71	case tea.WindowSizeMsg:
 72		msg.Height -= 1 // Make space for the status bar
 73		a.width, a.height = msg.Width, msg.Height
 74
 75		a.status, _ = a.status.Update(msg)
 76
 77		uh, _ := a.help.Update(msg)
 78		a.help = uh.(core.HelpCmp)
 79
 80		p, cmd := a.pages[a.currentPage].Update(msg)
 81		a.pages[a.currentPage] = p
 82		return a, cmd
 83	case core.DialogMsg:
 84		d, cmd := a.dialog.Update(msg)
 85		a.dialog = d.(core.DialogCmp)
 86		a.dialogVisible = true
 87		return a, cmd
 88	case core.DialogCloseMsg:
 89		d, cmd := a.dialog.Update(msg)
 90		a.dialog = d.(core.DialogCmp)
 91		a.dialogVisible = false
 92		return a, cmd
 93	case util.InfoMsg:
 94		a.status, _ = a.status.Update(msg)
 95	case util.ErrorMsg:
 96		a.status, _ = a.status.Update(msg)
 97	case tea.KeyMsg:
 98		if a.editorMode == vimtea.ModeNormal {
 99			switch {
100			case key.Matches(msg, keys.Quit):
101				return a, dialog.NewQuitDialogCmd()
102			case key.Matches(msg, keys.Back):
103				if a.previousPage != "" {
104					return a, a.moveToPage(a.previousPage)
105				}
106			case key.Matches(msg, keys.Return):
107				if a.showHelp {
108					a.ToggleHelp()
109					return a, nil
110				}
111			case key.Matches(msg, keys.Logs):
112				return a, a.moveToPage(page.LogsPage)
113			case key.Matches(msg, keys.Help):
114				a.ToggleHelp()
115				return a, nil
116			}
117		}
118	}
119	if a.dialogVisible {
120		d, cmd := a.dialog.Update(msg)
121		a.dialog = d.(core.DialogCmp)
122		return a, cmd
123	}
124	p, cmd := a.pages[a.currentPage].Update(msg)
125	a.pages[a.currentPage] = p
126	return a, cmd
127}
128
129func (a *appModel) ToggleHelp() {
130	if a.showHelp {
131		a.showHelp = false
132		a.height += a.help.Height()
133	} else {
134		a.showHelp = true
135		a.height -= a.help.Height()
136	}
137
138	if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
139		sizable.SetSize(a.width, a.height)
140	}
141}
142
143func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
144	var cmd tea.Cmd
145	if _, ok := a.loadedPages[pageID]; !ok {
146		cmd = a.pages[pageID].Init()
147		a.loadedPages[pageID] = true
148	}
149	a.previousPage = a.currentPage
150	a.currentPage = pageID
151	if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
152		sizable.SetSize(a.width, a.height)
153	}
154
155	return cmd
156}
157
158func (a appModel) View() string {
159	components := []string{
160		a.pages[a.currentPage].View(),
161	}
162
163	if a.showHelp {
164		bindings := layout.KeyMapToSlice(keys)
165		if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
166			bindings = append(bindings, p.BindingKeys()...)
167		}
168		if a.dialogVisible {
169			bindings = append(bindings, a.dialog.BindingKeys()...)
170		}
171		a.help.SetBindings(bindings)
172		components = append(components, a.help.View())
173	}
174
175	components = append(components, a.status.View())
176
177	appView := lipgloss.JoinVertical(lipgloss.Top, components...)
178
179	if a.dialogVisible {
180		overlay := a.dialog.View()
181		row := lipgloss.Height(appView) / 2
182		row -= lipgloss.Height(overlay) / 2
183		col := lipgloss.Width(appView) / 2
184		col -= lipgloss.Width(overlay) / 2
185		appView = layout.PlaceOverlay(
186			col,
187			row,
188			overlay,
189			appView,
190			true,
191		)
192	}
193	return appView
194}
195
196func New(app *app.App) tea.Model {
197	return &appModel{
198		currentPage: page.ReplPage,
199		loadedPages: make(map[page.PageID]bool),
200		status:      core.NewStatusCmp(),
201		help:        core.NewHelpCmp(),
202		dialog:      core.NewDialogCmp(),
203		pages: map[page.PageID]tea.Model{
204			page.LogsPage: page.NewLogsPage(),
205			page.InitPage: page.NewInitPage(),
206			page.ReplPage: page.NewReplPage(app),
207		},
208	}
209}