diff --git a/frontend/index.html b/frontend/index.html
index da6061e..ab21342 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -347,7 +347,7 @@
const title = document.createElement('div');
title.style.fontWeight = '600';
- title.textContent = ready ? 'Automatic paste ready' : 'Automatic paste needs ydotool setup';
+ title.textContent = ready ? 'Direct typing ready' : 'Direct typing needs ydotool setup';
el.appendChild(title);
const message = document.createElement('div');
diff --git a/internal/audio/recorder/recorder.go b/internal/audio/recorder/recorder.go
index 8164bd4..856ae4e 100644
--- a/internal/audio/recorder/recorder.go
+++ b/internal/audio/recorder/recorder.go
@@ -6,6 +6,7 @@ import (
"encoding/binary"
"fmt"
"os"
+ "runtime"
"sync"
"sync/atomic"
@@ -207,16 +208,15 @@ func (r *AudioRecorder) Stop() error {
// callbacks from writing to a closed file.
atomic.StoreInt32(&r.writing, 0)
- // Stop the capture device. We keep the device initialized between recordings
- // to avoid the expensive re-initialization cycle. ALSA privacy indicators
- // will still turn off because we've stopped the stream.
+ // Stop the capture device.
if r.device != nil {
if err := r.device.Stop(); err != nil {
logger.Error("Failed to stop audio device: %v", err)
}
- // Do NOT Uninit the device between recordings. Keeping it alive
- // avoids expensive ALSA device re-probe and reduces CPU spikes.
- // The device will be fully cleaned up in Cleanup().
+ if runtime.GOOS == "linux" {
+ r.device.Uninit()
+ r.device = nil
+ }
}
// Finalize WAV file
diff --git a/internal/xhotkey/hotkey_linux_portal.go b/internal/xhotkey/hotkey_linux_portal.go
index ceb3cdc..dcca8d4 100644
--- a/internal/xhotkey/hotkey_linux_portal.go
+++ b/internal/xhotkey/hotkey_linux_portal.go
@@ -338,34 +338,37 @@ func (hk *Hotkey) registerPortal() error {
return nil
}
-// safeSendKeydown sends a keydown event to the hotkey channel, recovering from panic
-// if the channel has been closed (e.g. during hotkey re-registration).
-func (hk *Hotkey) safeSendKeydown() {
+func (hk *Hotkey) sendPortalEvent(name string, ch chan<- Event) {
+ hk.mu.Lock()
+ stopCh := hk.portalStop
+ registered := hk.registered
+ hk.mu.Unlock()
+ if !registered || stopCh == nil {
+ return
+ }
+
defer func() {
if r := recover(); r != nil {
- logger.Debug("safeSendKeydown: recovered from panic: %v", r)
+ logger.Debug("sendPortalEvent(%s): recovered from panic: %v", name, r)
}
}()
+
select {
- case hk.keydownIn <- Event{}:
- default:
- // Channel buffer is full or closed; skip.
+ case ch <- Event{}:
+ case <-stopCh:
+ case <-time.After(2 * time.Second):
+ logger.Error("Timed out delivering Linux portal hotkey %s event", name)
}
}
-// safeSendKeyup sends a keyup event to the hotkey channel, recovering from panic
-// if the channel has been closed (e.g. during hotkey re-registration).
+// safeSendKeydown sends a keydown event to the hotkey channel.
+func (hk *Hotkey) safeSendKeydown() {
+ hk.sendPortalEvent("keydown", hk.keydownIn)
+}
+
+// safeSendKeyup sends a keyup event to the hotkey channel.
func (hk *Hotkey) safeSendKeyup() {
- defer func() {
- if r := recover(); r != nil {
- logger.Debug("safeSendKeyup: recovered from panic: %v", r)
- }
- }()
- select {
- case hk.keyupIn <- Event{}:
- default:
- // Channel buffer is full or closed; skip.
- }
+ hk.sendPortalEvent("keyup", hk.keyupIn)
}
func (hk *Hotkey) portalSignalLoop() {
@@ -458,4 +461,4 @@ func (hk *Hotkey) cleanupPortal() error {
_ = conn.Close()
}
return nil
-}
\ No newline at end of file
+}
diff --git a/linux_press_daemon.go b/linux_press_daemon.go
index 6968d9f..9465ec1 100644
--- a/linux_press_daemon.go
+++ b/linux_press_daemon.go
@@ -6,7 +6,10 @@ import (
"net/http"
"os"
"sync"
+ "sync/atomic"
"time"
+
+ "wis-free-v3/internal/logger"
)
const linuxPressAddr = "127.0.0.1:9876"
@@ -14,12 +17,13 @@ const linuxPressAddr = "127.0.0.1:9876"
// linuxPressState holds the daemon's state, protected by a mutex.
// All field access must be done while holding the mutex.
type linuxPressState struct {
- mu sync.Mutex
- releaseTimer *time.Timer
- detectTimer *time.Timer
- recording bool
- holdMode bool
- detectingHold bool
+ mu sync.Mutex
+ releaseTimer *time.Timer
+ detectTimer *time.Timer
+ recording bool
+ holdMode bool
+ detectingHold bool
+ cycle uint64
}
var pressState linuxPressState
@@ -68,12 +72,31 @@ func sendLinuxPressPing() error {
func (a *App) startLinuxPressDaemon() {
mux := http.NewServeMux()
mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ logger.Error("Linux press handler recovered from panic: %v", recovered)
+ http.Error(w, "press handler failed", http.StatusInternalServerError)
+ }
+ }()
+
+ if r.Method != http.MethodGet && r.Method != http.MethodPost {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
a.handleLinuxPressPing()
w.WriteHeader(http.StatusNoContent)
})
go func() {
- _ = http.ListenAndServe(linuxPressAddr, mux)
+ server := &http.Server{
+ Addr: linuxPressAddr,
+ Handler: mux,
+ ReadHeaderTimeout: 2 * time.Second,
+ }
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ logger.Error("Linux press daemon stopped: %v", err)
+ }
}()
}
@@ -83,10 +106,12 @@ func (a *App) handleLinuxPressPing() {
if !pressState.recording {
// First ping: start recording with hold detection
+ pressState.cycle++
+ cycle := pressState.cycle
pressState.recording = true
pressState.holdMode = false
pressState.detectingHold = true
- go a.StartRecording()
+ go a.startLinuxPressRecording(cycle)
if pressState.detectTimer != nil {
pressState.detectTimer.Stop()
@@ -94,7 +119,9 @@ func (a *App) handleLinuxPressPing() {
pressState.detectTimer = time.AfterFunc(linuxPressHoldDetectWindow, func() {
pressState.mu.Lock()
defer pressState.mu.Unlock()
- pressState.detectingHold = false
+ if pressState.cycle == cycle {
+ pressState.detectingHold = false
+ }
})
return
}
@@ -121,10 +148,11 @@ func (a *App) resetLinuxPressReleaseTimerLocked() {
pressState.releaseTimer.Stop()
}
+ cycle := pressState.cycle
pressState.releaseTimer = time.AfterFunc(linuxPressReleaseGrace, func() {
pressState.mu.Lock()
defer pressState.mu.Unlock()
- if pressState.recording && pressState.holdMode {
+ if pressState.cycle == cycle && pressState.recording && pressState.holdMode {
a.stopLinuxPressRecordingLocked()
}
})
@@ -132,6 +160,8 @@ func (a *App) resetLinuxPressReleaseTimerLocked() {
func (a *App) stopLinuxPressRecordingLocked() {
// Caller must hold pressState.mu
+ wasRecording := pressState.recording
+ pressState.cycle++
if pressState.releaseTimer != nil {
pressState.releaseTimer.Stop()
pressState.releaseTimer = nil
@@ -140,10 +170,55 @@ func (a *App) stopLinuxPressRecordingLocked() {
pressState.detectTimer.Stop()
pressState.detectTimer = nil
}
- if pressState.recording {
- pressState.recording = false
- pressState.holdMode = false
- pressState.detectingHold = false
- go a.StopRecording()
+ pressState.recording = false
+ pressState.holdMode = false
+ pressState.detectingHold = false
+ if wasRecording {
+ go a.stopLinuxPressRecording()
}
-}
\ No newline at end of file
+}
+
+func (a *App) startLinuxPressRecording(cycle uint64) {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ logger.Error("Linux press start recovered from panic: %v", recovered)
+ a.resetFailedLinuxPressCycle(cycle)
+ }
+ }()
+
+ a.StartRecording()
+ if atomic.LoadInt32(&a.recording) == 0 {
+ a.resetFailedLinuxPressCycle(cycle)
+ }
+}
+
+func (a *App) stopLinuxPressRecording() {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ logger.Error("Linux press stop recovered from panic: %v", recovered)
+ }
+ }()
+
+ a.StopRecording()
+}
+
+func (a *App) resetFailedLinuxPressCycle(cycle uint64) {
+ pressState.mu.Lock()
+ defer pressState.mu.Unlock()
+
+ if pressState.cycle != cycle {
+ return
+ }
+ pressState.cycle++
+ if pressState.releaseTimer != nil {
+ pressState.releaseTimer.Stop()
+ pressState.releaseTimer = nil
+ }
+ if pressState.detectTimer != nil {
+ pressState.detectTimer.Stop()
+ pressState.detectTimer = nil
+ }
+ pressState.recording = false
+ pressState.holdMode = false
+ pressState.detectingHold = false
+}
diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh
index f2b138b..a285831 100755
--- a/scripts/build-linux.sh
+++ b/scripts/build-linux.sh
@@ -41,6 +41,159 @@ command_exists() {
command -v "$1" >/dev/null 2>&1
}
+print_ydotool_install_help() {
+ if command_exists dnf; then
+ echo " sudo dnf install ydotool"
+ elif command_exists apt-get; then
+ echo " sudo apt install ydotool"
+ elif command_exists pacman; then
+ echo " sudo pacman -S ydotool"
+ else
+ echo " Install ydotool with your distribution's package manager."
+ fi
+}
+
+find_ydotoold() {
+ if command_exists ydotoold; then
+ command -v ydotoold
+ return 0
+ fi
+ for candidate in /usr/bin/ydotoold /usr/local/bin/ydotoold; do
+ if [ -x "$candidate" ]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ return 1
+}
+
+setup_ydotool_systemd() {
+ echo ""
+ echo "========================================"
+ echo " Setting up ydotool systemd user service"
+ echo "========================================"
+ echo ""
+
+ if ! command_exists systemctl; then
+ echo "[ERROR] systemctl is not available on this system."
+ return 1
+ fi
+
+ if ! command_exists ydotool; then
+ echo "[ERROR] ydotool is not installed."
+ echo "Install it first:"
+ print_ydotool_install_help
+ return 1
+ fi
+
+ YDOTOOLD_BIN="$(find_ydotoold || true)"
+ if [ -z "$YDOTOOLD_BIN" ]; then
+ echo "[ERROR] ydotoold was not found after installing ydotool."
+ echo "Check your distribution's ydotool package or install the daemon package if it is split out."
+ return 1
+ fi
+
+ SYSTEMD_USER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
+ mkdir -p "$SYSTEMD_USER_DIR"
+ SERVICE_FILE="$SYSTEMD_USER_DIR/ydotool.service"
+
+ cat > "$SERVICE_FILE" << YDSVCEOF
+[Unit]
+Description=ydotool daemon for WIS Free V3 direct keyboard injection
+Documentation=man:ydotool(1)
+After=graphical-session.target
+
+[Service]
+Type=simple
+Environment=YDOTOOL_SOCKET=%t/.ydotool_socket
+ExecStart=$YDOTOOLD_BIN
+Restart=on-failure
+RestartSec=2
+
+[Install]
+WantedBy=default.target
+YDSVCEOF
+
+ echo "[INFO] Created $SERVICE_FILE"
+
+ UDEV_RULE_FILE="/etc/udev/rules.d/80-uinput.rules"
+ if [ -f "$UDEV_RULE_FILE" ] && grep -q 'KERNEL=="uinput"' "$UDEV_RULE_FILE"; then
+ echo "[INFO] uinput udev rule already exists at $UDEV_RULE_FILE"
+ else
+ echo ""
+ echo " /dev/uinput permission rule is needed for ydotool direct typing:"
+ echo ""
+ echo ' KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"'
+ echo ""
+ read -p " Create or update $UDEV_RULE_FILE now? (requires sudo) (y/N): " CREATE_UDEV
+ if [[ "$CREATE_UDEV" == "y" || "$CREATE_UDEV" == "Y" ]]; then
+ echo 'KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' | \
+ sudo tee "$UDEV_RULE_FILE" > /dev/null
+ sudo udevadm control --reload-rules && sudo udevadm trigger
+ echo "[INFO] udev rule created and reloaded."
+ echo " Log out and back in, or reboot, if ydotool still cannot access /dev/uinput."
+ else
+ echo " Skipping udev rule creation. ydotoold may not be able to inject keystrokes."
+ fi
+ fi
+
+ echo ""
+ echo " Reloading systemd user daemon..."
+ systemctl --user daemon-reload
+ echo " Enabling ydotool user service..."
+ systemctl --user enable ydotool.service
+ echo " Starting ydotool user service..."
+ if systemctl --user restart ydotool.service; then
+ echo " ydotool systemd service is running."
+ else
+ echo " [WARN] Could not start ydotool.service. Check: systemctl --user status ydotool.service"
+ fi
+ echo ""
+}
+
+INSTALL_MODE="none" # none, system, user
+INSTALL_SYSTEMD=false
+SHOW_HELP=false
+
+for arg in "$@"; do
+ case "$arg" in
+ --install)
+ INSTALL_MODE="system"
+ ;;
+ --install-user)
+ INSTALL_MODE="user"
+ ;;
+ --install-systemd)
+ INSTALL_SYSTEMD=true
+ ;;
+ --help|-h)
+ SHOW_HELP=true
+ ;;
+ *)
+ echo "[ERROR] Unknown option: $arg"
+ echo "Usage: $0 [--install|--install-user|--install-systemd|--help]"
+ exit 1
+ ;;
+ esac
+done
+
+if [ "$SHOW_HELP" = true ]; then
+ echo "Usage: $0 [--install|--install-user|--install-systemd|--help]"
+ echo ""
+ echo " (no flags) Build only, then offer interactive install prompts"
+ echo " --install Build and install system-wide (/usr/local/bin)"
+ echo " --install-user Build and install per-user (~/.local/bin)"
+ echo " --install-systemd Generate, reload, enable, and start ydotool user service"
+ echo " --help Show this message"
+ echo ""
+ exit 0
+fi
+
+if [ "$INSTALL_SYSTEMD" = true ] && [ "$INSTALL_MODE" = "none" ]; then
+ setup_ydotool_systemd
+ exit $?
+fi
+
# 1. Check for basic tools
if ! command_exists go; then
echo "[ERROR] Go is not installed. Please install Go 1.23+."
@@ -123,28 +276,20 @@ for dep in "${DEPS[@]}"; do
fi
done
-# ydotool check for text injection
+# ydotool check for direct text injection
if ! command_exists ydotool; then
echo ""
echo "==============================================================="
echo " WARNING: ydotool is not installed"
echo "==============================================================="
echo ""
- echo " ydotool is required for automatic text injection (paste) on"
- echo " Wayland. Without it, transcribed text will only be copied to"
- echo " your clipboard."
+ echo " ydotool is required for direct keyboard injection on Wayland."
+ echo " Without it, WIS Free V3 cannot type transcribed text automatically"
+ echo " into the active Linux window."
echo ""
echo " Install ydotool and configure it:"
echo ""
- if command_exists dnf; then
- echo " sudo dnf install ydotool"
- elif command_exists apt-get; then
- echo " sudo apt install ydotool"
- elif command_exists pacman; then
- echo " sudo pacman -S ydotool"
- else
- echo " Install ydotool from your distribution's package manager."
- fi
+ print_ydotool_install_help
echo ""
echo " Then set up udev rules for /dev/uinput access:"
echo " echo 'KERNEL==\"uinput\", SUBSYSTEM==\"misc\", TAG+=\"uaccess\", OPTIONS+=\"static_node=uinput\"' |"
@@ -305,35 +450,6 @@ echo " Without flags, the build-only mode finishes here."
echo " Binary is ready at: $EXECUTABLE"
echo ""
-# Parse flags
-INSTALL_MODE="none" # none, system, user
-INSTALL_SYSTEMD=false
-
-for arg in "$@"; do
- case "$arg" in
- --install)
- INSTALL_MODE="system"
- ;;
- --install-user)
- INSTALL_MODE="user"
- ;;
- --install-systemd)
- INSTALL_SYSTEMD=true
- ;;
- --help|-h)
- echo "Usage: $0 [--install|--install-user|--install-systemd|--help]"
- echo ""
- echo " (no flags) Build only — outputs binary to $EXECUTABLE"
- echo " --install Build + install system-wide (/usr/local/bin)"
- echo " --install-user Build + install per-user (~/.local/bin)"
- echo " --install-systemd Generate and enable systemd user units for ydotool"
- echo " --help Show this message"
- echo ""
- exit 0
- ;;
- esac
-done
-
# If no install flags were passed, do interactive prompt (backward-compatible)
if [ "$INSTALL_MODE" = "none" ] && [ "$INSTALL_SYSTEMD" = false ]; then
read -p "Would you like to install it system-wide to /usr/local/bin? (y/n): " INSTALL
@@ -354,69 +470,7 @@ fi
# --- Systemd ydotool service setup ---
if [ "$INSTALL_SYSTEMD" = true ]; then
- echo ""
- echo "========================================"
- echo " Setting up ydotool systemd user service"
- echo "========================================"
- echo ""
-
- SYSTEMD_USER_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/systemd/user"
- mkdir -p "$SYSTEMD_USER_DIR"
-
- # Create ydotool.service for the user session
- cat > "$SYSTEMD_USER_DIR/ydotool.service" << 'YDSVCEOF'
-[Unit]
-Description=ydotool daemon — simulate keyboard input on Wayland
-Documentation=man:ydotool(1)
-
-[Service]
-Type=simple
-ExecStart=/usr/bin/ydotoold
-Restart=on-failure
-RestartSec=2
-
-[Install]
-WantedBy=default.target
-YDSVCEOF
-
- echo "[INFO] Created $SYSTEMD_USER_DIR/ydotool.service"
-
- # Check/create udev rule for uinput
- UDEV_RULE_FILE="/etc/udev/rules.d/80-uinput.rules"
- if [ ! -f "$UDEV_RULE_FILE" ]; then
- echo ""
- echo " /dev/uinput permission rule not found."
- echo " The following rule is needed for ydotool to inject keystrokes:"
- echo ""
- echo ' KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"'
- echo ""
- read -p " Create $UDEV_RULE_FILE now? (requires sudo) (y/N): " CREATE_UDEV
- if [[ "$CREATE_UDEV" == "y" || "$CREATE_UDEV" == "Y" ]]; then
- echo 'KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' | \
- sudo tee "$UDEV_RULE_FILE" > /dev/null
- sudo udevadm control --reload-rules && sudo udevadm trigger
- echo "[INFO] udev rule created and reloaded."
- echo " You must log out and back in (or reboot) for the permissions to take effect."
- else
- echo " Skipping udev rule creation. ydotoold may not be able to inject keystrokes."
- echo " You can create it manually later."
- fi
- else
- echo "[INFO] udev rule already exists at $UDEV_RULE_FILE"
- fi
-
- # Reload systemd user daemon and enable the service
- echo ""
- echo " Reloading systemd user daemon..."
- systemctl --user daemon-reload
- echo " Enabling ydotool user service..."
- systemctl --user enable ydotool.service
- echo " Starting ydotool user service..."
- systemctl --user start ydotool.service || echo " [WARN] Could not start (may need logout/login for udev perms)"
- echo ""
- echo " ydotool systemd service is now set up."
- echo " You can check its status with: systemctl --user status ydotool"
- echo ""
+ setup_ydotool_systemd
fi
# --- Binary installation ---
@@ -515,4 +569,4 @@ fi
echo "========================================"
echo " Build finished successfully!"
-echo "========================================"
\ No newline at end of file
+echo "========================================"
diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh
index 4fa66de..36c42c0 100644
--- a/scripts/package-linux.sh
+++ b/scripts/package-linux.sh
@@ -80,15 +80,15 @@ if ! pkg-config --exists gtk+-3.0 || ! webkit2_ok || ! pkg-config --exists alsa
echo ""
echo "Ubuntu/Debian:"
echo " sudo apt update && sudo apt install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev libasound2-dev libayatana-appindicator3-dev dpkg-dev rpm"
- echo " sudo apt install -y ydotool (recommended for automatic text injection)"
+ echo " sudo apt install -y ydotool (recommended for direct keyboard injection)"
echo ""
echo "Fedora:"
echo " sudo dnf install -y gcc gcc-c++ make pkgconf-pkg-config gtk3-devel webkit2gtk4.1-devel alsa-lib-devel libayatana-appindicator-gtk3-devel rpm-build"
- echo " sudo dnf install -y ydotool (recommended for automatic text injection)"
+ echo " sudo dnf install -y ydotool (recommended for direct keyboard injection)"
echo ""
echo "Arch Linux:"
echo " sudo pacman -S base-devel pkgconf gtk3 webkit2gtk alsa-lib libayatana-appindicator"
- echo " sudo pacman -S ydotool (recommended for automatic text injection)"
+ echo " sudo pacman -S ydotool (recommended for direct keyboard injection)"
echo ""
exit 1
fi
@@ -194,7 +194,7 @@ Depends: libgtk-3-0, libwebkit2gtk-4.0-37 | libwebkit2gtk-4.1-0, libasound2, lib
Recommends: ydotool
Description: $COMMENT
WIS Free V3 is a desktop voice dictation app built with Go and Wails.
- On Wayland, install ydotool for automatic text injection.
+ On Wayland, install ydotool for direct keyboard injection.
EOF
dpkg-deb --build "$DEB_ROOT" "$DIST_DIR/${APP_NAME}_${PKG_VERSION}-${PKG_RELEASE}_${ARCH_DEB}.deb"
@@ -215,10 +215,11 @@ License: $LICENSE
Requires: gtk3
Requires: alsa-lib
Requires: libayatana-appindicator-gtk3
+Recommends: ydotool
%description
WIS Free V3 is a desktop voice dictation app built with Go and Wails.
-On Wayland, install ydotool for automatic text injection.
+On Wayland, install ydotool for direct keyboard injection.
%install
rm -rf %{buildroot}
@@ -237,4 +238,4 @@ find "$RPM_TOP/RPMS" -type f -name "*.rpm" -exec cp {} "$DIST_DIR/" \;
echo ""
echo "Packages written to:"
-find "$DIST_DIR" -maxdepth 1 -type f \( -name "*.deb" -o -name "*.rpm" \) -print | sort
\ No newline at end of file
+find "$DIST_DIR" -maxdepth 1 -type f \( -name "*.deb" -o -name "*.rpm" \) -print | sort
diff --git a/text_insert_linux.go b/text_insert_linux.go
index 50c243f..a60db02 100644
--- a/text_insert_linux.go
+++ b/text_insert_linux.go
@@ -21,32 +21,16 @@ import (
func (a *App) insertTranscription(text string) {
a.releaseLinuxInputFocus()
- if isASCII(text) {
- 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 via ydotool: %v", err)
- }
+ if err := typeLinuxTextWithYdotool(text); err == nil {
+ logger.Info("Typed transcription on Linux using ydotool (%d chars)", utf8.RuneCountInString(text))
+ return
} else {
- logger.Info("Skipping ydotool direct typing fallback for non-ASCII transcript")
+ logger.Error("Linux direct typing unavailable via ydotool: %v", err)
}
- if err := wailsruntime.ClipboardSetText(a.ctx, text); err != nil {
- logger.Error("Failed to copy transcription to clipboard: %v", err)
- } else {
- waitForLinuxClipboardText(a.ctx, text)
- if err := pasteLinuxClipboardWithYdotool(); err == nil {
- logger.Info("Pasted transcription on Linux using clipboard + ydotool (%d chars)", utf8.RuneCountInString(text))
- return
- } else {
- logger.Error("Linux auto-paste unavailable via ydotool: %v", err)
- }
- }
-
- logger.Info("Copied transcription to clipboard; install ydotool with ydotoold/uinput access for automatic paste on GNOME Wayland")
+ logger.Info("Transcription was not inserted; install ydotool with ydotoold/uinput access for direct Linux typing")
if a.overlay != nil {
- a.overlay.Show("Copied transcript. Press Ctrl+V to paste.")
+ a.overlay.Show("Direct typing unavailable. Check ydotool setup.")
}
}
@@ -59,37 +43,6 @@ func (a *App) releaseLinuxInputFocus() {
time.Sleep(150 * 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) {
- current, err := wailsruntime.ClipboardGetText(ctx)
- if err == nil && current == text {
- return
- }
- time.Sleep(50 * time.Millisecond)
- }
-}
-
-func pasteLinuxClipboardWithYdotool() error {
- time.Sleep(150 * time.Millisecond)
-
- path, socketPath, err := getYdotoolCommand()
- if err != nil {
- return err
- }
-
- return runLinuxInputCommand(path, []string{"key", "-d", "20", "29:1", "47:1", "47:0", "29:0"}, "", 3*time.Second, socketPath)
-}
-
func typeLinuxTextWithYdotool(text string) error {
path, socketPath, err := getYdotoolCommand()
if err != nil {
@@ -126,7 +79,10 @@ func linuxYdotoolStatus() map[string]interface{} {
"socket_path": "",
"message": "",
"setup_commands": []string{
- "sudo dnf install ydotool",
+ "# 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",
@@ -155,11 +111,9 @@ func linuxYdotoolStatus() map[string]interface{} {
}
status["ready"] = true
- status["message"] = "ydotool is ready for automatic paste."
+ 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
@@ -167,12 +121,17 @@ func getYdotoolSocketPath() (string, error) {
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)
+ 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 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 {