ui.go

  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}