init.go 4.7 KB

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