From 0bdee07564d5af68ad401748a59a0d51ecb3f221 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Mon, 27 Jul 2026 23:00:14 +0700 Subject: [PATCH] Stop the KDE shortcut and the built-in listener from doubling a press The listener reads /dev/input and reacts the instant a key goes down. The KDE shortcut answers the same press by launching a whole Python process, which then talks over IPC, so its toggle lands a few hundred milliseconds later. The 400 ms guard caught that echo only when the machine happened to be quick, and otherwise the recording was started and stopped by one press: "No speech detected". Route the two apart. A toggle arriving from outside the process right behind a listener trigger is that echo, and its lateness also proves the KDE shortcut is live, which leaves the listener with nothing to do but double every press. So retire it, remember that in the config, and say so in the tray rather than changing behaviour silently. --- dikte.py | 44 +++++++++++++++++++++++++++++++++++++++----- hotkey.py | 4 +++- i18n.py | 4 ++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/dikte.py b/dikte.py index 2af09a2..0b711c4 100755 --- a/dikte.py +++ b/dikte.py @@ -34,6 +34,11 @@ from worker import Pipeline # noqa: E402 SERVER_NAME = "dikte-" + str(os.getuid()) IDLE, RECORDING, BUSY = "idle", "recording", "busy" +# 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 + class Dikte: def __init__(self, app): @@ -53,11 +58,12 @@ class Dikte: self.pipeline.stage.connect(self.overlay.show_busy) self.pipeline.finished.connect(self._on_finished) self.pipeline.failed.connect(self._on_error) - self.evdev.triggered.connect(self.toggle) + self.evdev.triggered.connect(self._on_evdev) self.evdev.failed.connect(self._on_error) self.elapsed = QElapsedTimer() self.last_toggle = QElapsedTimer() + self.last_evdev = QElapsedTimer() self.ticker = QTimer() self.ticker.setInterval(100) self.ticker.timeout.connect(self._tick) @@ -73,7 +79,7 @@ class Dikte: # 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.toggle_action.triggered.connect(self._toggle) self.menu.addAction(self.toggle_action) self.cancel_action = QAction(t("Cancel recording"), self.menu) @@ -102,7 +108,7 @@ class Dikte: def _tray_clicked(self, reason): if reason == QSystemTrayIcon.ActivationReason.Trigger: - self.toggle() + self._toggle() def _set_icon(self, name): icon = QIcon.fromTheme(name) @@ -129,8 +135,36 @@ class Dikte: # ---- actions --------------------------------------------------------- def toggle(self): - # With both the KDE shortcut and the built-in listener active the key - # arrives twice; swallow the immediate repeat. + """A toggle from outside this process: the KDE shortcut, or the CLI.""" + # 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. + if (self.evdev.running and self.last_evdev.isValid() + and self.last_evdev.elapsed() < ECHO_MS): + self._retire_listener() + return + self._toggle() + + def _on_evdev(self): + self.last_evdev.restart() + 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.last_toggle.isValid() and self.last_toggle.elapsed() < 400: return self.last_toggle.restart() diff --git a/hotkey.py b/hotkey.py index 9f82bc5..61ece98 100644 --- a/hotkey.py +++ b/hotkey.py @@ -122,7 +122,9 @@ class EvdevHotkey(QObject): held = set() try: while not self._stop.is_set(): - ready, _, _ = select.select(fds, [], [], 0.4) + # Short enough that stop() does not stall its caller waiting for + # the read to come back around. + ready, _, _ = select.select(fds, [], [], 0.15) for fd in ready: try: data = os.read(fd, self.EVENT_SIZE * 64) diff --git a/i18n.py b/i18n.py index 1b55013..992d838 100644 --- a/i18n.py +++ b/i18n.py @@ -61,6 +61,10 @@ TR = { "{service} hesapta kredi kalmadığını söylüyor (HTTP 402).", "{service} is rate limiting you (HTTP 429). Try again in a moment.": "{service} hız sınırı uyguluyor (HTTP 429). Birazdan tekrar dene.", + "The KDE shortcut is live now, so the built-in listener has been " + "turned off. It was doubling every key press.": + "KDE kısayolu artık çalışıyor, bu yüzden dahili dinleyici kapatıldı. " + "Her tuşa basışı ikiye katlıyordu.", "No speech detected": "Ses algılanmadı", "No speech detected ({level} dB)": "Ses algılanmadı ({level} dB)", "Discarded a stock phrase: “{text}”": "Kalıp cümle atıldı: “{text}”",