14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/home"
16 "github.com/charmbracelet/crush/internal/shell"
19// Prompt represents a template-based prompt generator.
28type PromptDat struct {
37 ContextFiles []ContextFile
40type ContextFile struct {
45type Option func(*Prompt)
47func WithTimeFunc(fn func() time.Time) Option {
48 return func(p *Prompt) {
53func WithPlatform(platform string) Option {
54 return func(p *Prompt) {
59func WithWorkingDir(workingDir string) Option {
60 return func(p *Prompt) {
61 p.workingDir = workingDir
65func NewPrompt(name, promptTemplate string, opts ...Option) (*Prompt, error) {
68 template: promptTemplate,
71 for _, opt := range opts {
77func (p *Prompt) Build(ctx context.Context, provider, model string, cfg config.Config) (string, error) {
78 t, err := template.New(p.name).Parse(p.template)
80 return "", fmt.Errorf("parsing template: %w", err)
82 var sb strings.Builder
83 d, err := p.promptData(ctx, provider, model, cfg)
87 if err := t.Execute(&sb, d); err != nil {
88 return "", fmt.Errorf("executing template: %w", err)
91 return sb.String(), nil
94func processFile(filePath string) *ContextFile {
95 content, err := os.ReadFile(filePath)
101 Content: string(content),
105func processContextPath(p string, cfg config.Config) []ContextFile {
106 var contexts []ContextFile
108 if !filepath.IsAbs(p) {
109 fullPath = filepath.Join(cfg.WorkingDir(), p)
111 info, err := os.Stat(fullPath)
116 filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
121 if result := processFile(path); result != nil {
122 contexts = append(contexts, *result)
128 result := processFile(fullPath)
130 contexts = append(contexts, *result)
136// expandPath expands ~ and environment variables in file paths
137func expandPath(path string, cfg config.Config) string {
138 path = home.Long(path)
139 // Handle environment variable expansion using the same pattern as config
140 if strings.HasPrefix(path, "$") {
141 if expanded, err := cfg.Resolver().ResolveValue(path); err == nil {
149func (p *Prompt) promptData(ctx context.Context, provider, model string, cfg config.Config) (PromptDat, error) {
150 workingDir := cmp.Or(p.workingDir, cfg.WorkingDir())
151 platform := cmp.Or(p.platform, runtime.GOOS)
153 files := map[string][]ContextFile{}
155 for _, pth := range cfg.Options.ContextPaths {
156 expanded := expandPath(pth, cfg)
157 pathKey := strings.ToLower(expanded)
158 if _, ok := files[pathKey]; ok {
161 content := processContextPath(expanded, cfg)
162 files[pathKey] = content
165 isGit := isGitRepo(cfg.WorkingDir())
170 WorkingDir: filepath.ToSlash(workingDir),
173 Date: p.now().Format("1/2/2006"),
177 data.GitStatus, err = getGitStatus(ctx, cfg.WorkingDir())
179 return PromptDat{}, err
183 for _, contextFiles := range files {
184 data.ContextFiles = append(data.ContextFiles, contextFiles...)
189func isGitRepo(dir string) bool {
190 _, err := os.Stat(filepath.Join(dir, ".git"))
194func getGitStatus(ctx context.Context, dir string) (string, error) {
195 sh := shell.NewShell(&shell.Options{
198 branch, err := getGitBranch(ctx, sh)
202 status, err := getGitStatusSummary(ctx, sh)
206 commits, err := getGitRecentCommits(ctx, sh)
210 return branch + status + commits, nil
213func getGitBranch(ctx context.Context, sh *shell.Shell) (string, error) {
214 out, _, err := sh.Exec(ctx, "git branch --show-current 2>/dev/null")
218 out = strings.TrimSpace(out)
222 return fmt.Sprintf("Current branch: %s\n", out), nil
225func getGitStatusSummary(ctx context.Context, sh *shell.Shell) (string, error) {
226 out, _, err := sh.Exec(ctx, "git status --short 2>/dev/null | head -20")
230 out = strings.TrimSpace(out)
232 return "Status: clean\n", nil
234 return fmt.Sprintf("Status:\n%s\n", out), nil
237func getGitRecentCommits(ctx context.Context, sh *shell.Shell) (string, error) {
238 out, _, err := sh.Exec(ctx, "git log --oneline -n 3 2>/dev/null")
239 if err != nil || out == "" {
242 out = strings.TrimSpace(out)
243 return fmt.Sprintf("Recent commits:\n%s\n", out), nil
246func (p *Prompt) Name() string {
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list