142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
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
|
|
}
|