Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,24 @@ def save_setting(key: str, value: str):
c.close()


def save_settings(settings: dict[str, str]):
"""Save multiple settings atomically."""
with _lock:
c = _conn()
try:
c.executemany(
"INSERT INTO settings(key,value) VALUES(?,?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
settings.items(),
)
c.commit()
except Exception:
c.rollback()
raise
finally:
c.close()


# ── Custom vocabulary (Layer A) ───────────────────────────────────────────

def list_vocabulary() -> list[tuple[str, str]]:
Expand Down
67 changes: 54 additions & 13 deletions hotkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
(frozenset_of_modifier_strings, trigger_key) — see hotkey_util.py.
"""

import queue
import threading
import time
from pynput import keyboard
import config
from logger import log
from hotkey_util import canonical_modifier, keys_match

_DEBOUNCE_SEC = 0.3 # minimum time between toggle actions
_STOP_CALLBACKS = object()


# ── Hotkey matching helpers ───────────────────────────────────────────────
Expand Down Expand Up @@ -63,6 +66,16 @@ def __init__(self, on_press_cb, on_release_cb,
# Currently held modifier keys (canonical names: "ctrl", "shift", etc.)
self._held_modifiers: set = set()
self._listener = None
# pynput invokes handlers on its event thread. Microphone startup can
# block there, which prevents a physical key release from being seen.
# Preserve callback order on a separate worker while keeping pynput's
# event thread responsive.
self._callback_queue = queue.Queue()
self._callback_stopped = threading.Event()
self._lifecycle_lock = threading.Lock()
self._callback_worker = threading.Thread(
target=self._run_callbacks, daemon=True)
self._callback_worker.start()

def _is_hold_mode(self) -> bool:
return getattr(config, "HOLD_TO_RECORD", True)
Expand Down Expand Up @@ -154,23 +167,51 @@ def force_stop_assistant(self):
if self._on_assist_release:
self._safe_call(self._on_assist_release, "Assistant timeout-stop")

def cancel_dictation_start(self):
"""Reset dictation key state after microphone startup fails."""
self._dict_recording = False
self._dict_pressed = False

def cancel_assistant_start(self):
"""Reset assistant key state after microphone startup fails."""
self._assist_recording = False
self._assist_pressed = False

# ── helpers ───────────────────────────────────────────────────────────

@staticmethod
def _safe_call(fn, label: str):
try:
fn()
except Exception as exc:
log.error("%s error: %s", label, exc)
def _safe_call(self, fn, label: str):
if not self._callback_stopped.is_set():
self._callback_queue.put((fn, label))

def _run_callbacks(self):
while True:
item = self._callback_queue.get()
if item is _STOP_CALLBACKS:
return
if self._callback_stopped.is_set():
continue
fn, label = item
try:
fn()
except Exception as exc:
log.error("%s error: %s", label, exc)

def start(self):
self._listener = keyboard.Listener(
on_press=self._handle_press,
on_release=self._handle_release,
)
self._listener.start()
with self._lifecycle_lock:
if self._callback_stopped.is_set():
return
self._listener = keyboard.Listener(
on_press=self._handle_press,
on_release=self._handle_release,
)
self._listener.start()
self._listener.wait()

def stop(self):
if self._listener is not None:
self._listener.stop()
self._callback_stopped.set()
with self._lifecycle_lock:
if self._listener is not None:
self._listener.stop()
self._callback_queue.put(_STOP_CALLBACKS)
if threading.current_thread() is not self._callback_worker:
self._callback_worker.join()
12 changes: 12 additions & 0 deletions locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
"lang_name": "English",

# main.py — widget messages
"microphone_starting": "Starting microphone...",
"language_prompt_question": "Set recognition language to {language}?",
"language_prompt_accept": "Use {language}",
"language_prompt_decline": "Keep Auto",
"show_notes": "📝 Here are your notes",
"show_appointments": "📅 Here is your agenda",
"show_reminders": "⏰ Here are your reminders",
Expand Down Expand Up @@ -221,6 +225,10 @@
"lang_name": "Italian",

"show_notes": "📝 Ecco le note",
"microphone_starting": "Avvio microfono...",
"language_prompt_question": "Impostare la lingua di riconoscimento su {language}?",
"language_prompt_accept": "Usa {language}",
"language_prompt_decline": "Mantieni Auto",
"show_appointments": "📅 Ecco l'agenda",
"show_reminders": "⏰ Ecco i reminder",
"assistant_error": "Errore assistente",
Expand Down Expand Up @@ -362,6 +370,10 @@
"lang_name": "German",

"show_notes": "📝 Hier sind Ihre Notizen",
"microphone_starting": "Mikrofon wird gestartet...",
"language_prompt_question": "Erkennungssprache auf {language} setzen?",
"language_prompt_accept": "{language} verwenden",
"language_prompt_decline": "Auto behalten",
"show_appointments": "📅 Hier ist Ihre Agenda",
"show_reminders": "⏰ Hier sind Ihre Erinnerungen",
"assistant_error": "Assistentenfehler",
Expand Down
Loading