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.
This commit is contained in:
John Doe
2026-07-10 14:58:25 -07:00
parent 26022d67fb
commit 5b6e39c5e8
11 changed files with 392 additions and 378 deletions
+32 -38
View File
@@ -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