onboarding.go

  1package model
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6	"strings"
  7
  8	"charm.land/bubbles/v2/key"
  9	tea "charm.land/bubbletea/v2"
 10	"charm.land/lipgloss/v2"
 11
 12	"github.com/charmbracelet/crush/internal/home"
 13	"github.com/charmbracelet/crush/internal/ui/common"
 14	"github.com/charmbracelet/crush/internal/ui/util"
 15)
 16
 17// markProjectInitialized marks the current project as initialized in the config.
 18func (m *UI) markProjectInitialized() tea.Msg {
 19	// TODO: handle error so we show it in the tui footer
 20	err := m.com.Workspace.MarkProjectInitialized()
 21	if err != nil {
 22		slog.Error(err.Error())
 23	}
 24	return nil
 25}
 26
 27// updateInitializeView handles keyboard input for the project initialization prompt.
 28func (m *UI) updateInitializeView(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
 29	switch {
 30	case key.Matches(msg, m.keyMap.Initialize.Enter):
 31		if m.onboarding.yesInitializeSelected {
 32			cmds = append(cmds, m.initializeProject())
 33		} else {
 34			cmds = append(cmds, m.skipInitializeProject())
 35		}
 36	case key.Matches(msg, m.keyMap.Initialize.Switch):
 37		m.onboarding.yesInitializeSelected = !m.onboarding.yesInitializeSelected
 38	case key.Matches(msg, m.keyMap.Initialize.Yes):
 39		cmds = append(cmds, m.initializeProject())
 40	case key.Matches(msg, m.keyMap.Initialize.No):
 41		cmds = append(cmds, m.skipInitializeProject())
 42	}
 43	return cmds
 44}
 45
 46// initializeProject starts project initialization and transitions to the landing view.
 47func (m *UI) initializeProject() tea.Cmd {
 48	// clear the session
 49	var cmds []tea.Cmd
 50	if cmd := m.newSession(); cmd != nil {
 51		cmds = append(cmds, cmd)
 52	}
 53	initialize := func() tea.Msg {
 54		initPrompt, err := m.com.Workspace.InitializePrompt()
 55		if err != nil {
 56			return util.InfoMsg{
 57				Type: util.InfoTypeError,
 58				Msg:  fmt.Sprintf("Failed to initialize project: %v", err),
 59			}
 60		}
 61		return sendMessageMsg{Content: initPrompt}
 62	}
 63	// Mark the project as initialized
 64	cmds = append(cmds, initialize, m.markProjectInitialized)
 65
 66	return tea.Sequence(cmds...)
 67}
 68
 69// skipInitializeProject skips project initialization and transitions to the landing view.
 70func (m *UI) skipInitializeProject() tea.Cmd {
 71	// TODO: initialize the project
 72	m.setState(uiLanding, uiFocusEditor)
 73	// mark the project as initialized
 74	return m.markProjectInitialized
 75}
 76
 77// initializeView renders the project initialization prompt with Yes/No buttons.
 78func (m *UI) initializeView() string {
 79	s := m.com.Styles.Initialize
 80	cwd := home.Short(m.com.Workspace.WorkingDir())
 81	initFile := m.com.Config().Options.InitializeAs
 82
 83	header := s.Header.Render("Would you like to initialize this project?")
 84	path := s.Accent.PaddingLeft(2).Render(cwd)
 85	desc := s.Content.Render(fmt.Sprintf("When I initialize your codebase I examine the project and put the result into an %s file which serves as general context.", initFile))
 86	hint := s.Content.Render("You can also initialize anytime via ") + s.Accent.Render("ctrl+p") + s.Content.Render(".")
 87	prompt := s.Content.Render("Would you like to initialize now?")
 88
 89	buttons := common.ButtonGroup(m.com.Styles, []common.ButtonOpts{
 90		{Text: "Yep!", Selected: m.onboarding.yesInitializeSelected},
 91		{Text: "Nope", Selected: !m.onboarding.yesInitializeSelected},
 92	}, " ")
 93
 94	// max width 60 so the text is compact
 95	width := min(m.layout.main.Dx(), 60)
 96
 97	return lipgloss.NewStyle().
 98		Width(width).
 99		Height(m.layout.main.Dy()).
100		PaddingBottom(1).
101		AlignVertical(lipgloss.Bottom).
102		Render(strings.Join(
103			[]string{
104				header,
105				path,
106				desc,
107				hint,
108				prompt,
109				buttons,
110			},
111			"\n\n",
112		))
113}