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

This commit is contained in:
2026-09-01 01:09:59 -04:00
parent dad09d1109
commit c89ff2b68e
18 changed files with 288 additions and 41 deletions
+76
View File
@@ -0,0 +1,76 @@
package injection
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// clipboardPasteBackend copies text to the primary selection and pastes it
// with ydotool's layout-independent Shift+Insert key codes.
//
// This preserves the regular clipboard, is independent of the active keyboard
// layout, and delivers the whole string at once.
type clipboardPasteBackend struct {
clipboard *clipboardBackend
ydotool *ydotoolBackend
}
func NewClipboardPasteBackend() Backend {
return &clipboardPasteBackend{
clipboard: NewClipboardBackend().(*clipboardBackend),
ydotool: NewYdotoolBackend().(*ydotoolBackend),
}
}
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.ydotool.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
}
// Shift+Insert pastes the primary selection, leaving the user's regular
// clipboard untouched.
primaryCopy := exec.CommandContext(ctx, "wl-copy", "--primary")
primaryCopy.Stdin = strings.NewReader(text)
if err := primaryCopy.Run(); err != nil {
return fmt.Errorf("copy transcription to primary selection: %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()
}
// Linux input event codes: KEY_LEFTSHIFT=42, KEY_INSERT=110. These keys do
// not depend on the alphabetic layout, and Shift+Insert is widely supported.
cmd := exec.CommandContext(ctx, "ydotool", "key", "42:1", "110:1", "110:0", "42:0")
if socketPath := c.ydotool.getSocketPath(); socketPath != "" {
cmd.Env = append(os.Environ(), "YDOTOOL_SOCKET="+socketPath)
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("paste primary selection with ydotool: %w", err)
}
return 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)
}