summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorEd Zynda <[email protected]>2025-05-16 14:06:28 +0300
committerGitHub <[email protected]>2025-05-16 06:06:28 -0500
commit623d132772b9c69dd6d99ed4004b26c46dbe43a4 (patch)
tree1547bc8b7f7b8487dbdc34c76998416a37db7618 /internal
parentd127a1c4ebe326344dc77fe3d136c033da6031fd (diff)
downloadopencode-623d132772b9c69dd6d99ed4004b26c46dbe43a4.tar.gz
opencode-623d132772b9c69dd6d99ed4004b26c46dbe43a4.zip
feat: Add non-interactive mode (#18)
Diffstat (limited to 'internal')
-rw-r--r--internal/format/format.go46
-rw-r--r--internal/format/format_test.go90
-rw-r--r--internal/tui/components/spinner/spinner.go102
-rw-r--r--internal/tui/components/spinner/spinner_test.go24
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