diff --git a/AGENTS.md b/AGENTS.md index b939e7b..18e0656 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index 74c7a77..1211837 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/DISTRO_COMPATIBILITY.md b/docs/DISTRO_COMPATIBILITY.md index 37658e2..0bbe87f 100644 --- a/docs/DISTRO_COMPATIBILITY.md +++ b/docs/DISTRO_COMPATIBILITY.md @@ -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 diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3d17d19..c49bf08 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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 diff --git a/install.sh b/install.sh index 4a216d0..c65331b 100755 --- a/install.sh +++ b/install.sh @@ -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, diff --git a/src/vocalinux/ui/config_manager.py b/src/vocalinux/ui/config_manager.py index b8c3179..e8b01bd 100644 --- a/src/vocalinux/ui/config_manager.py +++ b/src/vocalinux/ui/config_manager.py @@ -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: diff --git a/src/vocalinux/ui/first_run_dialog.py b/src/vocalinux/ui/first_run_dialog.py index d282788..4619f7d 100644 --- a/src/vocalinux/ui/first_run_dialog.py +++ b/src/vocalinux/ui/first_run_dialog.py @@ -66,8 +66,8 @@ class FirstRunDialog(Gtk.Dialog): quick_guide_label = Gtk.Label( label=( "Quick start\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, diff --git a/src/vocalinux/ui/keyboard_backends/base.py b/src/vocalinux/ui/keyboard_backends/base.py index 6935782..c22cb52 100644 --- a/src/vocalinux/ui/keyboard_backends/base.py +++ b/src/vocalinux/ui/keyboard_backends/base.py @@ -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" # --------------------------------------------------------------------------- diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py index 7a3903e..405203b 100644 --- a/src/vocalinux/ui/settings_dialog.py +++ b/src/vocalinux/ui/settings_dialog.py @@ -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) diff --git a/src/vocalinux/ui/tray_indicator.py b/src/vocalinux/ui/tray_indicator.py index 16b3f8d..d479043 100644 --- a/src/vocalinux/ui/tray_indicator.py +++ b/src/vocalinux/ui/tray_indicator.py @@ -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": diff --git a/tests/distro-testing-checklist.md b/tests/distro-testing-checklist.md index c39af3d..dd6ef4e 100644 --- a/tests/distro-testing-checklist.md +++ b/tests/distro-testing-checklist.md @@ -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) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 958aebf..dad2f10 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -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() diff --git a/tests/test_evdev_backend_ext.py b/tests/test_evdev_backend_ext.py index 49c810d..5d09a83 100644 --- a/tests/test_evdev_backend_ext.py +++ b/tests/test_evdev_backend_ext.py @@ -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 == [] diff --git a/tests/test_keyboard_shortcuts.py b/tests/test_keyboard_shortcuts.py index 44eab15..740cc0d 100644 --- a/tests/test_keyboard_shortcuts.py +++ b/tests/test_keyboard_shortcuts.py @@ -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): diff --git a/tests/test_pynput_backend_ext.py b/tests/test_pynput_backend_ext.py index a130ae7..add585a 100644 --- a/tests/test_pynput_backend_ext.py +++ b/tests/test_pynput_backend_ext.py @@ -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 diff --git a/tests/test_settings_shortcuts.py b/tests/test_settings_shortcuts.py index 9e44f7f..90035df 100644 --- a/tests/test_settings_shortcuts.py +++ b/tests/test_settings_shortcuts.py @@ -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__": diff --git a/web/PRODUCT.md b/web/PRODUCT.md index c759c04..2260989 100644 --- a/web/PRODUCT.md +++ b/web/PRODUCT.md @@ -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 diff --git a/web/src/app/faq/page.tsx b/web/src/app/faq/page.tsx index f9264eb..89fa5e5 100644 --- a/web/src/app/faq/page.tsx +++ b/web/src/app/faq/page.tsx @@ -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?", diff --git a/web/src/app/install/[distro]/page.tsx b/web/src/app/install/[distro]/page.tsx index 948964e..e0bb812 100644 --- a/web/src/app/install/[distro]/page.tsx +++ b/web/src/app/install/[distro]/page.tsx @@ -56,7 +56,7 @@ const distroConfig: Record = { ], 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}`), }, ], diff --git a/web/src/app/install/page.tsx b/web/src/app/install/page.tsx index 318def2..7e9011e 100644 --- a/web/src/app/install/page.tsx +++ b/web/src/app/install/page.tsx @@ -114,8 +114,7 @@ export default function InstallGuidesPage() { .
  • - 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.
  • Choose whisper.cpp for speed, Whisper for maximum model diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 053f4fe..c92db8e 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -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", }, ], diff --git a/web/src/app/shortcuts/page.tsx b/web/src/app/shortcuts/page.tsx index b1b451e..2161862 100644 --- a/web/src/app/shortcuts/page.tsx +++ b/web/src/app/shortcuts/page.tsx @@ -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

    - 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.

    @@ -155,19 +164,15 @@ export default function ShortcutsPage() {
    - Ctrl - - + - - Ctrl + Hold Right Alt
    → - Toggle Mode: Start/Stop Dictation + Push-to-Talk: Speak While Held

    - 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.

    Voice commands can be enabled or disabled from Settings depending on your workflow.