LLMIFY THIS MOFO
CI / Test (push) Successful in 46s

This commit is contained in:
2026-09-01 01:36:17 -04:00
parent dad09d1109
commit 034ee026b1
18 changed files with 318 additions and 41 deletions
+106
View File
@@ -0,0 +1,106 @@
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
}
+3 -1
View File
@@ -12,7 +12,7 @@ type Injector interface {
}
type Config struct {
Backends []string // Ordered list: "ydotool", "wtype", "clipboard"
Backends []string // Ordered list: "clipboard-paste", "ydotool", "wtype", "clipboard"
YdotoolTimeout time.Duration // Timeout for ydotool commands
WtypeTimeout time.Duration // Timeout for wtype commands
ClipboardTimeout time.Duration // Timeout for clipboard operations
@@ -34,6 +34,8 @@ func NewInjector(config Config) Injector {
backends = append(backends, NewWtypeBackend())
case "clipboard":
backends = append(backends, NewClipboardBackend())
case "clipboard-paste":
backends = append(backends, NewClipboardPasteBackend())
default:
log.Printf("Injection: unknown backend %q, skipping", name)
}
+11 -3
View File
@@ -66,9 +66,10 @@ func TestNewInjector_IgnoresUnknownBackends(t *testing.T) {
}
func TestInjector_Inject(t *testing.T) {
// Skip integration tests in CI environments
if os.Getenv("CI") == "true" {
t.Skip("Skipping integration test in CI environment")
// This test invokes real Wayland input tools and must run in a graphical
// session, not merely outside CI.
if os.Getenv("CI") == "true" || os.Getenv("WAYLAND_DISPLAY") == "" {
t.Skip("Skipping integration test outside a Wayland session")
}
tests := []struct {
@@ -202,6 +203,13 @@ func TestClipboardBackend(t *testing.T) {
t.Logf("clipboard is available")
}
func TestClipboardPasteBackend(t *testing.T) {
backend := NewClipboardPasteBackend()
if backend.Name() != "clipboard-paste" {
t.Errorf("Name() = %s, want clipboard-paste", backend.Name())
}
}
// TestInjector_ClipboardMode tests clipboard-only injection
func TestInjector_ClipboardMode(t *testing.T) {
config := Config{
+4
View File
@@ -85,9 +85,13 @@ func (y *ydotoolBackend) Inject(ctx context.Context, text string, timeout time.D
if err := y.Available(); err != nil {
return err
}
socketPath := y.getSocketPath()
// ydotool type -- "text"
cmd := exec.CommandContext(ctx, "ydotool", "type", "--", text)
if socketPath != "" {
cmd.Env = append(os.Environ(), "YDOTOOL_SOCKET="+socketPath)
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("ydotool failed: %w", err)
}