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}