init.go

  1package dialog
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7
  8	"github.com/charmbracelet/crush/internal/tui/styles"
  9	"github.com/charmbracelet/crush/internal/tui/util"
 10)
 11
 12// InitDialogCmp is a component that asks the user if they want to initialize the project.
 13type InitDialogCmp struct {
 14	width, height int
 15	selected      int
 16	keys          initDialogKeyMap
 17}
 18
 19// NewInitDialogCmp creates a new InitDialogCmp.
 20func NewInitDialogCmp() InitDialogCmp {
 21	return InitDialogCmp{
 22		selected: 0,
 23		keys:     initDialogKeyMap{},
 24	}
 25}
 26
 27type initDialogKeyMap struct {
 28	Tab    key.Binding
 29	Left   key.Binding
 30	Right  key.Binding
 31	Enter  key.Binding
 32	Escape key.Binding
 33	Y      key.Binding
 34	N      key.Binding
 35}
 36
 37// ShortHelp implements key.Map.
 38func (k initDialogKeyMap) ShortHelp() []key.Binding {
 39	return []key.Binding{
 40		key.NewBinding(
 41			key.WithKeys("tab", "left", "right"),
 42			key.WithHelp("tab/←/→", "toggle selection"),
 43		),
 44		key.NewBinding(
 45			key.WithKeys("enter"),
 46			key.WithHelp("enter", "confirm"),
 47		),
 48		key.NewBinding(
 49			key.WithKeys("esc", "q"),
 50			key.WithHelp("esc/q", "cancel"),
 51		),
 52		key.NewBinding(
 53			key.WithKeys("y", "n"),
 54			key.WithHelp("y/n", "yes/no"),
 55		),
 56	}
 57}
 58
 59// FullHelp implements key.Map.
 60func (k initDialogKeyMap) FullHelp() [][]key.Binding {
 61	return [][]key.Binding{k.ShortHelp()}
 62}
 63
 64// Init implements tea.Model.
 65func (m InitDialogCmp) Init() tea.Cmd {
 66	return nil
 67}
 68
 69// Update implements tea.Model.
 70func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 71	switch msg := msg.(type) {
 72	case tea.KeyPressMsg:
 73		switch {
 74		case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
 75			return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
 76		case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
 77			m.selected = (m.selected + 1) % 2
 78			return m, nil
 79		case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
 80			return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
 81		case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
 82			return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
 83		case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
 84			return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
 85		}
 86	case tea.WindowSizeMsg:
 87		m.width = msg.Width
 88		m.height = msg.Height
 89	}
 90	return m, nil
 91}
 92
 93// View implements tea.Model.
 94func (m InitDialogCmp) View() string {
 95	t := styles.CurrentTheme()
 96	baseStyle := t.S().Base
 97
 98	// Calculate width needed for content
 99	maxWidth := 60 // Width for explanation text
100
101	title := baseStyle.
102		Foreground(t.Primary).
103		Bold(true).
104		Width(maxWidth).
105		Padding(0, 1).
106		Render("Initialize Project")
107
108	explanation := t.S().Text.
109		Width(maxWidth).
110		Padding(0, 1).
111		Render("Initialization generates a new Crush.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
112
113	question := t.S().Text.
114		Width(maxWidth).
115		Padding(1, 1).
116		Render("Would you like to initialize this project?")
117
118	maxWidth = min(maxWidth, m.width-10)
119	yesStyle := t.S().Text
120	noStyle := yesStyle
121
122	if m.selected == 0 {
123		yesStyle = yesStyle.
124			Background(t.Primary).
125			Bold(true)
126		noStyle = noStyle.
127			Background(t.BgSubtle)
128	} else {
129		noStyle = noStyle.
130			Background(t.Primary).
131			Bold(true)
132		yesStyle = yesStyle.
133			Background(t.BgSubtle)
134	}
135
136	yes := yesStyle.Padding(0, 3).Render("Yes")
137	no := noStyle.Padding(0, 3).Render("No")
138
139	buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render("  "), no)
140	buttons = baseStyle.
141		Width(maxWidth).
142		Padding(1, 0).
143		Render(buttons)
144
145	content := lipgloss.JoinVertical(
146		lipgloss.Left,
147		title,
148		baseStyle.Width(maxWidth).Render(""),
149		explanation,
150		question,
151		buttons,
152		baseStyle.Width(maxWidth).Render(""),
153	)
154
155	return baseStyle.Padding(1, 2).
156		Border(lipgloss.RoundedBorder()).
157		BorderForeground(t.BorderFocus).
158		Width(lipgloss.Width(content) + 4).
159		Render(content)
160}
161
162// SetSize sets the size of the component.
163func (m *InitDialogCmp) SetSize(width, height int) {
164	m.width = width
165	m.height = height
166}
167
168// Bindings implements layout.Bindings.
169func (m InitDialogCmp) Bindings() []key.Binding {
170	return m.keys.ShortHelp()
171}
172
173// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
174type CloseInitDialogMsg struct {
175	Initialize bool
176}
177
178// ShowInitDialogMsg is a message that is sent to show the init dialog.
179type ShowInitDialogMsg struct {
180	Show bool
181}