"""Microphone picker — MIC_DEVICE in .env is a device index most people have no way to look up short of running a script. This dialog lists whatever sounddevice can actually see, lets you click one, and writes its index back to .env so it survives a restart. The index, not the name, is what gets saved: the same device is commonly listed once per host API (MME, DirectSound, WASAPI, WDM-KS, ...) under the same or a near-identical name, which makes a bare name an ambiguous pick that sounddevice will flatly refuse to resolve (see audio/mic.list_input_devices). The index is exact. The mic is only ever opened once, at startup (see controller.run()), so a pick made here still needs a restart before Bolt actually uses it — this dialog just makes finding and saving the right device painless, and offers to do the restart for you. """ from __future__ import annotations from typing import Callable, Optional from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, ) from ..audio.mic import list_input_devices _DEFAULT_LABEL = "System default" class MicPickerDialog(QDialog): def __init__( self, get_current: Callable[[], Optional[int]], on_save: Callable[[Optional[int]], None], ): super().__init__() self._get_current = get_current self._on_save = on_save self.setWindowTitle("Bolt — select microphone") self.resize(440, 320) self._list = QListWidget() refresh_button = QPushButton("Refresh") refresh_button.clicked.connect(self.refresh) top = QHBoxLayout() top.addWidget(QLabel("Pick the microphone Bolt should listen on:")) top.addStretch(1) top.addWidget(refresh_button) buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) buttons.accepted.connect(self._save) buttons.rejected.connect(self.reject) layout = QVBoxLayout(self) layout.addLayout(top) layout.addWidget(self._list) layout.addWidget(buttons) def refresh(self) -> None: self._list.clear() default_item = QListWidgetItem(_DEFAULT_LABEL) default_item.setData(Qt.UserRole, None) self._list.addItem(default_item) current = self._get_current() selected = default_item try: devices = list_input_devices() except Exception as exc: self._list.addItem(f"Could not list devices: {exc}") devices = [] for device in devices: item = QListWidgetItem( f"{device['name']} [{device['hostapi']}] (#{device['index']})" ) item.setData(Qt.UserRole, device["index"]) self._list.addItem(item) if current == device["index"]: selected = item self._list.setCurrentItem(selected) def _save(self) -> None: item = self._list.currentItem() value = item.data(Qt.UserRole) if item is not None else None self._on_save(value) self.accept() def show_refreshed(self) -> None: self.refresh() self.show() self.raise_() self.activateWindow()