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
+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":