//go:build linux // ============================================================ // LINUX-ONLY FILE — This file compiles ONLY on Linux. // Any changes here will NOT affect the Windows build. // For the Windows equivalent, see text_insert_nonlinux.go // ============================================================ package main import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "unicode/utf8" "wis-free-v3/internal/logger" wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime" ) func (a *App) insertTranscription(text string) { a.releaseLinuxInputFocus() if err := typeLinuxTextWithYdotool(text); err == nil { logger.Info("Typed transcription on Linux using ydotool (%d chars)", utf8.RuneCountInString(text)) return } else { logger.Error("Linux direct typing unavailable via ydotool: %v", err) } logger.Info("Transcription was not inserted; install ydotool with ydotoold/uinput access for direct Linux typing") if a.overlay != nil { a.overlay.Show("Direct typing unavailable. Check ydotool setup.") } } func (a *App) releaseLinuxInputFocus() { if a.ctx == nil { time.Sleep(30 * time.Millisecond) return } wailsruntime.WindowHide(a.ctx) time.Sleep(50 * time.Millisecond) } func typeLinuxTextWithYdotool(text string) error { path, socketPath, err := getYdotoolCommand() if err != nil { return err } fastArgs := []string{"type", "-d", "1", "--file", "-"} if err := runLinuxInputCommand(path, fastArgs, text, linuxTextTyperTimeout(text), socketPath); err == nil { return nil } return runLinuxInputCommand(path, []string{"type", "--file", "-"}, text, linuxTextTyperTimeout(text), socketPath) } 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{ "# Install ydotool with your package manager, for example:", "sudo apt install ydotool # Debian/Ubuntu", "sudo dnf install ydotool # Fedora", "sudo pacman -S ydotool # Arch", "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", "# Restart your computer, then open WIS Free V3 again.", }, } 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 direct typing." 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) } candidates := []string{ filepath.Join("/run/user", fmt.Sprintf("%d", os.Getuid()), ".ydotool_socket"), "/tmp/.ydotool_socket", } for _, socketPath := range candidates { if _, err := os.Stat(socketPath); err == nil { return socketPath, nil } } return "", fmt.Errorf("ydotoold socket not found; run `systemctl --user start ydotool.service` after configuring /dev/uinput permissions") } 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) } out, err := cmd.CombinedOutput() if ctx.Err() != nil { return ctx.Err() } if err != nil { msg := strings.TrimSpace(string(out)) if msg != "" { return fmt.Errorf("%w: %s", err, msg) } return err } return nil } func linuxTextTyperTimeout(text string) time.Duration { timeout := 5*time.Second + time.Duration(utf8.RuneCountInString(text))*30*time.Millisecond if timeout > 2*time.Minute { return 2 * time.Minute } return timeout }