Files
hyprvoice/internal/injection/clipboard_paste.go
T
thread 034ee026b1
CI / Test (push) Successful in 46s
LLMIFY THIS MOFO
2026-09-01 01:36:17 -04:00

107 lines
3.2 KiB
Go

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
}