From 9a83762580e7fb68ab2e4d2039397c76a051b3c9 Mon Sep 17 00:00:00 2001 From: xqrs <3bd3bdr@gmail.com> Date: Thu, 11 Dec 2025 00:02:38 +0300 Subject: [PATCH] feat(clipboard): add wayland support (#657) Signed-off-by: xqrs <3bd3bdr@gmail.com> Co-authored-by: Ayyan --- README.md | 5 +- cmd/application.go | 2 +- cmd/guilds_tree.go | 2 +- cmd/message_input.go | 2 +- cmd/messages_list.go | 2 +- cmd/state.go | 6 +-- internal/clipboard/clipboard.go | 10 ++++ internal/clipboard/clipboard_default.go | 11 ++++ internal/clipboard/clipboard_linux.go | 67 +++++++++++++++++++++++++ 9 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 internal/clipboard/clipboard.go create mode 100644 internal/clipboard/clipboard_default.go create mode 100644 internal/clipboard/clipboard_linux.go diff --git a/README.md b/README.md index ae03f5b..821eb1f 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,7 @@ go build . ### Wayland clipboard support -`x11-dev` is required for X11 clipboard compatibility: - -- Ubuntu: `apt install xwayland` -- Arch Linux: `pacman -S xorg-xwayland` +`wl-clipboard` is required for clipboard support. ## Usage diff --git a/cmd/application.go b/cmd/application.go index 7a66b25..21a653a 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -3,11 +3,11 @@ package cmd import ( "log/slog" + "github.com/ayn2op/discordo/internal/clipboard" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/login" "github.com/ayn2op/tview" "github.com/gdamore/tcell/v3" - "golang.design/x/clipboard" ) type application struct { diff --git a/cmd/guilds_tree.go b/cmd/guilds_tree.go index e65dc4f..52c9ee0 100644 --- a/cmd/guilds_tree.go +++ b/cmd/guilds_tree.go @@ -6,6 +6,7 @@ import ( "log/slog" "slices" + "github.com/ayn2op/discordo/internal/clipboard" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview" @@ -13,7 +14,6 @@ import ( "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/ningen/v3" "github.com/gdamore/tcell/v3" - "golang.design/x/clipboard" ) type guildsTree struct { diff --git a/cmd/message_input.go b/cmd/message_input.go index 40fd0b7..e54b7a8 100644 --- a/cmd/message_input.go +++ b/cmd/message_input.go @@ -15,6 +15,7 @@ import ( "unicode" "github.com/ayn2op/discordo/internal/cache" + "github.com/ayn2op/discordo/internal/clipboard" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" "github.com/ayn2op/discordo/internal/ui" @@ -29,7 +30,6 @@ import ( "github.com/ncruces/zenity" "github.com/sahilm/fuzzy" "github.com/yuin/goldmark/ast" - "golang.design/x/clipboard" ) const tmpFilePattern = consts.Name + "_*.md" diff --git a/cmd/messages_list.go b/cmd/messages_list.go index 126b1c3..307c142 100644 --- a/cmd/messages_list.go +++ b/cmd/messages_list.go @@ -14,6 +14,7 @@ import ( "sync" "time" + "github.com/ayn2op/discordo/internal/clipboard" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" "github.com/ayn2op/discordo/internal/markdown" @@ -30,7 +31,6 @@ import ( "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" - "golang.design/x/clipboard" ) type messagesList struct { diff --git a/cmd/state.go b/cmd/state.go index 1274f98..0e4bd1e 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -147,7 +147,7 @@ func onReady(r *gateway.ReadyEvent) { func onMessageCreate(message *gateway.MessageCreateEvent) { if app.chatView.selectedChannel != nil && - app.chatView.selectedChannel.ID == message.ChannelID { + app.chatView.selectedChannel.ID == message.ChannelID { app.chatView.messagesList.drawMessage(app.chatView.messagesList, message.Message) app.Draw() } @@ -159,14 +159,14 @@ func onMessageCreate(message *gateway.MessageCreateEvent) { func onMessageUpdate(message *gateway.MessageUpdateEvent) { if app.chatView.selectedChannel != nil && - app.chatView.selectedChannel.ID == message.ChannelID { + app.chatView.selectedChannel.ID == message.ChannelID { onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID}) } } func onMessageDelete(message *gateway.MessageDeleteEvent) { if app.chatView.selectedChannel != nil && - app.chatView.selectedChannel.ID == message.ChannelID { + app.chatView.selectedChannel.ID == message.ChannelID { messages, err := discordState.Cabinet.Messages(message.ChannelID) if err != nil { slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID) diff --git a/internal/clipboard/clipboard.go b/internal/clipboard/clipboard.go new file mode 100644 index 0000000..16a1ef5 --- /dev/null +++ b/internal/clipboard/clipboard.go @@ -0,0 +1,10 @@ +package clipboard + +import designClipb "golang.design/x/clipboard" + +type Format = designClipb.Format + +const ( + FmtText Format = designClipb.FmtText + FmtImage = designClipb.FmtImage +) diff --git a/internal/clipboard/clipboard_default.go b/internal/clipboard/clipboard_default.go new file mode 100644 index 0000000..70e68c2 --- /dev/null +++ b/internal/clipboard/clipboard_default.go @@ -0,0 +1,11 @@ +//go:build !linux + +package clipboard + +import designClipb "golang.design/x/clipboard" + +var ( + Init = designClipb.Init + Read = designClipb.Read + Write = designClipb.Write +) diff --git a/internal/clipboard/clipboard_linux.go b/internal/clipboard/clipboard_linux.go new file mode 100644 index 0000000..d3c4b67 --- /dev/null +++ b/internal/clipboard/clipboard_linux.go @@ -0,0 +1,67 @@ +//go:build linux + +package clipboard + +import ( + "bytes" + designClipb "golang.design/x/clipboard" + "log/slog" + "os" + "os/exec" +) + +var wayland bool + +func Init() error { + if _, ok := os.LookupEnv("WAYLAND_DISPLAY"); !ok { + return designClipb.Init() + } + if _, err := exec.LookPath("wl-copy"); err != nil { + return err + } + if _, err := exec.LookPath("wl-paste"); err != nil { + return err + } + wayland = true + return nil +} + +func Read(t Format) []byte { + if !wayland { + return designClipb.Read(designClipb.Format(t)) + } + // -n: Don't print a newline at the end + // -t type: MIME type specifier + cmd := exec.Command("wl-paste", "-nt", formatToType(t)) + outBuffer := bytes.Buffer{} + cmd.Stdout = &outBuffer + if err := cmd.Run(); err != nil { + slog.Error("failed to read clipboard", "err", err) + return nil + } + return outBuffer.Bytes() +} + +func Write(t Format, buf []byte) <-chan struct{} { + if !wayland { + return designClipb.Write(designClipb.Format(t), buf) + } + // -t type: MIME type specifier + cmd := exec.Command("wl-copy", "-t", formatToType(t)) + cmd.Stdin = bytes.NewReader(buf) + if err := cmd.Run(); err != nil { + slog.Error("failed to write to clipboard", "err", err) + } + return nil +} + +func formatToType(t Format) string { + switch t { + case FmtImage: + return "image" + case FmtText: + fallthrough + default: + return "text/plain;charset=utf-8" + } +}