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`),
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
+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
(`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.
+6 -2
View File
@@ -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()
+1
View File
@@ -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:
+4
View File
@@ -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",
+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
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
+27 -5
View File
@@ -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()
+13 -1
View File
@@ -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")