Files
dikte/dikte.py
T
yusufipk 0206303751 Let a dictation be a command for Claude Code, not only text
Dictation ends the same way every time: the words you said land in the window
you were in. But half of what you say to a machine is not text to place, it is
something to do, and the answer to "what is on my calendar today" is not that
sentence written out.

`claude -p` is that session run without a window: the same skills, the same
connected services, the same account. So a second shortcut records exactly as
the first one does and sends the transcript there instead, and what comes back
is pasted where the transcript would have been. Which is what puts "save that
to my calendar on Thursday at three" inside a text field that has nothing to do
with Claude.

Permissions are the part worth being deliberate about, because nothing here can
answer a prompt: a mode that would have asked denies instead. It runs in `auto`,
which decides for itself with the prompt-injection checks left on, and the other
two modes are one setting away. What Claude was not allowed to touch is reported
rather than swallowed, since a reply that reads perfectly normal is otherwise
the only sign that the job did not happen.

Commands close together stay in one conversation, so "and move that to Thursday"
knows what "that" is; half an hour of silence ends it, as does the tray. The
answer is written to be read where it lands: no headings, no lists, and a
sentence naming what was done when something was done.

Its output is read as it streams rather than waited out, so the corner names the
tool it is on. A calendar lookup took 29 s in testing, which a still indicator
would have made indistinguishable from a hang. The clock and the stop button are
watched from a thread of their own, because the stream blocks between lines and
a model that thinks for a minute sends none: they end the run by killing the
process, which closes the stream and unwinds everything else on its own.

Transcript cleanup is off on this path. Claude reads through "erm" and "hani"
without help, and skipping it saves an API call and a second or two in front of
a screen you are standing at.
2026-07-28 16:57:10 +07:00

704 lines
27 KiB
Python
Executable File

#!/usr/bin/env python3
"""Dikte: press Ctrl+Space, talk, press again to transcribe, clean up and paste.
Usage:
dikte.py run in the background (tray icon)
dikte.py toggle start / stop recording
dikte.py cancel discard the current recording
dikte.py ask start / stop recording a command for Claude Code
dikte.py ask-reset forget the conversation Claude has been following
dikte.py meeting start / end a meeting recording
dikte.py meeting-cancel discard the meeting being recorded
dikte.py settings open the settings window
dikte.py restart reload the running instance
dikte.py quit shut the application down
"""
import os
import sys
# A Wayland client cannot place a window in a screen corner, so the indicator
# is drawn through XWayland.
if os.environ.get("XDG_SESSION_TYPE") == "wayland" and os.environ.get("DISPLAY"):
os.environ.setdefault("QT_QPA_PLATFORM", "xcb")
from PyQt6.QtCore import QTimer, QElapsedTimer # noqa: E402
from PyQt6.QtGui import QAction, QIcon # noqa: E402
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon # noqa: E402
import assistant # noqa: E402
import audio # noqa: E402
import config as cfg # noqa: E402
import hotkey # noqa: E402
import meeting # noqa: E402
from i18n import t # noqa: E402
from meeting import MeetingPipeline # noqa: E402
from overlay import Overlay # noqa: E402
from settings_ui import SettingsWindow # noqa: E402
from worker import Pipeline # noqa: E402
SERVER_NAME = "dikte-" + str(os.getuid())
IDLE, RECORDING, BUSY = "idle", "recording", "busy"
# A meeting runs alongside dictation rather than through it: writing up an hour
# of audio takes minutes, and dictation should not be held hostage to it.
M_IDLE, M_RECORDING, M_WORKING = "idle", "recording", "working"
# The KDE shortcut answers a key press by launching a whole Python process, so
# its toggle lands well after the built-in listener has handled the same press.
# Anything arriving inside this window is that echo, not a second press.
ECHO_MS = 2000
# How long the indicator stays up at the start of a meeting. Long enough to see
# both halves of the waveform move, which is the one check that matters, and
# short enough not to sit in the corner for the rest of the hour.
PEEK_MS = 12000
class Dikte:
def __init__(self, app):
self.app = app
self.conf = cfg.Config()
self.state = IDLE
# Dictation and a command for Claude share the recorder and the state
# machine; which of the two is being recorded is decided when the
# recording starts, and holds until it is finished with.
self.ask_mode = False
self.meeting_state = M_IDLE
self.meeting_base = ""
self.meeting_message = ""
self.settings_window = None
self._quitting = False
self.overlay = Overlay(self.conf["overlay_corner"])
self.recorder = audio.Recorder()
self.pipeline = Pipeline(self.conf)
self.meeting_recorder = audio.MeetingRecorder()
self.meetings = MeetingPipeline(self.conf)
self.evdev = hotkey.EvdevHotkey()
self.recorder.level.connect(self.overlay.push_level)
self.recorder.stopped.connect(self._on_recorded)
self.recorder.failed.connect(self._on_error)
self.pipeline.stage.connect(self.overlay.show_busy)
self.pipeline.finished.connect(self._on_finished)
self.pipeline.failed.connect(self._on_error)
self.pipeline.cancelled.connect(self._on_cancelled)
self.meeting_recorder.levels.connect(self._on_meeting_levels)
self.meeting_recorder.stopped.connect(self._on_meeting_recorded)
self.meeting_recorder.died.connect(self._on_meeting_died)
self.meeting_recorder.failed.connect(self._on_meeting_error)
self.meetings.progress.connect(self._on_meeting_progress)
self.meetings.finished.connect(self._on_meeting_finished)
self.meetings.failed.connect(self._on_meeting_failed)
self.evdev.triggered.connect(self._on_evdev)
self.evdev.failed.connect(self._on_error)
self.elapsed = QElapsedTimer()
self.meeting_elapsed = QElapsedTimer()
self.last_toggle = QElapsedTimer()
self.last_evdev = {}
self.ticker = QTimer()
self.ticker.setInterval(100)
self.ticker.timeout.connect(self._tick)
self.meeting_ticker = QTimer()
self.meeting_ticker.setInterval(500)
self.meeting_ticker.timeout.connect(self._meeting_tick)
self.tray = QSystemTrayIcon()
self._apply_settings()
self.tray.show()
# ---- tray ----------------------------------------------------------
def _build_tray(self):
# Keep menu and actions on self: PyQt does not take ownership when they
# are only passed to addAction(), and garbage collection eats them.
self.menu = QMenu()
self.toggle_action = QAction(t("Start recording"), self.menu)
self.toggle_action.triggered.connect(self._toggle)
self.menu.addAction(self.toggle_action)
self.ask_action = QAction(t("Ask Claude"), self.menu)
self.ask_action.triggered.connect(self._toggle_ask)
self.menu.addAction(self.ask_action)
self.reset_action = QAction(t("Start a new conversation"), self.menu)
self.reset_action.triggered.connect(self.reset_conversation)
self.menu.addAction(self.reset_action)
self.cancel_action = QAction(t("Cancel recording"), self.menu)
self.cancel_action.triggered.connect(self.cancel)
self.cancel_action.setEnabled(False)
self.menu.addAction(self.cancel_action)
self.menu.addSeparator()
self.meeting_action = QAction(t("Record a meeting"), self.menu)
self.meeting_action.triggered.connect(self._toggle_meeting)
self.menu.addAction(self.meeting_action)
self.meeting_cancel_action = QAction(t("Discard the meeting"), self.menu)
self.meeting_cancel_action.triggered.connect(self.cancel_meeting)
self.meeting_cancel_action.setEnabled(False)
self.menu.addAction(self.meeting_cancel_action)
self.menu.addSeparator()
self.settings_action = QAction(t("Settings…"), self.menu)
self.settings_action.triggered.connect(self.open_settings)
self.menu.addAction(self.settings_action)
self.restart_action = QAction(t("Restart"), self.menu)
self.restart_action.triggered.connect(self.restart)
self.menu.addAction(self.restart_action)
self.menu.addSeparator()
self.quit_action = QAction(t("Quit"), self.menu)
self.quit_action.triggered.connect(self.app.quit)
self.menu.addAction(self.quit_action)
self.tray.setContextMenu(self.menu)
self.tray.setToolTip(t("Dikte: ready"))
self.tray.activated.connect(self._tray_clicked)
self._set_icon("audio-input-microphone")
def _tray_clicked(self, reason):
if reason == QSystemTrayIcon.ActivationReason.Trigger:
self._toggle()
def _set_icon(self, name):
icon = QIcon.fromTheme(name)
if icon.isNull():
icon = QIcon.fromTheme("audio-input-microphone")
self.tray.setIcon(icon)
# ---- state ----------------------------------------------------------
def _set_state(self, state):
self.state = state
if state == IDLE:
self.ask_mode = False
self._refresh_tray()
def _set_meeting_state(self, state):
self.meeting_state = state
if state != M_WORKING:
self.meeting_message = ""
self._refresh_tray()
def _refresh_tray(self):
labels = {
IDLE: ("Start recording", "audio-input-microphone", "Dikte: ready"),
RECORDING: ("Stop and transcribe", "media-record", "Dikte: recording"),
BUSY: ("Working…", "view-refresh", "Dikte: working"),
}
label, icon, tip = labels[self.state]
if self.ask_mode:
tip = "Dikte: talking to Claude"
if self.state == RECORDING:
label, tip = "Start recording", "Dikte: recording for Claude"
self.toggle_action.setText(t(label))
# Each of the two owns its own entry, so the one that is not recording
# goes grey rather than offering to end a recording it did not start.
self.toggle_action.setEnabled(
self.state == IDLE or (self.state == RECORDING and not self.ask_mode)
)
self.ask_action.setText(
t("Stop and ask Claude") if self.state == RECORDING and self.ask_mode
else t("Ask Claude")
)
self.ask_action.setEnabled(
self.state == IDLE or (self.state == RECORDING and self.ask_mode)
)
self.reset_action.setEnabled(self.state != BUSY)
# A Claude command is the one job long enough to be worth calling off
# once it is already running.
self.cancel_action.setText(
t("Stop Claude") if self.state == BUSY and self.ask_mode
else t("Cancel recording")
)
self.cancel_action.setEnabled(
self.state == RECORDING or (self.state == BUSY and self.ask_mode)
)
meeting_labels = {
M_IDLE: "Record a meeting",
M_RECORDING: "End the meeting and write it up",
M_WORKING: "Writing the meeting up…",
}
self.meeting_action.setText(t(meeting_labels[self.meeting_state]))
self.meeting_action.setEnabled(self.meeting_state != M_WORKING)
self.meeting_cancel_action.setEnabled(self.meeting_state == M_RECORDING)
# Dictation owns the icon while it is doing something, because it is the
# one you are waiting on; otherwise the meeting gets to speak.
if self.state == IDLE and self.meeting_state != M_IDLE:
if self.meeting_state == M_RECORDING:
icon, tip = "media-record", t("Dikte: in a meeting")
else:
icon = "view-refresh"
tip = self.meeting_message or t("Dikte: writing the meeting up")
self._set_icon(icon)
self.tray.setToolTip(tip)
return
self._set_icon(icon)
self.tray.setToolTip(t(tip))
# ---- actions ---------------------------------------------------------
def toggle(self):
"""A toggle from outside this process: the KDE shortcut, or the CLI."""
self._external("toggle", self._toggle)
def toggle_ask(self):
self._external("ask", self._toggle_ask)
def toggle_meeting(self):
self._external("meeting", self._toggle_meeting)
def _external(self, name, handler):
# The built-in listener sees the key press the instant it happens, so a
# toggle arriving right behind one is the KDE shortcut catching up on
# that same press. Its lateness is also the proof we were waiting for
# that the shortcut is live, which leaves the listener with nothing to
# do but double every press.
timer = self.last_evdev.get(name)
if self.evdev.running and timer is not None and timer.elapsed() < ECHO_MS:
self._retire_listener()
return
handler()
def _on_evdev(self, name):
timer = self.last_evdev.get(name)
if timer is None:
timer = self.last_evdev[name] = QElapsedTimer()
timer.restart()
handlers = {"meeting": self._toggle_meeting, "ask": self._toggle_ask}
handlers.get(name, self._toggle)()
def _retire_listener(self):
self.evdev.stop()
self.conf["evdev_hotkey"] = False
self.conf.save()
self.tray.showMessage(
"Dikte",
t("The KDE shortcut is live now, so the built-in listener has been "
"turned off. It was doubling every key press."),
QSystemTrayIcon.MessageIcon.Information, 8000,
)
def _toggle(self):
# Two /dev/input nodes can carry the same keyboard, and a menu click can
# land on top of a key press; swallow the immediate repeat.
if self._repeated():
return
if self.state == IDLE:
self.start()
elif self.state == RECORDING and not self.ask_mode:
self.stop()
# requests during BUSY, or for the other mode's recording, are ignored
def _toggle_ask(self):
if self._repeated():
return
if self.state == IDLE:
self.start(ask=True)
elif self.state == RECORDING and self.ask_mode:
self.stop()
def _repeated(self):
if self.last_toggle.isValid() and self.last_toggle.elapsed() < 400:
return True
self.last_toggle.restart()
return False
def start(self, ask=False):
if self.state != IDLE:
return
self.ask_mode = ask
self.overlay.show_recording(asking=ask)
self.elapsed.restart()
self.ticker.start()
self._set_state(RECORDING)
self.recorder.start(self.conf["mic_target"], self.conf["max_seconds"])
def start_ask(self):
self.start(ask=True)
def stop(self):
if self.state != RECORDING:
return
self.ticker.stop()
self._set_state(BUSY)
self.overlay.show_busy(t("Transcribing…"))
self.recorder.stop()
def cancel(self):
if self.state == BUSY:
# Only a Claude command can be called off once it is under way, and
# it says so itself when it lets go.
if self.ask_mode:
self.overlay.show_busy(t("Stopping…"))
self.pipeline.cancel()
return
if self.state != RECORDING:
return
self.ticker.stop()
self.recorder.cancel()
self.overlay.dismiss()
self._set_state(IDLE)
def reset_conversation(self):
"""Drop the thread Claude has been following, so the next command starts
a conversation of its own."""
assistant.clear_session()
self.overlay.show_done(t("Claude starts fresh next time."), 2500)
def _tick(self):
seconds = self.elapsed.elapsed() / 1000.0
self.overlay.set_seconds(seconds)
if seconds >= self.conf["max_seconds"]:
self.stop()
# ---- meetings ---------------------------------------------------------
def _toggle_meeting(self):
if self.meeting_state == M_IDLE:
self.start_meeting()
elif self.meeting_state == M_RECORDING:
self.stop_meeting()
def start_meeting(self):
if self.meeting_state != M_IDLE:
return
base = meeting.new_base()
_, wav_path = cfg.meeting_paths(base)
self.meeting_recorder.start(
str(wav_path),
self.conf["meeting_mic_target"] or self.conf["mic_target"],
self.conf["meeting_system_target"],
self.conf["meeting_max_seconds"],
)
if not self.meeting_recorder.active:
return # start() has already said what went wrong
self.meeting_base = base
self.meeting_elapsed.restart()
self.meeting_ticker.start()
self.overlay.show_meeting()
QTimer.singleShot(PEEK_MS, self._conceal_meeting_overlay)
self._set_meeting_state(M_RECORDING)
def stop_meeting(self):
if self.meeting_state != M_RECORDING:
return
self.meeting_ticker.stop()
self._set_meeting_state(M_WORKING)
self.overlay.show_busy(t("Ending the meeting…"))
self.meeting_recorder.stop()
def cancel_meeting(self):
if self.meeting_state != M_RECORDING:
return
self.meeting_ticker.stop()
self.meeting_recorder.cancel()
if self.overlay.state == "meeting":
self.overlay.dismiss()
self._set_meeting_state(M_IDLE)
def _conceal_meeting_overlay(self):
if self.overlay.state == "meeting":
self.overlay.dismiss()
def _on_meeting_levels(self, mine, theirs):
self.overlay.push_levels(mine, theirs)
def _meeting_tick(self):
seconds = self.meeting_elapsed.elapsed() / 1000.0
if self.overlay.state == "meeting":
self.overlay.set_seconds(seconds)
if self.state == IDLE:
self.tray.setToolTip(
t("Dikte: in a meeting ({time})", time=_clock(seconds))
)
if seconds >= self.conf["meeting_max_seconds"]:
self.stop_meeting()
def _on_meeting_recorded(self, path, duration):
entry = meeting.new_entry(self.meeting_base, duration)
try:
cfg.save_meeting(entry)
except OSError as exc:
self._on_meeting_failed(entry["base"], str(exc))
return
# On the way out there is no time to write anything up; the recording is
# on disk and listed, and the Minutes tab can pick it up next time.
if self._quitting:
return
if not self.meetings.run(entry):
self._set_meeting_state(M_IDLE)
self.tray.showMessage(
"Dikte",
t("Recording saved. The previous meeting is still being written "
"up, so start this one from Settings → Minutes when it is done."),
QSystemTrayIcon.MessageIcon.Information, 10000,
)
return
self.overlay.show_done(t("Meeting recorded, writing it up…"), 4000)
def _on_meeting_progress(self, _base, message):
self.meeting_message = message
if self.state == IDLE and self.meeting_state == M_WORKING:
self.tray.setToolTip(message)
def _on_meeting_finished(self, base, title):
self._set_meeting_state(M_IDLE)
doc_path, _ = cfg.meeting_paths(base)
self.overlay.show_done(t("Meeting written up: {title}", title=title), 5000)
self.tray.showMessage(
t("Dikte: the meeting is written up"), f"{title}\n{doc_path}",
QSystemTrayIcon.MessageIcon.Information, 10000,
)
def _on_meeting_failed(self, _base, error):
self._set_meeting_state(M_IDLE)
first_line = error.strip().splitlines()[0]
self.overlay.show_error(t("Meeting failed: {error}", error=first_line))
self.tray.showMessage(
t("Dikte: the meeting could not be written up"),
t("{error}\n\nThe recording has been kept. Settings → Minutes can "
"try again.", error=error),
QSystemTrayIcon.MessageIcon.Warning, 12000,
)
def _on_meeting_error(self, message):
"""The recorder itself could not run."""
self.meeting_ticker.stop()
if self.overlay.state == "meeting":
self.overlay.dismiss()
self._set_meeting_state(M_IDLE)
self._on_error(message)
def _on_meeting_died(self):
if self.meeting_state != M_RECORDING:
return
self.tray.showMessage(
"Dikte",
t("The recording stopped on its own; the sound device may have gone "
"away. Keeping what was captured."),
QSystemTrayIcon.MessageIcon.Warning, 10000,
)
self.stop_meeting()
def _on_recorded(self, wav_path, duration, rms_values):
self.pipeline.run(wav_path, duration, rms_values, ask=self.ask_mode)
def _on_finished(self, _raw, text, warning):
asked = self.ask_mode
if warning:
# The text was still pasted, but something on the way did not run.
# Say so loudly: a rejected key, or a tool Claude was not allowed to
# touch, otherwise looks exactly like a job that worked.
first_line = warning.splitlines()[0]
self.overlay.show_warning(
t("Claude answered, but: {error}", error=first_line) if asked
else t("Pasted raw, cleanup failed: {error}", error=first_line)
)
self.tray.showMessage(
t("Dikte: Claude could not do all of it") if asked
else t("Dikte: cleanup failed"),
f"{warning}\n\n{text}" if asked else warning,
QSystemTrayIcon.MessageIcon.Warning, 10000,
)
else:
preview = text.replace("\n", " ")
preview = preview[:48] + ("…" if len(preview) > 48 else "")
if asked:
# Longer than a dictation's flash: this one is an answer, and
# it is worth being able to read the start of it in the corner.
self.overlay.show_done(t("Claude: {preview}", preview=preview), 6000)
else:
action = t("Pasted") if self.conf["auto_paste"] else t("Copied")
self.overlay.show_done(
t("{action}: {preview}", action=action, preview=preview)
)
self._set_state(IDLE)
def _on_cancelled(self):
self.overlay.show_done(t("Stopped."), 2000)
self._set_state(IDLE)
def _on_error(self, message):
first_line = message.strip().splitlines()[0]
self.overlay.show_error(first_line)
if len(message) > len(first_line):
self.tray.showMessage("Dikte", message, QSystemTrayIcon.MessageIcon.Warning, 8000)
if self.state == RECORDING:
self.ticker.stop()
self._set_state(IDLE)
# ---- settings ---------------------------------------------------------
def open_settings(self):
if self.settings_window is None:
self.settings_window = SettingsWindow(
self.conf, launch_command(), meeting_command(), self.meetings,
ask_command(),
)
self.settings_window.applied.connect(self._apply_settings)
self.settings_window.finished.connect(self._settings_closed)
self.settings_window.show()
self.settings_window.raise_()
self.settings_window.activateWindow()
def _settings_closed(self, *_):
# Don't drop the object while its own signal is still being delivered.
QTimer.singleShot(0, lambda: setattr(self, "settings_window", None))
def _apply_settings(self):
self.overlay.corner = self.conf["overlay_corner"]
self._build_tray()
self._refresh_tray()
if self.conf["evdev_hotkey"]:
self.evdev.start({"toggle": self.conf["shortcut"],
"ask": self.conf["assistant_shortcut"],
"meeting": self.conf["meeting_shortcut"]})
else:
self.evdev.stop()
def restart(self):
"""Replace this process with a fresh one, picking up code and settings."""
if self.settings_window is not None:
self.settings_window.close()
self.shutdown()
QLocalServer.removeServer(SERVER_NAME)
script = os.path.realpath(__file__)
os.execv(sys.executable, [sys.executable, script])
def shutdown(self):
self._quitting = True
self.evdev.stop()
if self.state == RECORDING:
self.recorder.cancel()
# A meeting in progress is closed properly rather than thrown away: the
# WAV ends up valid and listed, ready to be written up after the restart.
if self.meeting_state == M_RECORDING:
self.meeting_ticker.stop()
self.meeting_recorder.stop()
self.overlay.dismiss()
self.tray.hide()
def _clock(seconds):
minutes, secs = divmod(int(seconds), 60)
hours, minutes = divmod(minutes, 60)
return (f"{hours}:{minutes:02d}:{secs:02d}" if hours
else f"{minutes}:{secs:02d}")
def launch_command():
"""The command the KDE shortcut will run."""
return f"{sys.executable} {os.path.realpath(__file__)} toggle"
def meeting_command():
return f"{sys.executable} {os.path.realpath(__file__)} meeting"
def ask_command():
return f"{sys.executable} {os.path.realpath(__file__)} ask"
def send_command(command, timeout=800):
"""Hand a command to the running instance; False when there is none."""
socket = QLocalSocket()
socket.connectToServer(SERVER_NAME)
if not socket.waitForConnected(timeout):
return False
socket.write(command.encode("utf-8"))
socket.flush()
socket.waitForBytesWritten(timeout)
socket.disconnectFromServer()
return True
def main():
args = [a for a in sys.argv[1:] if not a.startswith("-")]
command = args[0] if args else ""
if command and command not in ("toggle", "cancel", "settings", "restart",
"quit", "start", "stop", "ask", "ask-reset",
"meeting", "meeting-cancel"):
print(__doc__)
return 2
app = QApplication(sys.argv)
app.setApplicationName("Dikte")
app.setDesktopFileName("dikte")
app.setQuitOnLastWindowClosed(False)
# No command and an instance already running: bring its settings forward.
if send_command(command or "settings"):
return 0
if command in ("cancel", "quit", "stop", "restart", "meeting-cancel",
"ask-reset"):
return 0
if not QSystemTrayIcon.isSystemTrayAvailable():
print("dikte: no system tray found, running anyway")
dikte = Dikte(app)
server = QLocalServer()
# Qt puts the socket in /tmp, so keep it to this user: commands like
# "quit" should not be reachable by anyone else on the machine.
server.setSocketOptions(QLocalServer.SocketOption.UserAccessOption)
QLocalServer.removeServer(SERVER_NAME)
if not server.listen(SERVER_NAME):
print(f"dikte: could not open the IPC socket: {server.errorString()}")
def on_connection():
conn = server.nextPendingConnection()
if conn is None:
return
def read():
payload = bytes(conn.readAll()).decode("utf-8", "replace").strip()
handler = {
"toggle": dikte.toggle,
"start": dikte.start,
"stop": dikte.stop,
"cancel": dikte.cancel,
"ask": dikte.toggle_ask,
"ask-reset": dikte.reset_conversation,
"meeting": dikte.toggle_meeting,
"meeting-cancel": dikte.cancel_meeting,
"settings": dikte.open_settings,
"restart": dikte.restart,
"quit": app.quit,
}.get(payload)
if handler:
handler()
conn.disconnectFromServer()
conn.readyRead.connect(read)
server.newConnection.connect(on_connection)
app.aboutToQuit.connect(dikte.shutdown)
# No key for the chosen transcription provider means nothing can work yet,
# so the settings window is the only useful thing to open.
if command == "settings" or not dikte.conf.transcribe_target().api_key:
dikte.open_settings()
elif command == "toggle":
QTimer.singleShot(0, dikte.toggle)
elif command == "ask":
QTimer.singleShot(0, dikte.toggle_ask)
elif command == "meeting":
QTimer.singleShot(0, dikte.toggle_meeting)
return app.exec()
if __name__ == "__main__":
sys.exit(main())