Add microphone picker and device management features

- Implement `list_input_devices` to retrieve and filter input-capable audio devices.
- Create `MicPickerDialog` for selecting and saving microphone settings.
- Add `set_env_value` function to update .env file with selected microphone index.
- Update UI components to integrate microphone selection functionality.
- Enhance tests for microphone device handling and environment variable management.
This commit is contained in:
2026-09-24 23:31:18 -06:00
parent ad9925da02
commit 54030a7b31
8 changed files with 283 additions and 245 deletions
+33 -12
View File
@@ -11,13 +11,14 @@ from __future__ import annotations
import sys
from PySide6.QtCore import QThread
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication, QMessageBox
from .. import config, updater
from ..controller import PetController
from ..hotkey import GlobalHotkey
from ..state import PetState
from .history_window import HistoryWindow
from .mic_picker import MicPickerDialog
from .pet_window import PetWindow
from .tray import PetTray
from .wake_tuner import WakeTunerWindow
@@ -57,6 +58,19 @@ def run() -> int:
window.publish_monitors()
window.copied.connect(lambda text: _log(f"Copied to clipboard: {text[:60]}"))
# The updater has already moved the checkout by the time this fires; all
# that's left is to let Qt tear down cleanly (so the mic and the tray
# icon are released) and then exec the new code. Doing the exec after
# app.exec() returns, rather than from the controller thread, is what
# guarantees the audio device is free before the new process opens it.
# Defined early so the mic picker (below) can trigger the same restart
# a self-update or self-restart does.
pending_restart = {"tag": None}
def _handle_restart(tag: str) -> None:
pending_restart["tag"] = tag
app.quit()
history_window = HistoryWindow(controller.history)
tuner_window = WakeTunerWindow(
get_threshold=controller.wake_threshold,
@@ -65,6 +79,23 @@ def run() -> int:
on_reset=controller.reset_wake_stats,
)
def _save_mic_device(value) -> None:
config.set_env_value("MIC_DEVICE", str(value) if value is not None else "")
_log(f"Microphone set to device #{value}." if value is not None else
"Microphone set to system default.")
answer = QMessageBox.question(
None, "Restart Bolt?",
"Restart now so Bolt starts listening on the new microphone?",
QMessageBox.Yes | QMessageBox.No,
)
if answer == QMessageBox.Yes:
_handle_restart("microphone change")
mic_picker = MicPickerDialog(
get_current=lambda: config.MIC_DEVICE,
on_save=_save_mic_device,
)
def _set_nap(napping: bool) -> None:
# Clicking the tray item pins the state; the schedule takes over again
# only after a restart or a `petctl nap off`.
@@ -82,6 +113,7 @@ def run() -> int:
on_set_nap=_set_nap,
on_show_history=history_window.show_refreshed,
on_show_wake_tuner=tuner_window.show_refreshed,
on_show_mic_picker=mic_picker.show_refreshed,
on_reset_voice=controller.reset_voice,
)
@@ -104,17 +136,6 @@ def run() -> int:
elif hotkey.running:
_log(f"Push-to-talk: {config.PUSH_TO_TALK_HOTKEY}")
# The updater has already moved the checkout by the time this fires; all
# that's left is to let Qt tear down cleanly (so the mic and the tray
# icon are released) and then exec the new code. Doing the exec after
# app.exec() returns, rather than from the controller thread, is what
# guarantees the audio device is free before the new process opens it.
pending_restart = {"tag": None}
def _handle_restart(tag: str) -> None:
pending_restart["tag"] = tag
app.quit()
controller.restart_requested.connect(_handle_restart)
def _shutdown() -> None: