package injection import ( "context" "encoding/json" "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 ctrlShiftVClasses []string } func NewClipboardPasteBackend(ctrlShiftVClasses []string) Backend { return &clipboardPasteBackend{ clipboard: NewClipboardBackend().(*clipboardBackend), wtype: NewWtypeBackend().(*wtypeBackend), ctrlShiftVClasses: ctrlShiftVClasses, } } 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. Terminal emulators typically // reserve Ctrl+V, so configured window classes use Ctrl+Shift+V instead. args := []string{"-M", "ctrl"} if c.shouldUseCtrlShiftV(ctx) { args = append(args, "-M", "shift", "-k", "v", "-m", "shift") } else { args = append(args, "-k", "v") } args = append(args, "-m", "ctrl") cmd := exec.CommandContext(ctx, "wtype", args...) 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) shouldUseCtrlShiftV(ctx context.Context) bool { if len(c.ctrlShiftVClasses) == 0 { return false } cmd := exec.CommandContext(ctx, "hyprctl", "activewindow", "-j") output, err := cmd.Output() if err != nil { return false } var activeWindow struct { Class string `json:"class"` } if err := json.Unmarshal(output, &activeWindow); err != nil { return false } class := strings.ToLower(activeWindow.Class) for _, match := range c.ctrlShiftVClasses { if match = strings.TrimSpace(strings.ToLower(match)); match != "" && strings.Contains(class, match) { return true } } return false } 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 }