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:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user