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
---
TODO.md | 7 +
VoiceSnapGo/frontend/package-lock.json | 4
VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte | 2
VoiceSnapGo/internal/config/config.go | 17 +++
VoiceSnapGo/build/darwin/Info.plist | 4
CHANGELOG.md | 16 +++
VoiceSnapGo/build/config.yml | 2
VoiceSnapGo/frontend/src/components/settings/GeneralPage.svelte | 74 ++++++++++++++
VoiceSnapGo/services/config_service.go | 12 ++
VoiceSnapGo/app.go | 100 +++++++++++++++----
VoiceSnapGo/frontend/package.json | 2
VoiceSnapGo/frontend/src/lib/stores/config.ts | 1
VoiceSnapGo/build/windows/wails.exe.manifest | 2
VoiceSnapGo/frontend/src/lib/i18n/zh.json | 9 +
VoiceSnapGo/frontend/src/lib/i18n/en.json | 9 +
15 files changed, 227 insertions(+), 34 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 886faf2..cd87d89 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## v2.1.3 (2026-05-18)
+
+### 新功能
+
+- **语音输入方式可选**:设置页新增“点按 / 长按”模式。点按模式下,点一下快捷键开始说话,再点一下结束并输入;长按模式下,按住快捷键开始说话,松开后结束并输入。
+
+### 改进
+
+- 热键状态机拆分为清晰的点按与长按逻辑,避免短按/长按自动判断造成交互不确定。
+
+### 构建
+
+- build: `20260518.1651`
+
+---
+
## v2.1.2 (2026-05-18)
### Bug 修复
diff --git a/TODO.md b/TODO.md
index 3c44637..af44586 100644
--- a/TODO.md
+++ b/TODO.md
@@ -21,6 +21,13 @@
## Done
+- [2026-05-18] 增加语音输入交互方式设置。
+ - 新增配置 `HotkeyMode`,默认 `hold`。
+ - 设置页新增“语音输入方式”分段控件,支持“点按”和“长按”。
+ - 点按: 点一下快捷键开始说话,再点一下结束并识别输入。
+ - 长按: 长按快捷键开始录音,松开后结束并识别输入。
+ - 版本: `2.1.3`
+ - build: `20260518.1651`
- [2026-05-18] 修复全屏窗口下语音输入悬浮指示器不显示。
- 根因: macOS 全屏 Space 下普通 `NSWindow` + `NSFloatingWindowLevel` 不稳定,容易被全屏 App 压在下层。
- 修复: 悬浮指示器改为非激活 `NSPanel`,窗口层级提升到 `kCGStatusWindowLevelKey`,并补充 `Transient`、`IgnoresCycle`、`FullScreenAuxiliary`、`CanJoinAllSpaces` 行为。
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() {
diff --git a/VoiceSnapGo/build/config.yml b/VoiceSnapGo/build/config.yml
index 4e1cbbe..e29467f 100755
--- a/VoiceSnapGo/build/config.yml
+++ b/VoiceSnapGo/build/config.yml
@@ -7,7 +7,7 @@
description: "Offline voice to text tool"
copyright: "(c) 2025, VoiceSnap"
comments: "Press and hold hotkey to record, release to transcribe"
- version: "2.1.2"
+ version: "2.1.3"
dev_mode:
root_path: .
diff --git a/VoiceSnapGo/build/darwin/Info.plist b/VoiceSnapGo/build/darwin/Info.plist
index 926b27b..2eaf40f 100755
--- a/VoiceSnapGo/build/darwin/Info.plist
+++ b/VoiceSnapGo/build/darwin/Info.plist
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>2.1.2</string>
+ <string>2.1.3</string>
<key>CFBundleVersion</key>
- <string>20260518.1623</string>
+ <string>20260518.1651</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSMicrophoneUsageDescription</key>
diff --git a/VoiceSnapGo/build/windows/wails.exe.manifest b/VoiceSnapGo/build/windows/wails.exe.manifest
index b4dec20..9843877 100755
--- a/VoiceSnapGo/build/windows/wails.exe.manifest
+++ b/VoiceSnapGo/build/windows/wails.exe.manifest
@@ -3,7 +3,7 @@
<assemblyIdentity
type="win32"
name="VoiceSnap"
- version="2.1.2.0"
+ version="2.1.3.0"
processorArchitecture="*"/>
<dependency>
<dependentAssembly>
diff --git a/VoiceSnapGo/frontend/package-lock.json b/VoiceSnapGo/frontend/package-lock.json
index c3b8e4a..1b4944a 100755
--- a/VoiceSnapGo/frontend/package-lock.json
+++ b/VoiceSnapGo/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "voicesnap-frontend",
- "version": "2.1.2",
+ "version": "2.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "voicesnap-frontend",
- "version": "2.1.2",
+ "version": "2.1.3",
"dependencies": {
"@wailsio/runtime": "latest"
},
diff --git a/VoiceSnapGo/frontend/package.json b/VoiceSnapGo/frontend/package.json
index 824edf4..51ad42c 100755
--- a/VoiceSnapGo/frontend/package.json
+++ b/VoiceSnapGo/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "voicesnap-frontend",
"private": true,
- "version": "2.1.2",
+ "version": "2.1.3",
"type": "module",
"scripts": {
"dev": "vite dev",
diff --git a/VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte b/VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte
index c4ecfb7..418e1b0 100755
--- a/VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte
+++ b/VoiceSnapGo/frontend/src/components/settings/AboutPage.svelte
@@ -2,7 +2,7 @@
import { Call } from '@wailsio/runtime'
import { t } from '../../lib/i18n'
- let version = $state('2.1.2')
+ let version = $state('2.1.3')
let updateStatus = $state('')
let updateColor = $state('')
let checking = $state(false)
diff --git a/VoiceSnapGo/frontend/src/components/settings/GeneralPage.svelte b/VoiceSnapGo/frontend/src/components/settings/GeneralPage.svelte
index 091a855..d54ca49 100755
--- a/VoiceSnapGo/frontend/src/components/settings/GeneralPage.svelte
+++ b/VoiceSnapGo/frontend/src/components/settings/GeneralPage.svelte
@@ -2,7 +2,7 @@
import { Call } from '@wailsio/runtime'
import ToggleSwitch from '../shared/ToggleSwitch.svelte'
import { t } from '../../lib/i18n'
- import { autoHide, soundFeedback, copyToClipboard, startAtLogin, hotkeyVK } from '../../lib/stores/config'
+ import { autoHide, soundFeedback, copyToClipboard, startAtLogin, hotkeyVK, hotkeyMode } from '../../lib/stores/config'
import { deviceName, engineStatus, engineHardwareInfo } from '../../lib/stores/app'
import { hotkeyName } from '../../lib/stores/indicator'
@@ -21,6 +21,7 @@
let status = $state('loading')
let hwInfo = $state('')
let currentKeyName = $state('Ctrl')
+ let hotkeyModeVal: 'tap' | 'hold' = $state('hold')
let isRecording = $state(false)
let hintText = $state('')
let devices = $state<InputDevice[]>([])
@@ -34,6 +35,7 @@
const unsub6 = hotkeyName.subscribe(k => { currentKeyName = k })
const unsub7 = soundFeedback.subscribe(v => { soundFeedbackVal = v })
const unsub8 = copyToClipboard.subscribe(v => { copyToClipboardVal = v })
+ const unsub9 = hotkeyMode.subscribe(v => { hotkeyModeVal = v })
// Load actual device name from backend on mount
async function loadDeviceName() {
@@ -69,6 +71,12 @@
copyToClipboardVal = copy
} catch {}
try {
+ const mode: any = await Call.ByName('voicesnap/services.ConfigService.GetHotkeyMode')
+ const normalized = mode === 'tap' ? 'tap' : 'hold'
+ hotkeyMode.set(normalized)
+ hotkeyModeVal = normalized
+ } catch {}
+ try {
const hideDock: any = await Call.ByName('voicesnap/services.ConfigService.GetHideDockIcon')
hideDockIconVal = !!hideDock
} catch {}
@@ -94,6 +102,14 @@
copyToClipboard.set(checked)
try {
await Call.ByName('voicesnap/services.ConfigService.SetCopyToClipboard', checked)
+ } catch {}
+ }
+
+ async function onHotkeyModeChange(mode: 'tap' | 'hold') {
+ hotkeyMode.set(mode)
+ hotkeyModeVal = mode
+ try {
+ await Call.ByName('voicesnap/services.ConfigService.SetHotkeyMode', mode)
} catch {}
}
@@ -212,7 +228,7 @@
<!-- Header -->
<div class="header">
<h1 class="tagline">{t('home.tagline')}</h1>
- <p class="app-desc">{t('home.modeHint')}</p>
+ <p class="app-desc">{hotkeyModeVal === 'tap' ? t('home.modeHintTap') : t('home.modeHintHold')}</p>
</div>
<!-- Hotkey -->
@@ -232,6 +248,31 @@
{#if hintText}
<p class="hint" class:recording={isRecording}>{hintText}</p>
{/if}
+
+ <div class="divider"></div>
+
+ <div class="setting-row">
+ <div class="setting-info">
+ <span class="setting-label">{t('settings.hotkeyMode')}</span>
+ <span class="setting-desc">
+ {hotkeyModeVal === 'tap' ? t('settings.hotkeyModeTapDesc') : t('settings.hotkeyModeHoldDesc')}
+ </span>
+ </div>
+ <div class="segmented" role="group" aria-label={t('settings.hotkeyMode')}>
+ <button
+ class:active={hotkeyModeVal === 'tap'}
+ onclick={() => onHotkeyModeChange('tap')}
+ >
+ {t('settings.hotkeyModeTap')}
+ </button>
+ <button
+ class:active={hotkeyModeVal === 'hold'}
+ onclick={() => onHotkeyModeChange('hold')}
+ >
+ {t('settings.hotkeyModeHold')}
+ </button>
+ </div>
+ </div>
</div>
@@ -476,6 +517,35 @@
color: var(--color-tertiary-label);
}
+ .segmented {
+ display: inline-grid;
+ grid-template-columns: 1fr 1fr;
+ min-width: 148px;
+ padding: 2px;
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-secondary);
+ flex-shrink: 0;
+ }
+
+ .segmented button {
+ min-width: 66px;
+ height: 30px;
+ padding: 0 12px;
+ border: none;
+ border-radius: calc(var(--radius-sm) - 2px);
+ background: transparent;
+ color: var(--color-secondary-label);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ }
+
+ .segmented button.active {
+ background: var(--color-blue);
+ color: white;
+ }
+
.divider {
height: 1px;
background: var(--color-separator);
diff --git a/VoiceSnapGo/frontend/src/lib/i18n/en.json b/VoiceSnapGo/frontend/src/lib/i18n/en.json
index 4527cd3..5dacfa2 100755
--- a/VoiceSnapGo/frontend/src/lib/i18n/en.json
+++ b/VoiceSnapGo/frontend/src/lib/i18n/en.json
@@ -32,10 +32,17 @@
"tagline": "Snap · Voice to Text",
"description": "Hold {key} to speak, release to recognize and type",
"hotkeyLabel": "Trigger Hotkey",
- "modeHint": "Hold to record, release to type · Tap to start free talk, tap again to stop"
+ "modeHint": "Hold to record, release to type · Tap to start free talk, tap again to stop",
+ "modeHintTap": "Tap to start speaking, tap again to stop and type",
+ "modeHintHold": "Hold to speak, release to stop and type"
},
"settings": {
"inputDevice": "Input Device",
+ "hotkeyMode": "Voice Input Mode",
+ "hotkeyModeTap": "Tap",
+ "hotkeyModeHold": "Hold",
+ "hotkeyModeTapDesc": "Tap the hotkey to start speaking, then tap again to stop and type",
+ "hotkeyModeHoldDesc": "Hold the hotkey to speak, release to stop and type",
"autoHide": "Auto-hide Indicator",
"autoHideDesc": "Automatically hide the floating indicator after completion",
"soundFeedback": "Sound Feedback",
diff --git a/VoiceSnapGo/frontend/src/lib/i18n/zh.json b/VoiceSnapGo/frontend/src/lib/i18n/zh.json
index 8a52d67..38041b8 100755
--- a/VoiceSnapGo/frontend/src/lib/i18n/zh.json
+++ b/VoiceSnapGo/frontend/src/lib/i18n/zh.json
@@ -32,10 +32,17 @@
"tagline": "Snap · 语音即输入",
"description": "按住 {key} 说话,松开即识别并输入文字",
"hotkeyLabel": "触发热键",
- "modeHint": "长按录音,松开输入 · 短按开始自由说话,再按结束"
+ "modeHint": "长按录音,松开输入 · 短按开始自由说话,再按结束",
+ "modeHintTap": "点按开始说话,再点按结束并输入",
+ "modeHintHold": "长按开始说话,松开结束并输入"
},
"settings": {
"inputDevice": "输入设备",
+ "hotkeyMode": "语音输入方式",
+ "hotkeyModeTap": "点按",
+ "hotkeyModeHold": "长按",
+ "hotkeyModeTapDesc": "点一下快捷键开始说话,再点一下结束并输入",
+ "hotkeyModeHoldDesc": "按住快捷键开始说话,松开后结束并输入",
"autoHide": "自动隐藏指示器",
"autoHideDesc": "操作完成后自动隐藏浮动指示器",
"soundFeedback": "提示音",
diff --git a/VoiceSnapGo/frontend/src/lib/stores/config.ts b/VoiceSnapGo/frontend/src/lib/stores/config.ts
index 412c6f5..1c78f57 100755
--- a/VoiceSnapGo/frontend/src/lib/stores/config.ts
+++ b/VoiceSnapGo/frontend/src/lib/stores/config.ts
@@ -5,3 +5,4 @@
export const copyToClipboard = writable<boolean>(true);
export const startAtLogin = writable<boolean>(false);
export const hotkeyVK = writable<number>(0xa5);
+export const hotkeyMode = writable<"tap" | "hold">("hold");
diff --git a/VoiceSnapGo/internal/config/config.go b/VoiceSnapGo/internal/config/config.go
index a0b131d..04b54c6 100755
--- a/VoiceSnapGo/internal/config/config.go
+++ b/VoiceSnapGo/internal/config/config.go
@@ -7,9 +7,15 @@
"voicesnap/internal/paths"
)
+const (
+ HotkeyModeTap = "tap"
+ HotkeyModeHold = "hold"
+)
+
// Config holds the application configuration, compatible with the WPF version's config.json.
type Config struct {
HotkeyVK int `json:"HotkeyVK"`
+ HotkeyMode string `json:"HotkeyMode"`
AutoHide bool `json:"AutoHide"`
SoundFeedback bool `json:"SoundFeedback"`
HideDockIcon bool `json:"HideDockIcon"`
@@ -25,6 +31,7 @@
func Default() *Config {
return &Config{
HotkeyVK: 0xA5, // Right Alt
+ HotkeyMode: HotkeyModeHold,
AutoHide: true,
SoundFeedback: true,
HideDockIcon: true,
@@ -55,9 +62,19 @@
if err := json.Unmarshal(data, cfg); err != nil {
return nil, err
}
+ cfg.HotkeyMode = NormalizeHotkeyMode(cfg.HotkeyMode)
return cfg, nil
}
+func NormalizeHotkeyMode(mode string) string {
+ switch mode {
+ case HotkeyModeTap, HotkeyModeHold:
+ return mode
+ default:
+ return HotkeyModeHold
+ }
+}
+
// Save writes the config to disk.
func Save(cfg *Config) {
if err := paths.Ensure(); err != nil {
diff --git a/VoiceSnapGo/services/config_service.go b/VoiceSnapGo/services/config_service.go
index 55fe812..6bffa6f 100755
--- a/VoiceSnapGo/services/config_service.go
+++ b/VoiceSnapGo/services/config_service.go
@@ -42,6 +42,18 @@
return s.cfg.SoundFeedback
}
+// SetHotkeyMode sets the voice input interaction mode.
+func (s *ConfigService) SetHotkeyMode(mode string) {
+ s.cfg.HotkeyMode = config.NormalizeHotkeyMode(mode)
+ config.Save(s.cfg)
+}
+
+// GetHotkeyMode returns the voice input interaction mode.
+func (s *ConfigService) GetHotkeyMode() string {
+ s.cfg.HotkeyMode = config.NormalizeHotkeyMode(s.cfg.HotkeyMode)
+ return s.cfg.HotkeyMode
+}
+
// SetCopyToClipboard sets whether recognized text remains in the clipboard after input.
func (s *ConfigService) SetCopyToClipboard(enabled bool) {
s.cfg.CopyToClipboard = enabled
--
Gitblit v1.9.3