mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
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:
+31
-219
@@ -1,57 +1,31 @@
|
||||
//go:build linux
|
||||
|
||||
// ============================================================
|
||||
// LINUX-ONLY FILE — Local HTTP daemon that receives hotkey
|
||||
// press/release pings from the GNOME custom shortcut helper.
|
||||
// This is the Linux equivalent of the Windows hotkey listener.
|
||||
// Any changes here will NOT affect the Windows build.
|
||||
// ============================================================
|
||||
|
||||
// Linux shortcut compatibility helpers.
|
||||
//
|
||||
// Older releases used a loopback HTTP server for the `--press` command. That
|
||||
// made a normal desktop shortcut unreliable: a second key press inside the
|
||||
// hold-detection window could be mistaken for key auto-repeat, and another
|
||||
// local process could occupy the fixed TCP port. Linux already has a
|
||||
// per-user Unix socket for single-instance IPC, so use that authenticated
|
||||
// per-user path instead.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
)
|
||||
|
||||
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
|
||||
cycle uint64
|
||||
}
|
||||
|
||||
var pressState linuxPressState
|
||||
|
||||
const (
|
||||
// linuxPressHoldDetectWindow is the window in which a second ping indicates
|
||||
// the shortcut is being held (push-to-talk mode).
|
||||
linuxPressHoldDetectWindow = 1200 * time.Millisecond
|
||||
|
||||
// linuxPressReleaseGrace is how long after the last ping to wait before
|
||||
// stopping recording in hold mode.
|
||||
linuxPressReleaseGrace = 450 * time.Millisecond
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Keep `--press` working for existing custom shortcuts. A desktop shortcut
|
||||
// invokes a command once, so its correct, predictable behaviour is toggle
|
||||
// (start on the first press and stop on the next), not push-to-talk.
|
||||
if hasPressFlag(os.Args[1:]) {
|
||||
if err := sendLinuxPressPing(); err != nil {
|
||||
os.Exit(1)
|
||||
if tryNotifyRunningInstanceAction("toggle") {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(0)
|
||||
// Do not launch a second GUI process from this legacy helper. The current
|
||||
// settings UI emits --action=toggle, which can start the app if needed.
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,184 +38,22 @@ func hasPressFlag(args []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func sendLinuxPressPing() error {
|
||||
client := &http.Client{Timeout: 300 * time.Millisecond}
|
||||
resp, err := client.Get("http://" + linuxPressAddr + "/press")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return nil
|
||||
// linuxShortcutCommand returns a shell-safe command for GNOME/KDE custom
|
||||
// shortcuts. Quote the executable because user home directories can contain
|
||||
// spaces or shell metacharacters.
|
||||
func linuxShortcutCommand(executable string) string {
|
||||
return linuxShellQuote(executable) + " --action=toggle"
|
||||
}
|
||||
|
||||
// linuxPressServer holds the HTTP server reference for graceful shutdown.
|
||||
var linuxPressServer *http.Server
|
||||
|
||||
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() {
|
||||
server := &http.Server{
|
||||
Addr: linuxPressAddr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
IdleTimeout: 5 * time.Second,
|
||||
}
|
||||
linuxPressServer = server
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("Linux press daemon stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
func linuxShellQuote(value string) string {
|
||||
if value == "" {
|
||||
return "''"
|
||||
}
|
||||
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func (a *App) handleLinuxPressPing() {
|
||||
pressState.mu.Lock()
|
||||
defer pressState.mu.Unlock()
|
||||
// The old daemon API remains a no-op so the common lifecycle code and
|
||||
// non-Linux stub stay simple. Shortcut delivery now uses the instance socket.
|
||||
func (a *App) startLinuxPressDaemon() {}
|
||||
|
||||
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.startLinuxPressRecording(cycle)
|
||||
|
||||
if pressState.detectTimer != nil {
|
||||
pressState.detectTimer.Stop()
|
||||
}
|
||||
pressState.detectTimer = time.AfterFunc(linuxPressHoldDetectWindow, func() {
|
||||
pressState.mu.Lock()
|
||||
defer pressState.mu.Unlock()
|
||||
if pressState.cycle == cycle {
|
||||
pressState.detectingHold = false
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Already recording. If we're still in the hold-detect window or confirmed hold mode,
|
||||
// this is a hold-mode keepalive ping (key auto-repeat).
|
||||
if pressState.detectingHold || pressState.holdMode {
|
||||
pressState.holdMode = true
|
||||
pressState.detectingHold = false
|
||||
if pressState.detectTimer != nil {
|
||||
pressState.detectTimer.Stop()
|
||||
pressState.detectTimer = nil
|
||||
}
|
||||
a.resetLinuxPressReleaseTimerLocked()
|
||||
return
|
||||
}
|
||||
|
||||
// Already recording but not in hold mode: this is a toggle-off ping (second press).
|
||||
a.stopLinuxPressRecordingLocked()
|
||||
}
|
||||
|
||||
func (a *App) resetLinuxPressReleaseTimerLocked() {
|
||||
if pressState.releaseTimer != nil {
|
||||
pressState.releaseTimer.Stop()
|
||||
}
|
||||
|
||||
cycle := pressState.cycle
|
||||
pressState.releaseTimer = time.AfterFunc(linuxPressReleaseGrace, func() {
|
||||
pressState.mu.Lock()
|
||||
defer pressState.mu.Unlock()
|
||||
if pressState.cycle == cycle && pressState.recording && pressState.holdMode {
|
||||
a.stopLinuxPressRecordingLocked()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) stopLinuxPressRecordingLocked() {
|
||||
// Caller must hold pressState.mu
|
||||
wasRecording := pressState.recording
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func stopLinuxPressDaemon() {
|
||||
if linuxPressServer != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := linuxPressServer.Shutdown(ctx); err != nil {
|
||||
logger.Error("Error shutting down Linux press daemon: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func stopLinuxPressDaemon() {}
|
||||
|
||||
Reference in New Issue
Block a user