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 showFullHelp bool
30
31 keyMap KeyMap
32
33 chat *ChatModel
34 editor *EditorModel
35 dialog *dialog.Overlay
36 help help.Model
37
38 layout layout
39}
40
41func New(com *common.Common, app *app.App) *UI {
42 return &UI{
43 app: app,
44 com: com,
45 dialog: dialog.NewOverlay(),
46 keyMap: DefaultKeyMap(),
47 editor: NewEditorModel(com, app),
48 help: help.New(),
49 }
50}
51
52func (m *UI) Init() tea.Cmd {
53 return nil
54}
55
56func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
57 var cmds []tea.Cmd
58 switch msg := msg.(type) {
59 case tea.WindowSizeMsg:
60 m.updateLayout(msg.Width, msg.Height)
61 m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy())
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.showFullHelp = !m.showFullHelp
77 m.help.ShowAll = m.showFullHelp
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 layers = append(layers, lipgloss.NewLayer(
134 lipgloss.NewStyle().Width(chatRect.Dx()).
135 Height(chatRect.Dy()).
136 Background(lipgloss.ANSIColor(rand.Intn(256))).
137 Render(" Main View "),
138 ).X(chatRect.Min.X).Y(chatRect.Min.Y),
139 lipgloss.NewLayer(
140 lipgloss.NewStyle().Width(sideRect.Dx()).
141 Height(sideRect.Dy()).
142 Background(lipgloss.ANSIColor(rand.Intn(256))).
143 Render(" Side View "),
144 ).X(sideRect.Min.X).Y(sideRect.Min.Y),
145 lipgloss.NewLayer(m.editor.View()).
146 X(editRect.Min.X).Y(editRect.Min.Y),
147 lipgloss.NewLayer(
148 lipgloss.NewStyle().Width(helpRect.Dx()).
149 Height(helpRect.Dy()).
150 Background(lipgloss.ANSIColor(rand.Intn(256))).
151 Render(m.help.View(helpKeyMap)),
152 ).X(helpRect.Min.X).Y(helpRect.Min.Y),
153 )
154
155 v.Layer = lipgloss.NewCanvas(layers...)
156
157 return v
158}
159
160func (m *UI) focusedKeyMap() help.KeyMap {
161 if m.state == uiChat {
162 return m.chat
163 }
164 return m.editor
165}
166
167// updateDialogs updates the dialog overlay with the given message and appends
168// any resulting commands to the cmds slice.
169func (m *UI) updateDialogs(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
170 updatedDialog, cmd := m.dialog.Update(msg)
171 m.dialog = updatedDialog
172 if cmd != nil {
173 *cmds = append(*cmds, cmd)
174 }
175}
176
177// updateFocused updates the focused model (chat or editor) with the given message
178// and appends any resulting commands to the cmds slice.
179func (m *UI) updateFocused(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
180 switch m.state {
181 case uiChat:
182 m.updateChat(msg, cmds)
183 case uiEdit:
184 m.updateEditor(msg, cmds)
185 }
186}
187
188// updateChat updates the chat model with the given message and appends any
189// resulting commands to the cmds slice.
190func (m *UI) updateChat(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
191 updatedChat, cmd := m.chat.Update(msg)
192 m.chat = updatedChat
193 if cmd != nil {
194 *cmds = append(*cmds, cmd)
195 }
196}
197
198// updateEditor updates the editor model with the given message and appends any
199// resulting commands to the cmds slice.
200func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
201 updatedEditor, cmd := m.editor.Update(msg)
202 m.editor = updatedEditor
203 if cmd != nil {
204 *cmds = append(*cmds, cmd)
205 }
206}
207
208// updateLayout updates the layout based on the given terminal width and
209// height given in cells.
210func (m *UI) updateLayout(w, h int) {
211 // The screen area we're working with
212 area := image.Rect(1, 1, w-1, h-1) // -1 for margins
213 helpKeyMap := m.focusedKeyMap()
214 helpHeight := 1
215 if m.showFullHelp {
216 helpHeight = max(1, len(helpKeyMap.FullHelp()))
217 }
218
219 chatRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40))
220 chatRect, editRect := uv.SplitVertical(chatRect, uv.Fixed(area.Dy()-5-helpHeight))
221 // Add 1 line margin bottom of mainRect
222 chatRect, _ = uv.SplitVertical(chatRect, uv.Fixed(chatRect.Dy()-1))
223 editRect, helpRect := uv.SplitVertical(editRect, uv.Fixed(5))
224 // Add 1 line margin bottom of footRect
225 editRect, _ = uv.SplitVertical(editRect, uv.Fixed(editRect.Dy()-1))
226
227 m.layout = layout{
228 area: area,
229 chat: chatRect,
230 editor: editRect,
231 sidebar: sideRect,
232 help: helpRect,
233 }
234}
235
236// layout defines the positioning of UI elements.
237type layout struct {
238 // area is the overall available area.
239 area uv.Rectangle
240
241 // chat is the area for the chat pane.
242 chat uv.Rectangle
243
244 // editor is the area for the editor pane.
245 editor uv.Rectangle
246
247 // sidebar is the area for the sidebar.
248 sidebar uv.Rectangle
249
250 // help is the area for the help view.
251 help uv.Rectangle
252}