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