From 5bb605875191ade41f6c4ec6eda9c8356ce0bd88 Mon Sep 17 00:00:00 2001 From: purian23 Date: Tue, 1 Sep 2026 19:33:23 -0400 Subject: [PATCH] feat(clipboard): add a copy-as-text action for entries with a text alternates - New Entries captured from a mixed image+text offer keep the image as the primary mime i.e. bitmap - Copy Text context-menu item on atlernates Closes #2849 --- core/internal/server/clipboard/handlers.go | 13 +++++++++++-- .../Modals/Clipboard/ClipboardContextMenu.qml | 17 +++++++++++++++++ .../Clipboard/ClipboardHistoryContent.qml | 4 ++++ quickshell/Services/ClipboardService.qml | 8 +++++--- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/core/internal/server/clipboard/handlers.go b/core/internal/server/clipboard/handlers.go index 4030bb61d..0a2dc6e3b 100644 --- a/core/internal/server/clipboard/handlers.go +++ b/core/internal/server/clipboard/handlers.go @@ -205,6 +205,8 @@ func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) { return } + textOnly := params.BoolOpt(req.Params, "textOnly", false) && entry.AltMimeType != "" + if entry.AltMimeType == "" { filePath := m.EntryToFile(entry) if filePath != "" { @@ -220,8 +222,15 @@ func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) { } } - if err := m.SetClipboardEntry(entry); err != nil { - models.RespondError(conn, req.ID, err.Error()) + var setErr error + switch { + case textOnly: + setErr = m.SetClipboard(entry.AltData, entry.AltMimeType) + default: + setErr = m.SetClipboardEntry(entry) + } + if setErr != nil { + models.RespondError(conn, req.ID, setErr.Error()) return } diff --git a/quickshell/Modals/Clipboard/ClipboardContextMenu.qml b/quickshell/Modals/Clipboard/ClipboardContextMenu.qml index c281b0234..938505a6d 100644 --- a/quickshell/Modals/Clipboard/ClipboardContextMenu.qml +++ b/quickshell/Modals/Clipboard/ClipboardContextMenu.qml @@ -29,6 +29,7 @@ Item { readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null readonly property bool canEditEntry: !!entry && !(entry.isImage ?? false) + readonly property bool hasTextAlternative: !!entry && (entry.altMimeType ?? "") !== "" readonly property string pinText: entry?.pinned || hasPinnedDuplicate ? I18n.tr("Unpin") : I18n.tr("Pin") readonly property string pinIcon: entry?.pinned || hasPinnedDuplicate ? "keep_off" : "push_pin" @@ -48,6 +49,15 @@ Item { } ]; + if (hasTextAlternative) { + items.splice(1, 0, { + type: "item", + icon: "text_fields", + text: I18n.tr("Copy Text"), + action: copyEntryAsText + }); + } + if (canEditEntry) { items.push({ type: "item", @@ -180,6 +190,13 @@ Item { hide(); } + function copyEntryAsText() { + if (!entry || !hasTextAlternative) + return; + modal?.copyEntryAsText(entry); + hide(); + } + function togglePin() { if (!entry) return; diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml b/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml index a19e05fc0..de04c3c56 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml @@ -108,6 +108,10 @@ FocusScope { ClipboardService.copyEntry(entry, () => root.requestClose(false)); } + function copyEntryAsText(entry) { + ClipboardService.copyEntry(entry, () => root.requestClose(false), true); + } + function selectedEntry() { const entries = activeTab === "saved" ? pinnedEntries : unpinnedEntries; if (!entries || entries.length === 0 || selectedIndex < 0 || selectedIndex >= entries.length) diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index 1ba67326c..205beda11 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -202,15 +202,17 @@ Singleton { unpinnedEntries = []; } - function copyEntry(entry, closeCallback) { + function copyEntry(entry, closeCallback, textOnly) { + const asText = textOnly === true; DMSService.sendRequest("clipboard.copyEntry", { - "id": entry.id + "id": entry.id, + "textOnly": asText }, function (response) { if (response.error) { ToastService.showError(I18n.tr("Failed to copy entry")); return; } - ToastService.showInfo(entry.isImage ? I18n.tr("Image copied to clipboard") : I18n.tr("Copied to clipboard")); + ToastService.showInfo(entry.isImage && !asText ? I18n.tr("Image copied to clipboard") : I18n.tr("Copied to clipboard")); historyCopied(); if (closeCallback) { closeCallback();