diff options
| author | adamdottv <[email protected]> | 2025-07-08 08:08:53 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-07-08 08:09:01 -0500 |
| commit | 662d022a4859ee1c004133559ee42c5f7044dda7 (patch) | |
| tree | 304b17deceb254d0d70d29e9d46e865b1c4868cf /packages/tui/internal/components | |
| parent | 9efef03919f99750277a7f15722c24d7c7958224 (diff) | |
| download | opencode-662d022a4859ee1c004133559ee42c5f7044dda7.tar.gz opencode-662d022a4859ee1c004133559ee42c5f7044dda7.zip | |
feat(tui): paste images and pdfs
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 93 | ||||
| -rw-r--r-- | packages/tui/internal/components/textarea/textarea.go | 26 |
2 files changed, 82 insertions, 37 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index f32e5c32a..cc31fbef2 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -1,9 +1,12 @@ package chat import ( + "encoding/base64" "fmt" "log/slog" + "os" "path/filepath" + "strconv" "strings" "github.com/charmbracelet/bubbles/v2/spinner" @@ -15,10 +18,10 @@ import ( "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/textarea" - "github.com/sst/opencode/internal/image" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" + "golang.design/x/clipboard" ) type EditorComponent interface { @@ -63,6 +66,57 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } + case tea.PasteMsg: + text := string(msg) + text = strings.ReplaceAll(text, "\\", "") + text, err := strconv.Unquote(`"` + text + `"`) + if err != nil { + slog.Error("Failed to unquote text", "error", err) + m.textarea.InsertRunesFromUserInput([]rune(msg)) + return m, nil + } + if _, err := os.Stat(text); err != nil { + slog.Error("Failed to paste file", "error", err) + m.textarea.InsertRunesFromUserInput([]rune(msg)) + return m, nil + } + + filePath := text + ext := strings.ToLower(filepath.Ext(filePath)) + + mediaType := "" + switch ext { + case ".jpg": + mediaType = "image/jpeg" + case ".png", ".jpeg", ".gif", ".webp": + mediaType = "image/" + ext[1:] + case ".pdf": + mediaType = "application/pdf" + default: + mediaType = "text/plain" + } + + fileBytes, err := os.ReadFile(filePath) + if err != nil { + slog.Error("Failed to read file", "error", err) + m.textarea.InsertRunesFromUserInput([]rune(msg)) + return m, nil + } + base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes) + url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile) + + attachment := &textarea.Attachment{ + ID: uuid.NewString(), + Display: fmt.Sprintf("<%s>", filePath), + URL: url, + Filename: filePath, + MediaType: mediaType, + } + m.textarea.InsertAttachment(attachment) + m.textarea.InsertString(" ") + case tea.ClipboardMsg: + text := string(msg) + m.textarea.InsertRunesFromUserInput([]rune(text)) case dialog.ThemeSelectedMsg: m.textarea = m.resetTextareaStyles() m.spinner = createSpinner() @@ -269,24 +323,29 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) { } func (m *editorComponent) Paste() (tea.Model, tea.Cmd) { - _, text, err := image.GetImageFromClipboard() - if err != nil { - slog.Error(err.Error()) + imageBytes := clipboard.Read(clipboard.FmtImage) + if imageBytes != nil { + base64EncodedFile := base64.StdEncoding.EncodeToString(imageBytes) + attachment := &textarea.Attachment{ + ID: uuid.NewString(), + Display: "<clipboard-image>", + Filename: "clipboard-image", + MediaType: "image/png", + URL: fmt.Sprintf("data:image/png;base64,%s", base64EncodedFile), + } + m.textarea.InsertAttachment(attachment) + m.textarea.InsertString(" ") return m, nil } - // if len(imageBytes) != 0 { - // attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) - // attachment := app.Attachment{ - // FilePath: attachmentName, - // FileName: attachmentName, - // Content: imageBytes, - // MimeType: "image/png", - // } - // m.attachments = append(m.attachments, attachment) - // } else { - m.textarea.InsertString(text) - // } - return m, nil + + textBytes := clipboard.Read(clipboard.FmtText) + if textBytes != nil { + m.textarea.InsertRunesFromUserInput([]rune(string(textBytes))) + return m, nil + } + + // fallback to reading the clipboard using OSC52 + return m, tea.ReadClipboard } func (m *editorComponent) Newline() (tea.Model, tea.Cmd) { diff --git a/packages/tui/internal/components/textarea/textarea.go b/packages/tui/internal/components/textarea/textarea.go index 5ff936f17..41a4a3d91 100644 --- a/packages/tui/internal/components/textarea/textarea.go +++ b/packages/tui/internal/components/textarea/textarea.go @@ -11,7 +11,6 @@ import ( "slices" - "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/v2/cursor" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" @@ -653,12 +652,12 @@ func (m *Model) SetValue(s string) { // InsertString inserts a string at the cursor position. func (m *Model) InsertString(s string) { - m.insertRunesFromUserInput([]rune(s)) + m.InsertRunesFromUserInput([]rune(s)) } // InsertRune inserts a rune at the cursor position. func (m *Model) InsertRune(r rune) { - m.insertRunesFromUserInput([]rune{r}) + m.InsertRunesFromUserInput([]rune{r}) } // InsertAttachment inserts an attachment at the cursor position. @@ -730,8 +729,8 @@ func (m Model) GetAttachments() []*Attachment { return attachments } -// insertRunesFromUserInput inserts runes at the current cursor position. -func (m *Model) insertRunesFromUserInput(runes []rune) { +// InsertRunesFromUserInput inserts runes at the current cursor position. +func (m *Model) InsertRunesFromUserInput(runes []rune) { // Clean up any special characters in the input provided by the // clipboard. This avoids bugs due to e.g. tab characters and // whatnot. @@ -1429,8 +1428,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } switch msg := msg.(type) { - case tea.PasteMsg: - m.insertRunesFromUserInput([]rune(msg)) case tea.KeyPressMsg: switch { case key.Matches(msg, m.KeyMap.DeleteAfterCursor): @@ -1490,8 +1487,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.CursorDown() case key.Matches(msg, m.KeyMap.WordForward): m.wordRight() - case key.Matches(msg, m.KeyMap.Paste): - return m, Paste case key.Matches(msg, m.KeyMap.CharacterBackward): m.characterLeft(false /* insideLine */) case key.Matches(msg, m.KeyMap.LinePrevious): @@ -1512,11 +1507,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.transposeLeft() default: - m.insertRunesFromUserInput([]rune(msg.Text)) + m.InsertRunesFromUserInput([]rune(msg.Text)) } case pasteMsg: - m.insertRunesFromUserInput([]rune(msg)) + m.InsertRunesFromUserInput([]rune(msg)) case pasteErrMsg: m.Err = msg @@ -1908,15 +1903,6 @@ func (m *Model) splitLine(row, col int) { m.row++ } -// Paste is a command for pasting from the clipboard into the text input. -func Paste() tea.Msg { - str, err := clipboard.ReadAll() - if err != nil { - return pasteErrMsg{err} - } - return pasteMsg(str) -} - func wrapInterfaces(content []any, width int) [][]any { if width <= 0 { return [][]any{content} |
