From 5b6e39c5e84af03cb2d9d105a3841ab0b7d8138b Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 10 Jul 2026 14:58:25 -0700 Subject: [PATCH] Refactor Linux hotkey handling and improve installation process - Removed the legacy HTTP daemon for handling hotkey presses on Linux, replacing it with a simpler command-based approach using `--action=toggle`. - Updated the build script to support non-interactive installation for CI environments with the `--no-install` flag. - Enhanced the ydotool setup process to prefer distribution-provided systemd units and improved error messages for missing dependencies. - Changed the way active window titles are retrieved on Linux to avoid using robotgo, which can fail on Wayland. - Updated README and frontend hints to clarify the use of GlobalShortcuts portal and fallback commands for Linux. - Improved package building scripts to allow building only Debian or RPM packages based on command-line flags. - Adjusted the text insertion logic to handle different versions of ydotool more robustly. --- .github/workflows/build-linux.yml | 28 +--- README.md | 220 ++++++++++++++++++-------- active_window_linux.go | 11 ++ active_window_nonlinux.go | 9 ++ app.go | 70 ++++----- frontend/index.html | 10 +- linux_press_daemon.go | 250 ++++-------------------------- linux_press_daemon_stub.go | 4 + scripts/build-linux.sh | 45 ++++-- scripts/package-linux.sh | 63 +++++++- text_insert_linux.go | 60 +++++-- 11 files changed, 392 insertions(+), 378 deletions(-) create mode 100644 active_window_linux.go create mode 100644 active_window_nonlinux.go diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 04be63c..25c9e0b 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -35,37 +35,15 @@ jobs: libasound2-dev \ libayatana-appindicator3-dev - - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - - name: Install frontend dependencies - run: npm install + run: npm ci working-directory: frontend - - name: Determine version - id: version - run: | - if [ -f scripts/VERSION ]; then - VERSION=$(head -n1 scripts/VERSION | tr -d '\r\n' | xargs) - fi - if [ -z "$VERSION" ]; then - VERSION="dev" - fi - echo "app_version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Fix npm bin permissions - run: chmod +x frontend/node_modules/.bin/* 2>/dev/null || true - - name: Build app - run: | - WAILS_WEBKIT_TAGS="" - if pkg-config --exists webkit2gtk-4.1 2>/dev/null; then - WAILS_WEBKIT_TAGS="-tags webkit2_41" - fi - wails build -platform linux/amd64 -clean $WAILS_WEBKIT_TAGS -ldflags "-X main.AppVersion=${{ steps.version.outputs.app_version }}" + run: bash scripts/build-linux.sh --no-install - name: Upload binary uses: actions/upload-artifact@v4 with: name: wis-free-v3-linux-amd64 - path: build/bin/wis-free-v3 \ No newline at end of file + path: build/bin/wis-free-v3 diff --git a/README.md b/README.md index a9f0e3e..abf4ee5 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,170 @@ -# wis-free-v3 +# WIS Free V3 -A high-performance, cross-platform (Windows & Linux) voice dictation application built in Go using the Wails framework. **wis-free-v3** provides instant speech-to-text with AI-powered refinement, operating as a background service with global hotkey support. +WIS Free V3 is a desktop voice-dictation app built with Go and Wails. It records audio from a global shortcut, transcribes it with Groq Whisper or a local backend, optionally refines the result, and types the text into the focused application. ---- +Windows and Linux are supported. Linux packages and source builds currently target `amd64`/`x86_64`. -## 🚀 Features +## Linux installation -- **Blazing Fast**: Native implementation ensures zero lag during recording and transcription. -- **Global Accessibility**: Trigger from anywhere via configurable global hotkeys. -- **Cross-Platform**: Supports Windows natively and Linux (X11 strongly recommended; Wayland users may need to map desktop shortcut triggers). -- **AI-Powered Refinement**: Integrates Groq (Whisper + Llama) for intelligent punctuation and grammar fixing. -- **Offline Capability**: Supports local Whisper.cpp for sensitive or offline workflows. -- **Micro-Automation**: Automatically pastes transcribed text directly into your active window. -- **Ultra-Leighton**: Low memory footprint while running in the system tray. +Install a package with the native package manager so it resolves runtime dependencies. Do not use `dpkg -i` on its own. -## 📂 Project Structure +### Debian and Ubuntu -```text -. -├── assets/ # Branding and icons -├── build/ # Wails build artifacts and manifests -├── frontend/ # Svelte/Vue/React settings UI -├── internal/ # Private application logic -│ ├── audio/ # Sound capture and processing -│ ├── config/ # Persistent configuration management -│ ├── hotkey/ # Global keyboard hooks -│ ├── logger/ # Structured logging utilities -│ ├── services/ # Cloud and local AI providers -│ ├── system/ # Windows OS integration (Startup/Tray) -│ └── ui/ # Native overlay and window management -├── scripts/ # Development and deployment automation -├── app.go # Application lifecycle management -├── main.go # Entry point -└── wails.json # Project configuration +Download the `.deb` for your architecture, then run: + +```bash +sudo apt install ./wis-free-v3__amd64.deb ``` -## 🛠️ Getting Started +If a previous `dpkg -i` attempt left dependencies unfinished, repair them first: -### Prerequisites - -- **Go**: 1.23 or higher -- **Wails CLI**: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` -- **Compiler**: GCC (TDM-GCC recommended for Windows) - -### Installation & Build - -1. Clone the repository. -2. Run the build script: - ```powershell - .\scripts\build.bat - ``` -3. The executable will be available in `build\bin/wis-free-v3.exe`. - -## ⚙️ Configuration - -Settings are managed via the built-in UI (Right-click tray → Settings) or manually in `%USERPROFILE%\.wis-free-v3\config.json`. - -### Linux Wayland Paste Setup - -If Settings says automatic paste needs `ydotool` setup, run the commands shown there. After those commands finish, restart your computer, then open WIS Free V3 again. - -```json -{ - "api_key": "gsk_...", - "shortcut": "alt+z", - "whisper_model": "whisper-large-v3-turbo", - "ai_model": "llama-3.3-70b-versatile" -} +```bash +sudo apt -f install +sudo apt install ./wis-free-v3__amd64.deb ``` -## 🤝 Contributing +### Fedora and other RPM-based distributions -This project is maintained with a focus on code quality and modular architecture. Please ensure all logic remains within the `internal/` package to maintain clean boundaries. +Install the RPM with the distribution package manager: -## 📄 License +```bash +sudo dnf install ./wis-free-v3--1.x86_64.rpm +``` -Distributed under the MIT License. See `LICENSE` for more information. +For openSUSE, use `sudo zypper install ./wis-free-v3-.x86_64.rpm`. + +After installing, launch **WIS Free V3** from the application launcher or run `wis-free-v3` in a terminal. A reboot is not required. + +## Linux first-run setup + +### Global shortcut + +On current GNOME, KDE Plasma, and other desktops that implement the XDG GlobalShortcuts portal, set the shortcut in WIS Free V3 Settings. The desktop may ask you to approve or change the shortcut. + +If the portal is unavailable on your desktop, use the **Fallback System Shortcut** command shown in Settings: + +- GNOME: **Settings -> Keyboard -> Custom Shortcuts** +- KDE Plasma: **System Settings -> Shortcuts -> Command/URL** + +That command toggles recording: one press starts and the next press stops. It is shell-quoted automatically, including when the app is installed in a path containing spaces. It also works from a cold start because it uses `--action=toggle`. + +Set `WISFREE_USE_PORTAL_HOTKEY=0` before launching the app only to deliberately disable portal shortcuts and use the fallback command instead. + +### Direct typing with ydotool + +WIS Free V3 uses `ydotool` to type transcriptions into the active application. This works on both Wayland and X11, but it needs the persistent `ydotoold` service. + +Install the dependency, then enable the service for your user: + +```bash +# Debian / Ubuntu +sudo apt install ydotool + +# Fedora +sudo dnf install ydotool + +# Arch Linux +sudo pacman -S ydotool + +# All distributions that provide the standard user unit +systemctl --user enable --now ydotool.service +``` + +Distribution packages normally install the required `/dev/uinput` rule and the `ydotool.service` user unit. Do not add an extra manual udev rule unless your distribution's ydotool documentation says its package does not provide one. + +Open Settings again and check the **Direct typing** status. If the socket is still unavailable after enabling the service, log out and back in once, then run: + +```bash +systemctl --user restart ydotool.service +``` + +For a ydotool installation without a packaged user unit, the source checkout can create a separate WIS Free V3 user unit without overwriting distribution files: + +```bash +bash scripts/build-linux.sh --install-systemd +``` + +To diagnose it, use: + +```bash +systemctl --user status ydotool.service +journalctl --user -u ydotool.service -b +``` + +The app detects sockets from the standard user-runtime directory, `/tmp/.ydotool_socket`, and common system-service locations. You can override detection with `YDOTOOL_SOCKET=/path/to/socket` before launching WIS Free V3. + +### GNOME tray icon + +GNOME needs the **AppIndicator and KStatusNotifierItem Support** extension for a traditional system-tray icon. The app still runs without it, but use the launcher or a configured shortcut to open Settings. On Debian/Ubuntu: + +```bash +sudo apt install gnome-shell-extension-appindicator +``` + +Enable the extension in **Settings -> Extensions**. Other major desktops generally expose the tray icon without an extra extension. + +## Build from source on Linux + +Install Go 1.24+, Node.js 20+, and native dependencies. The build script handles WebKit 4.0 versus 4.1 and Wails tags automatically. + +### Debian / Ubuntu + +```bash +sudo apt update +sudo apt install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev libasound2-dev libayatana-appindicator3-dev +``` + +If your release does not provide `libwebkit2gtk-4.1-dev`, install `libwebkit2gtk-4.0-dev` instead. + +### Fedora + +```bash +sudo dnf install -y gcc gcc-c++ make pkgconf-pkg-config gtk3-devel webkit2gtk4.1-devel alsa-lib-devel libayatana-appindicator-gtk3-devel +``` + +### Arch Linux + +```bash +sudo pacman -S base-devel pkgconf gtk3 webkit2gtk alsa-lib libayatana-appindicator +``` + +Then clone the repository and build: + +```bash +git clone +cd wisp-open +go install github.com/wailsapp/wails/v2/cmd/wails@latest +bash scripts/build-linux.sh --install-user +``` + +Use `--install` for `/usr/local/bin`, `--install-user` for `~/.local/bin`, or `--no-install` for a non-interactive build. The binary is written to `build/bin/wis-free-v3`. + +## Building packages + +Create distribution packages from a Linux checkout: + +```bash +# Build only the format available on your build host +bash scripts/package-linux.sh --deb +bash scripts/package-linux.sh --rpm +``` + +With no flag, the script builds both formats and therefore requires both `dpkg-deb` and `rpmbuild`. Packages are placed in `dist/packages/`. The package metadata records the WebKit and AppIndicator ABI selected during the build, preventing a package built against one implementation from silently depending on the other. + +## Troubleshooting + +| Symptom | What to check | +| --- | --- | +| The package installed but the app will not launch | Install it with `apt install ./file.deb` or `dnf install ./file.rpm` so runtime libraries are resolved. Launch `wis-free-v3` from a terminal once to see any loader error. | +| The shortcut does nothing | Configure the portal shortcut in Settings. If your desktop rejects it, use the fallback command displayed there. | +| Transcription completes but text is not inserted | Open Settings and complete the ydotool setup. Check `systemctl --user status ydotool.service`. | +| `ydotoold socket not found` | Enable the user service, then log out/in once only if the service still cannot access `/dev/uinput`. | +| The app runs but no tray icon appears on GNOME | Install and enable the AppIndicator extension. | + +## Configuration + +Settings are available from the tray menu or the Settings window. The configuration file is stored in `~/.wis-free-v3/config.json`. + +## License + +Distributed under the MIT License. diff --git a/active_window_linux.go b/active_window_linux.go new file mode 100644 index 0000000..d5487e8 --- /dev/null +++ b/active_window_linux.go @@ -0,0 +1,11 @@ +//go:build linux + +package main + +// activeWindowTitle intentionally avoids robotgo on Linux. robotgo reads X11 +// state directly and can fail or emit noisy errors on Wayland. Refinement is +// optional, so an empty application-context string is safer than risking a +// failed transcription after recording has completed. +func activeWindowTitle() string { + return "" +} diff --git a/active_window_nonlinux.go b/active_window_nonlinux.go new file mode 100644 index 0000000..c20a248 --- /dev/null +++ b/active_window_nonlinux.go @@ -0,0 +1,9 @@ +//go:build !linux + +package main + +import "github.com/go-vgo/robotgo" + +func activeWindowTitle() string { + return robotgo.GetTitle() +} diff --git a/app.go b/app.go index 3729974..8d94dc1 100644 --- a/app.go +++ b/app.go @@ -20,7 +20,6 @@ import ( "wis-free-v3/internal/services/whisper" "wis-free-v3/internal/ui/tray" - "github.com/go-vgo/robotgo" wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -379,7 +378,10 @@ func (a *App) processRecording(recordingPath string) { logger.Info("Transcribed %d characters", len(text)) - activeWindow := robotgo.GetTitle() + // robotgo queries X11 directly. Do not invoke it from a Wayland session; + // the Linux helper returns an empty context instead of making transcription + // fail after the audio has already been captured. + activeWindow := activeWindowTitle() logger.Info("Active window for context: %s", activeWindow) // Refine text (optional) @@ -447,7 +449,7 @@ func (a *App) GetSettings() map[string]interface{} { // and text_insert_linux.go for the implementations. if runtime.GOOS == "linux" { if exePath, err := os.Executable(); err == nil { - conf["linux_press_command"] = exePath + " --press" + conf["linux_press_command"] = linuxShortcutCommand(exePath) } conf["linux_press_mode"] = true conf["linux_ydotool_status"] = linuxYdotoolStatus() @@ -465,41 +467,30 @@ func (a *App) SaveSettings(settings map[string]interface{}) string { a.config.APIKey = val } } - // PLATFORM NOTE: Shortcut saving is disabled on Linux because Linux uses - // the `--press` daemon approach (GNOME custom shortcuts) instead of the - // built-in hotkey listener. On Windows, we allow the user to configure - // the shortcut through the settings UI. - if runtime.GOOS != "linux" { - if val, ok := settings["shortcut"].(string); ok { - _, _, modOnly, ok := hotkey.ParseShortcut(val) - if !ok { - logger.Error("Invalid shortcut: %s (rejected)", val) - return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" - } - if modOnly && runtime.GOOS != "windows" { - return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" - } + if val, ok := settings["shortcut"].(string); ok { + _, _, modOnly, ok := hotkey.ParseShortcut(val) + if !ok { + logger.Error("Invalid shortcut: %s (rejected)", val) + return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" + } + if modOnly && runtime.GOOS != "windows" { + return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" + } - a.config.Shortcut = val - // Update existing listener with new shortcut (hot-swap) - if a.hotkeyListener != nil { - a.hotkeyListener.UpdateShortcut(val) - } else { - // Should not happen if app started correctly, but just in case - a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) - if runtime.GOOS != "windows" { - a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { - logger.Error("Linux hotkey registration failed: %v", err) - go func() { - time.Sleep(2 * time.Second) - if a.overlay != nil { - a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.") - } - }() - }) - } - a.hotkeyListener.Start() + a.config.Shortcut = val + // Hot-swap the listener on every platform. Linux uses the XDG + // GlobalShortcuts portal when the desktop implements it and otherwise + // exposes the custom-command fallback in Settings. + if a.hotkeyListener != nil { + a.hotkeyListener.UpdateShortcut(val) + } else { + a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) + if runtime.GOOS != "windows" { + a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { + logger.Error("Linux hotkey registration failed: %v", err) + }) } + a.hotkeyListener.Start() } } if val, ok := settings["whisper_model"].(string); ok { @@ -674,7 +665,7 @@ func (a *App) startupHeadless() { } a.hotkeyListener.Start() } else { - logger.Info("Linux portal hotkey disabled; use the --press command from Settings for GNOME shortcuts") + logger.Info("Linux portal hotkey disabled; use the command shown in Settings for a desktop shortcut") } logger.Info("Components initialized successfully!") @@ -686,7 +677,10 @@ func shouldStartBuiltInHotkeyListener() bool { if runtime.GOOS != "linux" { return true } - return os.Getenv("WISFREE_USE_PORTAL_HOTKEY") == "1" + // The portal is the safest cross-desktop implementation on modern Linux. + // WISFREE_USE_PORTAL_HOTKEY=0 remains an escape hatch for desktops with a + // broken portal; Settings always provides a custom-shortcut fallback. + return os.Getenv("WISFREE_USE_PORTAL_HOTKEY") != "0" } // Shutdown cleans up resources diff --git a/frontend/index.html b/frontend/index.html index ab21342..f335e2d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -50,14 +50,14 @@ -

Press the key combination you want to use to start/stop recording (e.g. alt+z, ctrl+shift+space). Modifier-only combos like ctrl+alt are also supported on Windows.

+

Uses the desktop GlobalShortcuts portal when available. Press the key combination you want to use to start/stop recording (e.g. alt+z, ctrl+shift+space). Modifier-only combos like ctrl+alt are supported only on Windows.

Input
- +