new injection strategies
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package injection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Backend represents a text injection method
|
||||
type Backend interface {
|
||||
Name() string
|
||||
Available() error
|
||||
Inject(ctx context.Context, text string, timeout time.Duration) error
|
||||
}
|
||||
@@ -9,43 +9,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func getClipboard(ctx context.Context, timeout time.Duration) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
type clipboardBackend struct{}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wl-paste", "--no-newline")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
func NewClipboardBackend() Backend {
|
||||
return &clipboardBackend{}
|
||||
}
|
||||
|
||||
func setClipboard(ctx context.Context, text string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wl-copy")
|
||||
cmd.Stdin = strings.NewReader(text)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("wl-copy failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func (c *clipboardBackend) Name() string {
|
||||
return "clipboard"
|
||||
}
|
||||
|
||||
func checkClipboardAvailable() error {
|
||||
func (c *clipboardBackend) Available() error {
|
||||
if _, err := exec.LookPath("wl-copy"); err != nil {
|
||||
return fmt.Errorf("wl-copy not found: %w (install wl-clipboard)", err)
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("wl-paste"); err != nil {
|
||||
return fmt.Errorf("wl-paste not found: %w (install wl-clipboard)", err)
|
||||
}
|
||||
|
||||
// Check for Wayland environment
|
||||
if os.Getenv("WAYLAND_DISPLAY") == "" {
|
||||
return fmt.Errorf("WAYLAND_DISPLAY not set - clipboard operations require Wayland session")
|
||||
}
|
||||
@@ -56,3 +34,21 @@ func checkClipboardAvailable() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clipboardBackend) 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
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wl-copy")
|
||||
cmd.Stdin = strings.NewReader(text)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("wl-copy failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package injection
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -11,19 +12,42 @@ type Injector interface {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode string // "clipboard", "type", "fallback"
|
||||
RestoreClipboard bool // Restore original clipboard after injection
|
||||
Backends []string // Ordered list: "ydotool", "wtype", "clipboard"
|
||||
YdotoolTimeout time.Duration // Timeout for ydotool commands
|
||||
WtypeTimeout time.Duration // Timeout for wtype commands
|
||||
ClipboardTimeout time.Duration // Timeout for clipboard operations
|
||||
}
|
||||
|
||||
type injector struct {
|
||||
config Config
|
||||
config Config
|
||||
backends []Backend
|
||||
}
|
||||
|
||||
func NewInjector(config Config) Injector {
|
||||
// Build backend chain from config
|
||||
backends := make([]Backend, 0, len(config.Backends))
|
||||
for _, name := range config.Backends {
|
||||
switch name {
|
||||
case "ydotool":
|
||||
backends = append(backends, NewYdotoolBackend())
|
||||
case "wtype":
|
||||
backends = append(backends, NewWtypeBackend())
|
||||
case "clipboard":
|
||||
backends = append(backends, NewClipboardBackend())
|
||||
default:
|
||||
log.Printf("Injection: unknown backend %q, skipping", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to clipboard if no valid backends
|
||||
if len(backends) == 0 {
|
||||
log.Printf("Injection: no valid backends configured, defaulting to clipboard")
|
||||
backends = append(backends, NewClipboardBackend())
|
||||
}
|
||||
|
||||
return &injector{
|
||||
config: config,
|
||||
config: config,
|
||||
backends: backends,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,58 +56,31 @@ func (i *injector) Inject(ctx context.Context, text string) error {
|
||||
return fmt.Errorf("cannot inject empty text")
|
||||
}
|
||||
|
||||
// Copy to clipboard for clipboard mode and fallback mode
|
||||
var originalClipboard string
|
||||
var err error
|
||||
|
||||
if i.config.Mode == "clipboard" || i.config.Mode == "fallback" {
|
||||
if err := checkClipboardAvailable(); err != nil {
|
||||
return fmt.Errorf("clipboard tools not available: %w", err)
|
||||
}
|
||||
|
||||
if i.config.RestoreClipboard {
|
||||
originalClipboard, _ = getClipboard(ctx, i.config.ClipboardTimeout)
|
||||
}
|
||||
|
||||
if err := setClipboard(ctx, text, i.config.ClipboardTimeout); err != nil {
|
||||
return fmt.Errorf("failed to copy text to clipboard: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different injection modes
|
||||
switch i.config.Mode {
|
||||
case "clipboard":
|
||||
// Already handled above
|
||||
return nil
|
||||
|
||||
case "type":
|
||||
err = typeText(ctx, text, i.config.WtypeTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to type text: %w", err)
|
||||
}
|
||||
|
||||
case "fallback":
|
||||
// Try typing first, fallback to clipboard
|
||||
err = typeText(ctx, text, i.config.WtypeTimeout)
|
||||
if err != nil {
|
||||
// Typing failed, but clipboard is already set from above
|
||||
// Just log the typing error but don't fail the injection
|
||||
// Try each backend in order
|
||||
var lastErr error
|
||||
for _, backend := range i.backends {
|
||||
timeout := i.getTimeout(backend.Name())
|
||||
err := backend.Inject(ctx, text, timeout)
|
||||
if err == nil {
|
||||
log.Printf("Injection: success via %s", backend.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported injection mode: %s", i.config.Mode)
|
||||
log.Printf("Injection: %s failed: %v, trying next backend", backend.Name(), err)
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
// Restore original clipboard if configured and we have it
|
||||
if i.config.RestoreClipboard && originalClipboard != "" {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
restoreCtx, cancel := context.WithTimeout(ctx, i.config.ClipboardTimeout)
|
||||
defer cancel()
|
||||
setClipboard(restoreCtx, originalClipboard, i.config.ClipboardTimeout)
|
||||
}()
|
||||
return fmt.Errorf("all injection backends failed, last error: %w", lastErr)
|
||||
}
|
||||
|
||||
func (i *injector) getTimeout(backendName string) time.Duration {
|
||||
switch backendName {
|
||||
case "ydotool":
|
||||
return i.config.YdotoolTimeout
|
||||
case "wtype":
|
||||
return i.config.WtypeTimeout
|
||||
case "clipboard":
|
||||
return i.config.ClipboardTimeout
|
||||
default:
|
||||
return 5 * time.Second
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
func TestNewInjector(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "fallback",
|
||||
RestoreClipboard: true,
|
||||
Backends: []string{"wtype", "clipboard"},
|
||||
YdotoolTimeout: 5 * time.Second,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
@@ -30,6 +30,41 @@ func TestNewInjector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInjector_DefaultsToClipboard(t *testing.T) {
|
||||
config := Config{
|
||||
Backends: []string{},
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
injector := NewInjector(config)
|
||||
if injector == nil {
|
||||
t.Errorf("NewInjector() returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
// Should default to clipboard backend - just test it works
|
||||
ctx := context.Background()
|
||||
err := injector.Inject(ctx, "test")
|
||||
// Will fail if no clipboard tools, but that's ok
|
||||
if err != nil {
|
||||
t.Logf("Injection failed (expected without tools): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInjector_IgnoresUnknownBackends(t *testing.T) {
|
||||
config := Config{
|
||||
Backends: []string{"unknown", "wtype", "invalid"},
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
injector := NewInjector(config)
|
||||
// Just verify it was created - we can't inspect internals
|
||||
if injector == nil {
|
||||
t.Errorf("NewInjector() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjector_Inject(t *testing.T) {
|
||||
// Skip integration tests in CI environments
|
||||
if os.Getenv("CI") == "true" {
|
||||
@@ -43,32 +78,28 @@ func TestInjector_Inject(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "inject with clipboard mode",
|
||||
name: "inject with clipboard backend",
|
||||
config: Config{
|
||||
Mode: "clipboard",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
Backends: []string{"clipboard"},
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
},
|
||||
text: "test text",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "inject with type mode",
|
||||
name: "inject with wtype backend",
|
||||
config: Config{
|
||||
Mode: "type",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
Backends: []string{"wtype"},
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
},
|
||||
text: "test text",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "inject with fallback mode",
|
||||
name: "inject with fallback chain",
|
||||
config: Config{
|
||||
Mode: "fallback",
|
||||
RestoreClipboard: false,
|
||||
Backends: []string{"ydotool", "wtype", "clipboard"},
|
||||
YdotoolTimeout: 5 * time.Second,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
},
|
||||
@@ -78,25 +109,12 @@ func TestInjector_Inject(t *testing.T) {
|
||||
{
|
||||
name: "inject empty text",
|
||||
config: Config{
|
||||
Mode: "clipboard",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
Backends: []string{"clipboard"},
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
},
|
||||
text: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "inject with invalid mode",
|
||||
config: Config{
|
||||
Mode: "invalid",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
},
|
||||
text: "test text",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -114,18 +132,14 @@ func TestInjector_Inject(t *testing.T) {
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "fallback",
|
||||
RestoreClipboard: true,
|
||||
Backends: []string{"ydotool", "wtype", "clipboard"},
|
||||
YdotoolTimeout: 5 * time.Second,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if config.Mode != "fallback" {
|
||||
t.Errorf("Mode mismatch: got %s, want %s", config.Mode, "fallback")
|
||||
}
|
||||
|
||||
if !config.RestoreClipboard {
|
||||
t.Errorf("RestoreClipboard should be true")
|
||||
if len(config.Backends) != 3 {
|
||||
t.Errorf("Backends length mismatch: got %d, want %d", len(config.Backends), 3)
|
||||
}
|
||||
|
||||
if config.WtypeTimeout != 5*time.Second {
|
||||
@@ -137,123 +151,61 @@ func TestConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeText tests the typeText function
|
||||
func TestTypeText(t *testing.T) {
|
||||
// Skip integration tests in CI environments
|
||||
if os.Getenv("CI") == "true" {
|
||||
t.Skip("Skipping integration test in CI environment")
|
||||
// TestWtypeBackend tests the wtype backend
|
||||
func TestWtypeBackend(t *testing.T) {
|
||||
backend := NewWtypeBackend()
|
||||
|
||||
if backend.Name() != "wtype" {
|
||||
t.Errorf("Name() = %s, want wtype", backend.Name())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "type normal text",
|
||||
text: "hello world",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "type empty text",
|
||||
text: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "type text with special characters",
|
||||
text: "hello\nworld\t!",
|
||||
wantErr: false,
|
||||
},
|
||||
err := backend.Available()
|
||||
if err != nil {
|
||||
t.Logf("wtype not available (expected): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := typeText(ctx, tt.text, 1*time.Second)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("typeText() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Logf("wtype is available")
|
||||
}
|
||||
|
||||
// TestCheckWtypeAvailable tests the wtype availability check
|
||||
func TestCheckWtypeAvailable(t *testing.T) {
|
||||
err := checkWtypeAvailable()
|
||||
// TestYdotoolBackend tests the ydotool backend
|
||||
func TestYdotoolBackend(t *testing.T) {
|
||||
backend := NewYdotoolBackend()
|
||||
|
||||
if backend.Name() != "ydotool" {
|
||||
t.Errorf("Name() = %s, want ydotool", backend.Name())
|
||||
}
|
||||
|
||||
err := backend.Available()
|
||||
if err != nil {
|
||||
t.Logf("checkWtypeAvailable() failed (expected if wtype not installed): %v", err)
|
||||
// Don't fail the test if wtype is not available
|
||||
t.Logf("ydotool not available (expected): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("checkWtypeAvailable() succeeded - wtype is available")
|
||||
t.Logf("ydotool is available")
|
||||
}
|
||||
|
||||
// TestGetClipboard tests the clipboard get functionality
|
||||
func TestGetClipboard(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
// TestClipboardBackend tests the clipboard backend
|
||||
func TestClipboardBackend(t *testing.T) {
|
||||
backend := NewClipboardBackend()
|
||||
|
||||
// Test getting clipboard content
|
||||
content, err := getClipboard(ctx, 1*time.Second)
|
||||
if backend.Name() != "clipboard" {
|
||||
t.Errorf("Name() = %s, want clipboard", backend.Name())
|
||||
}
|
||||
|
||||
err := backend.Available()
|
||||
if err != nil {
|
||||
t.Logf("getClipboard() failed (expected if wl-paste not available): %v", err)
|
||||
// Don't fail the test if clipboard tools are not available
|
||||
t.Logf("clipboard not available (expected): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("getClipboard() succeeded, content length: %d", len(content))
|
||||
}
|
||||
|
||||
// TestSetClipboard tests the clipboard set functionality
|
||||
func TestSetClipboard(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
testText := "test clipboard content"
|
||||
|
||||
err := setClipboard(ctx, testText, 1*time.Second)
|
||||
if err != nil {
|
||||
t.Logf("setClipboard() failed (expected if wl-copy not available): %v", err)
|
||||
// Don't fail the test if clipboard tools are not available
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("setClipboard() succeeded")
|
||||
|
||||
// Try to read it back
|
||||
content, err := getClipboard(ctx, 1*time.Second)
|
||||
if err != nil {
|
||||
t.Logf("Failed to read back clipboard content: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if content != testText {
|
||||
t.Logf("Clipboard content mismatch: got %q, want %q", content, testText)
|
||||
// Don't fail - clipboard might have been modified by other processes
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckClipboardAvailable tests the clipboard tools availability check
|
||||
func TestCheckClipboardAvailable(t *testing.T) {
|
||||
err := checkClipboardAvailable()
|
||||
if err != nil {
|
||||
t.Logf("checkClipboardAvailable() failed (expected if clipboard tools not installed): %v", err)
|
||||
// Don't fail the test if clipboard tools are not available
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("checkClipboardAvailable() succeeded - clipboard tools are available")
|
||||
t.Logf("clipboard is available")
|
||||
}
|
||||
|
||||
// TestInjector_ClipboardMode tests clipboard-only injection
|
||||
func TestInjector_ClipboardMode(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "clipboard",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
Backends: []string{"clipboard"},
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
@@ -264,20 +216,17 @@ func TestInjector_ClipboardMode(t *testing.T) {
|
||||
err := injector.Inject(ctx, "test clipboard text")
|
||||
if err != nil {
|
||||
t.Logf("Clipboard injection failed (expected if clipboard tools not available): %v", err)
|
||||
// Don't fail the test if clipboard tools are not available
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Clipboard injection succeeded")
|
||||
}
|
||||
|
||||
// TestInjector_TypeMode tests typing-only injection
|
||||
func TestInjector_TypeMode(t *testing.T) {
|
||||
// TestInjector_WtypeMode tests wtype-only injection
|
||||
func TestInjector_WtypeMode(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "type",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
Backends: []string{"wtype"},
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
injector := NewInjector(config)
|
||||
@@ -286,19 +235,18 @@ func TestInjector_TypeMode(t *testing.T) {
|
||||
|
||||
err := injector.Inject(ctx, "test typing text")
|
||||
if err != nil {
|
||||
t.Logf("Typing injection failed (expected if wtype not available): %v", err)
|
||||
// Don't fail the test if wtype is not available
|
||||
t.Logf("Wtype injection failed (expected if wtype not available): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Typing injection succeeded")
|
||||
t.Logf("Wtype injection succeeded")
|
||||
}
|
||||
|
||||
// TestInjector_FallbackMode tests fallback injection behavior
|
||||
func TestInjector_FallbackMode(t *testing.T) {
|
||||
// TestInjector_FallbackChain tests fallback chain injection
|
||||
func TestInjector_FallbackChain(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "fallback",
|
||||
RestoreClipboard: false,
|
||||
Backends: []string{"ydotool", "wtype", "clipboard"},
|
||||
YdotoolTimeout: 5 * time.Second,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
@@ -309,8 +257,7 @@ func TestInjector_FallbackMode(t *testing.T) {
|
||||
|
||||
err := injector.Inject(ctx, "test fallback text")
|
||||
if err != nil {
|
||||
t.Logf("Fallback injection failed (expected if both wtype and clipboard tools not available): %v", err)
|
||||
// Don't fail the test if tools are not available
|
||||
t.Logf("Fallback injection failed (expected if all tools not available): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,9 +267,7 @@ func TestInjector_FallbackMode(t *testing.T) {
|
||||
// TestInjector_EmptyText tests injection of empty text
|
||||
func TestInjector_EmptyText(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "clipboard",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
Backends: []string{"clipboard"},
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
@@ -339,27 +284,3 @@ func TestInjector_EmptyText(t *testing.T) {
|
||||
t.Errorf("Inject() error message = %q, want %q", err.Error(), "cannot inject empty text")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjector_InvalidMode tests injection with invalid mode
|
||||
func TestInjector_InvalidMode(t *testing.T) {
|
||||
config := Config{
|
||||
Mode: "invalid",
|
||||
RestoreClipboard: false,
|
||||
WtypeTimeout: 5 * time.Second,
|
||||
ClipboardTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
injector := NewInjector(config)
|
||||
ctx := context.Background()
|
||||
|
||||
err := injector.Inject(ctx, "test text")
|
||||
if err == nil {
|
||||
t.Errorf("Inject() should fail with invalid mode")
|
||||
return
|
||||
}
|
||||
|
||||
expectedError := "unsupported injection mode: invalid"
|
||||
if err.Error() != expectedError {
|
||||
t.Errorf("Inject() error message = %q, want %q", err.Error(), expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,29 +8,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func typeText(ctx context.Context, text string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
type wtypeBackend struct{}
|
||||
|
||||
if err := checkWtypeAvailable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wtype", text)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("wtype failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func NewWtypeBackend() Backend {
|
||||
return &wtypeBackend{}
|
||||
}
|
||||
|
||||
func checkWtypeAvailable() error {
|
||||
func (w *wtypeBackend) Name() string {
|
||||
return "wtype"
|
||||
}
|
||||
|
||||
func (w *wtypeBackend) Available() error {
|
||||
if _, err := exec.LookPath("wtype"); err != nil {
|
||||
return fmt.Errorf("wtype not found: %w (install wtype package)", err)
|
||||
}
|
||||
|
||||
// Check for Wayland environment
|
||||
if os.Getenv("WAYLAND_DISPLAY") == "" {
|
||||
return fmt.Errorf("WAYLAND_DISPLAY not set - wtype requires Wayland session")
|
||||
}
|
||||
@@ -41,3 +33,19 @@ func checkWtypeAvailable() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wtypeBackend) Inject(ctx context.Context, text string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
if err := w.Available(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wtype", "--", text)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("wtype failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package injection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ydotoolBackend struct{}
|
||||
|
||||
func NewYdotoolBackend() Backend {
|
||||
return &ydotoolBackend{}
|
||||
}
|
||||
|
||||
func (y *ydotoolBackend) Name() string {
|
||||
return "ydotool"
|
||||
}
|
||||
|
||||
func (y *ydotoolBackend) Available() error {
|
||||
if _, err := exec.LookPath("ydotool"); err != nil {
|
||||
return fmt.Errorf("ydotool not found: %w (install ydotool package)", err)
|
||||
}
|
||||
|
||||
// Check if ydotoold is running by checking socket
|
||||
socketPath := y.getSocketPath()
|
||||
if socketPath == "" {
|
||||
return fmt.Errorf("ydotoold socket not found - ensure ydotoold is running")
|
||||
}
|
||||
|
||||
// Try to connect to verify daemon is responsive
|
||||
conn, err := net.DialTimeout("unix", socketPath, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ydotoold not responding at %s: %w", socketPath, err)
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (y *ydotoolBackend) getSocketPath() string {
|
||||
// Check YDOTOOL_SOCKET env var first
|
||||
if sock := os.Getenv("YDOTOOL_SOCKET"); sock != "" {
|
||||
if _, err := os.Stat(sock); err == nil {
|
||||
return sock
|
||||
}
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
paths := []string{
|
||||
"/run/user/" + fmt.Sprint(os.Getuid()) + "/.ydotool_socket",
|
||||
"/tmp/.ydotool_socket",
|
||||
}
|
||||
|
||||
// Also check XDG_RUNTIME_DIR
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
paths = append([]string{filepath.Join(xdg, ".ydotool_socket")}, paths...)
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (y *ydotoolBackend) Inject(ctx context.Context, text string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
if err := y.Available(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ydotool type -- "text"
|
||||
cmd := exec.CommandContext(ctx, "ydotool", "type", "--", text)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("ydotool failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user