1package model
2
3import (
4 "image"
5 "math/rand"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/app"
11 "github.com/charmbracelet/crush/internal/ui/common"
12 "github.com/charmbracelet/crush/internal/ui/dialog"
13 "github.com/charmbracelet/lipgloss/v2"
14 uv "github.com/charmbracelet/ultraviolet"
15)
16
17type uiState uint8
18
19const (
20 uiChat uiState = iota
21 uiEdit
22)
23
24type UI struct {
25 app *app.App
26 com *common.Common
27
28 state uiState
29
30 keyMap KeyMap
31
32 chat *ChatModel
33 editor *EditorModel
34 dialog *dialog.Overlay
35 help help.Model
36
37 layout layout
38}
39
40func New(com *common.Common, app *app.App) *UI {
41 return &UI{
42 app: app,
43 com: com,
44 dialog: dialog.NewOverlay(),
45 keyMap: DefaultKeyMap(),
46 editor: NewEditorModel(com, app),
47 help: help.New(),
48 }
49}
50
51func (m *UI) Init() tea.Cmd {
52 return nil
53}
54
55func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
56 var cmds []tea.Cmd
57 switch msg := msg.(type) {
58 case tea.WindowSizeMsg:
59 m.updateLayout(msg.Width, msg.Height)
60 m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy())
61 m.help.Width = m.layout.help.Dx()
62 case tea.KeyPressMsg:
63 if m.dialog.HasDialogs() {
64 m.updateDialogs(msg, &cmds)
65 } else {
66 switch {
67 case key.Matches(msg, m.keyMap.Tab):
68 if m.state == uiChat {
69 m.state = uiEdit
70 cmds = append(cmds, m.editor.Focus())
71 } else {
72 m.state = uiChat
73 cmds = append(cmds, m.editor.Blur())
74 }
75 case key.Matches(msg, m.keyMap.Help):
76 m.help.ShowAll = !m.help.ShowAll
77 m.updateLayout(m.layout.area.Dx(), m.layout.area.Dy())
78 case key.Matches(msg, m.keyMap.Quit):
79 if !m.dialog.ContainsDialog(dialog.QuitDialogID) {
80 m.dialog.AddDialog(dialog.NewQuit(m.com))
81 return m, nil
82 }
83 default:
84 m.updateFocused(msg, &cmds)
85 }
86 }
87 }
88
89 return m, tea.Batch(cmds...)
90}
91
92func (m *UI) View() tea.View {
93 var v tea.View
94 v.AltScreen = true
95
96 layers := []*lipgloss.Layer{}
97
98 // Determine the help key map based on focus
99 helpKeyMap := m.focusedKeyMap()
100
101 // The screen areas we're working with
102 area := m.layout.area
103 chatRect := m.layout.chat
104 sideRect := m.layout.sidebar
105 editRect := m.layout.editor
106 helpRect := m.layout.help
107
108 if m.dialog.HasDialogs() {
109 if dialogView := m.dialog.View(); dialogView != "" {
110 // If the dialog has its own help, use that instead
111 if len(m.dialog.FullHelp()) > 0 || len(m.dialog.ShortHelp()) > 0 {
112 helpKeyMap = m.dialog
113 }
114
115 dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
116 dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
117 layers = append(layers,
118 lipgloss.NewLayer(dialogView).
119 X(dialogArea.Min.X).
120 Y(dialogArea.Min.Y).
121 Z(99),
122 )
123 }
124 }
125
126 if m.state == uiEdit && m.editor.Focused() {
127 cur := m.editor.Cursor()
128 cur.X++ // Adjust for app margins
129 cur.Y += editRect.Min.Y
130 v.Cursor = cur
131 }
132
133 mainLayer := lipgloss.NewLayer("").X(area.Min.X).Y(area.Min.Y).
134 Width(area.Dx()).Height(area.Dy()).
135 AddLayers(
136 lipgloss.NewLayer(
137 lipgloss.NewStyle().Width(chatRect.Dx()).
138 Height(chatRect.Dy()).
139 Background(lipgloss.ANSIColor(rand.Intn(256))).
140 Render(" Main View "),
141 ).X(chatRect.Min.X).Y(chatRect.Min.Y),
142 lipgloss.NewLayer(
143 lipgloss.NewStyle().Width(sideRect.Dx()).
144 Height(sideRect.Dy()).
145 Background(lipgloss.ANSIColor(rand.Intn(256))).
146 Render(" Side View "),
147 ).X(sideRect.Min.X).Y(sideRect.Min.Y),
148 lipgloss.NewLayer(m.editor.View()).
149 X(editRect.Min.X).Y(editRect.Min.Y),
150 lipgloss.NewLayer(m.help.View(helpKeyMap)).
151 X(helpRect.Min.X).Y(helpRect.Min.Y),
152 )
153
154 layers = append(layers, mainLayer)
155
156 v.Layer = lipgloss.NewCanvas(layers...)
157
158 return v
159}
160
161func (m *UI) focusedKeyMap() help.KeyMap {
162 if m.state == uiChat {
163 return m.chat
164 }
165 return m.editor
166}
167
168// updateDialogs updates the dialog overlay with the given message and appends
169// any resulting commands to the cmds slice.
170func (m *UI) updateDialogs(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
171 updatedDialog, cmd := m.dialog.Update(msg)
172 m.dialog = updatedDialog
173 if cmd != nil {
174 *cmds = append(*cmds, cmd)
175 }
176}
177
178// updateFocused updates the focused model (chat or editor) with the given message
179// and appends any resulting commands to the cmds slice.
180func (m *UI) updateFocused(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
181 switch m.state {
182 case uiChat:
183 m.updateChat(msg, cmds)
184 case uiEdit:
185 m.updateEditor(msg, cmds)
186 }
187}
188
189// updateChat updates the chat model with the given message and appends any
190// resulting commands to the cmds slice.
191func (m *UI) updateChat(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
192 updatedChat, cmd := m.chat.Update(msg)
193 m.chat = updatedChat
194 if cmd != nil {
195 *cmds = append(*cmds, cmd)
196 }
197}
198
199// updateEditor updates the editor model with the given message and appends any
200// resulting commands to the cmds slice.
201func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
202 updatedEditor, cmd := m.editor.Update(msg)
203 m.editor = updatedEditor
204 if cmd != nil {
205 *cmds = append(*cmds, cmd)
206 }
207}
208
209// updateLayout updates the layout based on the given terminal width and
210// height given in cells.
211func (m *UI) updateLayout(w, h int) {
212 // The screen area we're working with
213 area := image.Rect(0, 0, w, h)
214 helpKeyMap := m.focusedKeyMap()
215 helpHeight := 1
216 if m.dialog.HasDialogs() && len(m.dialog.FullHelp()) > 0 && len(m.dialog.ShortHelp()) > 0 {
217 helpKeyMap = m.dialog
218 }
219 if m.help.ShowAll {
220 for _, row := range helpKeyMap.FullHelp() {
221 helpHeight = max(helpHeight, len(row))
222 }
223 }
224
225 // Add app margins
226 mainRect := area
227 mainRect.Min.X += 1
228 mainRect.Min.Y += 1
229 mainRect.Max.X -= 1
230 mainRect.Max.Y -= 1
231
232 mainRect, helpRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-helpHeight))
233 chatRect, sideRect := uv.SplitHorizontal(mainRect, uv.Fixed(mainRect.Dx()-40))
234 chatRect, editRect := uv.SplitVertical(chatRect, uv.Fixed(mainRect.Dy()-5))
235
236 // Add 1 line margin bottom of chatRect
237 chatRect, _ = uv.SplitVertical(chatRect, uv.Fixed(chatRect.Dy()-1))
238 // Add 1 line margin bottom of editRect
239 editRect, _ = uv.SplitVertical(editRect, uv.Fixed(editRect.Dy()-1))
240
241 m.layout = layout{
242 area: area,
243 main: mainRect,
244 chat: chatRect,
245 editor: editRect,
246 sidebar: sideRect,
247 help: helpRect,
248 }
249}
250
251// layout defines the positioning of UI elements.
252type layout struct {
253 // area is the overall available area.
254 area uv.Rectangle
255
256 // main is the main area excluding help.
257 main uv.Rectangle
258
259 // chat is the area for the chat pane.
260 chat uv.Rectangle
261
262 // editor is the area for the editor pane.
263 editor uv.Rectangle
264
265 // sidebar is the area for the sidebar.
266 sidebar uv.Rectangle
267
268 // help is the area for the help view.
269 help uv.Rectangle
270}