From e6208418f928eb9a557efa2bf47970d8c1d7740a Mon Sep 17 00:00:00 2001 From: Aydogan Date: Thu, 27 Aug 2026 07:46:34 +0300 Subject: [PATCH 1/2] Add display selection for recording indicator --- README.md | 16 +++++++++++----- README.tr.md | 11 +++++++++-- dikte/app.py | 8 ++++++-- dikte/config.py | 1 + dikte/i18n.py | 4 ++++ dikte/overlay.py | 10 ++++++++-- dikte/settings_ui.py | 32 +++++++++++++++++++++++++++----- tests/test_ui.py | 14 +++++++++++++- 8 files changed, 79 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fe36166..85be8fc 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,17 @@ run here by default, on models of your own. The cloud is the other option: speech to text on **OpenAI**, **Groq** or **OpenRouter** (`gpt-4o-transcribe`), cleanup on OpenRouter (`google/gemini-3.5-flash-lite`) or, when either is installed, on Claude Code or Codex. The keys fall back to `OPENAI_API_KEY`, -`GROQ_API_KEY` and `OPENROUTER_API_KEY`, and are stored in -`~/.config/dikte/config.json`, mode 600, or in -`~/Library/Application Support/Dikte` on a Mac. Cleanup can be switched off, in -which case the raw transcript is pasted, and a thinking model's effort can be -set next to it. +`GROQ_API_KEY` and `OPENROUTER_API_KEY`. + +All settings, including the indicator screen and corner, are stored outside the +application: in `$XDG_CONFIG_HOME/dikte/config.json` (normally +`~/.config/dikte/config.json`) on Linux, `%APPDATA%\Dikte\config.json` on +Windows, and `~/Library/Application Support/Dikte/config.json` on macOS. The +file is mode 600 where Unix permissions apply. Rebuilding, replacing or +updating Dikte therefore keeps the settings. Only removing that file or +uninstalling with `--purge` resets them. Cleanup can be switched off, in which +case the raw transcript is pasted, and a thinking model's effort can be set +next to it. ## Using it diff --git a/README.tr.md b/README.tr.md index 5e88c69..0457c52 100644 --- a/README.tr.md +++ b/README.tr.md @@ -114,8 +114,15 @@ seçersen sesi yazıya çevirme **OpenAI**, **Groq** ya da **OpenRouter**'da (varsayılan `gpt-4o-transcribe`), temizleme OpenRouter'da (`google/gemini-3.5-flash-lite`) ya da kuruluysa Claude Code veya Codex'te çalışır. Anahtarları boş bırakırsan `OPENAI_API_KEY`, `GROQ_API_KEY` ve -`OPENROUTER_API_KEY` kullanılır; anahtarlar `~/.config/dikte/config.json` -içinde, izinler 600, Mac'te ise `~/Library/Application Support/Dikte` altında. +`OPENROUTER_API_KEY` kullanılır. + +Gösterge ekranı ve köşesi dahil bütün ayarlar uygulamanın dışında saklanır: +Linux'ta `$XDG_CONFIG_HOME/dikte/config.json` (normalde +`~/.config/dikte/config.json`), Windows'ta `%APPDATA%\Dikte\config.json`, +macOS'ta `~/Library/Application Support/Dikte/config.json`. Unix izinlerinin +geçerli olduğu sistemlerde dosyanın izni 600'dür. Bu nedenle Dikte'yi yeniden +derlemek, değiştirmek veya güncellemek ayarları silmez. Yalnızca bu dosyayı +silmek ya da kaldırıcıyı `--purge` ile çalıştırmak ayarları sıfırlar. Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript yapıştırılır; modelin yanındaki kutudan düşünme seviyesini de seçebilirsin. diff --git a/dikte/app.py b/dikte/app.py index 1da104e..5fb2837 100644 --- a/dikte/app.py +++ b/dikte/app.py @@ -146,11 +146,13 @@ class Dikte: # started it cannot stop the one that came after. self._run_id = 0 - self.overlay = Overlay(self.conf["overlay_corner"]) + self.overlay = Overlay(self.conf["overlay_corner"], + screen_name=self.conf["overlay_screen"]) # The agent's indicator sits on top of the dictation one when both are # up, and drops into the corner when it is alone there. self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay, - dismissable=True) + dismissable=True, + screen_name=self.conf["overlay_screen"]) self.recorder = audio.Recorder() self.pipeline = Pipeline(self.conf) self.ask_pipeline = Pipeline(self.conf) @@ -1049,7 +1051,9 @@ class Dikte: def _apply_settings(self): self.overlay.corner = self.conf["overlay_corner"] + self.overlay.screen_name = self.conf["overlay_screen"] self.ask_overlay.corner = self.conf["overlay_corner"] + self.ask_overlay.screen_name = self.conf["overlay_screen"] self._apply_local() self._build_tray() self._refresh_tray() diff --git a/dikte/config.py b/dikte/config.py index fb7683a..eeaa704 100644 --- a/dikte/config.py +++ b/dikte/config.py @@ -455,6 +455,7 @@ DEFAULTS = { "pause_shortcut": "", "evdev_hotkey": False, "overlay_corner": "bottom-left", + "overlay_screen": "", "keep_audio": False, "history_limit": 200, # A look at the releases page once a day, and nothing more than a look: diff --git a/dikte/i18n.py b/dikte/i18n.py index f7f4910..56ec263 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -150,6 +150,7 @@ TR = { # --- settings: tabs and general ------------------------------------ "Dikte Settings": "Dikte Ayarları", "General": "Genel", + "Display": "Görüntü", "API and models": "API ve modeller", "Cleanup rules": "Temizleme kuralları", "Audio file": "Ses dosyası", @@ -180,6 +181,9 @@ TR = { "macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.", "Restore the previous clipboard after pasting": "Yapıştırdıktan sonra eski pano içeriğini geri koy", + "Indicator screen": "Gösterge ekranı", + "Follow the mouse pointer": "Fare imlecini takip et", + "{name} (not connected)": "{name} (bağlı değil)", "Indicator corner": "Gösterge köşesi", "bottom-left": "sol-alt", "bottom-right": "sağ-alt", diff --git a/dikte/overlay.py b/dikte/overlay.py index 4f4d3f1..a4acbe3 100644 --- a/dikte/overlay.py +++ b/dikte/overlay.py @@ -40,9 +40,11 @@ class Overlay(QWidget): of covering it, which is what lets a dictation and a command to the agent be under way at the same time and still both be visible.""" - def __init__(self, corner="bottom-left", below=None, dismissable=False): + def __init__(self, corner="bottom-left", below=None, dismissable=False, + screen_name=""): super().__init__(None) self.corner = corner + self.screen_name = screen_name self.below = below # A job that can run for ten minutes should not have to be watched for # ten minutes. Clicking such an indicator puts the progress away; the @@ -245,7 +247,11 @@ class Overlay(QWidget): def _reposition(self): # On a multi-monitor setup, show up where the user actually is. - screen = QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen() + screen = next( + (item for item in QApplication.screens() if item.name() == self.screen_name), + None, + ) + screen = screen or QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen() area = screen.availableGeometry() left = "left" in self.corner top = "top" in self.corner diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index d3a97b7..a032ffc 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -553,6 +553,7 @@ class SettingsWindow(QDialog): tabs = self.tabs = QTabWidget(self) tabs.addTab(self._scrolled(self._general_tab()), t("General")) + tabs.addTab(self._scrolled(self._display_tab()), t("Display")) self.api_tab_index = tabs.addTab( self._scrolled(self._api_tab()), t("API and models")) tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules")) @@ -674,11 +675,6 @@ class SettingsWindow(QDialog): self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting")) form.addRow("", self.restore_clipboard) - self.corner = QComboBox() - for value in CORNERS: - self.corner.addItem(t(value), value) - form.addRow(t("Indicator corner"), self.corner) - self.max_seconds = QSpinBox() self.max_seconds.setRange(10, 3600) self.max_seconds.setSuffix(t(" s")) @@ -729,6 +725,27 @@ class SettingsWindow(QDialog): self.update_now)) return page + def _display_tab(self): + page = QWidget() + form = QFormLayout(page) + + self.indicator_screen = QComboBox() + self.indicator_screen.addItem(t("Follow the mouse pointer"), "") + for screen in QGuiApplication.screens(): + area = screen.geometry() + self.indicator_screen.addItem( + t("{name} ({width} × {height})", name=screen.name(), + width=area.width(), height=area.height()), + screen.name(), + ) + form.addRow(t("Indicator screen"), self.indicator_screen) + + self.corner = QComboBox() + for value in CORNERS: + self.corner.addItem(t(value), value) + form.addRow(t("Indicator corner"), self.corner) + return page + def _api_tab(self): page = QWidget() outer = QVBoxLayout(page) @@ -1591,6 +1608,10 @@ class SettingsWindow(QDialog): self.auto_paste.setChecked(conf["auto_paste"]) self.paste_shortcut.setCurrentText(conf["paste_shortcut"]) self.restore_clipboard.setChecked(conf["restore_clipboard"]) + screen_name = conf["overlay_screen"] + if screen_name and self.indicator_screen.findData(screen_name) < 0: + self.indicator_screen.addItem(t("{name} (not connected)", name=screen_name), screen_name) + self._select_data(self.indicator_screen, screen_name) self._select_data(self.corner, conf["overlay_corner"]) self.max_seconds.setValue(conf["max_seconds"]) self.skip_silent.setChecked(conf["skip_silent"]) @@ -1686,6 +1707,7 @@ class SettingsWindow(QDialog): conf["auto_paste"] = self.auto_paste.isChecked() conf["paste_shortcut"] = self.paste_shortcut.currentText().strip() conf["restore_clipboard"] = self.restore_clipboard.isChecked() + conf["overlay_screen"] = self.indicator_screen.currentData() or "" conf["overlay_corner"] = self.corner.currentData() or "bottom-left" conf["max_seconds"] = self.max_seconds.value() conf["skip_silent"] = self.skip_silent.isChecked() diff --git a/tests/test_ui.py b/tests/test_ui.py index 0b04be9..104be0d 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -42,6 +42,7 @@ CHANGED = { "paste_shortcut": "ctrl+shift+v", "restore_clipboard": True, "overlay_corner": "top-right", + "overlay_screen": "DP-1", "max_seconds": 120, "skip_silent": False, "silence_db": -42.0, @@ -159,7 +160,7 @@ class Settings(DikteTest): def test_the_window_opens_with_every_tab_on_it(self): window = self.window(cfg.Config()) tabs = window.findChildren(settings_ui.QTabWidget)[0] - self.assertEqual(tabs.count(), 9) + self.assertEqual(tabs.count(), 10) self.assertEqual(window.windowTitle(), "Dikte Settings") def test_no_tab_can_stretch_the_window_past_a_small_screen(self): @@ -596,6 +597,17 @@ class Overlay(DikteTest): widget.show_recording() widget._reposition() + def test_a_named_screen_is_used_instead_of_the_pointer_screen(self): + screen = mock.Mock() + screen.name.return_value = "DP-1" + screen.availableGeometry.return_value = settings_ui.QRect(1920, 0, 1920, 1080) + widget = self.overlay(screen_name="DP-1") + with mock.patch.object(QApplication, "screens", return_value=[screen]), \ + mock.patch.object(QApplication, "screenAt") as screen_at: + widget._reposition() + screen_at.assert_not_called() + self.assertEqual(widget.pos(), QPoint(1948, 995)) + def test_a_warning_and_an_error_both_show(self): widget = self.overlay() widget.show_warning("cleanup failed") From 23243c517123a9869a370b604273908ffce4225a Mon Sep 17 00:00:00 2001 From: yusufipk Date: Thu, 27 Aug 2026 15:52:02 +0300 Subject: [PATCH 2/2] Trim the README and polish the display selection The settings-storage paragraph goes back to the one sentence it was. The screen list now shows native resolutions rather than the scaled ones, the Turkish tab is named Ekran, and the repositioning comment says what a named screen changes. --- README.md | 16 +++++----------- README.tr.md | 11 ++--------- dikte/i18n.py | 2 +- dikte/overlay.py | 5 ++++- dikte/settings_ui.py | 6 +++++- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 85be8fc..fe36166 100644 --- a/README.md +++ b/README.md @@ -117,17 +117,11 @@ run here by default, on models of your own. The cloud is the other option: speech to text on **OpenAI**, **Groq** or **OpenRouter** (`gpt-4o-transcribe`), cleanup on OpenRouter (`google/gemini-3.5-flash-lite`) or, when either is installed, on Claude Code or Codex. The keys fall back to `OPENAI_API_KEY`, -`GROQ_API_KEY` and `OPENROUTER_API_KEY`. - -All settings, including the indicator screen and corner, are stored outside the -application: in `$XDG_CONFIG_HOME/dikte/config.json` (normally -`~/.config/dikte/config.json`) on Linux, `%APPDATA%\Dikte\config.json` on -Windows, and `~/Library/Application Support/Dikte/config.json` on macOS. The -file is mode 600 where Unix permissions apply. Rebuilding, replacing or -updating Dikte therefore keeps the settings. Only removing that file or -uninstalling with `--purge` resets them. Cleanup can be switched off, in which -case the raw transcript is pasted, and a thinking model's effort can be set -next to it. +`GROQ_API_KEY` and `OPENROUTER_API_KEY`, and are stored in +`~/.config/dikte/config.json`, mode 600, or in +`~/Library/Application Support/Dikte` on a Mac. Cleanup can be switched off, in +which case the raw transcript is pasted, and a thinking model's effort can be +set next to it. ## Using it diff --git a/README.tr.md b/README.tr.md index 0457c52..5e88c69 100644 --- a/README.tr.md +++ b/README.tr.md @@ -114,15 +114,8 @@ seçersen sesi yazıya çevirme **OpenAI**, **Groq** ya da **OpenRouter**'da (varsayılan `gpt-4o-transcribe`), temizleme OpenRouter'da (`google/gemini-3.5-flash-lite`) ya da kuruluysa Claude Code veya Codex'te çalışır. Anahtarları boş bırakırsan `OPENAI_API_KEY`, `GROQ_API_KEY` ve -`OPENROUTER_API_KEY` kullanılır. - -Gösterge ekranı ve köşesi dahil bütün ayarlar uygulamanın dışında saklanır: -Linux'ta `$XDG_CONFIG_HOME/dikte/config.json` (normalde -`~/.config/dikte/config.json`), Windows'ta `%APPDATA%\Dikte\config.json`, -macOS'ta `~/Library/Application Support/Dikte/config.json`. Unix izinlerinin -geçerli olduğu sistemlerde dosyanın izni 600'dür. Bu nedenle Dikte'yi yeniden -derlemek, değiştirmek veya güncellemek ayarları silmez. Yalnızca bu dosyayı -silmek ya da kaldırıcıyı `--purge` ile çalıştırmak ayarları sıfırlar. +`OPENROUTER_API_KEY` kullanılır; anahtarlar `~/.config/dikte/config.json` +içinde, izinler 600, Mac'te ise `~/Library/Application Support/Dikte` altında. Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript yapıştırılır; modelin yanındaki kutudan düşünme seviyesini de seçebilirsin. diff --git a/dikte/i18n.py b/dikte/i18n.py index 56ec263..c4f19d1 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -150,7 +150,7 @@ TR = { # --- settings: tabs and general ------------------------------------ "Dikte Settings": "Dikte Ayarları", "General": "Genel", - "Display": "Görüntü", + "Display": "Ekran", "API and models": "API ve modeller", "Cleanup rules": "Temizleme kuralları", "Audio file": "Ses dosyası", diff --git a/dikte/overlay.py b/dikte/overlay.py index a4acbe3..8fc561a 100644 --- a/dikte/overlay.py +++ b/dikte/overlay.py @@ -246,7 +246,10 @@ class Overlay(QWidget): self.resize(width, HEIGHT) def _reposition(self): - # On a multi-monitor setup, show up where the user actually is. + # The screen the settings name, or, when none is named or it is not + # plugged in right now, where the user actually is. Names are connector + # names on X11 and model names on macOS, where two identical monitors + # can share one; the first then wins. screen = next( (item for item in QApplication.screens() if item.name() == self.screen_name), None, diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index a032ffc..945e710 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -732,10 +732,14 @@ class SettingsWindow(QDialog): self.indicator_screen = QComboBox() self.indicator_screen.addItem(t("Follow the mouse pointer"), "") for screen in QGuiApplication.screens(): + # The native resolution, so that a scaled 4K screen reads + # 3840 × 2160 and not the 1920 × 1080 Qt sees through the scale. area = screen.geometry() + ratio = screen.devicePixelRatio() self.indicator_screen.addItem( t("{name} ({width} × {height})", name=screen.name(), - width=area.width(), height=area.height()), + width=round(area.width() * ratio), + height=round(area.height() * ratio)), screen.name(), ) form.addRow(t("Indicator screen"), self.indicator_screen)