mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
fix: prevent concurrent transcription races and mask API key
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,18 +26,19 @@ import (
|
|||||||
|
|
||||||
// App struct
|
// App struct
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
audioRecorder *recorder.AudioRecorder
|
audioRecorder *recorder.AudioRecorder
|
||||||
hotkeyListener *hotkey.Listener
|
hotkeyListener *hotkey.Listener
|
||||||
transcriber *transcriber.Client
|
transcriber *transcriber.Client
|
||||||
config *config.Config
|
config *config.Config
|
||||||
overlay platform.Overlay
|
overlay platform.Overlay
|
||||||
recordingPath string
|
recordingPath string
|
||||||
recording int32
|
recording int32
|
||||||
isQuitting bool
|
isQuitting bool
|
||||||
wasMediaPlaying bool
|
wasMediaPlaying bool
|
||||||
whisperManager *whisper.Manager
|
whisperManager *whisper.Manager
|
||||||
tempDir string
|
tempDir string
|
||||||
|
transcribing int32 // atomic: 1 = transcription in progress, prevents concurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App application struct
|
// NewApp creates a new App application struct
|
||||||
@@ -236,6 +238,8 @@ func (a *App) StartRecording() {
|
|||||||
err = a.audioRecorder.Start(a.recordingPath)
|
err = a.audioRecorder.Start(a.recordingPath)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Clean up the orphaned temp file since recording failed to start
|
||||||
|
os.Remove(a.recordingPath)
|
||||||
if a.overlay != nil {
|
if a.overlay != nil {
|
||||||
a.overlay.Hide()
|
a.overlay.Hide()
|
||||||
}
|
}
|
||||||
@@ -268,23 +272,25 @@ func (a *App) StopRecording() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if a.audioRecorder == nil {
|
if a.audioRecorder == nil {
|
||||||
atomic.StoreInt32(&a.recording, 0)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture the path BEFORE stopping the recorder, so it can't be
|
||||||
|
// overwritten by a concurrent StartRecording (which sets a.recordingPath).
|
||||||
|
pathToProcess := a.recordingPath
|
||||||
|
|
||||||
err := a.audioRecorder.Stop()
|
err := a.audioRecorder.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to stop recording: %v", err)
|
logger.Error("Failed to stop recording: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture the path before it can be overwritten by another immediate start
|
|
||||||
pathToProcess := a.recordingPath
|
|
||||||
// Transcribe in a goroutine to avoid blocking
|
// Transcribe in a goroutine to avoid blocking
|
||||||
go a.processRecording(pathToProcess)
|
go a.processRecording(pathToProcess)
|
||||||
}
|
}
|
||||||
|
|
||||||
// processRecording handles transcription and pasting
|
// processRecording handles transcription and pasting.
|
||||||
|
// Uses an atomic guard to prevent concurrent transcriptions.
|
||||||
func (a *App) processRecording(recordingPath string) {
|
func (a *App) processRecording(recordingPath string) {
|
||||||
if recordingPath == "" {
|
if recordingPath == "" {
|
||||||
logger.Error("No recording path set")
|
logger.Error("No recording path set")
|
||||||
@@ -295,6 +301,15 @@ func (a *App) processRecording(recordingPath string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent concurrent transcriptions: only one goroutine can process at a time.
|
||||||
|
// If another transcription is already in progress, discard this recording.
|
||||||
|
if !atomic.CompareAndSwapInt32(&a.transcribing, 0, 1) {
|
||||||
|
logger.Info("Another transcription already in progress, discarding recording: %s", recordingPath)
|
||||||
|
os.Remove(recordingPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer atomic.StoreInt32(&a.transcribing, 0)
|
||||||
|
|
||||||
// IDIOT-PROOFING: Ignore extremely short recordings (less than ~100ms or ~3KB)
|
// IDIOT-PROOFING: Ignore extremely short recordings (less than ~100ms or ~3KB)
|
||||||
// that are likely accidental clicks or hardware glitches.
|
// that are likely accidental clicks or hardware glitches.
|
||||||
stat, statErr := os.Stat(recordingPath)
|
stat, statErr := os.Stat(recordingPath)
|
||||||
@@ -407,7 +422,17 @@ func (a *App) processRecording(recordingPath string) {
|
|||||||
// GetSettings returns the current configuration
|
// GetSettings returns the current configuration
|
||||||
func (a *App) GetSettings() map[string]interface{} {
|
func (a *App) GetSettings() map[string]interface{} {
|
||||||
conf := make(map[string]interface{})
|
conf := make(map[string]interface{})
|
||||||
conf["api_key"] = a.config.APIKey
|
// Mask the API key: only reveal the last 4 characters so the user can verify
|
||||||
|
// which key is configured without exposing the full secret to the frontend.
|
||||||
|
if a.config.APIKey != "" {
|
||||||
|
key := a.config.APIKey
|
||||||
|
if len(key) > 4 {
|
||||||
|
key = "****" + key[len(key)-4:]
|
||||||
|
}
|
||||||
|
conf["api_key"] = key
|
||||||
|
} else {
|
||||||
|
conf["api_key"] = ""
|
||||||
|
}
|
||||||
conf["shortcut"] = a.config.Shortcut
|
conf["shortcut"] = a.config.Shortcut
|
||||||
conf["whisper_model"] = a.config.WhisperModel
|
conf["whisper_model"] = a.config.WhisperModel
|
||||||
conf["ai_model"] = a.config.AIModel
|
conf["ai_model"] = a.config.AIModel
|
||||||
@@ -433,7 +458,12 @@ func (a *App) GetSettings() map[string]interface{} {
|
|||||||
// SaveSettings updates the configuration
|
// SaveSettings updates the configuration
|
||||||
func (a *App) SaveSettings(settings map[string]interface{}) string {
|
func (a *App) SaveSettings(settings map[string]interface{}) string {
|
||||||
if val, ok := settings["api_key"].(string); ok {
|
if val, ok := settings["api_key"].(string); ok {
|
||||||
a.config.APIKey = val
|
// Only update the API key if it's not the masked value returned by GetSettings.
|
||||||
|
// GetSettings masks the key as "****abcd" so the frontend can show the last 4 chars.
|
||||||
|
// If the user didn't change it and sent back the masked value, preserve the real key.
|
||||||
|
if !strings.HasPrefix(val, "****") {
|
||||||
|
a.config.APIKey = val
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// PLATFORM NOTE: Shortcut saving is disabled on Linux because Linux uses
|
// PLATFORM NOTE: Shortcut saving is disabled on Linux because Linux uses
|
||||||
// the `--press` daemon approach (GNOME custom shortcuts) instead of the
|
// the `--press` daemon approach (GNOME custom shortcuts) instead of the
|
||||||
@@ -502,7 +532,9 @@ func (a *App) SaveSettings(settings map[string]interface{}) string {
|
|||||||
return fmt.Sprintf("Error saving settings: %v", err)
|
return fmt.Sprintf("Error saving settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-init transcriber with new settings
|
// Re-init transcriber with new settings.
|
||||||
|
// Note: a.config.APIKey is already updated by the api_key field above.
|
||||||
|
// Since GetSettings masks the key, we only update if it's not still the masked value.
|
||||||
a.transcriber = transcriber.NewClient(
|
a.transcriber = transcriber.NewClient(
|
||||||
a.config.APIKey,
|
a.config.APIKey,
|
||||||
a.config.WhisperModel,
|
a.config.WhisperModel,
|
||||||
@@ -668,6 +700,8 @@ func (a *App) Shutdown(ctx context.Context) {
|
|||||||
if a.overlay != nil {
|
if a.overlay != nil {
|
||||||
a.overlay.Close()
|
a.overlay.Close()
|
||||||
}
|
}
|
||||||
|
// Gracefully shut down the Linux press daemon HTTP server (no-op on Windows)
|
||||||
|
stopLinuxPressDaemon()
|
||||||
logger.Close()
|
logger.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,20 +715,30 @@ func (a *App) CheckOnline() bool {
|
|||||||
return whisper.CheckOnline()
|
return whisper.CheckOnline()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWhisperInstalled checks if offline whisper is installed
|
// IsWhisperInstalled checks if offline whisper is installed.
|
||||||
|
// Reuses the cached whisperManager if available.
|
||||||
func (a *App) IsWhisperInstalled() bool {
|
func (a *App) IsWhisperInstalled() bool {
|
||||||
mgr, err := whisper.NewManager()
|
mgr := a.whisperManager
|
||||||
if err != nil {
|
if mgr == nil {
|
||||||
return false
|
var err error
|
||||||
|
mgr, err = whisper.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return mgr.IsInstalled()
|
return mgr.IsInstalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWhisperInfo returns information about installed whisper
|
// GetWhisperInfo returns information about installed whisper.
|
||||||
|
// Reuses the cached whisperManager if available.
|
||||||
func (a *App) GetWhisperInfo() map[string]interface{} {
|
func (a *App) GetWhisperInfo() map[string]interface{} {
|
||||||
mgr, err := whisper.NewManager()
|
mgr := a.whisperManager
|
||||||
if err != nil {
|
if mgr == nil {
|
||||||
return map[string]interface{}{"installed": false}
|
var err error
|
||||||
|
mgr, err = whisper.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return map[string]interface{}{"installed": false}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mgr.IsInstalled() {
|
if !mgr.IsInstalled() {
|
||||||
@@ -741,10 +785,18 @@ func (a *App) UninstallWhisper() string {
|
|||||||
return "Whisper uninstalled successfully"
|
return "Whisper uninstalled successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAvailableWhisperModels returns list of available whisper models
|
// GetAvailableWhisperModels returns list of available whisper models,
|
||||||
|
// sorted by name for consistent UI display.
|
||||||
func (a *App) GetAvailableWhisperModels() []map[string]string {
|
func (a *App) GetAvailableWhisperModels() []map[string]string {
|
||||||
var models []map[string]string
|
var names []string
|
||||||
for name, info := range whisper.Models {
|
for name := range whisper.Models {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
models := make([]map[string]string, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
info := whisper.Models[name]
|
||||||
models = append(models, map[string]string{
|
models = append(models, map[string]string{
|
||||||
"name": name,
|
"name": name,
|
||||||
"size": info.Size,
|
"size": info.Size,
|
||||||
|
|||||||
@@ -262,14 +262,22 @@ func (r *AudioRecorder) Cleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// onAudioData is called by miniaudio when audio data is available.
|
// onAudioData is called by miniaudio when audio data is available.
|
||||||
// Safe to call without r.mu because atomic writing flag prevents writes
|
// Safe to call without r.mu: the atomic writing flag prevents writes after
|
||||||
// after Stop() clears the flag.
|
// Stop() clears it, and r.device.Stop() waits for in-flight callbacks to
|
||||||
|
// complete before returning, so r.outputFile is guaranteed valid here.
|
||||||
func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
|
func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
|
||||||
if atomic.LoadInt32(&r.writing) == 0 || r.outputFile == nil || len(inputSamples) == 0 {
|
if atomic.LoadInt32(&r.writing) == 0 || len(inputSamples) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := r.outputFile.Write(inputSamples)
|
// Double-check outputFile under the assumption that Stop() has already
|
||||||
|
// set writing=0 before touching the file handle.
|
||||||
|
f := r.outputFile
|
||||||
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := f.Write(inputSamples)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
atomic.AddUint32(&r.dataSize, uint32(n))
|
atomic.AddUint32(&r.dataSize, uint32(n))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Application defaults
|
// Application defaults
|
||||||
@@ -43,6 +44,9 @@ type Config struct {
|
|||||||
const CurrentConfigVersion = 1
|
const CurrentConfigVersion = 1
|
||||||
const MaxHistoryItems = 100
|
const MaxHistoryItems = 100
|
||||||
|
|
||||||
|
// saveMu protects concurrent writes to the config file.
|
||||||
|
var saveMu sync.Mutex
|
||||||
|
|
||||||
// DefaultConfig returns a new configuration with sensible default values.
|
// DefaultConfig returns a new configuration with sensible default values.
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
@@ -91,7 +95,11 @@ func (c *Config) migrate() {
|
|||||||
|
|
||||||
// Save writes the configuration to the specified file path.
|
// Save writes the configuration to the specified file path.
|
||||||
// If configPath is empty, it uses the default configuration path.
|
// If configPath is empty, it uses the default configuration path.
|
||||||
|
// It uses atomic writes (write-to-temp then rename) to prevent corruption.
|
||||||
func Save(c *Config, configPath string) error {
|
func Save(c *Config, configPath string) error {
|
||||||
|
saveMu.Lock()
|
||||||
|
defer saveMu.Unlock()
|
||||||
|
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
var err error
|
var err error
|
||||||
configPath, err = GetConfigPath()
|
configPath, err = GetConfigPath()
|
||||||
@@ -111,7 +119,13 @@ func Save(c *Config, configPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(configPath, data, 0600)
|
// Atomic write: write to temp file, then rename to prevent corruption
|
||||||
|
// if the app crashes mid-write.
|
||||||
|
tmpPath := configPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmpPath, configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigPath returns the default configuration file path.
|
// GetConfigPath returns the default configuration file path.
|
||||||
|
|||||||
@@ -54,12 +54,13 @@ func Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the log file handle.
|
// Close flushes buffered writes and closes the log file handle.
|
||||||
func Close() {
|
func Close() {
|
||||||
logMutex.Lock()
|
logMutex.Lock()
|
||||||
defer logMutex.Unlock()
|
defer logMutex.Unlock()
|
||||||
|
|
||||||
if logFile != nil {
|
if logFile != nil {
|
||||||
|
logFile.Sync() // Flush buffered writes so the last log entries are not lost
|
||||||
logFile.Close()
|
logFile.Close()
|
||||||
logFile = nil
|
logFile = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package whisper
|
package whisper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -295,7 +296,10 @@ func (m *Manager) Transcribe(audioPath string, language string) (string, error)
|
|||||||
|
|
||||||
// Run whisper with simple arguments: ./main -m model.bin -l language -f audio.wav
|
// Run whisper with simple arguments: ./main -m model.bin -l language -f audio.wav
|
||||||
// Vulkan build uses GPU by default, no need for -ngl
|
// Vulkan build uses GPU by default, no need for -ngl
|
||||||
cmd := exec.Command(binaryPath, "-m", modelPath, "-l", language, "-f", audioPath)
|
// Use a 5-minute timeout to prevent infinite hangs on long audio or stuck processes
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, binaryPath, "-m", modelPath, "-l", language, "-f", audioPath)
|
||||||
|
|
||||||
// Set working directory to the binary's location so it can find DLLs
|
// Set working directory to the binary's location so it can find DLLs
|
||||||
cmd.Dir = filepath.Dir(binaryPath)
|
cmd.Dir = filepath.Dir(binaryPath)
|
||||||
@@ -419,7 +423,10 @@ func CheckOnline() bool {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
return true
|
// Drain the response body to allow connection reuse and prevent resource leak
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
|
||||||
|
return resp.StatusCode == http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadProgress represents download progress
|
// DownloadProgress represents download progress
|
||||||
@@ -437,6 +444,10 @@ func downloadFile(url, dest string, progress chan<- DownloadProgress) error {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("download failed: server returned HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
out, err := os.Create(dest)
|
out, err := os.Create(dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"wis-free-v3/internal/config"
|
"wis-free-v3/internal/config"
|
||||||
"wis-free-v3/internal/logger"
|
"wis-free-v3/internal/logger"
|
||||||
@@ -47,7 +48,7 @@ var trayLabel = "wis-free-v3"
|
|||||||
// Menu item references for dynamic updates
|
// Menu item references for dynamic updates
|
||||||
var statusMenuItem *systray.MenuItem
|
var statusMenuItem *systray.MenuItem
|
||||||
var triggerCountItem *systray.MenuItem
|
var triggerCountItem *systray.MenuItem
|
||||||
var triggerCount int
|
var triggerCount int32
|
||||||
var iconsInitOnce sync.Once
|
var iconsInitOnce sync.Once
|
||||||
var startupMenuItem *systray.MenuItem
|
var startupMenuItem *systray.MenuItem
|
||||||
|
|
||||||
@@ -233,10 +234,13 @@ func icoToPNG(data []byte) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("no PNG image found in ico")
|
return nil, fmt.Errorf("no PNG image found in ico")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var statusMu sync.Mutex
|
||||||
var lastStatus string
|
var lastStatus string
|
||||||
|
|
||||||
// UpdateStatus updates the status text displayed in the tray menu.
|
// UpdateStatus updates the status text displayed in the tray menu.
|
||||||
func UpdateStatus(status string) {
|
func UpdateStatus(status string) {
|
||||||
|
statusMu.Lock()
|
||||||
|
defer statusMu.Unlock()
|
||||||
if statusMenuItem != nil && status != lastStatus {
|
if statusMenuItem != nil && status != lastStatus {
|
||||||
lastStatus = status
|
lastStatus = status
|
||||||
statusMenuItem.SetTitle("Status: " + status)
|
statusMenuItem.SetTitle("Status: " + status)
|
||||||
@@ -255,9 +259,9 @@ func UpdateStatus(status string) {
|
|||||||
|
|
||||||
// IncrementTriggerCount increments the troubleshooting counter in the tray.
|
// IncrementTriggerCount increments the troubleshooting counter in the tray.
|
||||||
func IncrementTriggerCount() {
|
func IncrementTriggerCount() {
|
||||||
triggerCount++
|
count := atomic.AddInt32(&triggerCount, 1)
|
||||||
if triggerCountItem != nil {
|
if triggerCountItem != nil {
|
||||||
triggerCountItem.SetTitle(fmt.Sprintf("Shortcut detected: %d times", triggerCount))
|
triggerCountItem.SetTitle(fmt.Sprintf("Shortcut detected: %d times", count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ type Overlay struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
bgBrush syscall.Handle
|
bgBrush syscall.Handle
|
||||||
|
barBrushWhite syscall.Handle // Pre-created GDI brush for white bars (recording animation)
|
||||||
|
barBrushOrange syscall.Handle // Pre-created GDI brush for orange bars (transcribing animation)
|
||||||
hFont syscall.Handle
|
hFont syscall.Handle
|
||||||
volume uint64 // atomic: float64 stored via math.Float64bits
|
volume uint64 // atomic: float64 stored via math.Float64bits
|
||||||
smoothedVolume uint64 // atomic: float64 stored via math.Float64bits
|
smoothedVolume uint64 // atomic: float64 stored via math.Float64bits
|
||||||
@@ -171,6 +173,12 @@ func (o *Overlay) Close() {
|
|||||||
if o.hFont != 0 {
|
if o.hFont != 0 {
|
||||||
procDeleteObject.Call(uintptr(o.hFont))
|
procDeleteObject.Call(uintptr(o.hFont))
|
||||||
}
|
}
|
||||||
|
if o.barBrushWhite != 0 {
|
||||||
|
procDeleteObject.Call(uintptr(o.barBrushWhite))
|
||||||
|
}
|
||||||
|
if o.barBrushOrange != 0 {
|
||||||
|
procDeleteObject.Call(uintptr(o.barBrushOrange))
|
||||||
|
}
|
||||||
close(o.stopCh)
|
close(o.stopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +201,12 @@ func (o *Overlay) run() {
|
|||||||
brushRec, _, _ := procCreateSolidBrush.Call(COLOR_BG_DARK)
|
brushRec, _, _ := procCreateSolidBrush.Call(COLOR_BG_DARK)
|
||||||
o.bgBrush = syscall.Handle(brushRec)
|
o.bgBrush = syscall.Handle(brushRec)
|
||||||
|
|
||||||
|
whiteRec, _, _ := procCreateSolidBrush.Call(uintptr(COLOR_WHITE))
|
||||||
|
o.barBrushWhite = syscall.Handle(whiteRec)
|
||||||
|
|
||||||
|
orangeRec, _, _ := procCreateSolidBrush.Call(uintptr(COLOR_MIC_ORANGE))
|
||||||
|
o.barBrushOrange = syscall.Handle(orangeRec)
|
||||||
|
|
||||||
fontName := syscall.StringToUTF16Ptr("Segoe UI")
|
fontName := syscall.StringToUTF16Ptr("Segoe UI")
|
||||||
fontRec, _, _ := procCreateFontW.Call(
|
fontRec, _, _ := procCreateFontW.Call(
|
||||||
18, 0, 0, 0, 600,
|
18, 0, 0, 0, 600,
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ func sendLinuxPressPing() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// linuxPressServer holds the HTTP server reference for graceful shutdown.
|
||||||
|
var linuxPressServer *http.Server
|
||||||
|
|
||||||
func (a *App) startLinuxPressDaemon() {
|
func (a *App) startLinuxPressDaemon() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -100,7 +103,9 @@ func (a *App) startLinuxPressDaemon() {
|
|||||||
Addr: linuxPressAddr,
|
Addr: linuxPressAddr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
ReadHeaderTimeout: 2 * time.Second,
|
ReadHeaderTimeout: 2 * time.Second,
|
||||||
|
IdleTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
linuxPressServer = server
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.Error("Linux press daemon stopped: %v", err)
|
logger.Error("Linux press daemon stopped: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
func (a *App) startLinuxPressDaemon() {}
|
func (a *App) startLinuxPressDaemon() {}
|
||||||
|
|
||||||
|
func stopLinuxPressDaemon() {}
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ func (a *App) insertTranscription(text string) {
|
|||||||
// Paste
|
// Paste
|
||||||
a.pasteText()
|
a.pasteText()
|
||||||
|
|
||||||
// Restore old clipboard after a short delay, but ONLY if the user
|
// Restore old clipboard after a delay, but ONLY if the clipboard still
|
||||||
// hasn't manually copied something else or another burst hasn't finished.
|
// contains our transcribed text (i.e. user hasn't copied something else).
|
||||||
if clipErr == nil && oldClip != "" {
|
if clipErr == nil && oldClip != "" {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(1000 * time.Millisecond)
|
time.Sleep(1500 * time.Millisecond)
|
||||||
current, _ := wailsruntime.ClipboardGetText(a.ctx)
|
current, currentErr := wailsruntime.ClipboardGetText(a.ctx)
|
||||||
if current == text {
|
// Only restore if no error reading AND clipboard still holds our text
|
||||||
|
// AND it hasn't been modified by another goroutine
|
||||||
|
if currentErr == nil && current == text {
|
||||||
wailsruntime.ClipboardSetText(a.ctx, oldClip)
|
wailsruntime.ClipboardSetText(a.ctx, oldClip)
|
||||||
logger.Info("Clipboard history restored")
|
logger.Info("Clipboard history restored")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user