From f91e7d39b50cab3e81e6d7b46cb0b0ba3afe25da Mon Sep 17 00:00:00 2001 From: ayn2op Date: Tue, 29 Jul 2025 14:25:19 -0400 Subject: [PATCH] feat: attach image from clipboard Closes #96 --- cmd/message_input.go | 45 ++++++++++++++++++++++++++----------- go.mod | 3 +++ go.sum | 6 +++++ internal/config/config.toml | 2 ++ internal/config/keys.go | 1 + 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/cmd/message_input.go b/cmd/message_input.go index cfc9785..3ac17e3 100644 --- a/cmd/message_input.go +++ b/cmd/message_input.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "fmt" "io" "log/slog" @@ -13,7 +14,6 @@ import ( "time" "unicode" - "github.com/atotto/clipboard" "github.com/ayn2op/discordo/internal/cache" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" @@ -28,6 +28,7 @@ import ( "github.com/ncruces/zenity" "github.com/sahilm/fuzzy" "github.com/yuin/goldmark/ast" + "golang.design/x/clipboard" ) const tmpFilePattern = consts.Name + "_*.md" @@ -56,15 +57,20 @@ func newMessageInput(cfg *config.Config) *messageInput { mentionsList: tview.NewList(), } + if err := clipboard.Init(); err != nil { + slog.Warn("failed to init clipboard", "err", err) + } else { + mi. + SetClipboard(func(s string) { + clipboard.Write(clipboard.FmtText, []byte(s)) + }, func() string { + data := clipboard.Read(clipboard.FmtText) + return string(data) + }) + } + mi.Box = ui.ConfigureBox(mi.Box, &cfg.Theme) - mi. - SetClipboard(func(s string) { - clipboard.WriteAll(s) - }, func() string { - text, _ := clipboard.ReadAll() - return text - }). - SetInputCapture(mi.onInputCapture) + mi.SetInputCapture(mi.onInputCapture) mi.mentionsList.Box = ui.ConfigureBox(mi.mentionsList.Box, &mi.cfg.Theme) mi.mentionsList. @@ -88,6 +94,10 @@ func (mi *messageInput) reset() { func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { switch event.Name() { + case mi.cfg.Keys.MessageInput.Paste: + mi.paste() + return tcell.NewEventKey(tcell.KeyCtrlV, 0, tcell.ModNone) + case mi.cfg.Keys.MessageInput.Send: if app.pages.GetVisibile(mentionsListPageName) { mi.tabComplete(false) @@ -140,6 +150,13 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return event } +func (mi *messageInput) paste() { + if data := clipboard.Read(clipboard.FmtImage); data != nil { + name := "clipboard.png" + mi.attach(name, sendpart.File{Name: name, Reader: bytes.NewReader(data)}) + } +} + func (mi *messageInput) send() { if !app.guildsTree.selectedChannelID.IsValid() { return @@ -486,9 +503,11 @@ func (mi *messageInput) openFilePicker() { } name := filepath.Base(path) - mi.sendMessageData.Files = append(mi.sendMessageData.Files, sendpart.File{Name: name, Reader: file}) - - title := fmt.Sprintf("Attached %s", name) - mi.addTitle(title) + mi.attach(name, sendpart.File{Name: name, Reader: file}) } } + +func (mi *messageInput) attach(name string, file sendpart.File) { + mi.sendMessageData.Files = append(mi.sendMessageData.Files, file) + mi.addTitle("Attached " + name) +} diff --git a/go.mod b/go.mod index 0cf60cb..fb8f92c 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/yuin/goldmark v1.7.13 github.com/zalando/go-keyring v0.2.6 + golang.design/x/clipboard v0.7.1 ) require ( @@ -47,7 +48,9 @@ require ( github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/twmb/murmur3 v1.1.8 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/exp/shiny v0.0.0-20250718183923-645b1fa84792 // indirect golang.org/x/image v0.29.0 // indirect + golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect diff --git a/go.sum b/go.sum index 9c7bf71..181a494 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c= +golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -194,6 +196,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp/shiny v0.0.0-20250718183923-645b1fa84792 h1:YkeFUC3wFzzmWTi5lYbb5af46Tpk9DLWHXXwGjLF3R0= +golang.org/x/exp/shiny v0.0.0-20250718183923-645b1fa84792/go.mod h1:DUdAjGCS1V5oj0c1HZTX5UNuMxBjfxuU/NIoy/wuiRw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas= @@ -209,6 +213,8 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 h1:Z6EFcPz8e1cx0ge5jWCwqafndPjdsDQf8fk4Kw3pJoI= +golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0/go.mod h1:kqVs191xxTTCd39tk8zK1UD3jyCS1SPrMHTpJ9ujxZg= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= diff --git a/internal/config/config.toml b/internal/config/config.toml index 4c7627f..9012276 100644 --- a/internal/config/config.toml +++ b/internal/config/config.toml @@ -81,6 +81,8 @@ yank_id = "Rune[i]" # Only while typing a message # Alt+Enter: Insert a new line to the current text. [keys.message_input] +# paste from clipboard (supports both text and images) +paste = "Ctrl+V" send = "Enter" # Remove existing text or cancel reply. cancel = "Esc" diff --git a/internal/config/keys.go b/internal/config/keys.go index cc48098..6480ad1 100644 --- a/internal/config/keys.go +++ b/internal/config/keys.go @@ -50,6 +50,7 @@ type ( } MessageInputKeys struct { + Paste string `toml:"paste"` Send string `toml:"send"` Cancel string `toml:"cancel"` TabComplete string `toml:"tab_complete"`