package injection import ( "context" "fmt" "os/exec" "strings" "time" ) // clipboardPasteBackend temporarily puts text on the regular clipboard, then // uses wtype to issue Ctrl+V. This works in applications that do not support // the primary selection, such as browsers and Electron apps. The previous // text clipboard is restored after the paste has been delivered. type clipboardPasteBackend struct { clipboard *clipboardBackend wtype *wtypeBackend } func NewClipboardPasteBackend() Backend { return &clipboardPasteBackend{ clipboard: NewClipboardBackend().(*clipboardBackend), wtype: NewWtypeBackend().(*wtypeBackend), } } func (c *clipboardPasteBackend) Name() string { return "clipboard-paste" } func (c *clipboardPasteBackend) Available() error { if err := c.clipboard.Available(); err != nil { return err } if err := c.wtype.Available(); err != nil { return err } return nil } func (c *clipboardPasteBackend) Inject(ctx context.Context, text string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() if err := c.Available(); err != nil { return err } // wl-paste only needs to keep the clipboard owner alive while it reads the // selection, so this command returns with the exact existing text content. // --no-newline prevents wl-paste from adding a terminal-friendly newline. previousClipboard, restore, err := c.readClipboard(ctx) if err != nil { return err } copyText := exec.CommandContext(ctx, "wl-copy") copyText.Stdin = strings.NewReader(text) if err := copyText.Run(); err != nil { return fmt.Errorf("copy transcription to clipboard: %w", err) } // wl-copy returns before every client has observed the new selection. Give // the compositor a short chance to publish it; this is still perceived as // an immediate paste and avoids inserting the previous clipboard contents. select { case <-time.After(150 * time.Millisecond): case <-ctx.Done(): return ctx.Err() } // wtype resolves the keysym itself, avoiding ydotool's physical-keycode // layout problem on Dvorak and Colemak setups. cmd := exec.CommandContext(ctx, "wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl") if err := cmd.Run(); err != nil { return fmt.Errorf("paste clipboard with wtype: %w", err) } if !restore { return nil } // Receiving a Wayland paste is asynchronous. Leave the transcription // available briefly, then put the user's prior clipboard back. select { case <-time.After(300 * time.Millisecond): case <-ctx.Done(): return ctx.Err() } restoreClipboard := exec.CommandContext(ctx, "wl-copy") restoreClipboard.Stdin = strings.NewReader(previousClipboard) if err := restoreClipboard.Run(); err != nil { return fmt.Errorf("restore previous clipboard: %w", err) } return nil } func (c *clipboardPasteBackend) readClipboard(ctx context.Context) (text string, restore bool, err error) { cmd := exec.CommandContext(ctx, "wl-paste", "--no-newline") output, err := cmd.Output() if err != nil { // No clipboard owner is normal. There is simply nothing to restore. if _, ok := err.(*exec.ExitError); ok { return "", false, nil } return "", false, fmt.Errorf("read existing clipboard: %w", err) } return string(output), true, nil }