mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
Merge remote-tracking branch 'origin/main' into onboarding
This commit is contained in:
2
go.mod
2
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/aymanbagabas/go-udiff v0.3.1
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/charlievieth/fastwalk v1.0.11
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
|
||||
github.com/charmbracelet/fang v0.1.0
|
||||
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
|
||||
|
||||
4
go.sum
4
go.sum
@@ -68,8 +68,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
|
||||
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 h1:CkMS9Ah9ac1Ego5JDC5NJyZyAAqu23Z+O0yDwsa3IxM=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||
github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
|
||||
github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
|
||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||
|
||||
@@ -98,7 +98,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
|
||||
// Start spinner if not in quiet mode
|
||||
var spinner *format.Spinner
|
||||
if !quiet {
|
||||
spinner = format.NewSpinner("Thinking...")
|
||||
spinner = format.NewSpinner(ctx, "Generating")
|
||||
spinner.Start()
|
||||
defer spinner.Stop()
|
||||
}
|
||||
@@ -129,6 +129,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
|
||||
}
|
||||
|
||||
result := <-done
|
||||
|
||||
// Stop spinner before printing output
|
||||
if !quiet && spinner != nil {
|
||||
spinner.Stop()
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
|
||||
slog.Info("Agent processing cancelled", "session_id", sess.ID)
|
||||
@@ -137,11 +143,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
|
||||
return fmt.Errorf("agent processing failed: %w", result.Error)
|
||||
}
|
||||
|
||||
// Stop spinner before printing output
|
||||
if !quiet && spinner != nil {
|
||||
spinner.Stop()
|
||||
}
|
||||
|
||||
// Get the text content from the response
|
||||
content := "No content available"
|
||||
if result.Message.Content().String() != "" {
|
||||
|
||||
@@ -2,101 +2,63 @@ package format
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/crush/internal/tui/components/anim"
|
||||
"github.com/charmbracelet/crush/internal/tui/styles"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
// Spinner wraps the bubbles spinner for non-interactive mode
|
||||
type Spinner struct {
|
||||
model spinner.Model
|
||||
done chan struct{}
|
||||
prog *tea.Program
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
prog *tea.Program
|
||||
}
|
||||
|
||||
// spinnerModel is the tea.Model for the spinner
|
||||
type spinnerModel struct {
|
||||
spinner spinner.Model
|
||||
message string
|
||||
quitting bool
|
||||
}
|
||||
|
||||
func (m spinnerModel) Init() tea.Cmd {
|
||||
return m.spinner.Tick
|
||||
}
|
||||
|
||||
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
case quitMsg:
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m spinnerModel) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
|
||||
}
|
||||
|
||||
// quitMsg is sent when we want to quit the spinner
|
||||
type quitMsg struct{}
|
||||
|
||||
// NewSpinner creates a new spinner with the given message
|
||||
func NewSpinner(message string) *Spinner {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
s.Style = s.Style.Foreground(s.Style.GetForeground())
|
||||
func NewSpinner(ctx context.Context, message string) *Spinner {
|
||||
t := styles.CurrentTheme()
|
||||
model := anim.New(anim.Settings{
|
||||
Size: 10,
|
||||
Label: message,
|
||||
LabelColor: t.FgBase,
|
||||
GradColorA: t.Primary,
|
||||
GradColorB: t.Secondary,
|
||||
CycleColors: true,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
model := spinnerModel{
|
||||
spinner: s,
|
||||
message: message,
|
||||
}
|
||||
|
||||
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
||||
prog := tea.NewProgram(
|
||||
model,
|
||||
tea.WithInput(nil),
|
||||
tea.WithOutput(os.Stderr),
|
||||
tea.WithContext(ctx),
|
||||
tea.WithoutCatchPanics(),
|
||||
)
|
||||
|
||||
return &Spinner{
|
||||
model: s,
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
prog: prog,
|
||||
done: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the spinner animation
|
||||
func (s *Spinner) Start() {
|
||||
go func() {
|
||||
defer close(s.done)
|
||||
go func() {
|
||||
<-s.ctx.Done()
|
||||
s.prog.Send(quitMsg{})
|
||||
}()
|
||||
_, err := s.prog.Run()
|
||||
if err != nil {
|
||||
// ensures line is cleared
|
||||
fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
|
||||
}
|
||||
close(s.done)
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop ends the spinner animation
|
||||
func (s *Spinner) Stop() {
|
||||
s.cancel()
|
||||
s.prog.Quit()
|
||||
<-s.done
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ const (
|
||||
maxAttachments = 5
|
||||
)
|
||||
|
||||
type openEditorMsg struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
func (m *editorCmp) openEditor(value string) tea.Cmd {
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
@@ -115,11 +119,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
|
||||
return util.ReportWarn("Message is empty")
|
||||
}
|
||||
os.Remove(tmpfile.Name())
|
||||
attachments := m.attachments
|
||||
m.attachments = nil
|
||||
return chat.SendMsg{
|
||||
Text: string(content),
|
||||
Attachments: attachments,
|
||||
return openEditorMsg{
|
||||
Text: strings.TrimSpace(string(content)),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -189,6 +190,9 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.completionsStartIndex = 0
|
||||
return m, nil
|
||||
}
|
||||
case openEditorMsg:
|
||||
m.textarea.SetValue(msg.Text)
|
||||
m.textarea.MoveToEnd()
|
||||
case tea.KeyPressMsg:
|
||||
switch {
|
||||
// Completions
|
||||
@@ -402,11 +406,11 @@ func New(app *app.App) Editor {
|
||||
t := styles.CurrentTheme()
|
||||
ta := textarea.New()
|
||||
ta.SetStyles(t.S().TextArea)
|
||||
ta.SetPromptFunc(4, func(lineIndex int, focused bool) string {
|
||||
if lineIndex == 0 {
|
||||
ta.SetPromptFunc(4, func(info textarea.PromptInfo) string {
|
||||
if info.LineNumber == 0 {
|
||||
return " > "
|
||||
}
|
||||
if focused {
|
||||
if info.Focused {
|
||||
return t.S().Base.Foreground(t.GreenDark).Render("::: ")
|
||||
} else {
|
||||
return t.S().Muted.Render("::: ")
|
||||
|
||||
28
vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go
generated
vendored
28
vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go
generated
vendored
@@ -133,6 +133,13 @@ type LineInfo struct {
|
||||
CharOffset int
|
||||
}
|
||||
|
||||
// PromptInfo is a struct that can be used to store information about the
|
||||
// prompt.
|
||||
type PromptInfo struct {
|
||||
LineNumber int
|
||||
Focused bool
|
||||
}
|
||||
|
||||
// CursorStyle is the style for real and virtual cursors.
|
||||
type CursorStyle struct {
|
||||
// Style styles the cursor block.
|
||||
@@ -287,7 +294,7 @@ type Model struct {
|
||||
|
||||
// If promptFunc is set, it replaces Prompt as a generator for
|
||||
// prompt strings at the beginning of each line.
|
||||
promptFunc func(line int, focused bool) string
|
||||
promptFunc func(PromptInfo) string
|
||||
|
||||
// promptWidth is the width of the prompt.
|
||||
promptWidth int
|
||||
@@ -983,14 +990,14 @@ func (m Model) Width() int {
|
||||
return m.width
|
||||
}
|
||||
|
||||
// moveToBegin moves the cursor to the beginning of the input.
|
||||
func (m *Model) moveToBegin() {
|
||||
// MoveToBegin moves the cursor to the beginning of the input.
|
||||
func (m *Model) MoveToBegin() {
|
||||
m.row = 0
|
||||
m.SetCursorColumn(0)
|
||||
}
|
||||
|
||||
// moveToEnd moves the cursor to the end of the input.
|
||||
func (m *Model) moveToEnd() {
|
||||
// MoveToEnd moves the cursor to the end of the input.
|
||||
func (m *Model) MoveToEnd() {
|
||||
m.row = len(m.value) - 1
|
||||
m.SetCursorColumn(len(m.value[m.row]))
|
||||
}
|
||||
@@ -1052,7 +1059,7 @@ func (m *Model) SetWidth(w int) {
|
||||
// promptWidth, it will be padded to the left. If it returns a prompt that is
|
||||
// longer, display artifacts may occur; the caller is responsible for computing
|
||||
// an adequate promptWidth.
|
||||
func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int, focused bool) string) {
|
||||
func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {
|
||||
m.promptFunc = fn
|
||||
m.promptWidth = promptWidth
|
||||
}
|
||||
@@ -1170,9 +1177,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
case key.Matches(msg, m.KeyMap.WordBackward):
|
||||
m.wordLeft()
|
||||
case key.Matches(msg, m.KeyMap.InputBegin):
|
||||
m.moveToBegin()
|
||||
m.MoveToBegin()
|
||||
case key.Matches(msg, m.KeyMap.InputEnd):
|
||||
m.moveToEnd()
|
||||
m.MoveToEnd()
|
||||
case key.Matches(msg, m.KeyMap.LowercaseWordForward):
|
||||
m.lowercaseRight()
|
||||
case key.Matches(msg, m.KeyMap.UppercaseWordForward):
|
||||
@@ -1320,7 +1327,10 @@ func (m Model) promptView(displayLine int) (prompt string) {
|
||||
if m.promptFunc == nil {
|
||||
return prompt
|
||||
}
|
||||
prompt = m.promptFunc(displayLine, m.focus)
|
||||
prompt = m.promptFunc(PromptInfo{
|
||||
LineNumber: displayLine,
|
||||
Focused: m.focus,
|
||||
})
|
||||
width := lipgloss.Width(prompt)
|
||||
if width < m.promptWidth {
|
||||
prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
|
||||
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -242,7 +242,7 @@ github.com/bmatcuk/doublestar/v4
|
||||
github.com/charlievieth/fastwalk
|
||||
github.com/charlievieth/fastwalk/internal/dirent
|
||||
github.com/charlievieth/fastwalk/internal/fmtdirent
|
||||
# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
|
||||
# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
|
||||
## explicit; go 1.23.0
|
||||
github.com/charmbracelet/bubbles/v2/cursor
|
||||
github.com/charmbracelet/bubbles/v2/filepicker
|
||||
|
||||
Reference in New Issue
Block a user