From fe156679866fd5633a2c2a70c765410c22ee356a Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 4 Feb 2026 11:36:41 -0500 Subject: [PATCH] clipboard: add watch -m for mime-types --- core/cmd/dms/commands_clipboard.go | 26 ++++++ core/internal/clipboard/watch.go | 130 ++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/core/cmd/dms/commands_clipboard.go b/core/cmd/dms/commands_clipboard.go index 554bbd2f..0cf4c4b7 100644 --- a/core/cmd/dms/commands_clipboard.go +++ b/core/cmd/dms/commands_clipboard.go @@ -112,6 +112,7 @@ var clipClearCmd = &cobra.Command{ } var clipWatchStore bool +var clipWatchMimes bool var clipSearchCmd = &cobra.Command{ Use: "search [query]", @@ -211,6 +212,7 @@ func init() { clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking") clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)") + clipWatchCmd.Flags().BoolVarP(&clipWatchMimes, "mimes", "m", false, "Show all offered MIME types") clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration") @@ -328,6 +330,30 @@ func runClipWatch(cmd *cobra.Command, args []string) { }); err != nil && err != context.Canceled { log.Fatalf("Watch error: %v", err) } + case clipWatchMimes: + if err := clipboard.WatchAll(ctx, func(data []byte, mimeType string, allMimes []string) { + if clipJSONOutput { + out := map[string]any{ + "data": string(data), + "mimeType": mimeType, + "mimeTypes": allMimes, + "timestamp": time.Now().Format(time.RFC3339), + "size": len(data), + } + j, _ := json.Marshal(out) + fmt.Println(string(j)) + return + } + fmt.Printf("=== Clipboard Change ===\n") + fmt.Printf("Selected: %s\n", mimeType) + fmt.Printf("All MIME types:\n") + for _, m := range allMimes { + fmt.Printf(" - %s\n", m) + } + fmt.Printf("Size: %d bytes\n\n", len(data)) + }); err != nil && err != context.Canceled { + log.Fatalf("Watch error: %v", err) + } case clipJSONOutput: if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { out := map[string]any{ diff --git a/core/internal/clipboard/watch.go b/core/internal/clipboard/watch.go index fcac02d8..327c5c5a 100644 --- a/core/internal/clipboard/watch.go +++ b/core/internal/clipboard/watch.go @@ -13,8 +13,9 @@ import ( ) type ClipboardChange struct { - Data []byte - MimeType string + Data []byte + MimeType string + MimeTypes []string } func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error { @@ -141,6 +142,131 @@ func Watch(ctx context.Context, callback func(data []byte, mimeType string)) err } } +func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error { + display, err := wlclient.Connect("") + if err != nil { + return fmt.Errorf("wayland connect: %w", err) + } + defer display.Destroy() + + wlCtx := display.Context() + registry, err := display.GetRegistry() + if err != nil { + return fmt.Errorf("get registry: %w", err) + } + defer registry.Destroy() + + var dataControlMgr *ext_data_control.ExtDataControlManagerV1 + var seat *wlclient.Seat + var bindErr error + + registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { + switch e.Interface { + case "ext_data_control_manager_v1": + dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx) + if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil { + bindErr = err + } + case "wl_seat": + if seat != nil { + return + } + seat = wlclient.NewSeat(wlCtx) + if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil { + bindErr = err + } + } + }) + + display.Roundtrip() + display.Roundtrip() + + if bindErr != nil { + return fmt.Errorf("registry bind: %w", bindErr) + } + if dataControlMgr == nil { + return fmt.Errorf("compositor does not support ext_data_control_manager_v1") + } + defer dataControlMgr.Destroy() + if seat == nil { + return fmt.Errorf("no seat available") + } + + device, err := dataControlMgr.GetDataDevice(seat) + if err != nil { + return fmt.Errorf("get data device: %w", err) + } + defer device.Destroy() + + offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string) + + device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) { + if e.Id == nil { + return + } + offerMimeTypes[e.Id] = nil + e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) { + offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType) + }) + }) + + device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) { + if e.Id == nil { + return + } + + mimes := offerMimeTypes[e.Id] + selectedMime := selectPreferredMimeType(mimes) + if selectedMime == "" { + return + } + + mimesCopy := make([]string, len(mimes)) + copy(mimesCopy, mimes) + + r, w, err := os.Pipe() + if err != nil { + return + } + + if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil { + w.Close() + r.Close() + return + } + w.Close() + + go func() { + defer r.Close() + data, err := io.ReadAll(r) + if err != nil || len(data) == 0 { + return + } + callback(data, selectedMime, mimesCopy) + }() + }) + + display.Roundtrip() + display.Roundtrip() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + if err := wlCtx.Dispatch(); err != nil { + if isTimeoutError(err) { + continue + } + return fmt.Errorf("dispatch: %w", err) + } + } + } +} + func isTimeoutError(err error) bool { if err == nil { return false