mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Merge pull request #34 from yusufipk/pause-resume
Let a recording be held while something else happens
This commit is contained in:
@@ -103,6 +103,7 @@ set next to it.
|
|||||||
| What | How |
|
| What | How |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Start / stop recording | `Ctrl+Space`, or click the tray icon |
|
| Start / stop recording | `Ctrl+Space`, or click the tray icon |
|
||||||
|
| Pause / resume the recording | Tray menu, `dikte pause`, or a key you set |
|
||||||
| Discard the recording | `Ctrl+Alt+Space`, tray menu, or `dikte cancel` |
|
| Discard the recording | `Ctrl+Alt+Space`, tray menu, or `dikte cancel` |
|
||||||
| Speak a command to an agent | Tray menu → *Ask Claude*, or `dikte ask` |
|
| Speak a command to an agent | Tray menu → *Ask Claude*, or `dikte ask` |
|
||||||
| Start / end a meeting | Tray menu → *Record a meeting*, or `dikte meeting` |
|
| Start / end a meeting | Tray menu → *Record a meeting*, or `dikte meeting` |
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ yanındaki kutudan düşünme seviyesini de seçebilirsin.
|
|||||||
| Ne | Nasıl |
|
| Ne | Nasıl |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Kaydı başlat / bitir | `Ctrl+Space`, ya da tepsi simgesine tıkla |
|
| Kaydı başlat / bitir | `Ctrl+Space`, ya da tepsi simgesine tıkla |
|
||||||
|
| Kaydı duraklat / sürdür | Tepsi menüsü, `dikte pause`, ya da atadığın bir tuş |
|
||||||
| Kaydı iptal et | `Ctrl+Alt+Space`, tepsi menüsü, ya da `dikte cancel` |
|
| Kaydı iptal et | `Ctrl+Alt+Space`, tepsi menüsü, ya da `dikte cancel` |
|
||||||
| Ajana sesle komut ver | Tepsi menüsü → *Claude'a sor*, ya da `dikte ask` |
|
| Ajana sesle komut ver | Tepsi menüsü → *Claude'a sor*, ya da `dikte ask` |
|
||||||
| Toplantıyı başlat / bitir | Tepsi menüsü → *Toplantı kaydet*, ya da `dikte meeting` |
|
| Toplantıyı başlat / bitir | Tepsi menüsü → *Toplantı kaydet*, ya da `dikte meeting` |
|
||||||
|
|||||||
@@ -72,12 +72,31 @@ class Recorder(QObject):
|
|||||||
self._rms = []
|
self._rms = []
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
self._stopping = False
|
self._stopping = False
|
||||||
|
self._paused = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
return self._thread is not None and self._thread.is_alive()
|
return self._thread is not None and self._thread.is_alive()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paused(self):
|
||||||
|
return self._paused
|
||||||
|
|
||||||
|
def pause(self, value=True):
|
||||||
|
"""Stop taking sound in without letting go of the microphone.
|
||||||
|
|
||||||
|
The capture program keeps running and keeps handing blocks over; they
|
||||||
|
are dropped as they arrive rather than kept. Stopping it instead would
|
||||||
|
mean asking the sound server for the device again on the way back, and
|
||||||
|
that is the one moment another application can take it: a recording
|
||||||
|
would be lost to the phone call it was paused for.
|
||||||
|
|
||||||
|
What was said while it was paused is gone, which is the point. The two
|
||||||
|
halves meet as one splice, with none of the room in between.
|
||||||
|
"""
|
||||||
|
self._paused = bool(value)
|
||||||
|
|
||||||
def start(self, target="", max_seconds=300):
|
def start(self, target="", max_seconds=300):
|
||||||
if self.active:
|
if self.active:
|
||||||
return
|
return
|
||||||
@@ -102,6 +121,7 @@ class Recorder(QObject):
|
|||||||
self._rms = []
|
self._rms = []
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
self._stopping = False
|
self._stopping = False
|
||||||
|
self._paused = False
|
||||||
self._max_bytes = int(max_seconds * RATE * SAMPLE_WIDTH * CHANNELS)
|
self._max_bytes = int(max_seconds * RATE * SAMPLE_WIDTH * CHANNELS)
|
||||||
self._thread = threading.Thread(target=self._pump, daemon=True)
|
self._thread = threading.Thread(target=self._pump, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
@@ -114,6 +134,11 @@ class Recorder(QObject):
|
|||||||
chunk = stdout.read(CHUNK_BYTES)
|
chunk = stdout.read(CHUNK_BYTES)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
|
if self._paused:
|
||||||
|
# Read and thrown away rather than left in the pipe: a pipe
|
||||||
|
# nobody empties fills up, and the capture program blocks on
|
||||||
|
# a full one instead of waiting quietly for the resume.
|
||||||
|
continue
|
||||||
peak, rms = chunk_levels(chunk)
|
peak, rms = chunk_levels(chunk)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._buffer.extend(chunk)
|
self._buffer.extend(chunk)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ GUI_VERBS = {"", "settings", "toggle", "ask", "meeting"}
|
|||||||
# Asking a process that is not there to stop, cancel or quit is not a failure;
|
# Asking a process that is not there to stop, cancel or quit is not a failure;
|
||||||
# it is already in the state that was asked for.
|
# it is already in the state that was asked for.
|
||||||
IDEMPOTENT_VERBS = {"cancel", "stop", "quit", "restart", "ask-cancel",
|
IDEMPOTENT_VERBS = {"cancel", "stop", "quit", "restart", "ask-cancel",
|
||||||
"ask-reset", "meeting-cancel"}
|
"ask-reset", "meeting-cancel", "pause"}
|
||||||
|
|
||||||
_app = None
|
_app = None
|
||||||
|
|
||||||
@@ -765,7 +765,8 @@ def cmd_status(opts):
|
|||||||
return fail(opts, "the running instance is from before it could answer "
|
return fail(opts, "the running instance is from before it could answer "
|
||||||
"questions; reload it with: dikte restart", 1, running=True)
|
"questions; reload it with: dikte restart", 1, running=True)
|
||||||
lines = [
|
lines = [
|
||||||
f"dictation: {reply.get('dictation', '?')}",
|
f"dictation: {reply.get('dictation', '?')}"
|
||||||
|
+ (" (paused)" if reply.get("paused") else ""),
|
||||||
f"agent: {reply.get('ask', '?')} ({reply.get('agent', '?')})",
|
f"agent: {reply.get('ask', '?')} ({reply.get('agent', '?')})",
|
||||||
f"meeting: {reply.get('meeting', '?')}"
|
f"meeting: {reply.get('meeting', '?')}"
|
||||||
+ (f" {reply['meeting_message']}" if reply.get("meeting_message") else ""),
|
+ (f" {reply['meeting_message']}" if reply.get("meeting_message") else ""),
|
||||||
@@ -879,6 +880,10 @@ def build_parser():
|
|||||||
page.add_argument("--timeout", type=float, default=0)
|
page.add_argument("--timeout", type=float, default=0)
|
||||||
page.set_defaults(func=cmd_toggle)
|
page.set_defaults(func=cmd_toggle)
|
||||||
|
|
||||||
|
# One verb for both halves, the way `toggle` is one verb: a key can only be
|
||||||
|
# pressed, so a key that resumed nothing would need a second key.
|
||||||
|
leaf(subs, "pause", "hold the recording, or take it up again"
|
||||||
|
).set_defaults(func=cmd_plain)
|
||||||
leaf(subs, "cancel", "throw away the recording").set_defaults(func=cmd_cancel)
|
leaf(subs, "cancel", "throw away the recording").set_defaults(func=cmd_cancel)
|
||||||
|
|
||||||
# --- the agent --------------------------------------------------------
|
# --- the agent --------------------------------------------------------
|
||||||
|
|||||||
@@ -435,6 +435,10 @@ DEFAULTS = {
|
|||||||
# trick lands on the toggle, Alt and Option being one key, so discarding
|
# trick lands on the toggle, Alt and Option being one key, so discarding
|
||||||
# gets a letter instead.
|
# gets a letter instead.
|
||||||
"cancel_shortcut": "Ctrl+Option+D" if _MACOS else "Ctrl+Alt+Space",
|
"cancel_shortcut": "Ctrl+Option+D" if _MACOS else "Ctrl+Alt+Space",
|
||||||
|
# Empty -> tray only. Holding a recording is not something a keyboard has a
|
||||||
|
# habit for, and a combination nobody asked for is one taken away from
|
||||||
|
# whatever else was using it.
|
||||||
|
"pause_shortcut": "",
|
||||||
"evdev_hotkey": False,
|
"evdev_hotkey": False,
|
||||||
"overlay_corner": "bottom-left",
|
"overlay_corner": "bottom-left",
|
||||||
"keep_audio": False,
|
"keep_audio": False,
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ class Dikte:
|
|||||||
self.ask_state = IDLE
|
self.ask_state = IDLE
|
||||||
# Which of the two the microphone is currently serving, or None.
|
# Which of the two the microphone is currently serving, or None.
|
||||||
self.recorder_owner = None
|
self.recorder_owner = None
|
||||||
|
# A recording that is running but taking nothing in. Not a state of its
|
||||||
|
# own: everything that can be done to a recording can be done to a
|
||||||
|
# paused one, and a fourth state would have to say so four times over.
|
||||||
|
self.paused = False
|
||||||
|
# Paused time, which is time the recording does not have: the clock on
|
||||||
|
# screen and the limit both go by what was actually captured.
|
||||||
|
self._paused_ms = 0
|
||||||
|
self._paused_at = 0
|
||||||
self.meeting_state = M_IDLE
|
self.meeting_state = M_IDLE
|
||||||
self.meeting_base = ""
|
self.meeting_base = ""
|
||||||
self.meeting_message = ""
|
self.meeting_message = ""
|
||||||
@@ -159,6 +167,12 @@ class Dikte:
|
|||||||
self.toggle_action.triggered.connect(self._toggle)
|
self.toggle_action.triggered.connect(self._toggle)
|
||||||
self.menu.addAction(self.toggle_action)
|
self.menu.addAction(self.toggle_action)
|
||||||
|
|
||||||
|
# Named in _refresh_tray as well, since it says one of two things.
|
||||||
|
self.pause_action = QAction(t("Pause the recording"), self.menu)
|
||||||
|
self.pause_action.triggered.connect(self._toggle_pause)
|
||||||
|
self.pause_action.setEnabled(False)
|
||||||
|
self.menu.addAction(self.pause_action)
|
||||||
|
|
||||||
# Named in _refresh_tray, which is where the chosen provider is known.
|
# Named in _refresh_tray, which is where the chosen provider is known.
|
||||||
self.ask_action = QAction("", self.menu)
|
self.ask_action = QAction("", self.menu)
|
||||||
self.ask_action.triggered.connect(self._toggle_ask)
|
self.ask_action.triggered.connect(self._toggle_ask)
|
||||||
@@ -283,6 +297,9 @@ class Dikte:
|
|||||||
or (self.ask_state == IDLE and not self.recording)
|
or (self.ask_state == IDLE and not self.recording)
|
||||||
)
|
)
|
||||||
self.reset_action.setEnabled(self.ask_state != BUSY)
|
self.reset_action.setEnabled(self.ask_state != BUSY)
|
||||||
|
self.pause_action.setText(t("Resume the recording") if self.paused
|
||||||
|
else t("Pause the recording"))
|
||||||
|
self.pause_action.setEnabled(self.recording)
|
||||||
self.cancel_action.setEnabled(self.recording)
|
self.cancel_action.setEnabled(self.recording)
|
||||||
# A command to the agent is the one job long enough to be worth calling
|
# A command to the agent is the one job long enough to be worth calling
|
||||||
# off once it is already running.
|
# off once it is already running.
|
||||||
@@ -299,6 +316,12 @@ class Dikte:
|
|||||||
else:
|
else:
|
||||||
icon, tip = "view-refresh", "Dikte: talking to Claude"
|
icon, tip = "view-refresh", "Dikte: talking to Claude"
|
||||||
|
|
||||||
|
# Whichever of the two is holding the microphone, a recording dot that
|
||||||
|
# keeps burning while nothing goes in is the icon telling the opposite
|
||||||
|
# of what is happening.
|
||||||
|
if self.paused and self.recording:
|
||||||
|
icon, tip = "media-playback-pause", "Dikte: paused"
|
||||||
|
|
||||||
meeting_labels = {
|
meeting_labels = {
|
||||||
M_IDLE: "Record a meeting",
|
M_IDLE: "Record a meeting",
|
||||||
M_RECORDING: "End the meeting and write it up",
|
M_RECORDING: "End the meeting and write it up",
|
||||||
@@ -334,6 +357,9 @@ class Dikte:
|
|||||||
def toggle_meeting(self):
|
def toggle_meeting(self):
|
||||||
self._external("meeting", self._toggle_meeting)
|
self._external("meeting", self._toggle_meeting)
|
||||||
|
|
||||||
|
def toggle_pause(self):
|
||||||
|
self._external("pause", self._toggle_pause)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self._external("cancel", self._cancel)
|
self._external("cancel", self._cancel)
|
||||||
|
|
||||||
@@ -358,7 +384,7 @@ class Dikte:
|
|||||||
timer = self.last_evdev[name] = QElapsedTimer()
|
timer = self.last_evdev[name] = QElapsedTimer()
|
||||||
timer.restart()
|
timer.restart()
|
||||||
handlers = {"meeting": self._toggle_meeting, "ask": self._toggle_ask,
|
handlers = {"meeting": self._toggle_meeting, "ask": self._toggle_ask,
|
||||||
"cancel": self._cancel}
|
"cancel": self._cancel, "pause": self._toggle_pause}
|
||||||
handlers.get(name, self._toggle)()
|
handlers.get(name, self._toggle)()
|
||||||
|
|
||||||
def _retire_listener(self):
|
def _retire_listener(self):
|
||||||
@@ -393,6 +419,7 @@ class Dikte:
|
|||||||
else:
|
else:
|
||||||
handler = {
|
handler = {
|
||||||
"cancel": self.cancel,
|
"cancel": self.cancel,
|
||||||
|
"pause": self.toggle_pause,
|
||||||
"ask-cancel": self.cancel_ask,
|
"ask-cancel": self.cancel_ask,
|
||||||
"ask-reset": self.reset_conversation,
|
"ask-reset": self.reset_conversation,
|
||||||
"meeting-cancel": self.cancel_meeting,
|
"meeting-cancel": self.cancel_meeting,
|
||||||
@@ -475,6 +502,7 @@ class Dikte:
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"running": True,
|
"running": True,
|
||||||
"dictation": self.state,
|
"dictation": self.state,
|
||||||
|
"paused": self.paused,
|
||||||
"ask": self.ask_state,
|
"ask": self.ask_state,
|
||||||
"meeting": self.meeting_state,
|
"meeting": self.meeting_state,
|
||||||
"meeting_base": self.meetings.running_base,
|
"meeting_base": self.meetings.running_base,
|
||||||
@@ -538,6 +566,7 @@ class Dikte:
|
|||||||
"""One microphone, so one of the two holds it at a time."""
|
"""One microphone, so one of the two holds it at a time."""
|
||||||
self.recorder_owner = owner
|
self.recorder_owner = owner
|
||||||
self._run_id += 1
|
self._run_id += 1
|
||||||
|
self._clear_pause()
|
||||||
self.elapsed.restart()
|
self.elapsed.restart()
|
||||||
self.ticker.start()
|
self.ticker.start()
|
||||||
self.recorder.start(self.conf["mic_target"], self.conf["max_seconds"])
|
self.recorder.start(self.conf["mic_target"], self.conf["max_seconds"])
|
||||||
@@ -546,6 +575,7 @@ class Dikte:
|
|||||||
if self.state != RECORDING:
|
if self.state != RECORDING:
|
||||||
return
|
return
|
||||||
self.ticker.stop()
|
self.ticker.stop()
|
||||||
|
self._clear_pause()
|
||||||
self._set_state(BUSY)
|
self._set_state(BUSY)
|
||||||
self.overlay.show_busy(t("Transcribing…"))
|
self.overlay.show_busy(t("Transcribing…"))
|
||||||
self.recorder.stop()
|
self.recorder.stop()
|
||||||
@@ -554,16 +584,50 @@ class Dikte:
|
|||||||
if self.ask_state != RECORDING:
|
if self.ask_state != RECORDING:
|
||||||
return
|
return
|
||||||
self.ticker.stop()
|
self.ticker.stop()
|
||||||
|
self._clear_pause()
|
||||||
self._set_ask_state(BUSY)
|
self._set_ask_state(BUSY)
|
||||||
self.ask_overlay.show_busy(t("Transcribing…"))
|
self.ask_overlay.show_busy(t("Transcribing…"))
|
||||||
self.recorder.stop()
|
self.recorder.stop()
|
||||||
|
|
||||||
|
def _toggle_pause(self):
|
||||||
|
"""Hold the recording where it is, or take it up again.
|
||||||
|
|
||||||
|
A pause is not a stop: the microphone stays ours and what has been said
|
||||||
|
so far stays in the buffer. What is said while it is held is dropped, so
|
||||||
|
the phone call in the middle of a dictation never reaches the model and
|
||||||
|
the sentence around it is still one sentence.
|
||||||
|
"""
|
||||||
|
if not self.recording or self._repeated():
|
||||||
|
return
|
||||||
|
self.paused = not self.paused
|
||||||
|
if self.paused:
|
||||||
|
self._paused_at = self.elapsed.elapsed()
|
||||||
|
else:
|
||||||
|
self._paused_ms += self.elapsed.elapsed() - self._paused_at
|
||||||
|
self.recorder.pause(self.paused)
|
||||||
|
self._recording_overlay().set_paused(self.paused)
|
||||||
|
self._refresh_tray()
|
||||||
|
|
||||||
|
def _clear_pause(self):
|
||||||
|
"""Every recording starts and ends taking sound in."""
|
||||||
|
self.paused = False
|
||||||
|
self._paused_ms = 0
|
||||||
|
self._paused_at = 0
|
||||||
|
|
||||||
|
def _recorded_seconds(self):
|
||||||
|
"""Wall clock less whatever was held: the length of what will be
|
||||||
|
transcribed, which is what the limit has to be measured against too."""
|
||||||
|
# A held recording is as long now as it was when it was held.
|
||||||
|
now = self._paused_at if self.paused else self.elapsed.elapsed()
|
||||||
|
return max(0.0, (now - self._paused_ms) / 1000.0)
|
||||||
|
|
||||||
def _cancel(self):
|
def _cancel(self):
|
||||||
"""Throw away whichever recording is running."""
|
"""Throw away whichever recording is running."""
|
||||||
if not self.recording:
|
if not self.recording:
|
||||||
return
|
return
|
||||||
asking = self.ask_state == RECORDING
|
asking = self.ask_state == RECORDING
|
||||||
self.ticker.stop()
|
self.ticker.stop()
|
||||||
|
self._clear_pause()
|
||||||
self.recorder.cancel()
|
self.recorder.cancel()
|
||||||
self.recorder_owner = None
|
self.recorder_owner = None
|
||||||
# What goes over the socket is read by a program as often as by a
|
# What goes over the socket is read by a program as often as by a
|
||||||
@@ -603,7 +667,7 @@ class Dikte:
|
|||||||
self._recording_overlay().push_level(level)
|
self._recording_overlay().push_level(level)
|
||||||
|
|
||||||
def _tick(self):
|
def _tick(self):
|
||||||
seconds = self.elapsed.elapsed() / 1000.0
|
seconds = self._recorded_seconds()
|
||||||
self._recording_overlay().set_seconds(seconds)
|
self._recording_overlay().set_seconds(seconds)
|
||||||
if seconds >= self.conf["max_seconds"]:
|
if seconds >= self.conf["max_seconds"]:
|
||||||
(self.stop_ask if self.recorder_owner == ASK else self.stop)()
|
(self.stop_ask if self.recorder_owner == ASK else self.stop)()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from i18n import t
|
|||||||
|
|
||||||
DESKTOP_ID = "dikte-toggle.desktop"
|
DESKTOP_ID = "dikte-toggle.desktop"
|
||||||
CANCEL_DESKTOP_ID = "dikte-cancel.desktop"
|
CANCEL_DESKTOP_ID = "dikte-cancel.desktop"
|
||||||
|
PAUSE_DESKTOP_ID = "dikte-pause.desktop"
|
||||||
MEETING_DESKTOP_ID = "dikte-meeting.desktop"
|
MEETING_DESKTOP_ID = "dikte-meeting.desktop"
|
||||||
ASK_DESKTOP_ID = "dikte-ask.desktop"
|
ASK_DESKTOP_ID = "dikte-ask.desktop"
|
||||||
APPLICATIONS_DIR = pathlib.Path.home() / ".local/share/applications"
|
APPLICATIONS_DIR = pathlib.Path.home() / ".local/share/applications"
|
||||||
@@ -39,7 +40,7 @@ GNOME_BINDING_SCHEMA = "org.gnome.settings-daemon.plugins.media-keys.custom-keyb
|
|||||||
|
|
||||||
Shortcut = collections.namedtuple("Shortcut", "verb desktop_id name setting fallback")
|
Shortcut = collections.namedtuple("Shortcut", "verb desktop_id name setting fallback")
|
||||||
|
|
||||||
# Every global shortcut in one place, because there are four of them and the
|
# Every global shortcut in one place, because there are five of them and the
|
||||||
# command line, the settings window and the installer each used to carry their
|
# command line, the settings window and the installer each used to carry their
|
||||||
# own copy of the list. `fallback` is what to register when the setting is
|
# own copy of the list. `fallback` is what to register when the setting is
|
||||||
# empty: only the toggle has one, since it is the key the application is
|
# empty: only the toggle has one, since it is the key the application is
|
||||||
@@ -47,6 +48,8 @@ Shortcut = collections.namedtuple("Shortcut", "verb desktop_id name setting fall
|
|||||||
SHORTCUTS = {
|
SHORTCUTS = {
|
||||||
"toggle": Shortcut("toggle", DESKTOP_ID, "Dikte: start/stop recording",
|
"toggle": Shortcut("toggle", DESKTOP_ID, "Dikte: start/stop recording",
|
||||||
"shortcut", "Ctrl+Space"),
|
"shortcut", "Ctrl+Space"),
|
||||||
|
"pause": Shortcut("pause", PAUSE_DESKTOP_ID,
|
||||||
|
"Dikte: pause/resume the recording", "pause_shortcut", ""),
|
||||||
"cancel": Shortcut("cancel", CANCEL_DESKTOP_ID, "Dikte: discard the recording",
|
"cancel": Shortcut("cancel", CANCEL_DESKTOP_ID, "Dikte: discard the recording",
|
||||||
"cancel_shortcut", ""),
|
"cancel_shortcut", ""),
|
||||||
"ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask Claude Code",
|
"ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask Claude Code",
|
||||||
|
|||||||
@@ -56,12 +56,15 @@ TR = {
|
|||||||
"Start recording": "Kaydı başlat",
|
"Start recording": "Kaydı başlat",
|
||||||
"Stop and transcribe": "Kaydı bitir ve yaz",
|
"Stop and transcribe": "Kaydı bitir ve yaz",
|
||||||
"Working…": "İşleniyor…",
|
"Working…": "İşleniyor…",
|
||||||
|
"Pause the recording": "Kaydı duraklat",
|
||||||
|
"Resume the recording": "Kayda devam et",
|
||||||
"Discard the recording": "Kaydı iptal et",
|
"Discard the recording": "Kaydı iptal et",
|
||||||
"Settings…": "Ayarlar…",
|
"Settings…": "Ayarlar…",
|
||||||
"Restart": "Yeniden başlat",
|
"Restart": "Yeniden başlat",
|
||||||
"Quit": "Çık",
|
"Quit": "Çık",
|
||||||
"Dikte: ready": "Dikte: hazır",
|
"Dikte: ready": "Dikte: hazır",
|
||||||
"Dikte: recording": "Dikte: kaydediyor",
|
"Dikte: recording": "Dikte: kaydediyor",
|
||||||
|
"Dikte: paused": "Dikte: duraklatıldı",
|
||||||
"Dikte: working": "Dikte: işleniyor",
|
"Dikte: working": "Dikte: işleniyor",
|
||||||
|
|
||||||
# --- overlay / pipeline -------------------------------------------
|
# --- overlay / pipeline -------------------------------------------
|
||||||
@@ -312,7 +315,14 @@ TR = {
|
|||||||
"Global kısayol kurulu değil. Tepsi menüsünden de soru sorulabilir.",
|
"Global kısayol kurulu değil. Tepsi menüsünden de soru sorulabilir.",
|
||||||
"No global shortcut installed. The tray menu discards it too.":
|
"No global shortcut installed. The tray menu discards it too.":
|
||||||
"Global kısayol kurulu değil. Kayıt tepsi menüsünden de iptal edilebilir.",
|
"Global kısayol kurulu değil. Kayıt tepsi menüsünden de iptal edilebilir.",
|
||||||
|
"No global shortcut installed. The tray menu holds it too.":
|
||||||
|
"Global kısayol kurulu değil. Kayıt tepsi menüsünden de duraklatılabilir.",
|
||||||
"Start and stop": "Başlat ve bitir",
|
"Start and stop": "Başlat ve bitir",
|
||||||
|
"Pause and resume": "Duraklat ve devam et",
|
||||||
|
"Holds the recording without ending it. Nothing said while it is paused is "
|
||||||
|
"kept, and the clock stops with it.":
|
||||||
|
"Kaydı bitirmeden duraklatır. Duraklatıldığı sürede konuşulanlar "
|
||||||
|
"kaydedilmez, süre sayacı da onunla birlikte durur.",
|
||||||
"Throws the recording away without transcribing it. Works on a dictation "
|
"Throws the recording away without transcribing it. Works on a dictation "
|
||||||
"and on a command for the agent alike, whichever is running.":
|
"and on a command for the agent alike, whichever is running.":
|
||||||
"Kaydı yazıya dökmeden atar. Hangisi çalışıyorsa ona işler: dikteye de, "
|
"Kaydı yazıya dökmeden atar. Hangisi çalışıyorsa ona işler: dikteye de, "
|
||||||
|
|||||||
+39
-4
@@ -26,6 +26,9 @@ WARN = QColor(240, 180, 80)
|
|||||||
THEM = QColor(110, 190, 255) # the other side of a meeting
|
THEM = QColor(110, 190, 255) # the other side of a meeting
|
||||||
|
|
||||||
ASK = QColor(150, 140, 255) # recording a command rather than a dictation
|
ASK = QColor(150, 140, 255) # recording a command rather than a dictation
|
||||||
|
# Recording, but nothing is going in. The same amber a warning gets, and for
|
||||||
|
# the same reason: it is the colour that stops you walking away from it.
|
||||||
|
HELD = WARN
|
||||||
|
|
||||||
STATE_COLORS = {"recording": REC, "asking": ASK, "meeting": REC, "busy": BUSY,
|
STATE_COLORS = {"recording": REC, "asking": ASK, "meeting": REC, "busy": BUSY,
|
||||||
"done": OK, "warning": WARN, "error": ERR}
|
"done": OK, "warning": WARN, "error": ERR}
|
||||||
@@ -47,6 +50,10 @@ class Overlay(QWidget):
|
|||||||
self.dismissable = dismissable
|
self.dismissable = dismissable
|
||||||
self.muted = False
|
self.muted = False
|
||||||
self._stacked = False
|
self._stacked = False
|
||||||
|
# A pause is not a state of its own: what is on screen is still the
|
||||||
|
# recording, held. Keeping it beside the state is what lets the ribbon
|
||||||
|
# stay where the pause found it instead of being cleared and rebuilt.
|
||||||
|
self.paused = False
|
||||||
self.state = "idle"
|
self.state = "idle"
|
||||||
self.message = ""
|
self.message = ""
|
||||||
self.levels = [0.0] * BARS
|
self.levels = [0.0] * BARS
|
||||||
@@ -102,6 +109,7 @@ class Overlay(QWidget):
|
|||||||
self.message = ""
|
self.message = ""
|
||||||
self.seconds = 0.0
|
self.seconds = 0.0
|
||||||
self.levels = [0.0] * BARS
|
self.levels = [0.0] * BARS
|
||||||
|
self.paused = False
|
||||||
self.muted = False # a new run starts visible, whatever the last one did
|
self.muted = False # a new run starts visible, whatever the last one did
|
||||||
self._hide_timer.stop()
|
self._hide_timer.stop()
|
||||||
self._appear()
|
self._appear()
|
||||||
@@ -113,6 +121,7 @@ class Overlay(QWidget):
|
|||||||
self.seconds = 0.0
|
self.seconds = 0.0
|
||||||
self.levels = [0.0] * BARS
|
self.levels = [0.0] * BARS
|
||||||
self.levels2 = [0.0] * BARS
|
self.levels2 = [0.0] * BARS
|
||||||
|
self.paused = False
|
||||||
self._hide_timer.stop()
|
self._hide_timer.stop()
|
||||||
self._appear()
|
self._appear()
|
||||||
|
|
||||||
@@ -174,6 +183,17 @@ class Overlay(QWidget):
|
|||||||
def set_seconds(self, seconds):
|
def set_seconds(self, seconds):
|
||||||
self.seconds = seconds
|
self.seconds = seconds
|
||||||
|
|
||||||
|
def set_paused(self, paused):
|
||||||
|
"""Held, or taking sound in again.
|
||||||
|
|
||||||
|
Everything about the ribbon says a recording is running: a dot that
|
||||||
|
pulses, bars that move, a clock that counts. A pause that only stopped
|
||||||
|
the sound would leave all three saying the words are still going in, so
|
||||||
|
it is the ribbon that has to say otherwise.
|
||||||
|
"""
|
||||||
|
self.paused = bool(paused)
|
||||||
|
self.update()
|
||||||
|
|
||||||
# ---- internals -----------------------------------------------------
|
# ---- internals -----------------------------------------------------
|
||||||
|
|
||||||
def _appear(self):
|
def _appear(self):
|
||||||
@@ -243,11 +263,11 @@ class Overlay(QWidget):
|
|||||||
# the corner when it does rather than leaving a gap where it was.
|
# the corner when it does rather than leaving a gap where it was.
|
||||||
if self.below is not None and self.below.showing != self._stacked:
|
if self.below is not None and self.below.showing != self._stacked:
|
||||||
self._reposition()
|
self._reposition()
|
||||||
if self.state in LIVE:
|
if self.state in LIVE and not self.paused:
|
||||||
# keep the ribbon moving even through a pause in speech
|
# keep the ribbon moving even through a pause in speech
|
||||||
self.levels = self.levels[1:] + [self.levels[-1] * 0.72]
|
self.levels = self.levels[1:] + [self.levels[-1] * 0.72]
|
||||||
if self.state == "meeting":
|
if self.state == "meeting":
|
||||||
self.levels2 = self.levels2[1:] + [self.levels2[-1] * 0.72]
|
self.levels2 = self.levels2[1:] + [self.levels2[-1] * 0.72]
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def _label_font(self):
|
def _label_font(self):
|
||||||
@@ -272,6 +292,8 @@ class Overlay(QWidget):
|
|||||||
painter.drawPath(path)
|
painter.drawPath(path)
|
||||||
|
|
||||||
accent = STATE_COLORS.get(self.state, MUTED)
|
accent = STATE_COLORS.get(self.state, MUTED)
|
||||||
|
if self._held:
|
||||||
|
accent = HELD
|
||||||
self._draw_indicator(painter, accent)
|
self._draw_indicator(painter, accent)
|
||||||
|
|
||||||
if self.state in LIVE:
|
if self.state in LIVE:
|
||||||
@@ -282,10 +304,23 @@ class Overlay(QWidget):
|
|||||||
if self._can_dismiss:
|
if self._can_dismiss:
|
||||||
self._draw_dismiss(painter)
|
self._draw_dismiss(painter)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _held(self):
|
||||||
|
"""A recording that is paused. Nothing else can be."""
|
||||||
|
return self.paused and self.state in LIVE
|
||||||
|
|
||||||
def _draw_indicator(self, painter, accent):
|
def _draw_indicator(self, painter, accent):
|
||||||
cx, cy = 26.0, self.height() / 2
|
cx, cy = 26.0, self.height() / 2
|
||||||
painter.setPen(Qt.PenStyle.NoPen)
|
painter.setPen(Qt.PenStyle.NoPen)
|
||||||
if self.state in LIVE:
|
if self._held:
|
||||||
|
# The two bars everything that plays sound uses, and no glow: a
|
||||||
|
# pulse is what says a recording is live.
|
||||||
|
painter.setBrush(accent)
|
||||||
|
for offset in (-4.4, 1.4):
|
||||||
|
painter.drawRoundedRect(
|
||||||
|
QRectF(cx + offset, cy - 6.5, 3.0, 13.0), 1.2, 1.2
|
||||||
|
)
|
||||||
|
elif self.state in LIVE:
|
||||||
pulse = 0.62 + 0.38 * (0.5 + 0.5 * math.sin(self._phase * 1.6))
|
pulse = 0.62 + 0.38 * (0.5 + 0.5 * math.sin(self._phase * 1.6))
|
||||||
glow = QColor(accent)
|
glow = QColor(accent)
|
||||||
glow.setAlphaF(0.22 * pulse)
|
glow.setAlphaF(0.22 * pulse)
|
||||||
|
|||||||
+10
-2
@@ -1321,6 +1321,14 @@ class SettingsWindow(QDialog):
|
|||||||
form, "toggle", t("Start and stop"),
|
form, "toggle", t("Start and stop"),
|
||||||
t("No global shortcut installed."), placeholder="Ctrl+Space",
|
t("No global shortcut installed."), placeholder="Ctrl+Space",
|
||||||
)
|
)
|
||||||
|
# The point of holding a recording is that something else came up, and
|
||||||
|
# something else is exactly when a hand is not free for a menu.
|
||||||
|
self._shortcut_row(
|
||||||
|
form, "pause", t("Pause and resume"),
|
||||||
|
t("No global shortcut installed. The tray menu holds it too."),
|
||||||
|
tooltip=t("Holds the recording without ending it. Nothing said "
|
||||||
|
"while it is paused is kept, and the clock stops with it."),
|
||||||
|
)
|
||||||
# Stopping is what sends the recording off to be transcribed, and that
|
# Stopping is what sends the recording off to be transcribed, and that
|
||||||
# is the step there is no taking back. By the time the tray menu is
|
# is the step there is no taking back. By the time the tray menu is
|
||||||
# open the sentence you did not mean to dictate is already on its way.
|
# open the sentence you did not mean to dictate is already on its way.
|
||||||
@@ -1700,8 +1708,8 @@ class SettingsWindow(QDialog):
|
|||||||
conf["file_cleanup"] = self.file_cleanup.isChecked()
|
conf["file_cleanup"] = self.file_cleanup.isChecked()
|
||||||
|
|
||||||
# Left empty, only the toggle falls back to a default: the application
|
# Left empty, only the toggle falls back to a default: the application
|
||||||
# is unusable without it. The other three stay empty, which is what
|
# is unusable without it. The rest stay empty, which is what turns
|
||||||
# turns them off.
|
# them off.
|
||||||
for which, (box, _status, _missing) in self._shortcut_rows.items():
|
for which, (box, _status, _missing) in self._shortcut_rows.items():
|
||||||
spec = hotkey.SHORTCUTS[which]
|
spec = hotkey.SHORTCUTS[which]
|
||||||
conf[spec.setting] = (box.currentText().strip()
|
conf[spec.setting] = (box.currentText().strip()
|
||||||
|
|||||||
@@ -280,6 +280,26 @@ class _StalledStream:
|
|||||||
self._released.set()
|
self._released.set()
|
||||||
|
|
||||||
|
|
||||||
|
class _HeldStream:
|
||||||
|
"""A capture that is paused and taken up again partway through, the way a
|
||||||
|
key press lands in the middle of a recording rather than between two."""
|
||||||
|
|
||||||
|
def __init__(self, data, recorder, pause_at, resume_at=None):
|
||||||
|
self._data = io.BytesIO(data)
|
||||||
|
self._recorder = recorder
|
||||||
|
self._pause_at = pause_at
|
||||||
|
self._resume_at = resume_at
|
||||||
|
self.reads = 0
|
||||||
|
|
||||||
|
def read(self, size):
|
||||||
|
if self.reads == self._pause_at:
|
||||||
|
self._recorder.pause()
|
||||||
|
elif self.reads == self._resume_at:
|
||||||
|
self._recorder.pause(False)
|
||||||
|
self.reads += 1
|
||||||
|
return self._data.read(size)
|
||||||
|
|
||||||
|
|
||||||
class RecordingCommand(OnLinux, DikteTest):
|
class RecordingCommand(OnLinux, DikteTest):
|
||||||
"""Which program captures the microphone, and how it is asked to."""
|
"""Which program captures the microphone, and how it is asked to."""
|
||||||
|
|
||||||
@@ -499,6 +519,51 @@ class RecorderChain(OnLinux, DikteTest):
|
|||||||
self.assertEqual(len(failures), 1)
|
self.assertEqual(len(failures), 1)
|
||||||
self.assertIn("0.3", failures[0])
|
self.assertIn("0.3", failures[0])
|
||||||
|
|
||||||
|
def held(self, data, pause_at, resume_at=None):
|
||||||
|
"""Record `data` with the recorder paused for part of it."""
|
||||||
|
recorder = audio.Recorder()
|
||||||
|
results = []
|
||||||
|
failures = []
|
||||||
|
recorder.stopped.connect(lambda *args: results.append(args))
|
||||||
|
recorder.failed.connect(failures.append)
|
||||||
|
proc = FakeProcess(data)
|
||||||
|
proc.stdout = _HeldStream(data, recorder, pause_at, resume_at)
|
||||||
|
with only_these_tools("pw-record"), \
|
||||||
|
mock.patch.object(subprocess, "Popen", return_value=proc):
|
||||||
|
recorder.start()
|
||||||
|
recorder._thread.join(timeout=5)
|
||||||
|
# Nothing has ended the capture: a pause holds the microphone.
|
||||||
|
self.assertEqual(proc.signals, [])
|
||||||
|
recorder.stop()
|
||||||
|
return results, failures
|
||||||
|
|
||||||
|
def test_what_is_said_while_it_is_held_is_not_in_the_recording(self):
|
||||||
|
"""The phone call in the middle of a dictation is the whole feature: it
|
||||||
|
must not reach the transcript, and the two halves must meet."""
|
||||||
|
results, failures = self.held(tone(2.0), pause_at=8, resume_at=16)
|
||||||
|
self.assertEqual(failures, [])
|
||||||
|
path, duration, _ = results[0]
|
||||||
|
self.addCleanup(os.unlink, path)
|
||||||
|
dropped = 8 * audio.CHUNK_FRAMES
|
||||||
|
self.assertAlmostEqual(duration, (2 * audio.RATE - dropped) / audio.RATE,
|
||||||
|
places=3)
|
||||||
|
|
||||||
|
def test_a_recording_held_all_the_way_through_captured_nothing(self):
|
||||||
|
results, failures = self.held(tone(2.0), pause_at=0)
|
||||||
|
self.assertEqual(results, [])
|
||||||
|
self.assertIn("0.3", failures[0])
|
||||||
|
|
||||||
|
def test_a_pause_does_not_outlive_the_recording_it_was_asked_for(self):
|
||||||
|
recorder = audio.Recorder()
|
||||||
|
recorder.pause()
|
||||||
|
proc = FakeProcess(tone(0.5))
|
||||||
|
with only_these_tools("pw-record"), \
|
||||||
|
mock.patch.object(subprocess, "Popen", return_value=proc):
|
||||||
|
recorder.start()
|
||||||
|
self.assertFalse(recorder.paused)
|
||||||
|
recorder._thread.join(timeout=5)
|
||||||
|
recorder.cancel()
|
||||||
|
|
||||||
def test_a_recorder_that_could_not_start(self):
|
def test_a_recorder_that_could_not_start(self):
|
||||||
recorder = audio.Recorder()
|
recorder = audio.Recorder()
|
||||||
failures = []
|
failures = []
|
||||||
|
|||||||
+9
-1
@@ -162,7 +162,7 @@ class Parser(unittest.TestCase):
|
|||||||
name)
|
name)
|
||||||
|
|
||||||
def test_every_verb_is_wired_to_something(self):
|
def test_every_verb_is_wired_to_something(self):
|
||||||
for verb in ("record", "toggle", "start", "stop", "cancel", "ask",
|
for verb in ("record", "toggle", "start", "stop", "pause", "cancel", "ask",
|
||||||
"session", "transcribe", "meeting", "meetings", "history",
|
"session", "transcribe", "meeting", "meetings", "history",
|
||||||
"config", "prompt", "devices", "models", "test-key",
|
"config", "prompt", "devices", "models", "test-key",
|
||||||
"doctor", "shortcut", "status", "settings", "restart",
|
"doctor", "shortcut", "status", "settings", "restart",
|
||||||
@@ -614,6 +614,14 @@ class Replies(DikteTest):
|
|||||||
captured():
|
captured():
|
||||||
self.assertEqual(cli.run(["cancel"]), 0)
|
self.assertEqual(cli.run(["cancel"]), 0)
|
||||||
|
|
||||||
|
def test_pausing_a_recording_nobody_started_is_not_a_failure_either(self):
|
||||||
|
"""A key that pauses can be pressed when there is nothing to pause, and
|
||||||
|
it must not start an application to tell you so."""
|
||||||
|
with mock.patch.object(ipc, "send", return_value=None), \
|
||||||
|
mock.patch.object(cli, "launch_gui") as launched, captured():
|
||||||
|
self.assertEqual(cli.run(["pause"]), 0)
|
||||||
|
self.assertFalse(launched.called)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ CHANGED = {
|
|||||||
"file_cleanup": False,
|
"file_cleanup": False,
|
||||||
"shortcut": "Ctrl+Alt+Space",
|
"shortcut": "Ctrl+Alt+Space",
|
||||||
"cancel_shortcut": "Meta+Shift+Space",
|
"cancel_shortcut": "Meta+Shift+Space",
|
||||||
|
"pause_shortcut": "Meta+P",
|
||||||
"evdev_hotkey": True,
|
"evdev_hotkey": True,
|
||||||
"history_limit": 50,
|
"history_limit": 50,
|
||||||
}
|
}
|
||||||
@@ -269,6 +270,7 @@ class Settings(DikteTest):
|
|||||||
self.assertTrue(conf["shortcut"])
|
self.assertTrue(conf["shortcut"])
|
||||||
self.assertEqual(conf["shortcut"], hotkey.default_combo("toggle"))
|
self.assertEqual(conf["shortcut"], hotkey.default_combo("toggle"))
|
||||||
self.assertEqual(conf["cancel_shortcut"], "")
|
self.assertEqual(conf["cancel_shortcut"], "")
|
||||||
|
self.assertEqual(conf["pause_shortcut"], "")
|
||||||
self.assertEqual(conf["assistant_shortcut"], "")
|
self.assertEqual(conf["assistant_shortcut"], "")
|
||||||
self.assertEqual(conf["meeting_shortcut"], "")
|
self.assertEqual(conf["meeting_shortcut"], "")
|
||||||
|
|
||||||
@@ -466,6 +468,27 @@ class Overlay(DikteTest):
|
|||||||
widget._conceal()
|
widget._conceal()
|
||||||
self.assertFalse(widget.showing)
|
self.assertFalse(widget.showing)
|
||||||
|
|
||||||
|
def test_a_held_recording_says_so_and_stops_moving(self):
|
||||||
|
"""Everything about the ribbon says a recording is running; a pause the
|
||||||
|
ribbon did not show would leave all of it saying the opposite."""
|
||||||
|
widget = self.overlay()
|
||||||
|
widget.show_recording()
|
||||||
|
widget.push_level(0.8)
|
||||||
|
widget.set_paused(True)
|
||||||
|
levels = list(widget.levels)
|
||||||
|
widget._tick()
|
||||||
|
self.assertEqual(widget.levels, levels)
|
||||||
|
widget.set_paused(False)
|
||||||
|
widget._tick()
|
||||||
|
self.assertNotEqual(widget.levels, levels)
|
||||||
|
|
||||||
|
def test_a_new_recording_is_never_the_last_one_still_held(self):
|
||||||
|
widget = self.overlay()
|
||||||
|
widget.show_recording()
|
||||||
|
widget.set_paused(True)
|
||||||
|
widget.show_recording()
|
||||||
|
self.assertFalse(widget.paused)
|
||||||
|
|
||||||
def test_a_meeting_shows_both_sides(self):
|
def test_a_meeting_shows_both_sides(self):
|
||||||
widget = self.overlay()
|
widget = self.overlay()
|
||||||
widget.show_meeting()
|
widget.show_meeting()
|
||||||
|
|||||||
+20
-7
@@ -1,11 +1,11 @@
|
|||||||
"""The three tray icons, drawn here for systems that have no icon theme.
|
"""The four tray icons, drawn here for systems that have no icon theme.
|
||||||
|
|
||||||
Linux hands out `audio-input-microphone`, `media-record` and `view-refresh`
|
Linux hands out `audio-input-microphone`, `media-record`, `view-refresh` and
|
||||||
from whatever icon theme is installed, and Qt finds them through
|
`media-playback-pause` from whatever icon theme is installed, and Qt finds them
|
||||||
QIcon.fromTheme. macOS has no such registry: fromTheme returns a null icon
|
through QIcon.fromTheme. macOS has no such registry: fromTheme returns a null
|
||||||
there, and a null icon in the menu bar is an item you cannot see, which is the
|
icon there, and a null icon in the menu bar is an item you cannot see, which is
|
||||||
whole of Dikte's interface gone. So the same three shapes are drawn here, and
|
the whole of Dikte's interface gone. So the same four shapes are drawn here,
|
||||||
used whenever the theme has nothing to offer.
|
and used whenever the theme has nothing to offer.
|
||||||
|
|
||||||
They are drawn as template images: one colour, transparent everywhere else,
|
They are drawn as template images: one colour, transparent everywhere else,
|
||||||
with isMask set. That is what lets macOS invert them for a dark menu bar and
|
with isMask set. That is what lets macOS invert them for a dark menu bar and
|
||||||
@@ -75,6 +75,18 @@ def _record(painter, size):
|
|||||||
painter.drawEllipse(QPointF(11 * unit, 11 * unit), 6.4 * unit, 6.4 * unit)
|
painter.drawEllipse(QPointF(11 * unit, 11 * unit), 6.4 * unit, 6.4 * unit)
|
||||||
|
|
||||||
|
|
||||||
|
def _paused(painter, size):
|
||||||
|
"""Two bars: the recording is still ours, and nothing is going into it."""
|
||||||
|
unit = size / 22.0
|
||||||
|
painter.setPen(Qt.PenStyle.NoPen)
|
||||||
|
painter.setBrush(INK)
|
||||||
|
for left in (6.4, 12.4):
|
||||||
|
painter.drawRoundedRect(
|
||||||
|
QRectF(left * unit, 5.0 * unit, 3.2 * unit, 12.0 * unit),
|
||||||
|
1.2 * unit, 1.2 * unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _working(painter, size):
|
def _working(painter, size):
|
||||||
"""An arrow chasing its own circle: transcribing, cleaning up, thinking."""
|
"""An arrow chasing its own circle: transcribing, cleaning up, thinking."""
|
||||||
unit = size / 22.0
|
unit = size / 22.0
|
||||||
@@ -102,6 +114,7 @@ def _working(painter, size):
|
|||||||
SHAPES = {
|
SHAPES = {
|
||||||
"audio-input-microphone": _microphone,
|
"audio-input-microphone": _microphone,
|
||||||
"media-record": _record,
|
"media-record": _record,
|
||||||
|
"media-playback-pause": _paused,
|
||||||
"view-refresh": _working,
|
"view-refresh": _working,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -90,7 +90,7 @@ echo "──────────────────"
|
|||||||
if ((MACOS)); then
|
if ((MACOS)); then
|
||||||
say "Nothing to unregister: macOS shortcuts live only while Dikte runs."
|
say "Nothing to unregister: macOS shortcuts live only while Dikte runs."
|
||||||
elif [[ -n "$PY" ]] && "$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
elif [[ -n "$PY" ]] && "$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
||||||
for which in toggle cancel ask meeting; do
|
for which in toggle pause cancel ask meeting; do
|
||||||
"$PY" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true
|
"$PY" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
ok "Global shortcuts unregistered"
|
ok "Global shortcuts unregistered"
|
||||||
@@ -151,7 +151,7 @@ if ((!MACOS)); then
|
|||||||
# Removing the shortcut takes its desktop file with it, but an install from
|
# Removing the shortcut takes its desktop file with it, but an install from
|
||||||
# before this script existed may have left one behind on a desktop that never
|
# before this script existed may have left one behind on a desktop that never
|
||||||
# used them.
|
# used them.
|
||||||
for id in dikte-toggle dikte-cancel dikte-ask dikte-meeting; do
|
for id in dikte-toggle dikte-pause dikte-cancel dikte-ask dikte-meeting; do
|
||||||
if [[ -e "$APP_DIR/$id.desktop" ]]; then
|
if [[ -e "$APP_DIR/$id.desktop" ]]; then
|
||||||
remove "$APP_DIR/$id.desktop"
|
remove "$APP_DIR/$id.desktop"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user