Skip to content

menu_bar

menu_bar

Menu-bar presence for background dictation.

A service with no terminal has no face: you cannot tell whether it is listening, whether it heard you, or what it just wrote. The status item is the smallest thing that fixes that — an icon that changes while recording, the last transcript one click away, and a way to quit without hunting for launchctl.

Design notes:

  • The menu model (title, item labels, what each does) is pure and tested; only run() touches rumps/AppKit. A menu bar cannot be asserted on in CI, so everything that can be decided without a screen, is.
  • Icons are text, not image assets: a bundled .icns would have to be regenerated and signed, and an emoji reads correctly in both light and dark menu bars at every size.

Classes

MenuItem dataclass

MenuItem(
    label: str,
    action: Optional[str] = None,
    enabled: bool = True,
)

One row of the status menu. action is None for a separator.

DictationMenuBar

DictationMenuBar(
    *,
    hotkey: str = "control",
    on_quit: Callable[[], None] | None = None,
)

Live status item. Import-safe: rumps is only touched in run().

Source code in src/diapason/desktop/menu_bar.py
def __init__(
    self, *, hotkey: str = "control", on_quit: Callable[[], None] | None = None
):
    self.hotkey = hotkey
    self._on_quit = on_quit
    self._app = None
    self._last_text = ""
Methods:
set_state
set_state(state: str) -> None

Called from the dictation status callback; safe if no UI is up.

Source code in src/diapason/desktop/menu_bar.py
def set_state(self, state: str) -> None:
    """Called from the dictation status callback; safe if no UI is up."""
    if self._app is None:
        return
    try:
        self._app.title = title_for(state)
    except Exception:  # noqa: BLE001 - a title update must never propagate
        logger.debug("menu title update failed", exc_info=True)
run
run() -> None

Start the status item. Blocks on the AppKit run loop.

Source code in src/diapason/desktop/menu_bar.py
def run(self) -> None:  # pragma: no cover - needs a window server
    """Start the status item. Blocks on the AppKit run loop."""
    import rumps  # type: ignore

    menubar = self

    class _App(rumps.App):
        @rumps.clicked("Copy last transcript")
        def _copy(self, _sender):
            copy_to_clipboard(menubar._last_text)

        @rumps.clicked("Open history…")
        def _history(self, _sender):
            import subprocess

            from diapason.desktop.dictation_history import default_history_path

            subprocess.run(
                ["open", "-t", str(default_history_path())],
                capture_output=True,
                check=False,
            )

        @rumps.clicked("Quit dictation")
        def _quit(self, _sender):
            if menubar._on_quit:
                menubar._on_quit()
            rumps.quit_application()

    self._app = _App(IDLE_TITLE, quit_button=None)
    self._app.menu = [
        f"Hold {self.hotkey.capitalize()} to dictate",
        None,
        "Copy last transcript",
        "Open history…",
        None,
        "Quit dictation",
    ]
    self._app.run()

Functions:

title_for

title_for(state: str) -> str

Map a dictation state to the menu-bar glyph.

Anything unknown falls back to idle rather than raising: a status icon is never worth crashing a running service over.

Source code in src/diapason/desktop/menu_bar.py
def title_for(state: str) -> str:
    """Map a dictation state to the menu-bar glyph.

    Anything unknown falls back to idle rather than raising: a status icon is
    never worth crashing a running service over.
    """
    return {
        "recording": RECORDING_TITLE,
        "transcribing": WORKING_TITLE,
        "pasting": WORKING_TITLE,
    }.get(state, IDLE_TITLE)

build_menu

build_menu(
    *,
    hotkey: str,
    last_text: str = "",
    running: bool = True,
) -> List[MenuItem]

The menu, as data.

The last transcript is shown truncated and is copyable — that is the answer to "it pasted into the wrong window", which is the most common way a dictation goes missing.

Source code in src/diapason/desktop/menu_bar.py
def build_menu(
    *, hotkey: str, last_text: str = "", running: bool = True
) -> List[MenuItem]:
    """The menu, as data.

    The last transcript is shown truncated and is copyable — that is the
    answer to "it pasted into the wrong window", which is the most common way
    a dictation goes missing.
    """
    items: List[MenuItem] = [
        MenuItem(
            f"Hold {hotkey.capitalize()} to dictate"
            if running
            else "Dictation is not running",
            action=None,
            enabled=False,
        ),
        MenuItem("", action=None),
    ]

    if last_text:
        preview = last_text if len(last_text) <= 60 else last_text[:57] + "…"
        items.append(MenuItem(f"Last: {preview}", action=None, enabled=False))
        items.append(MenuItem("Copy last transcript", action="copy_last"))
    else:
        items.append(MenuItem("No dictation yet", action=None, enabled=False))

    items.extend(
        [
            MenuItem("", action=None),
            MenuItem("Open history…", action="open_history"),
            MenuItem("", action=None),
            MenuItem("Quit dictation", action="quit"),
        ]
    )
    return items

copy_to_clipboard

copy_to_clipboard(text: str) -> bool

Put text on the pasteboard without pasting it anywhere.

Source code in src/diapason/desktop/menu_bar.py
def copy_to_clipboard(text: str) -> bool:
    """Put text on the pasteboard without pasting it anywhere."""
    if not text:
        return False
    try:
        from AppKit import NSPasteboard  # type: ignore

        pb = NSPasteboard.generalPasteboard()
        pb.clearContents()
        pb.setString_forType_(text, "public.utf8-plain-text")
        return True
    except Exception:  # noqa: BLE001
        logger.debug("clipboard copy failed", exc_info=True)
        return False