diff --git a/README.md b/README.md index 76b8ae0..dc78e9d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ set next to it. | --- | --- | | Start / stop recording | `Ctrl+Space`, or click the tray icon | | Cancel a recording | Tray menu → *Cancel recording*, or `dikte cancel` | -| Speak a command to Claude | 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` | | Settings | Tray menu → *Settings*, or `dikte settings` | | Reload after an update | Tray menu → *Restart*, or `dikte restart` | @@ -90,7 +90,7 @@ elapsed time, then the stage it is on. It never takes focus. Pressing a thing you can say to a window that is not Claude. Codex (`codex exec`) runs the same way, and OpenRouter is there as a plain question-and-answer fallback for a machine with neither CLI on it. Provider, model, permissions and working - directory are under Settings → Claude, and commands close together stay in one + directory are under Settings → Agent, and commands close together stay in one conversation. - **Meetings** are recorded from the microphone and the speaker output at the same time, which settles who said what by the channel a voice arrived on diff --git a/README.tr.md b/README.tr.md index 97e08bd..300e377 100644 --- a/README.tr.md +++ b/README.tr.md @@ -46,7 +46,7 @@ yapıştırılır; modelin yanındaki kutudan düşünme seviyesini de seçebili | --- | --- | | Kaydı başlat / bitir | `Ctrl+Space`, ya da tepsi simgesine tıkla | | Kaydı iptal et | Tepsi menüsü → *Kaydı iptal et*, ya da `dikte cancel` | -| Claude'a 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` | | Ayarlar | Tepsi menüsü → *Ayarlar*, ya da `dikte settings` | | Güncelleme sonrası yeniden yükle | Tepsi menüsü → *Yeniden başlat*, ya da `dikte restart` | @@ -88,7 +88,7 @@ süreyi, ardından hangi aşamada olduğunu gösterir. Odak almaz. Dikte çalı perşembe üçe takvime koy" cümlesini Claude olmayan bir pencerede söyleyebilir olmanı sağlayan da budur. Codex (`codex exec`) da aynı şekilde çalışır; OpenRouter ise ikisi de kurulu olmayan bir makinede düz soru cevap için - duruyor. Sağlayıcı, model, izinler ve çalışma dizini Ayarlar → Claude + duruyor. Sağlayıcı, model, izinler ve çalışma dizini Ayarlar → Ajan sekmesinde; arka arkaya verilen komutlar tek bir konuşmada kalır. - **Toplantılar** mikrofonla hoparlör çıkışından aynı anda kaydedilir; kimin ne dediği tahmin edilmez, sesin hangi kanaldan geldiğiyle belli olur. İki taraf diff --git a/assistant.py b/assistant.py index 7f97d21..11c99d2 100644 --- a/assistant.py +++ b/assistant.py @@ -178,7 +178,7 @@ def ask(prompt, conf, on_stage=None, should_stop=None): if not shutil.which(binary): raise AssistantError(t( "{binary} not found. Install it, or pick another provider under " - "Settings → Claude.", binary=binary, + "Settings → Agent.", binary=binary, )) run = _ask_claude if name == "claude" else _ask_codex diff --git a/config.py b/config.py index d53cec0..8a1fba0 100644 --- a/config.py +++ b/config.py @@ -232,8 +232,18 @@ follow, and no way to answer a question you ask back. - Reply in the language you were spoken to in - Keep it short. A sentence or two when that covers it. No preamble, no "here is what I found", no closing offer of further help +- Short is the answer, not the work. Being asked for one line is not being asked + to answer off the top of your head: when what was asked turns on something + current, specific or personal, go and look. Search the web, read the file, + open the calendar, run the command. Then answer in one line +- Never hand back a caveat in place of an answer. The moment you are about to + write that something falls after your training data, that you cannot be sure, + or that you have no way to know, is the moment to go and find out instead. You + have the tools. A guess and an apology are both worth less than the ten + seconds that checking costs - Plain prose. No headings, no bullet lists, no bold, and no code fence unless - what was asked for is code + what was asked for is code. Nothing appended after the answer either: no list + of sources, no links, no note on how you found it - When you did something rather than answered something, say what you did in one sentence, carrying the detail that confirms it: the day and time an event was saved for, the name of a file that was written @@ -254,8 +264,17 @@ da senin soracağın soruya verilecek bir yanıt yok. - Sana hangi dilde konuşulduysa o dilde cevap ver - Kısa tut. Yetiyorsa bir iki cümle. Giriş cümlesi kurma, "işte buldukların" deme, sonunda başka yardım teklif etme +- Kısa olması gereken cevap, iş değil. Tek satır istenmesi, aklından cevap ver + demek değildir: sorulan şey güncel, belirli ya da kişisel bir şeye bağlıysa + git bak. İnternette ara, dosyayı oku, takvime bak, komutu çalıştır. Sonra tek + satırla cevapla +- Cevabın yerine asla bir çekince koyma. Bir şeyin eğitim verinden sonrasına + denk geldiğini, emin olamayacağını ya da bilmene imkân olmadığını yazmak + üzereysen, tam o an gidip öğrenmenin zamanıdır. Araçların var. Bir tahmin de + bir özür de, bakmanın alacağı on saniyeden daha az değerlidir - Düz metin yaz. Başlık, madde işareti, kalın yazı kullanma; istenen şey kodun - kendisi değilse kod bloğu da açma + kendisi değilse kod bloğu da açma. Cevabın arkasına da bir şey ekleme: kaynak + listesi, bağlantı, nasıl bulduğuna dair not olmasın - Bir şeyi cevaplamak yerine yaptıysan, ne yaptığını tek cümleyle söyle ve onu doğrulayan ayrıntıyı da yaz: kaydın hangi güne ve saate düştüğü, yazdığın dosyanın adı diff --git a/dikte.py b/dikte.py index edcbc98..fdd5dd9 100755 --- a/dikte.py +++ b/dikte.py @@ -163,7 +163,15 @@ class Dikte: self._set_icon("audio-input-microphone") def _tray_clicked(self, reason): - if reason == QSystemTrayIcon.ActivationReason.Trigger: + if reason != QSystemTrayIcon.ActivationReason.Trigger: + return + # The icon ends whatever is being recorded rather than only a dictation. + # The two shortcuts are each tied to their own mode, on purpose, but the + # icon is one button: having it refuse to stop a recording it can see is + # just a button that does nothing. + if self.state == RECORDING and self.ask_mode: + self._toggle_ask() + else: self._toggle() def _set_icon(self, name): diff --git a/i18n.py b/i18n.py index 1a9df49..c5247fc 100644 --- a/i18n.py +++ b/i18n.py @@ -331,8 +331,8 @@ TR = { "Using {name}…": "{name} kullanıyor…", "Thinking…": "Düşünüyor…", "{binary} not found. Install it, or pick another provider under " - "Settings → Claude.": - "{binary} bulunamadı. Kur ya da Ayarlar → Claude sekmesinden başka bir " + "Settings → Agent.": + "{binary} bulunamadı. Kur ya da Ayarlar → Ajan sekmesinden başka bir " "sağlayıcı seç.", "Could not run {binary}: {error}": "{binary} çalıştırılamadı: {error}", "{service} exited with code {code}.": "{service} {code} koduyla çıktı.", @@ -344,8 +344,8 @@ TR = { "It was not allowed to use: {tools}": "Şunları kullanmasına izin yoktu: {tools}", "The model returned an empty reply.": "Model boş cevap döndürdü.", - # --- settings: Claude --------------------------------------------------- - "Claude": "Claude", + # --- settings: the agent ------------------------------------------------ + "Agent": "Ajan", "This shortcut records the same way dictation does, but the transcript is " "not what gets pasted. It goes to an agent as a command, and what comes " "back is pasted instead: the answer to a question, or a sentence saying " @@ -386,7 +386,7 @@ TR = { "The conversation": "Konuşma", "The answer": "Cevap", "Found: {path}": "Bulundu: {path}", - "No KDE shortcut installed. The tray menu asks Claude too.": + "No KDE shortcut installed. The tray menu asks it too.": "Kurulu KDE kısayolu yok. Tepsi menüsünden de sorulabilir.", "A name like “sonnet” always means the newest model of that line. Opus " "thinks harder and answers slower, which is felt here more than anywhere " diff --git a/settings_ui.py b/settings_ui.py index 984c38d..3b9a7d1 100644 --- a/settings_ui.py +++ b/settings_ui.py @@ -90,6 +90,15 @@ REASONING_LEVELS = [ ("Very high", "xhigh"), ("Maximum", "max"), ] PASTE_SHORTCUTS = ["ctrl+v", "ctrl+shift+v", "shift+insert"] +# Offered for all three global shortcuts, which keeps them one kind of field +# rather than three. The boxes stay editable: this is a shortlist of +# combinations that are usually free, not the set of ones that work. +SHORTCUTS = [ + "Ctrl+Space", "Ctrl+Alt+Space", "Ctrl+Shift+Space", "Meta+Space", + "Ctrl+Alt+A", "Ctrl+Alt+D", "Ctrl+Alt+M", "Ctrl+Alt+Q", + "Meta+A", "Meta+D", "Meta+M", + "Ctrl+Alt+F1", "Ctrl+Alt+F2", "Ctrl+Alt+F3", +] AUDIO_FILTER = ("*.mp3 *.wav *.m4a *.ogg *.opus *.flac *.aac *.wma " "*.mp4 *.mkv *.webm *.mov *.avi") @@ -122,7 +131,7 @@ class SettingsWindow(QDialog): tabs.addTab(self._general_tab(), t("General")) tabs.addTab(self._api_tab(), t("API and models")) tabs.addTab(self._prompt_tab(), t("Cleanup rules")) - tabs.addTab(self._assistant_tab(), t("Claude")) + tabs.addTab(self._assistant_tab(), t("Agent")) tabs.addTab(self._meeting_tab(), t("Meeting")) tabs.addTab(self._minutes_tab(), t("Minutes")) tabs.addTab(self._file_tab(), t("Audio file")) @@ -353,8 +362,7 @@ class SettingsWindow(QDialog): how = QGroupBox(t("How it runs")) how_form = QFormLayout(how) - self.assistant_shortcut = QLineEdit() - self.assistant_shortcut.setPlaceholderText(t("none")) + self.assistant_shortcut = self._shortcut_box(t("none")) install = QPushButton(t("Install as a KDE shortcut")) install.clicked.connect(self._install_ask_shortcut) remove = QPushButton(t("Remove")) @@ -604,8 +612,7 @@ class SettingsWindow(QDialog): )) recording_form.addRow("", self.meeting_keep_audio) - self.meeting_shortcut = QLineEdit() - self.meeting_shortcut.setPlaceholderText(t("none")) + self.meeting_shortcut = self._shortcut_box(t("none")) install = QPushButton(t("Install as a KDE shortcut")) install.clicked.connect(self._install_meeting_shortcut) remove = QPushButton(t("Remove")) @@ -752,8 +759,7 @@ class SettingsWindow(QDialog): page = QWidget() layout = QVBoxLayout(page) form = QFormLayout() - self.shortcut = QLineEdit() - self.shortcut.setPlaceholderText("Ctrl+Space") + self.shortcut = self._shortcut_box("Ctrl+Space") form.addRow(t("Shortcut"), self.shortcut) layout.addLayout(form) @@ -837,6 +843,17 @@ class SettingsWindow(QDialog): layout.addLayout(row) return page + @staticmethod + def _shortcut_box(placeholder=""): + """The field a global shortcut is typed or picked in.""" + box = QComboBox() + box.setEditable(True) + box.addItems(SHORTCUTS) + box.setCurrentText("") + if placeholder: + box.lineEdit().setPlaceholderText(placeholder) + return box + @staticmethod def _row(*widgets): """Widgets side by side in one form row; the first one takes the space.""" @@ -878,7 +895,7 @@ class SettingsWindow(QDialog): self.cleanup_prompt.setPlainText(conf["cleanup_prompt"] or cfg.default_cleanup_prompt()) self.transcribe_prompt.setPlainText(conf["transcribe_prompt"]) - self.assistant_shortcut.setText(conf["assistant_shortcut"]) + self.assistant_shortcut.setCurrentText(conf["assistant_shortcut"]) self._select_data(self.assistant_provider, conf["assistant_provider"]) self.assistant_model.setCurrentText(conf["assistant_model"]) self._select_data(self.assistant_permission, conf["assistant_permission_mode"]) @@ -906,7 +923,7 @@ class SettingsWindow(QDialog): self.meeting_cleanup.setChecked(conf["meeting_cleanup"]) self.meeting_max_minutes.setValue(max(5, int(conf["meeting_max_seconds"]) // 60)) self.meeting_keep_audio.setChecked(conf["meeting_keep_audio"]) - self.meeting_shortcut.setText(conf["meeting_shortcut"]) + self.meeting_shortcut.setCurrentText(conf["meeting_shortcut"]) self.meeting_prompt.setPlainText( conf["meeting_prompt"] or cfg.default_meeting_prompt() ) @@ -915,7 +932,7 @@ class SettingsWindow(QDialog): self.file_cleanup.setChecked(conf["file_cleanup"]) self.file_path = "" - self.shortcut.setText(conf["shortcut"]) + self.shortcut.setCurrentText(conf["shortcut"]) self.evdev_enabled.setChecked(conf["evdev_hotkey"]) self.history_limit.setValue(max(0, int(conf["history_limit"]))) @@ -962,7 +979,7 @@ class SettingsWindow(QDialog): conf["cleanup_prompt"] = "" if prompt == cfg.default_cleanup_prompt() else prompt conf["transcribe_prompt"] = self.transcribe_prompt.toPlainText().strip() - conf["assistant_shortcut"] = self.assistant_shortcut.text().strip() + conf["assistant_shortcut"] = self.assistant_shortcut.currentText().strip() conf["assistant_provider"] = self.assistant_provider.currentData() or "claude" conf["assistant_model"] = (self.assistant_model.currentText().strip() or cfg.DEFAULTS["assistant_model"]) @@ -1001,7 +1018,7 @@ class SettingsWindow(QDialog): conf["meeting_cleanup"] = self.meeting_cleanup.isChecked() conf["meeting_max_seconds"] = self.meeting_max_minutes.value() * 60 conf["meeting_keep_audio"] = self.meeting_keep_audio.isChecked() - conf["meeting_shortcut"] = self.meeting_shortcut.text().strip() + conf["meeting_shortcut"] = self.meeting_shortcut.currentText().strip() meeting_prompt = self.meeting_prompt.toPlainText().strip() conf["meeting_prompt"] = ("" if meeting_prompt == cfg.default_meeting_prompt() else meeting_prompt) @@ -1009,7 +1026,7 @@ class SettingsWindow(QDialog): conf["file_timestamps"] = self.file_timestamps.isChecked() conf["file_cleanup"] = self.file_cleanup.isChecked() - conf["shortcut"] = self.shortcut.text().strip() or "Ctrl+Space" + conf["shortcut"] = self.shortcut.currentText().strip() or "Ctrl+Space" conf["evdev_hotkey"] = self.evdev_enabled.isChecked() conf["history_limit"] = self.history_limit.value() conf.save() @@ -1217,7 +1234,7 @@ class SettingsWindow(QDialog): # ---- shortcut -------------------------------------------------------- def _install_shortcut(self): - combo = self.shortcut.text().strip() or "Ctrl+Space" + combo = self.shortcut.currentText().strip() or "Ctrl+Space" clashes = hotkey.conflicting_shortcuts(combo) if clashes: answer = QMessageBox.question( @@ -1246,7 +1263,7 @@ class SettingsWindow(QDialog): ) def _install_meeting_shortcut(self): - combo = self.meeting_shortcut.text().strip() + combo = self.meeting_shortcut.currentText().strip() if not combo: QMessageBox.information(self, t("Shortcut"), t("Type a key combination first.")) @@ -1284,7 +1301,7 @@ class SettingsWindow(QDialog): # ---- Claude ---------------------------------------------------------- def _install_ask_shortcut(self): - combo = self.assistant_shortcut.text().strip() + combo = self.assistant_shortcut.currentText().strip() if not combo: QMessageBox.information(self, t("Shortcut"), t("Type a key combination first.")) @@ -1316,7 +1333,7 @@ class SettingsWindow(QDialog): current = hotkey.kde_shortcut_status(hotkey.ASK_DESKTOP_ID) self.assistant_shortcut_status.setText( t("Registered in KDE: {shortcut}", shortcut=current) if current - else t("No KDE shortcut installed. The tray menu asks Claude too.") + else t("No KDE shortcut installed. The tray menu asks it too.") ) def _assistant_provider_changed(self):