package clipboard import ( "fmt" "io" "os" "os/exec" "syscall" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) func Copy(data []byte, mimeType string) error { return CopyOpts(data, mimeType, false, false) } func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { if !foreground { return copyFork(data, mimeType, pasteOnce) } return copyServe(data, mimeType, pasteOnce) } func copyFork(data []byte, mimeType string, pasteOnce bool) error { args := []string{os.Args[0], "cl", "copy", "--foreground"} if pasteOnce { args = append(args, "--paste-once") } args = append(args, "--type", mimeType) cmd := exec.Command(args[0], args[1:]...) cmd.Stdin = nil cmd.Stdout = nil cmd.Stderr = nil cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} stdin, err := cmd.StdinPipe() if err != nil { return fmt.Errorf("stdin pipe: %w", err) } if err := cmd.Start(); err != nil { return fmt.Errorf("start: %w", err) } if _, err := stdin.Write(data); err != nil { stdin.Close() return fmt.Errorf("write stdin: %w", err) } stdin.Close() return nil } func copyServe(data []byte, mimeType string, pasteOnce bool) error { display, err := wlclient.Connect("") if err != nil { return fmt.Errorf("wayland connect: %w", err) } defer display.Destroy() ctx := 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(ctx) 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(ctx) 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() source, err := dataControlMgr.CreateDataSource() if err != nil { return fmt.Errorf("create data source: %w", err) } if err := source.Offer(mimeType); err != nil { return fmt.Errorf("offer mime type: %w", err) } if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" { if err := source.Offer("text/plain"); err != nil { return fmt.Errorf("offer text/plain: %w", err) } if err := source.Offer("text/plain;charset=utf-8"); err != nil { return fmt.Errorf("offer text/plain;charset=utf-8: %w", err) } if err := source.Offer("UTF8_STRING"); err != nil { return fmt.Errorf("offer UTF8_STRING: %w", err) } if err := source.Offer("STRING"); err != nil { return fmt.Errorf("offer STRING: %w", err) } if err := source.Offer("TEXT"); err != nil { return fmt.Errorf("offer TEXT: %w", err) } } cancelled := make(chan struct{}) pasted := make(chan struct{}, 1) source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { defer syscall.Close(e.Fd) file := os.NewFile(uintptr(e.Fd), "pipe") defer file.Close() file.Write(data) select { case pasted <- struct{}{}: default: } }) source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) { close(cancelled) }) if err := device.SetSelection(source); err != nil { return fmt.Errorf("set selection: %w", err) } display.Roundtrip() for { select { case <-cancelled: return nil case <-pasted: if pasteOnce { return nil } default: if err := ctx.Dispatch(); err != nil { return nil } } } } func CopyText(text string) error { return Copy([]byte(text), "text/plain;charset=utf-8") } func Paste() ([]byte, string, error) { display, err := wlclient.Connect("") if err != nil { return nil, "", fmt.Errorf("wayland connect: %w", err) } defer display.Destroy() ctx := display.Context() registry, err := display.GetRegistry() if err != nil { return nil, "", 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(ctx) 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(ctx) if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil { bindErr = err } } }) display.Roundtrip() display.Roundtrip() if bindErr != nil { return nil, "", fmt.Errorf("registry bind: %w", bindErr) } if dataControlMgr == nil { return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1") } defer dataControlMgr.Destroy() if seat == nil { return nil, "", fmt.Errorf("no seat available") } device, err := dataControlMgr.GetDataDevice(seat) if err != nil { return nil, "", 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) }) }) var selectionOffer *ext_data_control.ExtDataControlOfferV1 gotSelection := false device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) { selectionOffer = e.Id gotSelection = true }) display.Roundtrip() display.Roundtrip() if !gotSelection || selectionOffer == nil { return nil, "", fmt.Errorf("no clipboard data") } mimeTypes := offerMimeTypes[selectionOffer] selectedMime := selectPreferredMimeType(mimeTypes) if selectedMime == "" { return nil, "", fmt.Errorf("no supported mime type") } r, w, err := os.Pipe() if err != nil { return nil, "", fmt.Errorf("create pipe: %w", err) } defer r.Close() if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil { w.Close() return nil, "", fmt.Errorf("receive: %w", err) } w.Close() display.Roundtrip() data, err := io.ReadAll(r) if err != nil { return nil, "", fmt.Errorf("read: %w", err) } return data, selectedMime, nil } func PasteText() (string, error) { data, _, err := Paste() if err != nil { return "", err } return string(data), nil } func selectPreferredMimeType(mimes []string) string { preferred := []string{ "text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "STRING", "TEXT", "image/png", "image/jpeg", } for _, pref := range preferred { for _, mime := range mimes { if mime == pref { return mime } } } if len(mimes) > 0 { return mimes[0] } return "" } func IsImageMimeType(mime string) bool { return len(mime) > 6 && mime[:6] == "image/" }