init.go

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