From 8f11df460a247dfcaafd84ce3046605724d11998 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 18 May 2026 17:20:11 +0800
Subject: [PATCH] feat: add hotkey input mode setting
---
VoiceSnapGo/app.go | 100 +++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 78 insertions(+), 22 deletions(-)
diff --git a/VoiceSnapGo/app.go b/VoiceSnapGo/app.go
index 2805702..595d31c 100755
--- a/VoiceSnapGo/app.go
+++ b/VoiceSnapGo/app.go
@@ -26,8 +26,8 @@
)
const (
- appVersion = "2.1.2"
- appBuild = "20260518.1623"
+ appVersion = "2.1.3"
+ appBuild = "20260518.1651"
appDisplayVersion = appVersion + " (build " + appBuild + ")"
appName = "VoiceSnap"
@@ -249,9 +249,10 @@
a.hotkeyPollSeen = true
a.lastHotkeyDown = isDown
logger.Info(
- "Hotkey poll state: key=%s vk=0x%X down=%t active=%t recording=%t freetalk=%t engineReady=%t recordingHotkey=%t",
+ "Hotkey poll state: key=%s vk=0x%X mode=%s down=%t active=%t recording=%t freetalk=%t engineReady=%t recordingHotkey=%t",
hotkey.GetKeyName(a.cfg.HotkeyVK),
a.cfg.HotkeyVK,
+ config.NormalizeHotkeyMode(a.cfg.HotkeyMode),
isDown,
a.hotkeyActive,
a.isRecording,
@@ -284,18 +285,27 @@
return
}
+ switch config.NormalizeHotkeyMode(a.cfg.HotkeyMode) {
+ case config.HotkeyModeTap:
+ a.pollTapHotkeyLocked(isDown)
+ default:
+ a.pollHoldHotkeyLocked(isDown)
+ }
+}
+
+func (a *App) pollHoldHotkeyLocked(isDown bool) {
if isDown {
if !a.hotkeyActive {
// Key just pressed
a.hotkeyActive = true
a.isCombination = false
a.hotkeyPressTime = time.Now()
- logger.Info("Hotkey accepted: key=%s vk=0x%X freetalk=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, a.isFreetalking)
+ logger.Info("Hotkey accepted: key=%s vk=0x%X mode=%s freetalk=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, config.HotkeyModeHold, a.isFreetalking)
- // If in free talk mode, stop on next press
+ // If the mode changed while a tap recording was active, let the next press stop it.
if a.isFreetalking {
- logger.Info("Hotkey action: stop free talk")
- a.stopFreetalkLocked()
+ logger.Info("Hotkey action: stop tap recording after mode switch")
+ a.stopTapRecordingLocked()
return
}
} else {
@@ -317,17 +327,54 @@
} else if a.hotkeyActive {
// Key released
pressDuration := time.Since(a.hotkeyPressTime)
- logger.Info("Hotkey released: key=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), pressDuration.Milliseconds(), a.isRecording, a.isCombination)
+ logger.Info("Hotkey released: key=%s mode=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeHold, pressDuration.Milliseconds(), a.isRecording, a.isCombination)
a.hotkeyActive = false
if a.isRecording {
// Hold-to-talk: release stops recording
a.stopRecordingLocked(a.isCombination)
- } else if !a.isCombination && pressDuration < 300*time.Millisecond && time.Since(a.lastStopTime) > 500*time.Millisecond {
- // Short tap (<300ms): start free talk mode
- logger.Info("Hotkey action: start free talk after short tap")
- a.startFreetalkLocked()
}
+ }
+}
+
+func (a *App) pollTapHotkeyLocked(isDown bool) {
+ if isDown {
+ if !a.hotkeyActive {
+ a.hotkeyActive = true
+ a.isCombination = false
+ a.hotkeyPressTime = time.Now()
+ logger.Info("Hotkey accepted: key=%s vk=0x%X mode=%s recording=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, config.HotkeyModeTap, a.isFreetalking)
+ return
+ }
+
+ if !a.isCombination && a.hk.IsAnyOtherKeyPressedSince(a.cfg.HotkeyVK, a.hotkeyPressTime) {
+ a.isCombination = true
+ logger.Info("Hotkey marked as combination: key=%s mode=%s", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeTap)
+ }
+ return
+ }
+
+ if !a.hotkeyActive {
+ return
+ }
+
+ pressDuration := time.Since(a.hotkeyPressTime)
+ logger.Info("Hotkey released: key=%s mode=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeTap, pressDuration.Milliseconds(), a.isRecording, a.isCombination)
+ a.hotkeyActive = false
+
+ if a.isCombination {
+ return
+ }
+
+ if a.isFreetalking {
+ logger.Info("Hotkey action: stop tap recording")
+ a.stopTapRecordingLocked()
+ return
+ }
+
+ if time.Since(a.lastStopTime) > 500*time.Millisecond {
+ logger.Info("Hotkey action: start tap recording")
+ a.startTapRecordingLocked()
}
}
@@ -379,7 +426,7 @@
go a.recognizeAndPaste(hasVoice, samples)
}
-func (a *App) startFreetalkLocked() {
+func (a *App) startTapRecordingLocked() {
if a.isRecording || a.isFreetalking {
return
}
@@ -389,7 +436,7 @@
a.silenceSince = time.Time{}
a.hideGen.Add(1)
- logger.Info("Free talk started")
+ logger.Info("Tap recording started")
a.positionIndicator()
a.indicator.SetStatus(overlay.StatusFreetalking, "0:00")
a.indicator.Show()
@@ -398,7 +445,7 @@
}
if err := a.recorder.Start(); err != nil {
- logger.Error("Failed to start free talk recording: %v", err)
+ logger.Error("Failed to start tap recording: %v", err)
a.isFreetalking = false
a.isRecording = false
return
@@ -431,7 +478,7 @@
}()
}
-func (a *App) stopFreetalkLocked() {
+func (a *App) stopTapRecordingLocked() {
if !a.isFreetalking {
return
}
@@ -442,19 +489,20 @@
hasVoice := a.recorder.HasVoiceActivity()
samples := a.recorder.StopAndGetSamples()
- logger.Info("Free talk stopped")
+ logger.Info("Tap recording stopped")
a.indicator.SetStatus(overlay.StatusProcessing, "识别中")
go a.recognizeAndPaste(hasVoice, samples)
}
-// checkSilenceTimeout monitors volume during free-talk mode and auto-stops
-// recording if silence persists for silenceTimeoutDuration.
+// checkSilenceTimeout monitors legacy free-talk recordings. The explicit tap
+// mode is user-stopped by pressing the hotkey again, so it does not auto-stop
+// on silence.
// Called from the audio volume callback (~every 50ms).
func (a *App) checkSilenceTimeout(vol float64) {
a.mu.Lock()
defer a.mu.Unlock()
- if !a.isFreetalking {
+ if !a.isFreetalking || config.NormalizeHotkeyMode(a.cfg.HotkeyMode) == config.HotkeyModeTap {
a.silenceSince = time.Time{}
return
}
@@ -502,7 +550,7 @@
}
// recognizeAndPaste runs ASR on the recorded samples and pastes the result.
-// Shared by both hold-to-talk and free-talk modes.
+// Shared by both hold and tap modes.
func (a *App) recognizeAndPaste(hasVoice bool, samples []float32) {
// Snapshot config values under lock to avoid data races
a.mu.Lock()
@@ -631,6 +679,7 @@
// Show indicator briefly — snapshot config under lock
a.mu.Lock()
hotkeyVK := a.cfg.HotkeyVK
+ hotkeyMode := config.NormalizeHotkeyMode(a.cfg.HotkeyMode)
autoHide := a.cfg.AutoHide
indX := a.cfg.IndicatorX
indY := a.cfg.IndicatorY
@@ -638,11 +687,18 @@
keyName := hotkey.GetKeyName(hotkeyVK)
a.positionIndicatorAt(indX, indY)
- a.indicator.SetStatus(overlay.StatusReady, "按住"+keyName+"说话")
+ a.indicator.SetStatus(overlay.StatusReady, hotkeyReadyText(keyName, hotkeyMode))
a.indicator.Show()
a.delayedHideIf(autoHide, 2000)
}
+func hotkeyReadyText(keyName, mode string) string {
+ if mode == config.HotkeyModeTap {
+ return "点按" + keyName + "说话"
+ }
+ return "按住" + keyName + "说话"
+}
+
// positionIndicator places the indicator at the saved position, or bottom-center if not saved.
// Caller must hold a.mu (reads a.cfg).
func (a *App) positionIndicator() {
--
Gitblit v1.9.3