diff --git a/core/cmd/dms/commands_clipboard.go b/core/cmd/dms/commands_clipboard.go index 13f51b2f..8a9b8585 100644 --- a/core/cmd/dms/commands_clipboard.go +++ b/core/cmd/dms/commands_clipboard.go @@ -53,7 +53,6 @@ var ( clipCopyPasteOnce bool clipCopyType string clipCopyDownload bool - clipCopyCacheFile string clipJSONOutput bool ) @@ -192,8 +191,6 @@ 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") @@ -224,13 +221,6 @@ 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/cmd/dms/main.go b/core/cmd/dms/main.go index d6fe9b8d..aa1bcb54 100644 --- a/core/cmd/dms/main.go +++ b/core/cmd/dms/main.go @@ -5,6 +5,7 @@ package main import ( "os" + "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" ) @@ -28,6 +29,8 @@ func init() { } func main() { + clipboard.MaybeServeAndExit() + if os.Geteuid() == 0 { log.Fatal("This program should not be run as root. Exiting.") } diff --git a/core/cmd/dms/main_distro.go b/core/cmd/dms/main_distro.go index 339bf40a..dcde6e8e 100644 --- a/core/cmd/dms/main_distro.go +++ b/core/cmd/dms/main_distro.go @@ -5,6 +5,7 @@ package main import ( "os" + "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" ) @@ -25,6 +26,8 @@ func init() { } func main() { + clipboard.MaybeServeAndExit() + if os.Geteuid() == 0 { log.Fatal("This program should not be run as root. Exiting.") } diff --git a/core/internal/clipboard/clipboard.go b/core/internal/clipboard/clipboard.go index c2194cee..d27d6ff4 100644 --- a/core/internal/clipboard/clipboard.go +++ b/core/internal/clipboard/clipboard.go @@ -12,35 +12,95 @@ import ( wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) +const envServe = "_DMS_CLIPBOARD_SERVE" +const envMime = "_DMS_CLIPBOARD_MIME" +const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE" +const envCacheFile = "_DMS_CLIPBOARD_CACHE" + +// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard +// child. Reads source data into memory, deletes any cache file, then serves. +func MaybeServeAndExit() { + if os.Getenv(envServe) == "" { + return + } + + mimeType := os.Getenv(envMime) + pasteOnce := os.Getenv(envPasteOnce) == "1" + cachePath := os.Getenv(envCacheFile) + + var data []byte + var err error + + switch { + case cachePath != "": + data, err = os.ReadFile(cachePath) + os.Remove(cachePath) + default: + data, err = io.ReadAll(os.Stdin) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err) + os.Exit(1) + } + + if err := serveClipboard(data, mimeType, pasteOnce); err != nil { + fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err) + os.Exit(1) + } + os.Exit(0) +} + func Copy(data []byte, mimeType string) error { return copyForkCached(data, mimeType, false) } func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { if foreground { - return copyServeWithWriter(func(writer io.Writer) error { - total := 0 - for total < len(data) { - n, err := writer.Write(data[total:]) - total += n - if err != nil { - return err - } - } - if total != len(data) { - return io.ErrShortWrite - } - return nil - }, mimeType, pasteOnce) + return serveClipboard(data, mimeType, pasteOnce) } return copyForkCached(data, mimeType, pasteOnce) } func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error { - if !foreground { - return copyFork(data, mimeType, pasteOnce) + if foreground { + buf, err := io.ReadAll(data) + if err != nil { + return fmt.Errorf("read source: %w", err) + } + return serveClipboard(buf, mimeType, pasteOnce) } - return copyServeReader(data, mimeType, pasteOnce) + return copyFork(data, mimeType, pasteOnce) +} + +func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd { + cmd := exec.Command(os.Args[0]) + cmd.Stderr = nil + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + cmd.Env = append(os.Environ(), + envServe+"=1", + envMime+"="+mimeType, + ) + if pasteOnce { + cmd.Env = append(cmd.Env, envPasteOnce+"=1") + } + cmd.Env = append(cmd.Env, extra...) + return cmd +} + +func waitReady(cmd *exec.Cmd) error { + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("stdout pipe: %w", err) + } + if err := cmd.Start(); err != nil { + 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 copyForkCached(data []byte, mimeType string, pasteOnce bool) error { @@ -60,65 +120,34 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error { 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 := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath) 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 { + if err := waitReady(cmd); 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 err } return nil } func copyFork(data io.Reader, 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.Stderr = nil - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} - cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1") - - stdout, err := cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("stdout pipe: %w", err) - } + cmd := newForkCmd(mimeType, pasteOnce) switch src := data.(type) { case *os.File: cmd.Stdin = src - if err := cmd.Start(); err != nil { - return fmt.Errorf("start: %w", err) - } + return waitReady(cmd) default: stdin, err := cmd.StdinPipe() if err != nil { return fmt.Errorf("stdin pipe: %w", err) } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("stdout pipe: %w", err) + } + if err := cmd.Start(); err != nil { return fmt.Errorf("start: %w", err) } @@ -129,63 +158,22 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { if err := stdin.Close(); err != nil { return fmt.Errorf("close stdin: %w", err) } - } - var buf [1]byte - if _, err := stdout.Read(buf[:]); err != nil { - return fmt.Errorf("waiting for clipboard ready: %w", err) + var buf [1]byte + if _, err := stdout.Read(buf[:]); err != nil { + return fmt.Errorf("waiting for clipboard ready: %w", err) + } + return nil } - return nil } func signalReady() { - if os.Getenv("DMS_CLIP_FORKED") == "" { + if os.Getenv(envServe) == "" { return } 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 { - return fmt.Errorf("create clipboard cache file: %w", err) - } - defer os.Remove(cachedData.Name()) - - if _, err := io.Copy(cachedData, data); err != nil { - return fmt.Errorf("cache clipboard data: %w", err) - } - if err := cachedData.Close(); err != nil { - return fmt.Errorf("close temp cache file: %w", err) - } - - return copyServeWithWriter(func(writer io.Writer) error { - cachedFile, err := os.Open(cachedData.Name()) - if err != nil { - return fmt.Errorf("open temp cache file: %w", err) - } - defer cachedFile.Close() - - if _, err := io.Copy(writer, cachedFile); err != nil { - return fmt.Errorf("write clipboard data: %w", err) - } - return nil - }, mimeType, pasteOnce) -} - func createClipboardCacheFile() (*os.File, error) { preferredDirs := []string{} @@ -206,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) { return os.CreateTemp("", "dms-clipboard-*") } -func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error { +func serveClipboard(data []byte, mimeType string, pasteOnce bool) error { display, err := wlclient.Connect("") if err != nil { return fmt.Errorf("wayland connect: %w", err) @@ -248,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn 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") } @@ -292,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn cancelled := make(chan struct{}) pasted := make(chan struct{}, 1) - sendErr := make(chan error, 1) source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { - defer syscall.Close(e.Fd) + _ = syscall.SetNonblock(e.Fd, false) file := os.NewFile(uintptr(e.Fd), "pipe") defer file.Close() - if err := writeTo(file); err != nil { - select { - case sendErr <- err: - default: - } - } + _, _ = file.Write(data) select { case pasted <- struct{}{}: default: @@ -325,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn select { case <-cancelled: return nil - case err := <-sendErr: - return err case <-pasted: if pasteOnce { return nil @@ -580,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error { 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") } @@ -613,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error { pasted := make(chan struct{}, 1) source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { - defer syscall.Close(e.Fd) + _ = syscall.SetNonblock(e.Fd, false) file := os.NewFile(uintptr(e.Fd), "pipe") defer file.Close() if data, ok := offerMap[e.MimeType]; ok { - file.Write(data) + _, _ = file.Write(data) } select {