mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-20 07:10:40 -07:00
fix(captures): use platform hotkey defaults
This commit is contained in:
@@ -30,7 +30,7 @@ import { useProfiles } from '@/lib/hooks/useProfiles';
|
|||||||
import { usePlatform } from '@/platform/PlatformContext';
|
import { usePlatform } from '@/platform/PlatformContext';
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { displayLabelForKey, modifierSideHint } from '@/lib/utils/keyCodes';
|
import { defaultChordKeys, displayLabelForKey, modifierSideHint } from '@/lib/utils/keyCodes';
|
||||||
import type { Qwen3ModelSize, VoiceProfileResponse, WhisperModelSize } from '@/lib/api/types';
|
import type { Qwen3ModelSize, VoiceProfileResponse, WhisperModelSize } from '@/lib/api/types';
|
||||||
import { SettingRow, SettingSection } from './SettingRow';
|
import { SettingRow, SettingSection } from './SettingRow';
|
||||||
|
|
||||||
@@ -135,8 +135,8 @@ export function CapturesPage() {
|
|||||||
const allowAutoPaste = settings?.allow_auto_paste ?? true;
|
const allowAutoPaste = settings?.allow_auto_paste ?? true;
|
||||||
const defaultVoiceId = settings?.default_playback_voice_id ?? null;
|
const defaultVoiceId = settings?.default_playback_voice_id ?? null;
|
||||||
const hotkeyEnabled = settings?.hotkey_enabled ?? false;
|
const hotkeyEnabled = settings?.hotkey_enabled ?? false;
|
||||||
const pushToTalkKeys = settings?.chord_push_to_talk_keys ?? ['MetaRight', 'AltGr'];
|
const pushToTalkKeys = settings?.chord_push_to_talk_keys ?? defaultChordKeys('push');
|
||||||
const toggleToTalkKeys = settings?.chord_toggle_to_talk_keys ?? ['MetaRight', 'AltGr', 'Space'];
|
const toggleToTalkKeys = settings?.chord_toggle_to_talk_keys ?? defaultChordKeys('toggle');
|
||||||
|
|
||||||
// Mock-only settings — not yet wired to a backend. Keep local so the UI
|
// Mock-only settings — not yet wired to a backend. Keep local so the UI
|
||||||
// still responds while Phase 7 (hotkey / clipboard / paste) catches up.
|
// still responds while Phase 7 (hotkey / clipboard / paste) catches up.
|
||||||
|
|||||||
@@ -213,9 +213,9 @@ export interface CaptureSettings {
|
|||||||
/** Whether the global keyboard hotkey is armed. Off by default — turning
|
/** Whether the global keyboard hotkey is armed. Off by default — turning
|
||||||
* this on triggers the macOS Input Monitoring TCC prompt. */
|
* this on triggers the macOS Input Monitoring TCC prompt. */
|
||||||
hotkey_enabled: boolean;
|
hotkey_enabled: boolean;
|
||||||
/** rdev::Key variant names. Defaults: ["MetaRight","AltGr"]. */
|
/** keytap key names. Defaults are platform-specific right-hand modifiers. */
|
||||||
chord_push_to_talk_keys: string[];
|
chord_push_to_talk_keys: string[];
|
||||||
/** rdev::Key variant names. Defaults: ["MetaRight","AltGr","Space"]. */
|
/** keytap key names. Toggle adds Space to the platform-specific PTT chord. */
|
||||||
chord_toggle_to_talk_keys: string[];
|
chord_toggle_to_talk_keys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Stable key-name vocabulary shared with the Rust `key_codes` module.
|
* Stable key-name vocabulary shared with the Rust `key_codes` module.
|
||||||
*
|
*
|
||||||
* The chord persistence layer stores rdev `Key` variant names ("MetaRight",
|
* The chord persistence layer stores keytap `Key` variant names ("MetaRight",
|
||||||
* "AltGr", "KeyA", …) so the same array round-trips losslessly between
|
* "AltGr", "KeyA", …) so the same array round-trips losslessly between
|
||||||
* the picker UI, the SQLite settings row, and the global hotkey listener.
|
* the picker UI, the SQLite settings row, and the global hotkey listener.
|
||||||
*
|
*
|
||||||
* This module owns the conversions between three vocabularies:
|
* This module owns the conversions between three vocabularies:
|
||||||
* - browser `KeyboardEvent` (`event.code` like "MetaRight" / "AltRight")
|
* - browser `KeyboardEvent` (`event.code` like "MetaRight" / "AltRight")
|
||||||
* - canonical chord key names (matches rdev variants)
|
* - canonical chord key names (matches keytap variants)
|
||||||
* - human display labels ("⌘", "⌥", "A", …)
|
* - human display labels ("⌘", "⌥", "A", …)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
* `null` for keys we don't support in chords (dead keys, IME composition,
|
* `null` for keys we don't support in chords (dead keys, IME composition,
|
||||||
* etc.).
|
* etc.).
|
||||||
*
|
*
|
||||||
* Browser quirk: right-Option on macOS is reported as `"AltRight"`; rdev
|
* Browser quirk: right-Option on macOS is reported as `"AltRight"`; keytap
|
||||||
* calls it `"AltGr"`. Normalize to rdev's name so the Rust side recognizes
|
* calls it `"AltGr"`. Normalize to keytap's name so the Rust side recognizes
|
||||||
* it without an aliasing layer.
|
* it without an aliasing layer.
|
||||||
*/
|
*/
|
||||||
export function canonicalKeyFromEvent(event: KeyboardEvent): string | null {
|
export function canonicalKeyFromEvent(event: KeyboardEvent): string | null {
|
||||||
@@ -53,7 +53,7 @@ export function canonicalKeyFromEvent(event: KeyboardEvent): string | null {
|
|||||||
default:
|
default:
|
||||||
// Browser names like "MetaRight", "MetaLeft", "ControlLeft",
|
// Browser names like "MetaRight", "MetaLeft", "ControlLeft",
|
||||||
// "ShiftRight", "Space", "KeyA", "Digit1", "F5" all match the
|
// "ShiftRight", "Space", "KeyA", "Digit1", "F5" all match the
|
||||||
// rdev variant names directly.
|
// keytap variant names directly.
|
||||||
if (
|
if (
|
||||||
/^(Meta|Control|Shift)(Left|Right)$/.test(code) ||
|
/^(Meta|Control|Shift)(Left|Right)$/.test(code) ||
|
||||||
/^Key[A-Z]$/.test(code) ||
|
/^Key[A-Z]$/.test(code) ||
|
||||||
@@ -72,6 +72,13 @@ export function canonicalKeyFromEvent(event: KeyboardEvent): string | null {
|
|||||||
const PLATFORM_IS_MAC =
|
const PLATFORM_IS_MAC =
|
||||||
typeof navigator !== 'undefined' && /mac/i.test(navigator.platform);
|
typeof navigator !== 'undefined' && /mac/i.test(navigator.platform);
|
||||||
|
|
||||||
|
export function defaultChordKeys(mode: 'push' | 'toggle'): string[] {
|
||||||
|
const base = PLATFORM_IS_MAC
|
||||||
|
? ['MetaRight', 'AltGr']
|
||||||
|
: ['ControlRight', 'ShiftRight'];
|
||||||
|
return mode === 'toggle' ? [...base, 'Space'] : base;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pretty label for a canonical key name. Picks platform-appropriate
|
* Pretty label for a canonical key name. Picks platform-appropriate
|
||||||
* modifier glyphs so macOS users see ⌘ and Windows/Linux users see Win.
|
* modifier glyphs so macOS users see ⌘ and Windows/Linux users see Win.
|
||||||
|
|||||||
@@ -17,11 +17,17 @@ Adding a new migration:
|
|||||||
(idempotent) and print a short message when it does real work.
|
(idempotent) and print a short message when it does real work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from sqlalchemy import inspect, text
|
from sqlalchemy import inspect, text
|
||||||
|
|
||||||
|
from ..utils.capture_chords import (
|
||||||
|
default_push_to_talk_chord,
|
||||||
|
default_toggle_to_talk_chord,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -200,6 +206,8 @@ def _migrate_capture_settings(engine, inspector, tables: set[str]) -> None:
|
|||||||
if "capture_settings" not in tables:
|
if "capture_settings" not in tables:
|
||||||
return
|
return
|
||||||
columns = _get_columns(inspector, "capture_settings")
|
columns = _get_columns(inspector, "capture_settings")
|
||||||
|
push_default = json.dumps(default_push_to_talk_chord())
|
||||||
|
toggle_default = json.dumps(default_toggle_to_talk_chord())
|
||||||
if "allow_auto_paste" not in columns:
|
if "allow_auto_paste" not in columns:
|
||||||
_add_column(
|
_add_column(
|
||||||
engine,
|
engine,
|
||||||
@@ -218,14 +226,14 @@ def _migrate_capture_settings(engine, inspector, tables: set[str]) -> None:
|
|||||||
_add_column(
|
_add_column(
|
||||||
engine,
|
engine,
|
||||||
"capture_settings",
|
"capture_settings",
|
||||||
"chord_push_to_talk_keys TEXT NOT NULL DEFAULT '[\"MetaRight\",\"AltGr\"]'",
|
f"chord_push_to_talk_keys TEXT NOT NULL DEFAULT '{push_default}'",
|
||||||
"chord_push_to_talk_keys",
|
"chord_push_to_talk_keys",
|
||||||
)
|
)
|
||||||
if "chord_toggle_to_talk_keys" not in columns:
|
if "chord_toggle_to_talk_keys" not in columns:
|
||||||
_add_column(
|
_add_column(
|
||||||
engine,
|
engine,
|
||||||
"capture_settings",
|
"capture_settings",
|
||||||
"chord_toggle_to_talk_keys TEXT NOT NULL DEFAULT '[\"MetaRight\",\"AltGr\",\"Space\"]'",
|
f"chord_toggle_to_talk_keys TEXT NOT NULL DEFAULT '{toggle_default}'",
|
||||||
"chord_toggle_to_talk_keys",
|
"chord_toggle_to_talk_keys",
|
||||||
)
|
)
|
||||||
if "hotkey_enabled" not in columns:
|
if "hotkey_enabled" not in columns:
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import uuid
|
|||||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey, Boolean, JSON
|
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey, Boolean, JSON
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from ..utils.capture_chords import (
|
||||||
|
default_push_to_talk_chord,
|
||||||
|
default_toggle_to_talk_chord,
|
||||||
|
)
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
@@ -205,14 +210,13 @@ class CaptureSettings(Base):
|
|||||||
# "Voicebox would like to receive keystrokes from any application" dialog
|
# "Voicebox would like to receive keystrokes from any application" dialog
|
||||||
# before they've even opened the Captures tab.
|
# before they've even opened the Captures tab.
|
||||||
hotkey_enabled = Column(Boolean, nullable=False, default=False)
|
hotkey_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
# Lists of rdev::Key variant names (e.g. "MetaRight", "AltGr"). Right-hand
|
# Lists of keytap key names (e.g. "MetaRight", "ControlRight"). Right-hand
|
||||||
# modifiers by default so they don't collide with left-hand system
|
# modifiers by default so they don't collide with left-hand shortcuts.
|
||||||
# shortcuts (Cmd+Opt+I devtools, Cmd+Opt+Esc force-quit).
|
|
||||||
chord_push_to_talk_keys = Column(
|
chord_push_to_talk_keys = Column(
|
||||||
JSON, nullable=False, default=lambda: ["MetaRight", "AltGr"]
|
JSON, nullable=False, default=default_push_to_talk_chord
|
||||||
)
|
)
|
||||||
chord_toggle_to_talk_keys = Column(
|
chord_toggle_to_talk_keys = Column(
|
||||||
JSON, nullable=False, default=lambda: ["MetaRight", "AltGr", "Space"]
|
JSON, nullable=False, default=default_toggle_to_talk_chord
|
||||||
)
|
)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -6,6 +6,11 @@ from pydantic import BaseModel, Field
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .utils.capture_chords import (
|
||||||
|
default_push_to_talk_chord,
|
||||||
|
default_toggle_to_talk_chord,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VoiceProfileCreate(BaseModel):
|
class VoiceProfileCreate(BaseModel):
|
||||||
"""Request model for creating a voice profile."""
|
"""Request model for creating a voice profile."""
|
||||||
@@ -253,9 +258,11 @@ class CaptureSettingsResponse(BaseModel):
|
|||||||
allow_auto_paste: bool = True
|
allow_auto_paste: bool = True
|
||||||
default_playback_voice_id: Optional[str] = None
|
default_playback_voice_id: Optional[str] = None
|
||||||
hotkey_enabled: bool = False
|
hotkey_enabled: bool = False
|
||||||
chord_push_to_talk_keys: List[str] = Field(default_factory=lambda: ["MetaRight", "AltGr"])
|
chord_push_to_talk_keys: List[str] = Field(
|
||||||
|
default_factory=default_push_to_talk_chord
|
||||||
|
)
|
||||||
chord_toggle_to_talk_keys: List[str] = Field(
|
chord_toggle_to_talk_keys: List[str] = Field(
|
||||||
default_factory=lambda: ["MetaRight", "AltGr", "Space"]
|
default_factory=default_toggle_to_talk_chord
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from ..database import CaptureSettings as DBCaptureSettings
|
from ..database import CaptureSettings as DBCaptureSettings
|
||||||
from ..database import GenerationSettings as DBGenerationSettings
|
from ..database import GenerationSettings as DBGenerationSettings
|
||||||
|
from ..utils.capture_chords import (
|
||||||
|
default_push_to_talk_chord,
|
||||||
|
default_toggle_to_talk_chord,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SINGLETON_ID = 1
|
SINGLETON_ID = 1
|
||||||
@@ -21,7 +25,11 @@ SINGLETON_ID = 1
|
|||||||
def _get_or_create_capture_row(db: Session) -> DBCaptureSettings:
|
def _get_or_create_capture_row(db: Session) -> DBCaptureSettings:
|
||||||
row = db.query(DBCaptureSettings).filter(DBCaptureSettings.id == SINGLETON_ID).first()
|
row = db.query(DBCaptureSettings).filter(DBCaptureSettings.id == SINGLETON_ID).first()
|
||||||
if row is None:
|
if row is None:
|
||||||
row = DBCaptureSettings(id=SINGLETON_ID)
|
row = DBCaptureSettings(
|
||||||
|
id=SINGLETON_ID,
|
||||||
|
chord_push_to_talk_keys=default_push_to_talk_chord(),
|
||||||
|
chord_toggle_to_talk_keys=default_toggle_to_talk_chord(),
|
||||||
|
)
|
||||||
db.add(row)
|
db.add(row)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Platform defaults for capture hotkey chords."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
MAC_PUSH_TO_TALK = ["MetaRight", "AltGr"]
|
||||||
|
MAC_TOGGLE_TO_TALK = ["MetaRight", "AltGr", "Space"]
|
||||||
|
NON_MAC_PUSH_TO_TALK = ["ControlRight", "ShiftRight"]
|
||||||
|
NON_MAC_TOGGLE_TO_TALK = ["ControlRight", "ShiftRight", "Space"]
|
||||||
|
|
||||||
|
|
||||||
|
def default_push_to_talk_chord() -> list[str]:
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return MAC_PUSH_TO_TALK.copy()
|
||||||
|
return NON_MAC_PUSH_TO_TALK.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def default_toggle_to_talk_chord() -> list[str]:
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return MAC_TOGGLE_TO_TALK.copy()
|
||||||
|
return NON_MAC_TOGGLE_TO_TALK.copy()
|
||||||
Reference in New Issue
Block a user