on main help sign in
Merge remote-tracking branch 'origin/main' into onboarding
commitcdb0bda734dea0cb92f99a210eacca8b2fef72ea authorKujtim Hoxha <kujtimii.h@gmail.com> dateThu 10 Jul 2025, 21:06 (1 year ago) parent3509036dd50968
7 files changed+72−95
go.mod+1−1 go.sum+2−2 internal/app/app.go+7−6 internal/format/spinner.go+30−68 internal/tui/components/chat/editor/editor.go+12−8 vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go+19−9 vendor/modules.txt+1−1
go.mod+1−1
@@ -16,7 +16,7 @@ require (
1616 github.com/aymanbagabas/go-udiff v0.3.1
1717 github.com/bmatcuk/doublestar/v4 v4.8.1
1818 github.com/charlievieth/fastwalk v1.0.11
19− github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
19+ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
2020 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
2121 github.com/charmbracelet/fang v0.1.0
2222 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
go.sum+2−2
@@ -68,8 +68,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
6868github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
6969github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
7070github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
71−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
72−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
71+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 h1:CkMS9Ah9ac1Ego5JDC5NJyZyAAqu23Z+O0yDwsa3IxM=
72+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
7373github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
7474github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
7575github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
internal/app/app.go+7−6
@@ -98,7 +98,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
9898 // Start spinner if not in quiet mode
9999 var spinner *format.Spinner
100100 if !quiet {
101− spinner = format.NewSpinner("Thinking...")
101+ spinner = format.NewSpinner(ctx, "Generating")
102102 spinner.Start()
103103 defer spinner.Stop()
104104 }
@@ -129,6 +129,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
129129 }
130130
131131 result := <-done
132+
133+ // Stop spinner before printing output
134+ if !quiet && spinner != nil {
135+ spinner.Stop()
136+ }
137+
132138 if result.Error != nil {
133139 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
134140 slog.Info("Agent processing cancelled", "session_id", sess.ID)
@@ -137,11 +143,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
137143 return fmt.Errorf("agent processing failed: %w", result.Error)
138144 }
139145
140− // Stop spinner before printing output
141− if !quiet && spinner != nil {
142− spinner.Stop()
143− }
144−
145146 // Get the text content from the response
146147 content := "No content available"
147148 if result.Message.Content().String() != "" {
internal/format/spinner.go+30−68
@@ -2,101 +2,63 @@ package format
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "os"
78
8− "github.com/charmbracelet/bubbles/v2/spinner"
99 tea "github.com/charmbracelet/bubbletea/v2"
10+ "github.com/charmbracelet/crush/internal/tui/components/anim"
11+ "github.com/charmbracelet/crush/internal/tui/styles"
12+ "github.com/charmbracelet/x/ansi"
1013)
1114
1215// Spinner wraps the bubbles spinner for non-interactive mode
1316type Spinner struct {
14− model spinner.Model
15− done chan struct{}
16− prog *tea.Program
17− ctx context.Context
18− cancel context.CancelFunc
17+ done chan struct{}
18+ prog *tea.Program
1919}
2020
21−// spinnerModel is the tea.Model for the spinner
22−type spinnerModel struct {
23− spinner spinner.Model
24− message string
25− quitting bool
26−}
27−
28−func (m spinnerModel) Init() tea.Cmd {
29− return m.spinner.Tick
30−}
31−
32−func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
33− switch msg := msg.(type) {
34− case tea.KeyPressMsg:
35− m.quitting = true
36− return m, tea.Quit
37− case spinner.TickMsg:
38− var cmd tea.Cmd
39− m.spinner, cmd = m.spinner.Update(msg)
40− return m, cmd
41− case quitMsg:
42− m.quitting = true
43− return m, tea.Quit
44− default:
45− return m, nil
46− }
47−}
48−
49−func (m spinnerModel) View() string {
50− if m.quitting {
51− return ""
52− }
53− return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
54−}
55−
56−// quitMsg is sent when we want to quit the spinner
57−type quitMsg struct{}
58−
5921// NewSpinner creates a new spinner with the given message
60−func NewSpinner(message string) *Spinner {
61− s := spinner.New()
62− s.Spinner = spinner.Dot
63− s.Style = s.Style.Foreground(s.Style.GetForeground())
64−
65− ctx, cancel := context.WithCancel(context.Background())
66−
67− model := spinnerModel{
68− spinner: s,
69− message: message,
70− }
22+func NewSpinner(ctx context.Context, message string) *Spinner {
23+ t := styles.CurrentTheme()
24+ model := anim.New(anim.Settings{
25+ Size: 10,
26+ Label: message,
27+ LabelColor: t.FgBase,
28+ GradColorA: t.Primary,
29+ GradColorB: t.Secondary,
30+ CycleColors: true,
31+ })
7132
72− prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
33+ prog := tea.NewProgram(
34+ model,
35+ tea.WithInput(nil),
36+ tea.WithOutput(os.Stderr),
37+ tea.WithContext(ctx),
38+ tea.WithoutCatchPanics(),
39+ )
7340
7441 return &Spinner{
75− model: s,
76− done: make(chan struct{}),
77− prog: prog,
78− ctx: ctx,
79− cancel: cancel,
42+ prog: prog,
43+ done: make(chan struct{}, 1),
8044 }
8145}
8246
8347// Start begins the spinner animation
8448func (s *Spinner) Start() {
8549 go func() {
86− defer close(s.done)
87− go func() {
88− <-s.ctx.Done()
89− s.prog.Send(quitMsg{})
90− }()
9150 _, err := s.prog.Run()
92− if err != nil {
51+ // ensures line is cleared
52+ fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
53+ if err != nil && !errors.Is(err, context.Canceled) {
9354 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
9455 }
56+ close(s.done)
9557 }()
9658}
9759
9860// Stop ends the spinner animation
9961func (s *Spinner) Stop() {
100− s.cancel()
62+ s.prog.Quit()
10163 <-s.done
10264}
internal/tui/components/chat/editor/editor.go+12−8
@@ -80,6 +80,10 @@ const (
8080 maxAttachments = 5
8181)
8282
83+type openEditorMsg struct {
84+ Text string
85+}
86+
8387func (m *editorCmp) openEditor(value string) tea.Cmd {
8488 editor := os.Getenv("EDITOR")
8589 if editor == "" {
@@ -115,11 +119,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
115119 return util.ReportWarn("Message is empty")
116120 }
117121 os.Remove(tmpfile.Name())
118− attachments := m.attachments
119− m.attachments = nil
120− return chat.SendMsg{
121− Text: string(content),
122− Attachments: attachments,
122+ return openEditorMsg{
123+ Text: strings.TrimSpace(string(content)),
123124 }
124125 })
125126}
@@ -189,6 +190,9 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189190 m.completionsStartIndex = 0
190191 return m, nil
191192 }
193+ case openEditorMsg:
194+ m.textarea.SetValue(msg.Text)
195+ m.textarea.MoveToEnd()
192196 case tea.KeyPressMsg:
193197 switch {
194198 // Completions
@@ -402,11 +406,11 @@ func New(app *app.App) Editor {
402406 t := styles.CurrentTheme()
403407 ta := textarea.New()
404408 ta.SetStyles(t.S().TextArea)
405− ta.SetPromptFunc(4, func(lineIndex int, focused bool) string {
406− if lineIndex == 0 {
409+ ta.SetPromptFunc(4, func(info textarea.PromptInfo) string {
410+ if info.LineNumber == 0 {
407411 return " > "
408412 }
409− if focused {
413+ if info.Focused {
410414 return t.S().Base.Foreground(t.GreenDark).Render("::: ")
411415 } else {
412416 return t.S().Muted.Render("::: ")
vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go+19−9
@@ -133,6 +133,13 @@ type LineInfo struct {
133133 CharOffset int
134134}
135135
136+// PromptInfo is a struct that can be used to store information about the
137+// prompt.
138+type PromptInfo struct {
139+ LineNumber int
140+ Focused bool
141+}
142+
136143// CursorStyle is the style for real and virtual cursors.
137144type CursorStyle struct {
138145 // Style styles the cursor block.
@@ -287,7 +294,7 @@ type Model struct {
287294
288295 // If promptFunc is set, it replaces Prompt as a generator for
289296 // prompt strings at the beginning of each line.
290− promptFunc func(line int, focused bool) string
297+ promptFunc func(PromptInfo) string
291298
292299 // promptWidth is the width of the prompt.
293300 promptWidth int
@@ -983,14 +990,14 @@ func (m Model) Width() int {
983990 return m.width
984991}
985992
986−// moveToBegin moves the cursor to the beginning of the input.
987−func (m *Model) moveToBegin() {
993+// MoveToBegin moves the cursor to the beginning of the input.
994+func (m *Model) MoveToBegin() {
988995 m.row = 0
989996 m.SetCursorColumn(0)
990997}
991998
992−// moveToEnd moves the cursor to the end of the input.
993−func (m *Model) moveToEnd() {
999+// MoveToEnd moves the cursor to the end of the input.
1000+func (m *Model) MoveToEnd() {
9941001 m.row = len(m.value) - 1
9951002 m.SetCursorColumn(len(m.value[m.row]))
9961003}
@@ -1052,7 +1059,7 @@ func (m *Model) SetWidth(w int) {
10521059// promptWidth, it will be padded to the left. If it returns a prompt that is
10531060// longer, display artifacts may occur; the caller is responsible for computing
10541061// an adequate promptWidth.
1055−func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int, focused bool) string) {
1062+func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {
10561063 m.promptFunc = fn
10571064 m.promptWidth = promptWidth
10581065}
@@ -1170,9 +1177,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
11701177 case key.Matches(msg, m.KeyMap.WordBackward):
11711178 m.wordLeft()
11721179 case key.Matches(msg, m.KeyMap.InputBegin):
1173− m.moveToBegin()
1180+ m.MoveToBegin()
11741181 case key.Matches(msg, m.KeyMap.InputEnd):
1175− m.moveToEnd()
1182+ m.MoveToEnd()
11761183 case key.Matches(msg, m.KeyMap.LowercaseWordForward):
11771184 m.lowercaseRight()
11781185 case key.Matches(msg, m.KeyMap.UppercaseWordForward):
@@ -1320,7 +1327,10 @@ func (m Model) promptView(displayLine int) (prompt string) {
13201327 if m.promptFunc == nil {
13211328 return prompt
13221329 }
1323− prompt = m.promptFunc(displayLine, m.focus)
1330+ prompt = m.promptFunc(PromptInfo{
1331+ LineNumber: displayLine,
1332+ Focused: m.focus,
1333+ })
13241334 width := lipgloss.Width(prompt)
13251335 if width < m.promptWidth {
13261336 prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
vendor/modules.txt+1−1
@@ -242,7 +242,7 @@ github.com/bmatcuk/doublestar/v4
242242github.com/charlievieth/fastwalk
243243github.com/charlievieth/fastwalk/internal/dirent
244244github.com/charlievieth/fastwalk/internal/fmtdirent
245−# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
245+# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
246246## explicit; go 1.23.0
247247github.com/charmbracelet/bubbles/v2/cursor
248248github.com/charmbracelet/bubbles/v2/filepicker