mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
feat: enhance ydotool integration for direct typing on Linux; improve error handling and logging in audio processing; update build scripts for better installation guidance
This commit is contained in:
+1
-1
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+80
-5
@@ -6,7 +6,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
)
|
||||
|
||||
const linuxPressAddr = "127.0.0.1:9876"
|
||||
@@ -20,6 +23,7 @@ type linuxPressState struct {
|
||||
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()
|
||||
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()
|
||||
if wasRecording {
|
||||
go a.stopLinuxPressRecording()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+159
-105
@@ -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 ---
|
||||
|
||||
@@ -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}
|
||||
|
||||
+17
-58
@@ -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)
|
||||
}
|
||||
} 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 {
|
||||
|
||||
Reference in New Issue
Block a user