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:
Jatin K Malik
2026-08-08 00:07:54 -07:00
committed by GitHub
parent 06e8809c2a
commit c337d8f56d
22 changed files with 165 additions and 77 deletions
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+30 -5
View File
@@ -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:
+2 -2
View File
@@ -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,
+4 -2
View File
@@ -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"
# ---------------------------------------------------------------------------
+13 -9
View File
@@ -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)
+4 -3
View File
@@ -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":
+2 -2
View File
@@ -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)
+46
View File
@@ -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()
+2 -2
View File
@@ -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 == []
+11 -10
View File
@@ -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):
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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?",
+2 -2
View File
@@ -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}`),
},
],
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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",
},
],
+18 -13
View File
@@ -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.