diff options
| author | Ed Zynda <[email protected]> | 2025-05-16 14:06:28 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-05-16 06:06:28 -0500 |
| commit | 623d132772b9c69dd6d99ed4004b26c46dbe43a4 (patch) | |
| tree | 1547bc8b7f7b8487dbdc34c76998416a37db7618 /internal | |
| parent | d127a1c4ebe326344dc77fe3d136c033da6031fd (diff) | |
| download | opencode-623d132772b9c69dd6d99ed4004b26c46dbe43a4.tar.gz opencode-623d132772b9c69dd6d99ed4004b26c46dbe43a4.zip | |
feat: Add non-interactive mode (#18)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/format/format.go | 46 | ||||
| -rw-r--r-- | internal/format/format_test.go | 90 | ||||
| -rw-r--r-- | internal/tui/components/spinner/spinner.go | 102 | ||||
| -rw-r--r-- | internal/tui/components/spinner/spinner_test.go | 24 |
4 files changed, 262 insertions, 0 deletions
diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 000000000..321f5c102 --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,46 @@ +package format + +import ( + "encoding/json" + "fmt" +) + +// OutputFormat represents the format for non-interactive mode output +type OutputFormat string + +const ( + // TextFormat is plain text output (default) + TextFormat OutputFormat = "text" + + // JSONFormat is output wrapped in a JSON object + JSONFormat OutputFormat = "json" +) + +// IsValid checks if the output format is valid +func (f OutputFormat) IsValid() bool { + return f == TextFormat || f == JSONFormat +} + +// String returns the string representation of the output format +func (f OutputFormat) String() string { + return string(f) +} + +// FormatOutput formats the given content according to the specified format +func FormatOutput(content string, format OutputFormat) (string, error) { + switch format { + case TextFormat: + return content, nil + case JSONFormat: + jsonData := map[string]string{ + "response": content, + } + jsonBytes, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal JSON: %w", err) + } + return string(jsonBytes), nil + default: + return "", fmt.Errorf("unsupported output format: %s", format) + } +} diff --git a/internal/format/format_test.go b/internal/format/format_test.go new file mode 100644 index 000000000..04054a7c4 --- /dev/null +++ b/internal/format/format_test.go @@ -0,0 +1,90 @@ +package format + +import ( + "testing" +) + +func TestOutputFormat_IsValid(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + format OutputFormat + want bool + }{ + { + name: "text format", + format: TextFormat, + want: true, + }, + { + name: "json format", + format: JSONFormat, + want: true, + }, + { + name: "invalid format", + format: "invalid", + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.format.IsValid(); got != tt.want { + t.Errorf("OutputFormat.IsValid() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFormatOutput(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + format OutputFormat + want string + wantErr bool + }{ + { + name: "text format", + content: "test content", + format: TextFormat, + want: "test content", + wantErr: false, + }, + { + name: "json format", + content: "test content", + format: JSONFormat, + want: "{\n \"response\": \"test content\"\n}", + wantErr: false, + }, + { + name: "invalid format", + content: "test content", + format: "invalid", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := FormatOutput(tt.content, tt.format) + if (err != nil) != tt.wantErr { + t.Errorf("FormatOutput() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("FormatOutput() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/tui/components/spinner/spinner.go b/internal/tui/components/spinner/spinner.go new file mode 100644 index 000000000..42b98810a --- /dev/null +++ b/internal/tui/components/spinner/spinner.go @@ -0,0 +1,102 @@ +package spinner + +import ( + "context" + "fmt" + "os" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" +) + +// Spinner wraps the bubbles spinner for both interactive and non-interactive mode +type Spinner struct { + model spinner.Model + done chan struct{} + prog *tea.Program + ctx context.Context + cancel context.CancelFunc +} + +// 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.KeyMsg: + 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()) + + ctx, cancel := context.WithCancel(context.Background()) + + model := spinnerModel{ + spinner: s, + message: message, + } + + prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics()) + + return &Spinner{ + model: s, + done: make(chan struct{}), + prog: prog, + ctx: ctx, + cancel: cancel, + } +} + +// 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 { + fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err) + } + }() +} + +// Stop ends the spinner animation +func (s *Spinner) Stop() { + s.cancel() + <-s.done +}
\ No newline at end of file diff --git a/internal/tui/components/spinner/spinner_test.go b/internal/tui/components/spinner/spinner_test.go new file mode 100644 index 000000000..065726e91 --- /dev/null +++ b/internal/tui/components/spinner/spinner_test.go @@ -0,0 +1,24 @@ +package spinner + +import ( + "testing" + "time" +) + +func TestSpinner(t *testing.T) { + t.Parallel() + + // Create a spinner + s := NewSpinner("Test spinner") + + // Start the spinner + s.Start() + + // Wait a bit to let it run + time.Sleep(100 * time.Millisecond) + + // Stop the spinner + s.Stop() + + // If we got here without panicking, the test passes +}
\ No newline at end of file |
