Add display selection for recording indicator

This commit is contained in:
Aydogan
2026-08-27 07:46:34 +03:00
parent 3663c7fd57
commit e6208418f9
8 changed files with 79 additions and 17 deletions
+11 -5
View File
@@ -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`), speech to text on **OpenAI**, **Groq** or **OpenRouter** (`gpt-4o-transcribe`),
cleanup on OpenRouter (`google/gemini-3.5-flash-lite`) or, when either is 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`, 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 `GROQ_API_KEY` and `OPENROUTER_API_KEY`.
`~/.config/dikte/config.json`, mode 600, or in
`~/Library/Application Support/Dikte` on a Mac. Cleanup can be switched off, in All settings, including the indicator screen and corner, are stored outside the
which case the raw transcript is pasted, and a thinking model's effort can be application: in `$XDG_CONFIG_HOME/dikte/config.json` (normally
set next to it. `~/.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 ## Using it
+9 -2
View File
@@ -114,8 +114,15 @@ seçersen sesi yazıya çevirme **OpenAI**, **Groq** ya da **OpenRouter**'da
(varsayılan `gpt-4o-transcribe`), temizleme OpenRouter'da (varsayılan `gpt-4o-transcribe`), temizleme OpenRouter'da
(`google/gemini-3.5-flash-lite`) ya da kuruluysa Claude Code veya Codex'te (`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 ç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` `OPENROUTER_API_KEY` kullanılır.
içinde, izinler 600, Mac'te ise `~/Library/Application Support/Dikte` altında.
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 Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript yapıştırılır; modelin
yanındaki kutudan düşünme seviyesini de seçebilirsin. yanındaki kutudan düşünme seviyesini de seçebilirsin.
+6 -2
View File
@@ -146,11 +146,13 @@ class Dikte:
# started it cannot stop the one that came after. # started it cannot stop the one that came after.
self._run_id = 0 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 # 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. # up, and drops into the corner when it is alone there.
self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay, 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.recorder = audio.Recorder()
self.pipeline = Pipeline(self.conf) self.pipeline = Pipeline(self.conf)
self.ask_pipeline = Pipeline(self.conf) self.ask_pipeline = Pipeline(self.conf)
@@ -1049,7 +1051,9 @@ class Dikte:
def _apply_settings(self): def _apply_settings(self):
self.overlay.corner = self.conf["overlay_corner"] 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.corner = self.conf["overlay_corner"]
self.ask_overlay.screen_name = self.conf["overlay_screen"]
self._apply_local() self._apply_local()
self._build_tray() self._build_tray()
self._refresh_tray() self._refresh_tray()
+1
View File
@@ -455,6 +455,7 @@ DEFAULTS = {
"pause_shortcut": "", "pause_shortcut": "",
"evdev_hotkey": False, "evdev_hotkey": False,
"overlay_corner": "bottom-left", "overlay_corner": "bottom-left",
"overlay_screen": "",
"keep_audio": False, "keep_audio": False,
"history_limit": 200, "history_limit": 200,
# A look at the releases page once a day, and nothing more than a look: # A look at the releases page once a day, and nothing more than a look:
+4
View File
@@ -150,6 +150,7 @@ TR = {
# --- settings: tabs and general ------------------------------------ # --- settings: tabs and general ------------------------------------
"Dikte Settings": "Dikte Ayarları", "Dikte Settings": "Dikte Ayarları",
"General": "Genel", "General": "Genel",
"Display": "Görüntü",
"API and models": "API ve modeller", "API and models": "API ve modeller",
"Cleanup rules": "Temizleme kuralları", "Cleanup rules": "Temizleme kuralları",
"Audio file": "Ses dosyası", "Audio file": "Ses dosyası",
@@ -180,6 +181,9 @@ TR = {
"macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.", "macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.",
"Restore the previous clipboard after pasting": "Restore the previous clipboard after pasting":
"Yapıştırdıktan sonra eski pano içeriğini geri koy", "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", "Indicator corner": "Gösterge köşesi",
"bottom-left": "sol-alt", "bottom-left": "sol-alt",
"bottom-right": "sağ-alt", "bottom-right": "sağ-alt",
+8 -2
View File
@@ -40,9 +40,11 @@ class Overlay(QWidget):
of covering it, which is what lets a dictation and a command to the agent be 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.""" 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) super().__init__(None)
self.corner = corner self.corner = corner
self.screen_name = screen_name
self.below = below self.below = below
# A job that can run for ten minutes should not have to be watched for # 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 # ten minutes. Clicking such an indicator puts the progress away; the
@@ -245,7 +247,11 @@ class Overlay(QWidget):
def _reposition(self): def _reposition(self):
# On a multi-monitor setup, show up where the user actually is. # 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() area = screen.availableGeometry()
left = "left" in self.corner left = "left" in self.corner
top = "top" in self.corner top = "top" in self.corner
+27 -5
View File
@@ -553,6 +553,7 @@ class SettingsWindow(QDialog):
tabs = self.tabs = QTabWidget(self) tabs = self.tabs = QTabWidget(self)
tabs.addTab(self._scrolled(self._general_tab()), t("General")) 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.api_tab_index = tabs.addTab(
self._scrolled(self._api_tab()), t("API and models")) self._scrolled(self._api_tab()), t("API and models"))
tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules")) 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")) self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting"))
form.addRow("", self.restore_clipboard) 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 = QSpinBox()
self.max_seconds.setRange(10, 3600) self.max_seconds.setRange(10, 3600)
self.max_seconds.setSuffix(t(" s")) self.max_seconds.setSuffix(t(" s"))
@@ -729,6 +725,27 @@ class SettingsWindow(QDialog):
self.update_now)) self.update_now))
return page 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): def _api_tab(self):
page = QWidget() page = QWidget()
outer = QVBoxLayout(page) outer = QVBoxLayout(page)
@@ -1591,6 +1608,10 @@ class SettingsWindow(QDialog):
self.auto_paste.setChecked(conf["auto_paste"]) self.auto_paste.setChecked(conf["auto_paste"])
self.paste_shortcut.setCurrentText(conf["paste_shortcut"]) self.paste_shortcut.setCurrentText(conf["paste_shortcut"])
self.restore_clipboard.setChecked(conf["restore_clipboard"]) 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._select_data(self.corner, conf["overlay_corner"])
self.max_seconds.setValue(conf["max_seconds"]) self.max_seconds.setValue(conf["max_seconds"])
self.skip_silent.setChecked(conf["skip_silent"]) self.skip_silent.setChecked(conf["skip_silent"])
@@ -1686,6 +1707,7 @@ class SettingsWindow(QDialog):
conf["auto_paste"] = self.auto_paste.isChecked() conf["auto_paste"] = self.auto_paste.isChecked()
conf["paste_shortcut"] = self.paste_shortcut.currentText().strip() conf["paste_shortcut"] = self.paste_shortcut.currentText().strip()
conf["restore_clipboard"] = self.restore_clipboard.isChecked() 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["overlay_corner"] = self.corner.currentData() or "bottom-left"
conf["max_seconds"] = self.max_seconds.value() conf["max_seconds"] = self.max_seconds.value()
conf["skip_silent"] = self.skip_silent.isChecked() conf["skip_silent"] = self.skip_silent.isChecked()
+13 -1
View File
@@ -42,6 +42,7 @@ CHANGED = {
"paste_shortcut": "ctrl+shift+v", "paste_shortcut": "ctrl+shift+v",
"restore_clipboard": True, "restore_clipboard": True,
"overlay_corner": "top-right", "overlay_corner": "top-right",
"overlay_screen": "DP-1",
"max_seconds": 120, "max_seconds": 120,
"skip_silent": False, "skip_silent": False,
"silence_db": -42.0, "silence_db": -42.0,
@@ -159,7 +160,7 @@ class Settings(DikteTest):
def test_the_window_opens_with_every_tab_on_it(self): def test_the_window_opens_with_every_tab_on_it(self):
window = self.window(cfg.Config()) window = self.window(cfg.Config())
tabs = window.findChildren(settings_ui.QTabWidget)[0] tabs = window.findChildren(settings_ui.QTabWidget)[0]
self.assertEqual(tabs.count(), 9) self.assertEqual(tabs.count(), 10)
self.assertEqual(window.windowTitle(), "Dikte Settings") self.assertEqual(window.windowTitle(), "Dikte Settings")
def test_no_tab_can_stretch_the_window_past_a_small_screen(self): def test_no_tab_can_stretch_the_window_past_a_small_screen(self):
@@ -596,6 +597,17 @@ class Overlay(DikteTest):
widget.show_recording() widget.show_recording()
widget._reposition() 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): def test_a_warning_and_an_error_both_show(self):
widget = self.overlay() widget = self.overlay()
widget.show_warning("cleanup failed") widget.show_warning("cleanup failed")