Skip to content

overlay

overlay

The floating dictation indicator β€” proof that the key was heard.

A menu-bar glyph changing from πŸŽ™ to πŸ”΄ is technically feedback, but it is 20 pixels away from where you are looking and it does not move. When you hold a key and speak into a machine that gives no answer, the question is never "which icon is showing" β€” it is "is this thing listening to me right now?". So the indicator sits above the Dock, in the middle of the screen, and its bars move with your voice: it answers the real question, because a bar that responds to sound proves the microphone is genuinely open, not merely that a code path was entered.

Two constraints shape everything here:

  • It must never steal focus. Dictation ends by sending Cmd+V to the frontmost app; a panel that activates would make itself frontmost and the text would land in the void. Hence a non-activating NSPanel that ignores mouse events entirely and is ordered front "regardless".
  • It must never touch AppKit off the main thread. Levels and state changes arrive on the audio and key-tap threads. Rather than marshal each one, those threads only assign to plain attributes; a main-thread timer reads them and does all the drawing. One direction, no locks, no dispatch.

The geometry β€” how tall each bar is for a given level β€” is pure and tested. Only :class:DictationOverlay touches a window server.

Classes

DictationOverlay

DictationOverlay()

The live panel. Import-safe: AppKit is only touched in start().

Call :meth:set_state and :meth:set_level from any thread.

Source code in src/diapason/desktop/overlay.py
def __init__(self) -> None:
    # Written from the tap/audio threads, read from the main thread. Plain
    # attribute assignment is atomic under the GIL, and a frame drawn from
    # a half-updated pair is indistinguishable from the frame before it.
    self.state = "idle"
    self.level = 0.0
    # Spectral shape, written from the audio thread like level. A tuple so
    # a frame can never read a half-rebuilt list.
    self.bands: tuple = ()
    self._phase = 0.0
    self._alpha = 0.0
    self._hide_at = 0.0
    self._window = None
    self._view = None
    self._timer = None
    # True once the WebGL entity is up; False means the drawn-bar fallback.
    self._web = False
    self._bridge_at = 0.0
    self._pushed_state = ""
Methods:
set_bands
set_bands(bands) -> None

Per-band energies, 0–1, low frequencies first.

Source code in src/diapason/desktop/overlay.py
def set_bands(self, bands) -> None:
    """Per-band energies, 0–1, low frequencies first."""
    self.bands = tuple(bands)
on_status
on_status(message: str) -> None

Adapter for DictationService(on_status=…).

Source code in src/diapason/desktop/overlay.py
def on_status(self, message: str) -> None:
    """Adapter for ``DictationService(on_status=…)``."""
    self.set_state(state_for_status(message))
start
start() -> bool

Create the panel and begin the animation timer. False if no GUI.

Source code in src/diapason/desktop/overlay.py
def start(self) -> bool:  # pragma: no cover - needs a window server
    """Create the panel and begin the animation timer. False if no GUI."""
    try:
        self._build()
        self._start_timer()
        return True
    except Exception:  # noqa: BLE001 - headless or no window server
        logger.debug("overlay unavailable", exc_info=True)
        self._window = None
        return False

Functions:

overlay_page

overlay_page() -> Path

Where the bundled entity page lives inside the package.

Source code in src/diapason/desktop/overlay.py
def overlay_page() -> Path:
    """Where the bundled entity page lives inside the package."""
    return Path(__file__).with_name("overlay_page.html")

normalized_level

normalized_level(level: float) -> float

Map an RMS level (0–100 scale) onto 0–1 with a usable curve.

Source code in src/diapason/desktop/overlay.py
def normalized_level(level: float) -> float:
    """Map an RMS level (0–100 scale) onto 0–1 with a usable curve."""
    if level <= 0.0:
        return 0.0
    return min(1.0, math.sqrt(level / FULL_SCALE))

bar_heights

bar_heights(
    level: float,
    *,
    state: str = "recording",
    phase: float = 0.0,
    bars: int = BARS,
) -> List[float]

Height of each bar, 0–1, for a level and animation phase.

phase advances one full cycle per second; it is what keeps the bars alive while the level is steady. Unknown states render as idle rather than raising β€” an indicator is never worth crashing dictation over.

Source code in src/diapason/desktop/overlay.py
def bar_heights(
    level: float, *, state: str = "recording", phase: float = 0.0, bars: int = BARS
) -> List[float]:
    """Height of each bar, 0–1, for a level and animation phase.

    ``phase`` advances one full cycle per second; it is what keeps the bars
    alive while the level is steady. Unknown states render as idle rather
    than raising β€” an indicator is never worth crashing dictation over.
    """
    if state == "transcribing":
        # No microphone is open, so there is no level to show. A travelling
        # wave says "working" without pretending to be a meter.
        return [
            0.22 + 0.5 * (0.5 + 0.5 * math.sin(phase * 2 * math.pi * 1.4 - i * 0.9))
            for i in range(bars)
        ]
    if state != "recording":
        return [MIN_HEIGHT] * bars

    norm = normalized_level(level)
    out: List[float] = []
    for i in range(bars):
        shape = SHAPE[i % len(SHAPE)]
        # Per-bar phase offset so they ripple instead of pumping in unison.
        wobble = 0.78 + 0.22 * math.sin(phase * 2 * math.pi + i * 2.1)
        height = MIN_HEIGHT + (1.0 - MIN_HEIGHT) * norm * shape * wobble
        out.append(max(MIN_HEIGHT, min(1.0, height)))
    return out

state_for_status

state_for_status(message: str) -> str

Map a DictationService status line onto an overlay state.

The service speaks in human sentences ("captured 2.3s (level 1.4)"); the overlay has four faces. Keeping the mapping here means the status strings stay free to change wording without breaking the UI.

Source code in src/diapason/desktop/overlay.py
def state_for_status(message: str) -> str:
    """Map a DictationService status line onto an overlay state.

    The service speaks in human sentences ("captured 2.3s (level 1.4)"); the
    overlay has four faces. Keeping the mapping here means the status strings
    stay free to change wording without breaking the UI.
    """
    if message.startswith("recording"):
        return "recording"
    if message.startswith(("transcribing", "captured", "pasting")):
        return "transcribing"
    if message.startswith("pasted"):
        return "done"
    if message.startswith(("cancelled", "ERROR", "no audio", "empty", "skipped")):
        return "hide"
    return "idle"