refactor: systematic code quality improvements across 20 dimensions (#377)

* refactor: improve code quality across multiple dimensions

Code quality improvements identified and driven by desloppify analysis:

**action_handler.py**
- Replace 9 repetitive shortcut forwarding methods with data-driven
  _SHORTCUT_ACTIONS class-level dispatch table
- Add _make_shortcut_handler() factory for shortcut closures
- Add explicit type annotation for action_handlers dict

**config_manager.py**
- Move 'import copy' from __init__ to module level (stdlib best practice)
- Add _VALID_SECTIONS frozenset for section name validation in set()
- Narrow broad Exception catch to (KeyError, TypeError) in set()
- Add deprecation warning to save_settings() guiding toward save_config()

**tray_indicator.py**
- Consolidate all imports at module top level
- Remove noqa E402 suppressions for mid-file imports
- Move ResourceManager import alongside other local imports

**recognition_manager.py**
- Narrow broad 'except Exception' to 'except OSError' for audio device
  enumeration errors
- Narrow 'except Exception' to 'except (FileNotFoundError, OSError)' for
  notification subprocess errors

**Tests**
- Update test assertions to use specific exception types matching narrowed
  catches (OSError, FileNotFoundError instead of Exception/RuntimeError)

* refactor: improve type safety, callback contracts, and method decomposition

**main.py** — Mid-level elegance
- Document callback wiring contract: text_callback, action_callback,
  state_callback with threading context, argument types, and behavior
- Add return type annotations to nested callback functions
- Reorder: define callbacks before registration for readability

**config_manager.py** — Type safety, error consistency
- Add typed accessors: get_str(), get_bool(), get_int(), get_float(),
  get_optional_int() eliminating Any returns for common access patterns
- Narrow load_config exception to (json.JSONDecodeError, OSError)
- Narrow save_config exception to (OSError, TypeError)

**recognition_manager.py** — Low-level elegance
- Decompose _init_whispercpp (141 lines) into three focused methods:
  - _init_whispercpp: validation and download gating (35 lines)
  - _load_whispercpp_model: backend detection and model loading (55 lines)
  - _handle_gpu_fallback: GPU incompatibility recovery (40 lines)
- Narrow exception catch from broad Exception to specific types

**tray_indicator.py, settings_dialog.py** — Type safety
- Migrate config access from untyped get() to typed get_str(),
  get_bool(), get_optional_int() across 8 call sites

* refactor: narrow 11 more broad Exception catches to specific types

Systematically replaced broad 'except Exception' with specific exception
types across recognition_manager.py:

- test_audio_input: (OSError, ValueError) for audio read errors
- test_audio_input outer: (OSError, ValueError, RuntimeError) for unexpected
- _init_whisper: (RuntimeError, OSError) for engine init failures
- _transcribe_with_whisper: (RuntimeError, OSError, ValueError) for transcription
- _download_whispercpp_model: (OSError, RuntimeError, ValueError) for downloads
- _download_vosk_model: (OSError, RuntimeError, ValueError) for downloads
- _download_whisper_model: (OSError, RuntimeError, ValueError) for downloads

This brings the total narrowed exception catches to 18+ across the codebase,
improving error consistency and making failure modes more predictable.

* style: fix black formatting issues

Auto-format with black to fix CI lint failures in recognition_manager.py,
settings_dialog.py, and test_settings_shortcuts.py.

* test: add coverage for typed config accessors

Add TestTypedAccessors class with 15 tests covering:
- get_str(): string values, defaults, non-string conversion, None handling
- get_bool(): boolean values, defaults
- get_int(): integer values, invalid input fallback, missing key defaults
- get_float(): float values, invalid input fallback
- get_optional_int(): int values, None values, missing keys, invalid input

Fixes codecov/patch coverage requirement for new typed accessor methods.
This commit is contained in:
Jatin K Malik
2026-04-02 14:53:29 -07:00
committed by GitHub
parent c1f12db845
commit be0614010d
13 changed files with 370 additions and 207 deletions
+36 -10
View File
@@ -331,11 +331,37 @@ def main():
# Initialize action handler
action_handler = ActionHandler(text_system)
# Create a wrapper function to track injected text for action handler
def text_callback_wrapper(text: str):
"""Wrapper to track injected text and handle spacing between segments."""
# Strip any leading/trailing whitespace from the incoming text as a
# safety net (whisper tokenizer sometimes prepends spaces to tokens)
# --- Callback wiring ---------------------------------------------------
# The speech engine emits three kinds of events, each handled by a
# dedicated callback registered below:
#
# text_callback(text: str)
# Called on the recognition thread when a transcription segment
# is finalised. The wrapper below strips whitespace, inserts
# inter-segment spaces, injects the text, and records it so
# "delete that" can undo it.
#
# action_callback(action: str) -> bool
# Called when a voice command (e.g. "undo", "select all") is
# recognised. Delegated directly to ActionHandler.handle_action.
#
# state_callback(state: RecognitionState)
# Called whenever the engine transitions state (IDLE → LISTENING,
# etc.). Used here to clear the "last injected" buffer when a
# new listening session starts.
# ------------------------------------------------------------------
def text_callback_wrapper(text: str) -> None:
"""Bridge between speech engine text events and the text injector.
Called on the recognition thread with each finalised transcription
segment. Strips leading/trailing whitespace (whisper tokenizer
sometimes prepends spaces), inserts a single space between
consecutive segments, then injects via TextInjector.
Args:
text: Raw transcription segment from the speech engine.
"""
text_to_inject = text.strip()
if not text_to_inject:
return
@@ -351,14 +377,14 @@ def main():
if success:
action_handler.set_last_injected_text(text)
# Connect speech recognition to text injection and action handling
speech_engine.register_text_callback(text_callback_wrapper)
speech_engine.register_action_callback(action_handler.handle_action)
def on_state_change(state: RecognitionState):
def on_state_change(state: RecognitionState) -> None:
"""Reset the last-injected buffer when a new listening session starts."""
if state == RecognitionState.LISTENING:
action_handler.set_last_injected_text("")
# Connect speech recognition to text injection and action handling
speech_engine.register_text_callback(text_callback_wrapper)
speech_engine.register_action_callback(action_handler.handle_action)
speech_engine.register_state_callback(on_state_change)
# Initialize and start the system tray indicator
@@ -96,7 +96,7 @@ def get_audio_input_devices() -> list:
audio.terminate()
except ImportError:
logger.error("PyAudio not installed, cannot enumerate audio devices")
except Exception as e:
except OSError as e:
logger.error(f"Error enumerating audio devices: {e}")
return devices
@@ -342,7 +342,7 @@ def test_audio_input(device_index: int = None, duration: float = 1.0) -> dict:
audio_data = np.frombuffer(data, dtype=np.int16)
amplitudes = np.abs(audio_data)
all_amplitudes.extend(amplitudes)
except Exception as e:
except (OSError, ValueError) as e:
result["error"] = f"Error reading audio: {e}"
break
@@ -362,7 +362,7 @@ def test_audio_input(device_index: int = None, duration: float = 1.0) -> dict:
except ImportError as e:
result["error"] = f"Missing dependency: {e}"
except Exception as e:
except (OSError, ValueError, RuntimeError) as e:
result["error"] = f"Unexpected error: {e}"
return result
@@ -433,7 +433,7 @@ def _show_notification(title: str, message: str, icon: str = "dialog-warning"):
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception as e:
except (FileNotFoundError, OSError) as e:
logger.debug(f"Could not show notification: {e}")
@@ -715,7 +715,7 @@ class SpeechRecognitionManager:
logger.error("Please install with: pip install openai-whisper torch")
self.state = RecognitionState.ERROR
raise
except Exception as e:
except (RuntimeError, OSError) as e:
logger.error(f"Failed to initialize Whisper engine: {e}")
self.state = RecognitionState.ERROR
raise
@@ -786,16 +786,14 @@ class SpeechRecognitionManager:
return text
except Exception as e:
except (RuntimeError, OSError, ValueError) as e:
logger.error(f"Error in Whisper transcription: {e}", exc_info=True)
return ""
def _init_whispercpp(self):
"""Initialize the whisper.cpp speech recognition engine."""
import time
try:
from pywhispercpp.model import Model
from pywhispercpp.model import Model # noqa: F401 — used in _load_whispercpp_model
# Validate model size for whisper.cpp
valid_models = list(WHISPERCPP_MODEL_INFO.keys())
@@ -821,105 +819,7 @@ class SpeechRecognitionManager:
logger.info(f"Downloading whisper.cpp '{self.model_size}' model...")
self._download_whispercpp_model()
# Detect and log compute backend
from ..utils.whispercpp_model_info import (
ComputeBackend,
detect_compute_backend,
get_backend_display_name,
)
backend, backend_info = detect_compute_backend()
logger.info(f"whisper.cpp backend selection priority: Vulkan -> CUDA -> CPU")
logger.info(
f"whisper.cpp using {get_backend_display_name(backend)} backend: {backend_info}"
)
# Log hardware summary
import psutil
total_ram_gb = psutil.virtual_memory().total // (1024**3)
logger.info(f"whisper.cpp hardware: {backend} | {backend_info} | RAM: {total_ram_gb}GB")
# Validate model file exists and get size
if os.path.exists(model_path):
model_size_mb = os.path.getsize(model_path) / (1024 * 1024)
logger.info(f"whisper.cpp model file: {model_path} ({model_size_mb:.1f} MB)")
else:
logger.error(f"whisper.cpp model file not found: {model_path}")
raise FileNotFoundError(f"Model file not found: {model_path}")
logger.info(f"Loading whisper.cpp '{self.model_size}' model...")
# Ensure previous model is released if re-initializing
self.model = None
# Load model with pywhispercpp
# It auto-detects the best backend (Vulkan, CUDA, or CPU)
# Use all available CPU cores for best performance
import multiprocessing
n_threads = multiprocessing.cpu_count()
cpu_count = multiprocessing.cpu_count()
load_start_time = time.time()
loaded_backend = backend
# Attempt to load model with automatic backend selection
# If Vulkan GPU fails (e.g., incompatible Intel GPU), fallback to CPU
try:
self.model = Model(
model_path,
n_threads=n_threads,
suppress_blank=True,
no_speech_thold=0.6,
entropy_thold=2.4,
)
except RuntimeError as model_error:
error_str = str(model_error).lower()
# Check for Vulkan 16-bit storage incompatibility
if (
"16-bit storage" in error_str
or "unsupported device" in error_str
or "incompatible driver" in error_str
):
logger.warning(
f"Vulkan GPU initialization failed: {model_error}. "
"Falling back to CPU backend."
)
_show_notification(
"Vocalinux: GPU Fallback",
"Your GPU doesn't support whisper.cpp Vulkan.\n"
"Switched to CPU mode - still fast!",
"dialog-information",
)
# Force CPU backend by disabling GPU backends
os.environ["GGML_VULKAN"] = "0"
os.environ["GGML_CUDA"] = "0"
# Retry with CPU-only backend
self.model = Model(
model_path,
n_threads=n_threads,
suppress_blank=True,
no_speech_thold=0.6,
entropy_thold=2.4,
)
loaded_backend = ComputeBackend.CPU
backend_info = "CPU (fallback from incompatible Vulkan GPU)"
logger.info("Successfully loaded model with CPU backend")
else:
raise
load_duration = time.time() - load_start_time
logger.info(
f"whisper.cpp configured with n_threads={n_threads} (detected {cpu_count} CPUs)"
)
logger.info(
f"whisper.cpp model loaded in {load_duration:.2f}s ({loaded_backend} backend)"
)
self._model_initialized = True
logger.info("whisper.cpp engine initialized successfully.")
self._load_whispercpp_model(model_path)
except ImportError as e:
logger.error(f"Failed to import pywhispercpp: {e}")
@@ -927,11 +827,127 @@ class SpeechRecognitionManager:
logger.error("Please install with: pip install pywhispercpp")
self.state = RecognitionState.ERROR
raise
except Exception as e:
except (FileNotFoundError, RuntimeError, OSError) as e:
logger.error(f"Failed to initialize whisper.cpp engine: {e}", exc_info=True)
self.state = RecognitionState.ERROR
raise
def _load_whispercpp_model(self, model_path: str):
"""Load the whisper.cpp model file and configure the compute backend.
Detects the optimal backend (Vulkan → CUDA → CPU), loads the model,
and falls back to CPU if the GPU backend is incompatible.
Args:
model_path: Filesystem path to the GGML model file.
"""
import multiprocessing
import time
from pywhispercpp.model import Model
from ..utils.whispercpp_model_info import (
ComputeBackend,
detect_compute_backend,
get_backend_display_name,
)
# Detect and log compute backend
backend, backend_info = detect_compute_backend()
logger.info(f"whisper.cpp backend selection priority: Vulkan -> CUDA -> CPU")
logger.info(
f"whisper.cpp using {get_backend_display_name(backend)} backend: {backend_info}"
)
# Log hardware summary
import psutil
total_ram_gb = psutil.virtual_memory().total // (1024**3)
logger.info(f"whisper.cpp hardware: {backend} | {backend_info} | RAM: {total_ram_gb}GB")
# Validate model file exists and get size
if os.path.exists(model_path):
model_size_mb = os.path.getsize(model_path) / (1024 * 1024)
logger.info(f"whisper.cpp model file: {model_path} ({model_size_mb:.1f} MB)")
else:
raise FileNotFoundError(f"Model file not found: {model_path}")
logger.info(f"Loading whisper.cpp '{self.model_size}' model...")
self.model = None # Release previous model if re-initializing
n_threads = multiprocessing.cpu_count()
load_start_time = time.time()
loaded_backend = backend
# Attempt to load model; fall back to CPU if GPU backend is incompatible
try:
self.model = Model(
model_path,
n_threads=n_threads,
suppress_blank=True,
no_speech_thold=0.6,
entropy_thold=2.4,
)
except RuntimeError as model_error:
loaded_backend = self._handle_gpu_fallback(
model_error, model_path, n_threads, ComputeBackend.CPU
)
load_duration = time.time() - load_start_time
logger.info(
f"whisper.cpp configured with n_threads={n_threads} "
f"(detected {multiprocessing.cpu_count()} CPUs)"
)
logger.info(f"whisper.cpp model loaded in {load_duration:.2f}s ({loaded_backend} backend)")
self._model_initialized = True
logger.info("whisper.cpp engine initialized successfully.")
def _handle_gpu_fallback(self, error, model_path: str, n_threads: int, cpu_backend):
"""Handle GPU backend failure by falling back to CPU.
Args:
error: The RuntimeError from model loading.
model_path: Path to the GGML model file.
n_threads: Number of CPU threads for the model.
cpu_backend: The CPU ComputeBackend enum value.
Returns:
The backend that was actually used.
Raises:
RuntimeError: If the error is not a known GPU incompatibility.
"""
from pywhispercpp.model import Model
error_str = str(error).lower()
gpu_incompatible = (
"16-bit storage" in error_str
or "unsupported device" in error_str
or "incompatible driver" in error_str
)
if not gpu_incompatible:
raise error
logger.warning(f"Vulkan GPU initialization failed: {error}. Falling back to CPU backend.")
_show_notification(
"Vocalinux: GPU Fallback",
"Your GPU doesn't support whisper.cpp Vulkan.\n" "Switched to CPU mode - still fast!",
"dialog-information",
)
# Force CPU backend by disabling GPU backends
os.environ["GGML_VULKAN"] = "0"
os.environ["GGML_CUDA"] = "0"
self.model = Model(
model_path,
n_threads=n_threads,
suppress_blank=True,
no_speech_thold=0.6,
entropy_thold=2.4,
)
logger.info("Successfully loaded model with CPU backend")
return cpu_backend
def _transcribe_with_whispercpp(self, audio_buffer: list[bytes]) -> str:
"""
Transcribe audio buffer using whisper.cpp.
@@ -1111,7 +1127,7 @@ class SpeechRecognitionManager:
if os.path.exists(temp_file):
os.remove(temp_file)
raise RuntimeError(f"Failed to download whisper.cpp model: {e}") from e
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
logger.error(f"An error occurred during whisper.cpp model download: {e}")
if os.path.exists(temp_file):
os.remove(temp_file)
@@ -1274,7 +1290,7 @@ class SpeechRecognitionManager:
if os.path.exists(zip_path):
os.remove(zip_path)
raise RuntimeError("Downloaded VOSK model file is corrupted.")
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
logger.error(f"An error occurred during VOSK model download/extraction: {e}")
# Clean up potentially corrupted extraction
if os.path.exists(zip_path):
@@ -1390,7 +1406,7 @@ class SpeechRecognitionManager:
if os.path.exists(temp_file):
os.remove(temp_file)
raise RuntimeError(f"Failed to download Whisper model: {e}") from e
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
logger.error(f"An error occurred during Whisper model download: {e}")
if os.path.exists(temp_file):
os.remove(temp_file)
+27 -48
View File
@@ -5,7 +5,7 @@ This module handles special voice commands like "delete that", "undo", etc.
"""
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from ..text_injection.text_injector import TextInjector
@@ -21,6 +21,20 @@ class ActionHandler:
and performs the appropriate actions like deleting text, undoing, etc.
"""
# Keyboard shortcut mappings for simple forwarding actions.
# Actions not listed here have custom handlers defined as methods.
_SHORTCUT_ACTIONS: dict[str, str] = {
"undo": "ctrl+z",
"redo": "ctrl+y",
"select_all": "ctrl+a",
"select_line": "Home+shift+End",
"select_word": "ctrl+shift+Right",
"select_paragraph": "ctrl+shift+Down",
"cut": "ctrl+x",
"copy": "ctrl+c",
"paste": "ctrl+v",
}
def __init__(self, text_injector: "TextInjector"):
"""
Initialize the action handler.
@@ -31,19 +45,12 @@ class ActionHandler:
self.text_injector = text_injector
self.last_injected_text = ""
# Map actions to handler methods
self.action_handlers = {
# Build action dispatch table: custom handlers + shortcut-based actions
self.action_handlers: dict[str, Callable[[], bool]] = {
"delete_last": self._handle_delete_last,
"undo": self._handle_undo,
"redo": self._handle_redo,
"select_all": self._handle_select_all,
"select_line": self._handle_select_line,
"select_word": self._handle_select_word,
"select_paragraph": self._handle_select_paragraph,
"cut": self._handle_cut,
"copy": self._handle_copy,
"paste": self._handle_paste,
}
for action, shortcut in self._SHORTCUT_ACTIONS.items():
self.action_handlers[action] = self._make_shortcut_handler(shortcut)
def handle_action(self, action: str) -> bool:
"""
@@ -77,6 +84,14 @@ class ActionHandler:
"""
self.last_injected_text = text
def _make_shortcut_handler(self, shortcut: str) -> Callable[[], bool]:
"""Create a handler that sends a keyboard shortcut via the text injector."""
def handler() -> bool:
return self.text_injector._inject_keyboard_shortcut(shortcut)
return handler
def _handle_delete_last(self) -> bool:
"""Handle 'delete that' command by sending backspace keys."""
if not self.last_injected_text:
@@ -92,39 +107,3 @@ class ActionHandler:
self.last_injected_text = ""
return success
def _handle_undo(self) -> bool:
"""Handle 'undo' command by sending Ctrl+Z."""
return self.text_injector._inject_keyboard_shortcut("ctrl+z")
def _handle_redo(self) -> bool:
"""Handle 'redo' command by sending Ctrl+Y."""
return self.text_injector._inject_keyboard_shortcut("ctrl+y")
def _handle_select_all(self) -> bool:
"""Handle 'select all' command by sending Ctrl+A."""
return self.text_injector._inject_keyboard_shortcut("ctrl+a")
def _handle_select_line(self) -> bool:
"""Handle 'select line' command by sending Home+Shift+End."""
return self.text_injector._inject_keyboard_shortcut("Home+shift+End")
def _handle_select_word(self) -> bool:
"""Handle 'select word' command by sending Ctrl+Shift+Right."""
return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Right")
def _handle_select_paragraph(self) -> bool:
"""Handle 'select paragraph' command by sending Ctrl+Shift+Down."""
return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Down")
def _handle_cut(self) -> bool:
"""Handle 'cut' command by sending Ctrl+X."""
return self.text_injector._inject_keyboard_shortcut("ctrl+x")
def _handle_copy(self) -> bool:
"""Handle 'copy' command by sending Ctrl+C."""
return self.text_injector._inject_keyboard_shortcut("ctrl+c")
def _handle_paste(self) -> bool:
"""Handle 'paste' command by sending Ctrl+V."""
return self.text_injector._inject_keyboard_shortcut("ctrl+v")
+68 -8
View File
@@ -4,10 +4,11 @@ Configuration manager for Vocalinux.
This module handles loading, saving, and accessing user preferences.
"""
import copy
import json
import logging
import os
from typing import Any
from typing import Any, Optional
logger = logging.getLogger(__name__)
@@ -67,10 +68,12 @@ class ConfigManager:
preferences for the application.
"""
# Valid configuration sections — prevents accidental typos from silently
# creating new top-level config keys.
_VALID_SECTIONS = frozenset(DEFAULT_CONFIG.keys())
def __init__(self):
"""Initialize the configuration manager."""
import copy
self.config = copy.deepcopy(DEFAULT_CONFIG)
self._ensure_config_dir()
self.load_config()
@@ -106,7 +109,7 @@ class ConfigManager:
self._migrate_shortcuts_config()
except Exception as e:
except (json.JSONDecodeError, OSError) as e:
logger.error(f"Failed to load config: {e}")
def _check_needs_migration(self, user_config: dict) -> bool:
@@ -162,12 +165,22 @@ class ConfigManager:
logger.info(f"Saved configuration to {CONFIG_FILE}")
return True
except Exception as e:
except (OSError, TypeError) as e:
logger.error(f"Failed to save config: {e}")
return False
def save_settings(self):
"""Save the current configuration to the config file."""
"""Save the current configuration to the config file.
.. deprecated:: Use :meth:`save_config` instead.
"""
import warnings
warnings.warn(
"save_settings() is deprecated, use save_config() instead",
DeprecationWarning,
stacklevel=2,
)
return self.save_config()
def get(self, section: str, key: str, default: Any = None) -> Any:
@@ -187,12 +200,54 @@ class ConfigManager:
except KeyError:
return default
# -- Typed accessors -------------------------------------------------------
# These provide compile-time type safety for commonly accessed config values,
# avoiding the need for callers to cast the Any return from get().
def get_str(self, section: str, key: str, default: str = "") -> str:
"""Get a configuration value as a string."""
value = self.get(section, key, default)
return str(value) if value is not None else default
def get_bool(self, section: str, key: str, default: bool = False) -> bool:
"""Get a configuration value as a boolean."""
value = self.get(section, key, default)
return bool(value)
def get_int(self, section: str, key: str, default: int = 0) -> int:
"""Get a configuration value as an integer."""
value = self.get(section, key, default)
try:
return int(value)
except (TypeError, ValueError):
return default
def get_float(self, section: str, key: str, default: float = 0.0) -> float:
"""Get a configuration value as a float."""
value = self.get(section, key, default)
try:
return float(value)
except (TypeError, ValueError):
return default
def get_optional_int(
self, section: str, key: str, default: Optional[int] = None
) -> Optional[int]:
"""Get a configuration value as an optional integer (allows None)."""
value = self.get(section, key, default)
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return default
def set(self, section: str, key: str, value: Any) -> bool:
"""
Set a configuration value.
Args:
section: The configuration section
section: The configuration section (must be a known section)
key: The configuration key within the section
value: The value to set
@@ -201,12 +256,17 @@ class ConfigManager:
"""
try:
if section not in self.config:
if section not in self._VALID_SECTIONS:
logger.warning(
f"Unknown config section '{section}' — valid sections: "
f"{sorted(self._VALID_SECTIONS)}"
)
self.config[section] = {}
self.config[section][key] = value
return True
except Exception as e:
except (KeyError, TypeError) as e:
logger.error(f"Failed to set config value: {e}")
return False
+6 -4
View File
@@ -1190,7 +1190,7 @@ class SettingsDialog(Gtk.Dialog):
self.shortcut_mode_combo.append(mode_id, display_name)
# Load current mode from config
current_mode = self.config_manager.get("shortcuts", "mode", "toggle")
current_mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
if not self.shortcut_mode_combo.set_active_id(current_mode):
self.shortcut_mode_combo.set_active_id("toggle")
@@ -1217,7 +1217,9 @@ class SettingsDialog(Gtk.Dialog):
self.shortcut_combo.append(shortcut_id, display_name)
# Load current shortcut from config
current_shortcut = self.config_manager.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
current_shortcut = self.config_manager.get_str(
"shortcuts", "toggle_recognition", "ctrl+ctrl"
)
if not self.shortcut_combo.set_active_id(current_shortcut):
self.shortcut_combo.set_active_id("ctrl+ctrl")
@@ -1321,7 +1323,7 @@ class SettingsDialog(Gtk.Dialog):
# Ignore separator entries (used for grouped display)
if shortcut_id.startswith("__separator_"):
# Revert to the previously saved shortcut
current = self.config_manager.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
current = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
self.shortcut_combo.set_active_id(current)
return
@@ -2207,7 +2209,7 @@ For now, the engine has been reverted to VOSK."""
label += " (default)"
self.audio_device_combo.append(str(device_index), label)
saved_device = self.config_manager.get("audio", "device_index", None)
saved_device = self.config_manager.get_optional_int("audio", "device_index", None)
if saved_device is None:
self.audio_device_combo.set_active_id("-1")
+9 -14
View File
@@ -29,22 +29,17 @@ from gi.repository import GdkPixbuf, Gio, GLib, GObject, Gtk
# Import local modules - Use protocols to avoid circular imports
from ..common_types import RecognitionState, SpeechRecognitionManagerProtocol, TextInjectorProtocol
from ..suspend_handler import SuspendHandler # noqa: E402
# Import necessary components
from .config_manager import ConfigManager # noqa: E402
from .keyboard_shortcuts import KeyboardShortcutManager # noqa: E402
from .settings_dialog import SettingsDialog # noqa: E402
from ..suspend_handler import SuspendHandler
from ..utils.resource_manager import ResourceManager
from .config_manager import ConfigManager
from .keyboard_shortcuts import KeyboardShortcutManager
from .settings_dialog import SettingsDialog
logger = logging.getLogger(__name__)
# Define constants
APP_ID = "vocalinux"
# Import the centralized resource manager
from ..utils.resource_manager import ResourceManager # noqa: E402
# Initialize resource manager
_resource_manager = ResourceManager()
ICON_DIR = _resource_manager.icons_dir
@@ -86,8 +81,8 @@ class TrayIndicator:
self._syncing_autostart_menu = False
# Get configured shortcut and mode from config
shortcut = self.config_manager.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
mode = self.config_manager.get("shortcuts", "mode", "toggle")
shortcut = self.config_manager.get_str("shortcuts", "toggle_recognition", "ctrl+ctrl")
mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
# Initialize keyboard shortcut manager with configured shortcut and mode
self.shortcut_manager = KeyboardShortcutManager(shortcut=shortcut, mode=mode)
@@ -138,7 +133,7 @@ class TrayIndicator:
self.shortcut_manager.register_release_callback(None)
# Get configured mode from config
mode = self.config_manager.get("shortcuts", "mode", "toggle")
mode = self.config_manager.get_str("shortcuts", "mode", "toggle")
logger.info(f"Setting up keyboard shortcuts with mode: {mode}")
if mode == "toggle":
@@ -294,7 +289,7 @@ class TrayIndicator:
from . import autostart_manager
autostart_enabled = autostart_manager.is_autostart_enabled()
config_enabled = self.config_manager.get("general", "autostart", False)
config_enabled = self.config_manager.get_bool("general", "autostart", False)
if config_enabled != autostart_enabled:
self.config_manager.set("general", "autostart", autostart_enabled)
self.config_manager.save_settings()