From 0bc1b7a3c22c79876dca13ccc2c9844b8e793887 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 6 Apr 2026 10:30:39 -0400 Subject: [PATCH] clipboard: fix reliability of modal/popout --- core/cmd/dms/commands_clipboard.go | 10 +++ core/internal/clipboard/clipboard.go | 65 ++++++++++++++++++- .../Modals/Clipboard/ClipboardContent.qml | 4 +- .../Clipboard/ClipboardHistoryModal.qml | 7 +- .../Clipboard/ClipboardHistoryPopout.qml | 13 ++-- quickshell/Services/ClipboardService.qml | 6 ++ 6 files changed, 87 insertions(+), 18 deletions(-) diff --git a/core/cmd/dms/commands_clipboard.go b/core/cmd/dms/commands_clipboard.go index 8a9b8585..13f51b2f 100644 --- a/core/cmd/dms/commands_clipboard.go +++ b/core/cmd/dms/commands_clipboard.go @@ -53,6 +53,7 @@ var ( clipCopyPasteOnce bool clipCopyType string clipCopyDownload bool + clipCopyCacheFile string clipJSONOutput bool ) @@ -191,6 +192,8 @@ func init() { clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste") clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type") clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file") + clipCopyCmd.Flags().StringVar(&clipCopyCacheFile, "cache-file", "", "") + clipCopyCmd.Flags().MarkHidden("cache-file") clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") @@ -221,6 +224,13 @@ func init() { } func runClipCopy(cmd *cobra.Command, args []string) { + if clipCopyCacheFile != "" { + if err := clipboard.ServeCacheFile(clipCopyCacheFile, clipCopyType, clipCopyPasteOnce); err != nil { + log.Fatalf("serve cache file: %v", err) + } + return + } + var data []byte copyFromStdin := false diff --git a/core/internal/clipboard/clipboard.go b/core/internal/clipboard/clipboard.go index c758a973..c2194cee 100644 --- a/core/internal/clipboard/clipboard.go +++ b/core/internal/clipboard/clipboard.go @@ -1,7 +1,6 @@ package clipboard import ( - "bytes" "fmt" "io" "os" @@ -14,7 +13,7 @@ import ( ) func Copy(data []byte, mimeType string) error { - return CopyReader(bytes.NewReader(data), mimeType, false, false) + return copyForkCached(data, mimeType, false) } func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { @@ -34,7 +33,7 @@ func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { return nil }, mimeType, pasteOnce) } - return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce) + return copyForkCached(data, mimeType, pasteOnce) } func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error { @@ -44,6 +43,53 @@ func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) err return copyServeReader(data, mimeType, pasteOnce) } +func copyForkCached(data []byte, mimeType string, pasteOnce bool) error { + cacheFile, err := createClipboardCacheFile() + if err != nil { + return fmt.Errorf("create cache file: %w", err) + } + cachePath := cacheFile.Name() + + if _, err := cacheFile.Write(data); err != nil { + cacheFile.Close() + os.Remove(cachePath) + return fmt.Errorf("write cache file: %w", err) + } + if err := cacheFile.Close(); err != nil { + os.Remove(cachePath) + return fmt.Errorf("close cache file: %w", err) + } + + args := []string{os.Args[0], "cl", "copy", "--foreground", "--cache-file", cachePath} + if pasteOnce { + args = append(args, "--paste-once") + } + args = append(args, "--type", mimeType) + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = nil + cmd.Stderr = nil + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1") + + stdout, err := cmd.StdoutPipe() + if err != nil { + os.Remove(cachePath) + return fmt.Errorf("stdout pipe: %w", err) + } + + if err := cmd.Start(); err != nil { + os.Remove(cachePath) + return fmt.Errorf("start: %w", err) + } + + var buf [1]byte + if _, err := stdout.Read(buf[:]); err != nil { + return fmt.Errorf("waiting for clipboard ready: %w", err) + } + return nil +} + func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { args := []string{os.Args[0], "cl", "copy", "--foreground"} if pasteOnce { @@ -99,6 +145,19 @@ func signalReady() { os.Stdout.Write([]byte{1}) } +func ServeCacheFile(path, mimeType string, pasteOnce bool) error { + defer os.Remove(path) + return copyServeWithWriter(func(writer io.Writer) error { + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("open cache file: %w", err) + } + defer f.Close() + _, err = io.Copy(writer, f) + return err + }, mimeType, pasteOnce) +} + func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { cachedData, err := createClipboardCacheFile() if err != nil { diff --git a/quickshell/Modals/Clipboard/ClipboardContent.qml b/quickshell/Modals/Clipboard/ClipboardContent.qml index 12eadc0a..b0a022ab 100644 --- a/quickshell/Modals/Clipboard/ClipboardContent.qml +++ b/quickshell/Modals/Clipboard/ClipboardContent.qml @@ -129,7 +129,7 @@ Item { } StyledText { - text: I18n.tr("No recent clipboard entries found") + text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…") anchors.centerIn: parent font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText @@ -195,7 +195,7 @@ Item { } StyledText { - text: I18n.tr("No saved clipboard entries") + text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…") anchors.centerIn: parent font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml index 411f03fd..2f76cc78 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -60,15 +60,12 @@ DankModal { } function show() { - if (!clipboardAvailable) { - ToastService.showError(I18n.tr("Clipboard service not available")); - return; - } open(); activeImageLoads = 0; shouldHaveFocus = true; ClipboardService.reset(); - ClipboardService.refresh(); + if (clipboardAvailable) + ClipboardService.refresh(); keyboardController.reset(); Qt.callLater(function () { diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml b/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml index 7037b11f..c4f0296f 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml @@ -50,14 +50,11 @@ DankPopout { } function show() { - if (!clipboardAvailable) { - ToastService.showError(I18n.tr("Clipboard service not available")); - return; - } open(); activeImageLoads = 0; ClipboardService.reset(); - ClipboardService.refresh(); + if (clipboardAvailable) + ClipboardService.refresh(); keyboardController.reset(); Qt.callLater(function () { @@ -122,10 +119,10 @@ DankPopout { onBackgroundClicked: hide() onShouldBeVisibleChanged: { - if (!shouldBeVisible) { + if (!shouldBeVisible) return; - } - ClipboardService.refresh(); + if (clipboardAvailable) + ClipboardService.refresh(); keyboardController.reset(); Qt.callLater(function () { if (contentLoader.item?.searchField) { diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index 19d4fd7f..4c092551 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -255,6 +255,12 @@ Singleton { return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash); } + onClipboardAvailableChanged: { + if (!clipboardAvailable || refCount <= 0) + return; + refresh(); + } + Connections { target: DMSService enabled: root.refCount > 0