feat(shortcuts): default to hold Right Alt (push-to-talk) for new installs (#648)
* feat(shortcuts): default to hold Right Alt push-to-talk on first install Double-tap Ctrl conflicts with many desktop and app shortcuts, so new installs now use hold Right Alt (Option) in push-to-talk mode. Existing config.json values are preserved unchanged. Updates README, user guide, AGENTS.md, first-run dialog, website copy, and tests to match the new defaults. * refactor(shortcuts): trim redundant default-change test and comments Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage, remove the duplicated Right-Alt rationale from config_manager, and drop a vacuous source-string check from the settings shortcuts test. * fix(shortcuts): align install.sh seeds and preserve legacy toggle mode install.sh still wrote ctrl+ctrl without mode, so merges with the new push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for new installs, and migrate existing shortcut-only configs to mode=toggle so prior behavior is unchanged. * style(config): fix black formatting after shortcuts migration * fix(shortcuts): preserve legacy defaults when shortcuts section is missing Existing config files without a shortcuts section previously inherited ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on upgrade so the new first-install Right Alt push-to-talk defaults do not silently change behavior. * docs(agents): unwrap hard-broken dictation control paragraph Keep the AGENTS.md paragraph as a single line like the surrounding prose.
This commit is contained in:
@@ -260,7 +260,7 @@ Two things are missing from the base session and must be set up once per boot (p
|
||||
|
||||
Also set `audio.device_index` to `null` in `~/.config/vocalinux/config.json` (a stale index prevents opening the stream).
|
||||
|
||||
**Dictation control is Ctrl-based and stateful.** Default activation is double-tap `Ctrl` in toggle mode (`shortcuts` in the config). The app catches synthetic X key events (`xdotool key Control_L`), so it can be driven from a script, but toggle state persists across dictations — and any stray `Ctrl` (e.g. `Ctrl+A`/`Ctrl+C` for clipboard) will flip recording on/off. For a deterministic run, launch a fresh (idle) app instance, then: focus the target window → double-tap `Ctrl` → `paplay` the wav → wait ~5s for the 2s silence timeout + whisper.cpp transcription + xdotool injection into the focused window.
|
||||
**Dictation control is Right-Alt hold by default and stateful for toggle mode.** Default activation is hold `Right Alt` (Option) in push-to-talk mode (`shortcuts` in the config). Existing installs keep whatever is already saved in `~/.config/vocalinux/config.json`. The app catches synthetic X key events (`xdotool key Alt_R`), so it can be driven from a script. For push-to-talk: focus the target window → hold Right Alt → `paplay` the wav → release Right Alt → wait for whisper.cpp transcription + xdotool injection into the focused window. If testing toggle mode instead, launch a fresh (idle) app instance; toggle state persists across dictations, and any stray configured key tap will flip recording.
|
||||
|
||||
- **Headless-only speech check (no GUI/audio setup):** drive the pipeline programmatically — transcribe with `from pywhispercpp.model import Model` and format via `vocalinux.speech_recognition.command_processor.CommandProcessor`. The first transcription downloads the ~74MB whisper.cpp `tiny` model (needs network); it is cached afterward.
|
||||
- **Website dev server:** `cd web && npm run dev -- -H 0.0.0.0 -p 3456` (per `web/AGENTS.md`). Lint currently reports 3 pre-existing `@next/next/no-html-link-for-pages` errors in `src/components/seo-subpage-shell.tsx`; typecheck, tests, and build are clean.
|
||||
|
||||
@@ -272,9 +272,9 @@ Or launch it from your application menu!
|
||||
|
||||
### Voice Dictation
|
||||
|
||||
1. **Toggle mode**: Double-tap the shortcut key (default Ctrl) to start recording
|
||||
1. **Push-to-talk (default)**: Hold Right Alt (Option on Mac-layout keyboards) and speak
|
||||
2. Speak clearly into your microphone
|
||||
3. **Toggle mode**: Double-tap again (or pause speaking) to stop, or **Push-to-Talk mode**: release the key to stop
|
||||
3. **Release** the key to stop, or switch to **Toggle mode** in Settings (double-tap a key to start/stop)
|
||||
|
||||
### Voice Commands
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ Use this checklist when testing on a new distribution:
|
||||
- [ ] Launch from terminal: `~/.local/bin/vocalinux-gui`
|
||||
- [ ] Launch from application menu
|
||||
- [ ] Verify tray icon appears
|
||||
- [ ] Test keyboard shortcuts (Ctrl+Alt+M by default)
|
||||
- [ ] Test keyboard shortcuts (Hold Right Alt by default)
|
||||
- [ ] Test audio recording
|
||||
- [ ] Test text injection in different applications
|
||||
- [ ] Test Settings dialog opens correctly
|
||||
|
||||
+4
-4
@@ -32,9 +32,9 @@ Vocalinux supports login autostart using the standard Linux desktop-session mech
|
||||
|
||||
1. **Launch the application**: Run `vocalinux` in a terminal or launch it from your application menu
|
||||
2. **Find the tray icon**: Look for the microphone icon in your system tray
|
||||
3. **Start voice typing**: Click the tray icon and select "Start Voice Typing", or use your configured shortcut mode (toggle or push-to-talk)
|
||||
3. **Start voice typing**: Hold Right Alt (Option) by default, or use the tray menu / your configured shortcut
|
||||
4. **Speak clearly**: As you speak, your words will be transcribed into the currently focused application
|
||||
5. **Stop voice typing**: Click the tray icon and select "Stop Voice Typing" when you're done, double-tap again in toggle mode, or release the key in push-to-talk mode
|
||||
5. **Stop voice typing**: Release the key in push-to-talk (default), double-tap again in toggle mode, or use the tray menu
|
||||
|
||||
### Dictation formatting
|
||||
|
||||
@@ -84,8 +84,8 @@ Vocalinux supports several commands that you can speak to control formatting:
|
||||
|
||||
Vocalinux supports two shortcut modes for controlling voice typing:
|
||||
|
||||
- **Toggle mode (default)**: Double-tap the shortcut key (Ctrl by default) to start/stop voice typing
|
||||
- **Push-to-talk mode**: Hold the configured shortcut key to speak, then release to stop
|
||||
- **Push-to-talk mode (default)**: Hold Right Alt (Option on Mac-layout keyboards) to speak, then release to stop
|
||||
- **Toggle mode**: Double-tap the configured shortcut key to start/stop voice typing
|
||||
- Configure mode and key in **Settings -> Shortcuts**
|
||||
|
||||
### Model Settings
|
||||
|
||||
+10
-5
@@ -2843,7 +2843,8 @@ install_whispercpp_with_gpu_support() {
|
||||
"device_name": null
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
@@ -3019,7 +3020,8 @@ PY
|
||||
"device_name": null
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
@@ -3072,7 +3074,8 @@ WHISPER_CONFIG
|
||||
"device_name": null
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
@@ -3113,7 +3116,8 @@ FALLBACK_CONFIG
|
||||
"device_name": null
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
@@ -3176,7 +3180,8 @@ VOSK_CONFIG
|
||||
"device_name": null
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk"
|
||||
},
|
||||
"ui": {
|
||||
"start_minimized": false,
|
||||
|
||||
@@ -44,8 +44,8 @@ DEFAULT_CONFIG = {
|
||||
"enabled": True, # Play sounds for recording start/stop/error
|
||||
},
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl", # Double-tap modifier key
|
||||
"mode": "toggle", # "toggle" or "push_to_talk"
|
||||
"toggle_recognition": "right_alt+right_alt",
|
||||
"mode": "push_to_talk", # "toggle" or "push_to_talk"
|
||||
# Pure-modifier gestures: "ctrl+ctrl", "alt+alt", "shift+shift" (and
|
||||
# left_/right_ variants) — double-tap (toggle) or hold (push_to_talk).
|
||||
# Modifier+key combos are also supported, e.g. "alt+r", "ctrl+alt+r",
|
||||
@@ -152,7 +152,7 @@ class ConfigManager:
|
||||
if needs_migration:
|
||||
self._migrate_config(user_config)
|
||||
|
||||
self._migrate_shortcuts_config()
|
||||
self._migrate_shortcuts_config(user_config)
|
||||
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
@@ -190,15 +190,40 @@ class ConfigManager:
|
||||
self.save_config()
|
||||
logger.info("Config migrated to new per-engine model format")
|
||||
|
||||
def _migrate_shortcuts_config(self):
|
||||
def _migrate_shortcuts_config(self, user_config: Optional[dict] = None):
|
||||
"""Migrate deprecated shortcuts and preserve legacy defaults when omitted."""
|
||||
shortcuts_config = self.config.get("shortcuts", {})
|
||||
shortcut = shortcuts_config.get("toggle_recognition")
|
||||
changed = False
|
||||
|
||||
if shortcut == "super+super":
|
||||
shortcuts_config["toggle_recognition"] = "ctrl+ctrl"
|
||||
self.save_config()
|
||||
changed = True
|
||||
logger.info("Migrated deprecated super+super shortcut to ctrl+ctrl")
|
||||
|
||||
# Existing config files that never stored shortcuts (or only stored the
|
||||
# key) previously inherited ctrl+ctrl + toggle from DEFAULT_CONFIG.
|
||||
# Pin those historical defaults so the new first-install defaults do
|
||||
# not silently change behavior for upgrades.
|
||||
if user_config is not None:
|
||||
user_shortcuts = user_config.get("shortcuts")
|
||||
if not isinstance(user_shortcuts, dict):
|
||||
shortcuts_config["toggle_recognition"] = "ctrl+ctrl"
|
||||
shortcuts_config["mode"] = "toggle"
|
||||
changed = True
|
||||
logger.info(
|
||||
"Migrated missing shortcuts section to legacy ctrl+ctrl toggle defaults"
|
||||
)
|
||||
elif "mode" not in user_shortcuts:
|
||||
shortcuts_config["mode"] = "toggle"
|
||||
if "toggle_recognition" not in user_shortcuts:
|
||||
shortcuts_config["toggle_recognition"] = "ctrl+ctrl"
|
||||
changed = True
|
||||
logger.info("Migrated missing shortcuts.mode to toggle for existing config")
|
||||
|
||||
if changed:
|
||||
self.save_config()
|
||||
|
||||
def save_config(self):
|
||||
"""Save the current configuration to the config file."""
|
||||
try:
|
||||
|
||||
@@ -66,8 +66,8 @@ class FirstRunDialog(Gtk.Dialog):
|
||||
quick_guide_label = Gtk.Label(
|
||||
label=(
|
||||
"<b>Quick start</b>\n"
|
||||
"1. Open the tray icon and choose Start Voice Typing\n"
|
||||
"2. Open Settings from the tray menu to configure shortcuts, model, and audio"
|
||||
"1. Hold Right Alt (Option) to dictate, release to stop\n"
|
||||
"2. Open Settings from the tray menu to change shortcuts, model, and audio"
|
||||
),
|
||||
use_markup=True,
|
||||
wrap=True,
|
||||
|
||||
@@ -90,7 +90,9 @@ SHORTCUT_MODE_DISPLAY_NAMES = {
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_SHORTCUT = "ctrl+ctrl"
|
||||
# Hold Right Alt (Option on Mac-layout keyboards). Avoids double-tap Ctrl
|
||||
# colliding with common desktop/app shortcuts out of the box.
|
||||
DEFAULT_SHORTCUT = "right_alt+right_alt"
|
||||
|
||||
# Supported shortcut modes
|
||||
SHORTCUT_MODES = {
|
||||
@@ -98,7 +100,7 @@ SHORTCUT_MODES = {
|
||||
"push_to_talk": "Push-to-Talk (hold to speak)",
|
||||
}
|
||||
|
||||
DEFAULT_SHORTCUT_MODE = "toggle"
|
||||
DEFAULT_SHORTCUT_MODE = "push_to_talk"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -59,6 +59,8 @@ from ..utils.whispercpp_model_info import is_model_downloaded as is_whispercpp_m
|
||||
from ..version import __copyright__, __description__, __url__, __version__ # noqa: E402
|
||||
from .config_manager import DEFAULT_CONFIG # noqa: E402
|
||||
from .keyboard_backends import ( # noqa: E402
|
||||
DEFAULT_SHORTCUT,
|
||||
DEFAULT_SHORTCUT_MODE,
|
||||
SHORTCUT_DISPLAY_NAMES,
|
||||
SHORTCUT_GROUPS,
|
||||
SHORTCUT_MODES,
|
||||
@@ -2439,9 +2441,9 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.shortcut_mode_combo.append(mode_id, display_name)
|
||||
|
||||
# Load current mode from config
|
||||
current_mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
|
||||
current_mode = self.config_manager.get_str("shortcuts", "mode", DEFAULT_SHORTCUT_MODE)
|
||||
if not self.shortcut_mode_combo.set_active_id(current_mode):
|
||||
self.shortcut_mode_combo.set_active_id("toggle")
|
||||
self.shortcut_mode_combo.set_active_id(DEFAULT_SHORTCUT_MODE)
|
||||
|
||||
mode_row = PreferenceRow(
|
||||
title="Shortcut Mode",
|
||||
@@ -2469,7 +2471,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
# Load current shortcut from config
|
||||
current_shortcut = self.config_manager.get_str(
|
||||
"shortcuts", "toggle_recognition", "ctrl+ctrl"
|
||||
"shortcuts", "toggle_recognition", DEFAULT_SHORTCUT
|
||||
)
|
||||
|
||||
self.shortcut_row = PreferenceRow(
|
||||
@@ -2577,7 +2579,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
blocked = False
|
||||
try:
|
||||
if not self.shortcut_combo.set_active_id(active_id):
|
||||
self.shortcut_combo.set_active_id("ctrl+ctrl")
|
||||
self.shortcut_combo.set_active_id(DEFAULT_SHORTCUT)
|
||||
finally:
|
||||
if blocked:
|
||||
self.shortcut_combo.handler_unblock_by_func(self._on_shortcut_changed)
|
||||
@@ -2719,7 +2721,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
assuming a bare-modifier gesture. Reads the saved shortcut from config
|
||||
(the single source of truth for both preset and custom shortcuts).
|
||||
"""
|
||||
shortcut = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
||||
shortcut = self.config_manager.get_str("shortcuts", "toggle_recognition", DEFAULT_SHORTCUT)
|
||||
is_combo = is_valid_shortcut(shortcut) and parse_shortcut_spec(shortcut).is_combo
|
||||
# e.g. "Press Alt+R" (combo toggle), "Double-tap Ctrl", "Hold Alt+R".
|
||||
action = get_shortcut_display_name(shortcut, mode)
|
||||
@@ -2765,7 +2767,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
# Apply from saved config (source of truth for both preset and custom).
|
||||
if self.shortcut_update_callback:
|
||||
shortcut_id = self.config_manager.get_str(
|
||||
"shortcuts", "toggle_recognition", "ctrl+ctrl"
|
||||
"shortcuts", "toggle_recognition", DEFAULT_SHORTCUT
|
||||
)
|
||||
success = self.shortcut_update_callback(shortcut_id, mode_id)
|
||||
if success:
|
||||
@@ -2793,9 +2795,9 @@ class SettingsDialog(Gtk.Dialog):
|
||||
visible while a preset is still the active binding. Also clear the
|
||||
temporary Record/Set hint so the mode description matches the UI.
|
||||
"""
|
||||
current = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
||||
current = self.config_manager.get_str("shortcuts", "toggle_recognition", DEFAULT_SHORTCUT)
|
||||
self._sync_shortcut_selection_ui(current)
|
||||
mode_id = self.shortcut_mode_combo.get_active_id() or "toggle"
|
||||
mode_id = self.shortcut_mode_combo.get_active_id() or DEFAULT_SHORTCUT_MODE
|
||||
self._update_shortcut_ui_for_mode(mode_id)
|
||||
|
||||
def _on_shortcut_changed(self, widget):
|
||||
@@ -2815,7 +2817,9 @@ class SettingsDialog(Gtk.Dialog):
|
||||
# Selecting "Custom Shortcut" does not change the active binding until
|
||||
# the user records/sets one; just focus the custom entry for convenience.
|
||||
if shortcut_id == "__custom__":
|
||||
current = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
||||
current = self.config_manager.get_str(
|
||||
"shortcuts", "toggle_recognition", DEFAULT_SHORTCUT
|
||||
)
|
||||
if not self._is_preset_shortcut(current):
|
||||
self.custom_shortcut_entry.set_text(current)
|
||||
self._set_custom_shortcut_row_visible(True)
|
||||
|
||||
@@ -41,6 +41,7 @@ from ..utils.resource_manager import ResourceManager
|
||||
from ..utils.update_checker import ReleaseInfo
|
||||
from ..utils.update_monitor import UpdateMonitor
|
||||
from .config_manager import ConfigManager
|
||||
from .keyboard_backends import DEFAULT_SHORTCUT, DEFAULT_SHORTCUT_MODE
|
||||
from .keyboard_shortcuts import KeyboardShortcutManager
|
||||
from .settings_dialog import SettingsDialog
|
||||
|
||||
@@ -107,8 +108,8 @@ class TrayIndicator:
|
||||
self._syncing_autostart_menu = False
|
||||
|
||||
# Get configured shortcut and mode from config
|
||||
shortcut = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
||||
mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
|
||||
shortcut = self.config_manager.get_str("shortcuts", "toggle_recognition", DEFAULT_SHORTCUT)
|
||||
mode = self.config_manager.get_str("shortcuts", "mode", DEFAULT_SHORTCUT_MODE)
|
||||
|
||||
# Initialize keyboard shortcut manager with configured shortcut and mode
|
||||
self.shortcut_manager = KeyboardShortcutManager(shortcut=shortcut, mode=mode)
|
||||
@@ -195,7 +196,7 @@ class TrayIndicator:
|
||||
self.shortcut_manager.register_release_callback(None)
|
||||
|
||||
# Get configured mode from config
|
||||
mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
|
||||
mode = self.config_manager.get_str("shortcuts", "mode", DEFAULT_SHORTCUT_MODE)
|
||||
logger.info(f"Setting up keyboard shortcuts with mode: {mode}")
|
||||
|
||||
if mode == "toggle":
|
||||
|
||||
@@ -80,7 +80,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
|
||||
- [ ] Check for startup errors
|
||||
- [ ] Verify the application starts
|
||||
- [ ] Check that tray icon appears
|
||||
- [ ] Verify keyboard shortcut works (Ctrl+Alt+M by default)
|
||||
- [ ] Verify keyboard shortcut works (Hold Right Alt by default)
|
||||
|
||||
### Launch from Application Menu
|
||||
- [ ] Launch from desktop environment's application menu
|
||||
@@ -98,7 +98,7 @@ Use this checklist when testing Vocalinux on a new Linux distribution.
|
||||
- [ ] "Quit" menu item exits the application
|
||||
|
||||
#### Keyboard Shortcuts
|
||||
- [ ] Default shortcut (Ctrl+Alt+M) toggles recording
|
||||
- [ ] Default shortcut (Hold Right Alt) starts push-to-talk recording
|
||||
- [ ] Custom shortcuts can be configured in Settings
|
||||
- [ ] Shortcut works in different applications (terminal, text editor, browser)
|
||||
|
||||
|
||||
@@ -434,6 +434,52 @@ class TestConfigManager(unittest.TestCase):
|
||||
self.assertEqual(config_manager.config["shortcuts"]["toggle_recognition"], "alt+alt")
|
||||
self.assertEqual(config_manager.config["shortcuts"]["mode"], "push_to_talk")
|
||||
|
||||
def test_existing_ctrl_toggle_config_preserved(self):
|
||||
"""Existing installs keep saved Ctrl toggle defaults after default change."""
|
||||
test_config = {
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl",
|
||||
"mode": "toggle",
|
||||
}
|
||||
}
|
||||
|
||||
with open(self.temp_config_file, "w") as f:
|
||||
json.dump(test_config, f)
|
||||
|
||||
config_manager = ConfigManager()
|
||||
self.assertEqual(config_manager.config["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
|
||||
self.assertEqual(config_manager.config["shortcuts"]["mode"], "toggle")
|
||||
|
||||
def test_legacy_shortcut_without_mode_stays_toggle(self):
|
||||
"""Installer/old seeds with only toggle_recognition keep toggle mode."""
|
||||
test_config = {
|
||||
"shortcuts": {
|
||||
"toggle_recognition": "ctrl+ctrl",
|
||||
}
|
||||
}
|
||||
|
||||
with open(self.temp_config_file, "w") as f:
|
||||
json.dump(test_config, f)
|
||||
|
||||
config_manager = ConfigManager()
|
||||
self.assertEqual(config_manager.config["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
|
||||
self.assertEqual(config_manager.config["shortcuts"]["mode"], "toggle")
|
||||
|
||||
def test_legacy_config_without_shortcuts_section_keeps_ctrl_toggle(self):
|
||||
"""Configs that never stored shortcuts keep legacy ctrl+ctrl toggle."""
|
||||
test_config = {
|
||||
"speech_recognition": {
|
||||
"engine": "whisper_cpp",
|
||||
}
|
||||
}
|
||||
|
||||
with open(self.temp_config_file, "w") as f:
|
||||
json.dump(test_config, f)
|
||||
|
||||
config_manager = ConfigManager()
|
||||
self.assertEqual(config_manager.config["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
|
||||
self.assertEqual(config_manager.config["shortcuts"]["mode"], "toggle")
|
||||
|
||||
def test_sound_effects_enabled_by_default(self):
|
||||
"""Test that sound effects are enabled by default."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
@@ -212,8 +212,8 @@ class TestEvdevKeyboardBackendInit:
|
||||
def test_init_default_values(self):
|
||||
"""Test initialization with default values."""
|
||||
backend = EvdevKeyboardBackend()
|
||||
assert backend.shortcut == "ctrl+ctrl"
|
||||
assert backend.mode == "toggle"
|
||||
assert backend.shortcut == "right_alt+right_alt"
|
||||
assert backend.mode == "push_to_talk"
|
||||
assert backend.active is False
|
||||
assert backend.devices == []
|
||||
assert backend.device_fds == []
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
self.mock_backend.key_press_callback = None
|
||||
self.mock_backend.key_release_callback = None
|
||||
self.mock_backend.start.return_value = True
|
||||
self.mock_backend.shortcut = "ctrl+ctrl"
|
||||
self.mock_backend.shortcut = "right_alt+right_alt"
|
||||
self.mock_create_backend.return_value = self.mock_backend
|
||||
|
||||
# Create a new KSM for each test
|
||||
@@ -54,12 +54,12 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
# Verify the shortcut was passed to create_backend
|
||||
# Verify the shortcut was passed to create_backend
|
||||
self.mock_create_backend.assert_called_with(
|
||||
preferred_backend=None, shortcut="alt+alt", mode="toggle"
|
||||
preferred_backend=None, shortcut="alt+alt", mode="push_to_talk"
|
||||
)
|
||||
|
||||
def test_default_shortcut(self):
|
||||
"""Test that default shortcut is ctrl+ctrl."""
|
||||
self.assertEqual(DEFAULT_SHORTCUT, "ctrl+ctrl")
|
||||
"""Test that default shortcut is right_alt+right_alt (hold Right Alt)."""
|
||||
self.assertEqual(DEFAULT_SHORTCUT, "right_alt+right_alt")
|
||||
|
||||
def test_supported_shortcuts(self):
|
||||
"""Test that all expected shortcuts are supported."""
|
||||
@@ -104,12 +104,12 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
|
||||
def test_shortcut_property(self):
|
||||
"""Test the shortcut property."""
|
||||
self.assertEqual(self.ksm.shortcut, "ctrl+ctrl")
|
||||
self.assertEqual(self.ksm.shortcut, "right_alt+right_alt")
|
||||
|
||||
def test_shortcut_display_name_property(self):
|
||||
"""Test the shortcut_display_name property."""
|
||||
display_name = self.ksm.shortcut_display_name
|
||||
self.assertEqual(display_name, SHORTCUT_DISPLAY_NAMES["ctrl+ctrl"])
|
||||
self.assertEqual(display_name, SHORTCUT_DISPLAY_NAMES["right_alt+right_alt"])
|
||||
|
||||
def test_set_shortcut_valid(self):
|
||||
"""Test setting a valid shortcut."""
|
||||
@@ -124,7 +124,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
result = self.ksm.set_shortcut("invalid+shortcut")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(self.ksm.shortcut, "ctrl+ctrl") # Should remain unchanged
|
||||
self.assertEqual(self.ksm.shortcut, "right_alt+right_alt") # Should remain unchanged
|
||||
|
||||
def test_restart_with_shortcut_valid(self):
|
||||
"""Test restarting with a valid shortcut."""
|
||||
@@ -148,7 +148,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
result = self.ksm.restart_with_shortcut("invalid+shortcut")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(self.ksm.shortcut, "ctrl+ctrl") # Should remain unchanged
|
||||
self.assertEqual(self.ksm.shortcut, "right_alt+right_alt") # Should remain unchanged
|
||||
|
||||
def test_restart_with_shortcut_same_shortcut(self):
|
||||
"""Test restarting with the same shortcut (no-op)."""
|
||||
@@ -158,7 +158,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
self.mock_backend.stop.reset_mock()
|
||||
self.mock_backend.start.reset_mock()
|
||||
|
||||
result = self.ksm.restart_with_shortcut("ctrl+ctrl")
|
||||
result = self.ksm.restart_with_shortcut("right_alt+right_alt")
|
||||
|
||||
self.assertTrue(result)
|
||||
# Should not have stopped or started since shortcut is the same
|
||||
@@ -179,6 +179,7 @@ class TestKeyboardShortcuts(unittest.TestCase):
|
||||
def test_restart_with_shortcut_preserves_callback(self):
|
||||
"""Test that restart preserves the registered callback."""
|
||||
callback = MagicMock()
|
||||
self.ksm.set_mode("toggle")
|
||||
self.mock_backend.double_tap_callback = callback
|
||||
|
||||
# Start the listener
|
||||
@@ -626,7 +627,7 @@ class TestBackendFactory(unittest.TestCase):
|
||||
result = create_backend(shortcut="alt+alt")
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
MockPynput.assert_called_once_with(shortcut="alt+alt", mode="toggle")
|
||||
MockPynput.assert_called_once_with(shortcut="alt+alt", mode="push_to_talk")
|
||||
|
||||
|
||||
class TestShortcutParseFunction(unittest.TestCase):
|
||||
|
||||
@@ -51,8 +51,8 @@ class TestPynputKeyboardBackendInit:
|
||||
def test_init_default_values(self):
|
||||
"""Test initialization with default values."""
|
||||
backend = PynputKeyboardBackend()
|
||||
assert backend.shortcut == "ctrl+ctrl"
|
||||
assert backend.mode == "toggle"
|
||||
assert backend.shortcut == "right_alt+right_alt"
|
||||
assert backend.mode == "push_to_talk"
|
||||
assert backend.active is False
|
||||
assert backend.listener is None
|
||||
|
||||
|
||||
@@ -59,9 +59,7 @@ class TestSettingsDialogShortcutsSection(unittest.TestCase):
|
||||
|
||||
def test_shortcut_config_read(self):
|
||||
"""Test that shortcut is read from config."""
|
||||
self.assertIn(
|
||||
'self.config_manager.get_str("shortcuts", "toggle_recognition"', self.source_code
|
||||
)
|
||||
self.assertIn('"shortcuts", "toggle_recognition"', self.source_code)
|
||||
|
||||
def test_shortcut_changed_handler_exists(self):
|
||||
"""Test that shortcut changed handler exists."""
|
||||
@@ -204,7 +202,7 @@ class TestKeyboardBackendsBase(unittest.TestCase):
|
||||
"""Test that DEFAULT_SHORTCUT is defined."""
|
||||
from vocalinux.ui.keyboard_backends.base import DEFAULT_SHORTCUT
|
||||
|
||||
self.assertEqual(DEFAULT_SHORTCUT, "ctrl+ctrl")
|
||||
self.assertEqual(DEFAULT_SHORTCUT, "right_alt+right_alt")
|
||||
|
||||
def test_keyboard_backend_shortcut_property(self):
|
||||
"""Test that KeyboardBackend has shortcut property."""
|
||||
@@ -239,7 +237,8 @@ class TestConfigManagerShortcuts(unittest.TestCase):
|
||||
|
||||
self.assertIn("shortcuts", DEFAULT_CONFIG)
|
||||
self.assertIn("toggle_recognition", DEFAULT_CONFIG["shortcuts"])
|
||||
self.assertEqual(DEFAULT_CONFIG["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
|
||||
self.assertEqual(DEFAULT_CONFIG["shortcuts"]["toggle_recognition"], "right_alt+right_alt")
|
||||
self.assertEqual(DEFAULT_CONFIG["shortcuts"]["mode"], "push_to_talk")
|
||||
|
||||
def test_config_manager_get_shortcut(self):
|
||||
"""Test getting shortcut from config manager."""
|
||||
@@ -249,8 +248,9 @@ class TestConfigManagerShortcuts(unittest.TestCase):
|
||||
from vocalinux.ui.config_manager import ConfigManager
|
||||
|
||||
config = ConfigManager()
|
||||
shortcut = config.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
||||
self.assertEqual(shortcut, "ctrl+ctrl")
|
||||
shortcut = config.get("shortcuts", "toggle_recognition", "right_alt+right_alt")
|
||||
self.assertEqual(shortcut, "right_alt+right_alt")
|
||||
self.assertEqual(config.get("shortcuts", "mode"), "push_to_talk")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Offline-first Linux voice typing with real desktop integration (system tray, X11
|
||||
- Engines: whisper.cpp (default), OpenAI Whisper, VOSK, optional Remote API
|
||||
- Speech languages: large selectable catalog (~33 + Auto-detect) shared by Settings/CLI; VOSK only lists languages with official Alphacephei models; remaining Whisper languages available via Auto-detect
|
||||
- Display servers: X11 and Wayland
|
||||
- Shortcut modes: toggle and push-to-talk; left/right modifier distinction; configurable modifier+key combos
|
||||
- Shortcut modes: push-to-talk default (hold Right Alt / Option); toggle available; left/right modifier distinction; configurable modifier+key combos
|
||||
- Optional voice commands (English-only); Silero neural VAD with amplitude fallback
|
||||
- Continuous dictation polish: capitalize after sentence punctuation; trailing space after each completed utterance
|
||||
- Optional auto-pause while configured apps run; optional idle model keep-alive unload
|
||||
|
||||
@@ -83,7 +83,7 @@ const faqCategories = [
|
||||
questions: [
|
||||
{
|
||||
q: "How do I start and stop dictation?",
|
||||
a: "By default, toggle mode uses double-tap Ctrl to start/stop dictation. You can also switch to push-to-talk mode (hold key to speak, release to stop) in Settings.",
|
||||
a: "By default, hold Right Alt (Option on Mac-layout keyboards) to speak and release to stop (push-to-talk). You can switch to toggle mode (double-tap a key to start/stop) or change the key in Settings.",
|
||||
},
|
||||
{
|
||||
q: "Does Vocalinux work with Wayland?",
|
||||
|
||||
@@ -56,7 +56,7 @@ const distroConfig: Record<DistroSlug, DistroConfig> = {
|
||||
],
|
||||
postInstallChecks: [
|
||||
"Run vocalinux and confirm the tray indicator appears.",
|
||||
"Use toggle mode (double-tap Ctrl) or push-to-talk mode and verify dictation starts.",
|
||||
"Hold Right Alt (Option) to dictate, or switch to toggle mode in Settings and verify dictation starts.",
|
||||
"Set language and model size in the settings panel for your workload.",
|
||||
],
|
||||
},
|
||||
@@ -264,7 +264,7 @@ export default async function DistroInstallPage({
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Start dictating",
|
||||
text: "Launch vocalinux and use toggle mode or push-to-talk mode to begin dictation.",
|
||||
text: "Launch vocalinux and hold Right Alt (Option) to begin dictation, or choose toggle mode in Settings.",
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -114,8 +114,7 @@ export default function InstallGuidesPage() {
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
Use toggle mode (double-tap Ctrl) or push-to-talk to control
|
||||
dictation in any text field.
|
||||
Hold Right Alt (Option) to dictate, or switch to toggle mode in Settings.
|
||||
</li>
|
||||
<li>
|
||||
Choose whisper.cpp for speed, Whisper for maximum model
|
||||
|
||||
@@ -162,7 +162,7 @@ const homeJsonLd = [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Start dictating",
|
||||
text: "Launch vocalinux and use toggle or push-to-talk shortcut mode to begin dictation.",
|
||||
text: "Launch vocalinux and hold Right Alt (Option) to begin dictation, or choose toggle mode in Settings.",
|
||||
url: "https://vocalinux.com/#install",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,11 +10,19 @@ const shortcutCategories = [
|
||||
icon: Mic,
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Ctrl", "Ctrl"], action: "Toggle mode: start/stop dictation", note: "Default mode" },
|
||||
{ keys: ["Hold Ctrl"], action: "Push-to-talk mode: speak while held", note: "Release to stop" },
|
||||
{
|
||||
keys: ["Hold Right Alt"],
|
||||
action: "Push-to-talk (default): speak while held",
|
||||
note: "Release to stop · Option on Mac-layout keyboards",
|
||||
},
|
||||
{
|
||||
keys: ["Double-tap key"],
|
||||
action: "Toggle mode: start/stop dictation",
|
||||
note: "Optional — switch in Settings",
|
||||
},
|
||||
{
|
||||
keys: ["Settings"],
|
||||
action: "Choose Left Ctrl vs Right Ctrl in Settings for more control",
|
||||
action: "Choose Left/Right Ctrl, Alt, or Shift in Settings",
|
||||
note: "Modifier key side selection",
|
||||
},
|
||||
{
|
||||
@@ -144,8 +152,9 @@ export default function ShortcutsPage() {
|
||||
Keyboard Shortcuts & Voice Commands
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
Master Vocalinux with keyboard shortcuts and voice commands. Use toggle mode (double-tap)
|
||||
or push-to-talk mode, then apply voice commands for editing and formatting.
|
||||
Master Vocalinux with keyboard shortcuts and voice commands. New installs default to
|
||||
push-to-talk (hold Right Alt), or switch to toggle mode (double-tap), then apply voice
|
||||
commands for editing and formatting.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -155,19 +164,15 @@ export default function ShortcutsPage() {
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
Ctrl
|
||||
</kbd>
|
||||
<span className="text-muted-foreground">+</span>
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
Ctrl
|
||||
Hold Right Alt
|
||||
</kbd>
|
||||
</div>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium">Toggle Mode: Start/Stop Dictation</span>
|
||||
<span className="font-medium">Push-to-Talk: Speak While Held</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
Push-to-talk mode is also available: hold your configured key to dictate, release to stop.
|
||||
You can switch modes in Settings.
|
||||
Right Alt is the Option key on Mac-layout keyboards. Toggle mode is also available:
|
||||
double-tap your configured key to start/stop. You can switch modes and keys in Settings.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Voice commands can be enabled or disabled from Settings depending on your workflow.
|
||||
|
||||
Reference in New Issue
Block a user