onboarding.go

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