mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
feat: implement ydotool support for Linux with auto-paste functionality and add setup script
This commit is contained in:
+111
-49
@@ -6,7 +6,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -21,19 +23,22 @@ func (a *App) insertTranscription(text string) {
|
||||
logger.Error("Failed to copy transcription to clipboard: %v", err)
|
||||
} else {
|
||||
waitForLinuxClipboardText(a.ctx, text)
|
||||
if typer, err := pasteLinuxClipboardWithoutXTest(); err == nil {
|
||||
logger.Info("Pasted transcription on Linux using %s (%d chars)", typer, utf8.RuneCountInString(text))
|
||||
a.releaseLinuxInputFocus()
|
||||
if err := pasteLinuxClipboardWithYdotool(); err == nil {
|
||||
logger.Info("Pasted transcription on Linux using ydotool (%d chars)", utf8.RuneCountInString(text))
|
||||
return
|
||||
} else {
|
||||
logger.Error("Linux auto-paste unavailable without XTEST: %v", err)
|
||||
logger.Error("Linux auto-paste unavailable via ydotool: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if typer, err := typeLinuxTextWithoutXTest(text); err == nil {
|
||||
logger.Info("Typed transcription on Linux using %s (%d chars)", typer, utf8.RuneCountInString(text))
|
||||
if !isASCII(text) {
|
||||
logger.Info("Skipping ydotool direct typing fallback for non-ASCII transcript")
|
||||
} else if err := typeLinuxTextWithYdotool(text); err == nil {
|
||||
logger.Info("Typed transcription on Linux using ydotool (%d chars)", utf8.RuneCountInString(text))
|
||||
return
|
||||
} else {
|
||||
logger.Error("Linux auto-type unavailable without XTEST: %v", err)
|
||||
logger.Error("Linux auto-type unavailable via ydotool: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("Copied transcription to clipboard; install ydotool with ydotoold/uinput access for automatic paste on GNOME Wayland")
|
||||
@@ -42,6 +47,24 @@ func (a *App) insertTranscription(text string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) releaseLinuxInputFocus() {
|
||||
if a.ctx == nil {
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
return
|
||||
}
|
||||
wailsruntime.WindowHide(a.ctx)
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
func isASCII(text string) bool {
|
||||
for _, r := range text {
|
||||
if r > 127 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func waitForLinuxClipboardText(ctx context.Context, text string) {
|
||||
deadline := time.Now().Add(700 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -53,63 +76,102 @@ func waitForLinuxClipboardText(ctx context.Context, text string) {
|
||||
}
|
||||
}
|
||||
|
||||
func pasteLinuxClipboardWithoutXTest() (string, error) {
|
||||
func pasteLinuxClipboardWithYdotool() error {
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
var errs []string
|
||||
|
||||
if path, err := exec.LookPath("ydotool"); err == nil {
|
||||
if err := runLinuxInputCommand(path, []string{"key", "-d", "20", "29:1", "47:1", "47:0", "29:0"}, "", 3*time.Second); err == nil {
|
||||
return "ydotool", nil
|
||||
} else {
|
||||
errs = append(errs, "ydotool: "+err.Error())
|
||||
}
|
||||
path, socketPath, err := getYdotoolCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if path, err := exec.LookPath("wtype"); err == nil {
|
||||
if err := runLinuxInputCommand(path, []string{"-M", "ctrl", "v", "-m", "ctrl"}, "", 3*time.Second); err == nil {
|
||||
return "wtype", nil
|
||||
} else {
|
||||
errs = append(errs, "wtype: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return "", errors.New("install ydotool with ydotoold/uinput access for GNOME Wayland auto-paste")
|
||||
}
|
||||
return "", errors.New(strings.Join(errs, "; "))
|
||||
return runLinuxInputCommand(path, []string{"key", "-d", "20", "29:1", "47:1", "47:0", "29:0"}, "", 3*time.Second, socketPath)
|
||||
}
|
||||
|
||||
func typeLinuxTextWithoutXTest(text string) (string, error) {
|
||||
var errs []string
|
||||
|
||||
if path, err := exec.LookPath("ydotool"); err == nil {
|
||||
if err := runLinuxInputCommand(path, []string{"type", "--file", "-"}, text, linuxTextTyperTimeout(text)); err == nil {
|
||||
return "ydotool", nil
|
||||
} else {
|
||||
errs = append(errs, "ydotool: "+err.Error())
|
||||
}
|
||||
func typeLinuxTextWithYdotool(text string) error {
|
||||
path, socketPath, err := getYdotoolCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if path, err := exec.LookPath("wtype"); err == nil {
|
||||
if err := runLinuxInputCommand(path, []string{"-"}, text, linuxTextTyperTimeout(text)); err == nil {
|
||||
return "wtype", nil
|
||||
} else {
|
||||
errs = append(errs, "wtype: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return "", errors.New("install ydotool with ydotoold/uinput access for GNOME Wayland auto-typing")
|
||||
}
|
||||
return "", errors.New(strings.Join(errs, "; "))
|
||||
return runLinuxInputCommand(path, []string{"type", "--file", "-"}, text, linuxTextTyperTimeout(text), socketPath)
|
||||
}
|
||||
|
||||
func runLinuxInputCommand(path string, args []string, stdin string, timeout time.Duration) error {
|
||||
func getYdotoolCommand() (string, string, error) {
|
||||
path, err := exec.LookPath("ydotool")
|
||||
if err != nil {
|
||||
return "", "", errors.New("ydotool not found; install ydotool and start the ydotool user service")
|
||||
}
|
||||
|
||||
socketPath, err := getYdotoolSocketPath()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return path, socketPath, nil
|
||||
}
|
||||
|
||||
func linuxYdotoolStatus() map[string]interface{} {
|
||||
status := map[string]interface{}{
|
||||
"ready": false,
|
||||
"installed": false,
|
||||
"socket": false,
|
||||
"socket_path": "",
|
||||
"message": "",
|
||||
"setup_commands": []string{
|
||||
"sudo dnf install ydotool",
|
||||
"echo 'KERNEL==\"uinput\", SUBSYSTEM==\"misc\", TAG+=\"uaccess\", OPTIONS+=\"static_node=uinput\"' | sudo tee /etc/udev/rules.d/80-uinput.rules",
|
||||
"sudo udevadm control --reload-rules && sudo udevadm trigger",
|
||||
"systemctl --user enable --now ydotool.service",
|
||||
},
|
||||
}
|
||||
|
||||
path, err := exec.LookPath("ydotool")
|
||||
if err != nil {
|
||||
status["message"] = "ydotool is not installed."
|
||||
return status
|
||||
}
|
||||
status["installed"] = true
|
||||
|
||||
socketPath, err := getYdotoolSocketPath()
|
||||
if err != nil {
|
||||
status["message"] = err.Error()
|
||||
return status
|
||||
}
|
||||
status["socket"] = true
|
||||
status["socket_path"] = socketPath
|
||||
|
||||
if err := runLinuxInputCommand(path, []string{"key", "-d", "1", "0"}, "", 800*time.Millisecond, socketPath); err != nil {
|
||||
status["message"] = "ydotool is installed, but the daemon test failed: " + err.Error()
|
||||
return status
|
||||
}
|
||||
|
||||
status["ready"] = true
|
||||
status["message"] = "ydotool is ready for automatic paste."
|
||||
return status
|
||||
}
|
||||
|
||||
func getYdotoolSocketPath() (string, error) {
|
||||
if socketPath := strings.TrimSpace(os.Getenv("YDOTOOL_SOCKET")); socketPath != "" {
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
return socketPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("YDOTOOL_SOCKET is set but not accessible: %s", socketPath)
|
||||
}
|
||||
|
||||
socketPath := filepath.Join("/run/user", fmt.Sprintf("%d", os.Getuid()), ".ydotool_socket")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
return "", fmt.Errorf("ydotoold socket not found at %s; run `systemctl --user start ydotool.service` after configuring /dev/uinput permissions", socketPath)
|
||||
}
|
||||
|
||||
return socketPath, nil
|
||||
}
|
||||
|
||||
func runLinuxInputCommand(path string, args []string, stdin string, timeout time.Duration, socketPath string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
cmd.Env = append(os.Environ(), "YDOTOOL_SOCKET="+socketPath)
|
||||
if stdin != "" {
|
||||
cmd.Stdin = strings.NewReader(stdin)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user