mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Put the modules in a package and the scripts in a folder
Twenty-two files at the top of the tree was the first thing anybody saw of this repository. They are one package now, imported relatively, and the three scripts that are not the front door moved under scripts/. install.sh stays where the README has always said it is. What starts the application is dikte/__main__.py: python3 -m dikte runs it, and so does naming the file, which is what the launcher symlink, both .desktop files, the macOS bundle and every registered shortcut do. Run by path there is no package around it, so it puts the checkout on sys.path itself. The installers now keep the keys you chose when they are given none, which is what an update is: update.sh no longer has to read them out and pass them back. An updater from before this commit cannot read them at all, so the one thing it can say, the default key with an empty discard key, is read as "nothing was asked for" rather than obeyed. That guard can go once nobody is updating across this commit.
This commit is contained in:
@@ -71,6 +71,6 @@ jobs:
|
|||||||
- name: Check the installer parses
|
- name: Check the installer parses
|
||||||
run: |
|
run: |
|
||||||
bash -n install.sh
|
bash -n install.sh
|
||||||
bash -n install-mac.sh
|
bash -n scripts/install-mac.sh
|
||||||
bash -n update.sh
|
bash -n scripts/update.sh
|
||||||
bash -n uninstall.sh
|
bash -n scripts/uninstall.sh
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ tools instead:
|
|||||||
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
On macOS the same `./install.sh` runs and hands over to `install-mac.sh`, which
|
On macOS the same `./install.sh` runs and hands over to `scripts/install-mac.sh`, which
|
||||||
puts down a `Dikte.app` in `~/Applications`, the `dikte` command and a
|
puts down a `Dikte.app` in `~/Applications`, the `dikte` command and a
|
||||||
LaunchAgent:
|
LaunchAgent:
|
||||||
|
|
||||||
@@ -84,9 +84,10 @@ transcribe in the cloud. A meeting needs BlackHole or Loopback
|
|||||||
(`brew install blackhole-2ch`); dictation does not.
|
(`brew install blackhole-2ch`); dictation does not.
|
||||||
|
|
||||||
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
||||||
two global shortcuts, whose keys are its two arguments. `./update.sh` pulls and
|
two global shortcuts, whose keys are its two arguments, or the ones already in
|
||||||
puts all of that back, keeping the keys you chose; `./uninstall.sh` takes it away
|
your settings when it is given none. `./scripts/update.sh` pulls and puts all of
|
||||||
again and leaves your settings and dictations alone unless you pass `--purge`.
|
that back; `./scripts/uninstall.sh` takes it away again and leaves your settings
|
||||||
|
and dictations alone unless you pass `--purge`.
|
||||||
|
|
||||||
Speech to text and cleanup each pick a provider in the settings window, and both
|
Speech to text and cleanup each pick a provider in the settings window, and both
|
||||||
run here by default, on models of your own. The cloud is the other option:
|
run here by default, on models of your own. The cloud is the other option:
|
||||||
@@ -201,8 +202,13 @@ keys.
|
|||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
Everything below is in the `dikte` package, which is what `python3 -m dikte`
|
||||||
|
runs and what the `__main__.py` in it hands to every launcher and shortcut.
|
||||||
|
`scripts/` holds install-mac.sh, update.sh and uninstall.sh; install.sh stays at
|
||||||
|
the top, and `tests/` has a file per module.
|
||||||
|
|
||||||
```
|
```
|
||||||
dikte.py entry point, tray icon, state machine
|
app.py entry point, tray icon, state machine
|
||||||
cli.py the command line: every verb, and what it answers with
|
cli.py the command line: every verb, and what it answers with
|
||||||
ipc.py one request and one reply over the local socket
|
ipc.py one request and one reply over the local socket
|
||||||
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
audio.py PCM capture: pw-record for dictation, ffmpeg for a meeting
|
||||||
@@ -224,7 +230,7 @@ i18n.py the string table
|
|||||||
```
|
```
|
||||||
|
|
||||||
The indicator is drawn through XWayland, because a Wayland client cannot place a
|
The indicator is drawn through XWayland, because a Wayland client cannot place a
|
||||||
window in a screen corner; `dikte.py` sets `QT_QPA_PLATFORM=xcb` for that.
|
window in a screen corner; `app.py` sets `QT_QPA_PLATFORM=xcb` for that.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -55,7 +55,7 @@ araçlarıyla çalışır:
|
|||||||
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
sudo apt install pulseaudio-utils xclip xdotool ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
macOS'ta da aynı `./install.sh` çalışır, işi `install-mac.sh`'a devreder;
|
macOS'ta da aynı `./install.sh` çalışır, işi `scripts/install-mac.sh`'a devreder;
|
||||||
`~/Applications` içine bir `Dikte.app`, `dikte` komutunu ve bir LaunchAgent
|
`~/Applications` içine bir `Dikte.app`, `dikte` komutunu ve bir LaunchAgent
|
||||||
kurar:
|
kurar:
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ BlackHole veya Loopback gerekiyor (`brew install blackhole-2ch`); dikte için
|
|||||||
gerekmiyor.
|
gerekmiyor.
|
||||||
|
|
||||||
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
||||||
başlatmayı ve iki global kısayolu kurar; tuşları da iki argümanı. `./update.sh`
|
başlatmayı ve iki global kısayolu kurar; tuşları iki argümanı, argüman
|
||||||
son sürümü çeker ve bunları senin seçtiğin tuşlarla yerine koyar;
|
verilmezse ayarlarında duranlar. `./scripts/update.sh` son sürümü çeker ve
|
||||||
`./uninstall.sh` hepsini geri alır, `--purge` demedikçe ayarlarına ve
|
bunları yerine koyar; `./scripts/uninstall.sh` hepsini geri alır, `--purge`
|
||||||
diktelerine dokunmaz.
|
demedikçe ayarlarına ve diktelerine dokunmaz.
|
||||||
|
|
||||||
Sesi yazıya çevirme ve temizleme, ayarlar penceresinde ayrı ayrı sağlayıcı
|
Sesi yazıya çevirme ve temizleme, ayarlar penceresinde ayrı ayrı sağlayıcı
|
||||||
seçer; ikisi de varsayılan olarak burada, kendi modellerinle çalışır. Bulutu
|
seçer; ikisi de varsayılan olarak burada, kendi modellerinle çalışır. Bulutu
|
||||||
@@ -198,8 +198,13 @@ Kısayollar sekmesi bağlanacak komutu gösterir.
|
|||||||
|
|
||||||
## Dosyalar
|
## Dosyalar
|
||||||
|
|
||||||
|
Aşağıdakilerin hepsi `dikte` paketinin içinde: `python3 -m dikte` bunu çalıştırır,
|
||||||
|
içindeki `__main__.py` de her başlatıcının ve kısayolun adlandırdığı dosyadır.
|
||||||
|
`scripts/` altında install-mac.sh, update.sh ve uninstall.sh var; install.sh en
|
||||||
|
üstte kalır, `tests/` içinde de her modülün bir dosyası.
|
||||||
|
|
||||||
```
|
```
|
||||||
dikte.py giriş noktası, tepsi simgesi, durum makinesi
|
app.py giriş noktası, tepsi simgesi, durum makinesi
|
||||||
cli.py komut satırı: bütün fiiller ve verdikleri cevap
|
cli.py komut satırı: bütün fiiller ve verdikleri cevap
|
||||||
ipc.py yerel sokette bir istek, bir cevap
|
ipc.py yerel sokette bir istek, bir cevap
|
||||||
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
audio.py PCM kaydı: diktede pw-record, toplantıda ffmpeg
|
||||||
@@ -221,7 +226,7 @@ i18n.py metin tablosu
|
|||||||
```
|
```
|
||||||
|
|
||||||
Gösterge XWayland üzerinden çizilir; Wayland'da bir pencereyi belirli bir köşeye
|
Gösterge XWayland üzerinden çizilir; Wayland'da bir pencereyi belirli bir köşeye
|
||||||
yerleştirmenin yolu yok, `dikte.py` bu yüzden `QT_QPA_PLATFORM=xcb` ayarlar.
|
yerleştirmenin yolu yok, `app.py` bu yüzden `QT_QPA_PLATFORM=xcb` ayarlar.
|
||||||
|
|
||||||
## Lisans
|
## Lisans
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Dikte: press a key, talk, press again to transcribe, clean up and paste.
|
||||||
|
|
||||||
|
The package is the application. Nothing is imported here on purpose: `dikte
|
||||||
|
config get` runs through the same package as the tray icon does, and it has no
|
||||||
|
business loading Qt to answer one question.
|
||||||
|
"""
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""What `python3 -m dikte` and the installed `dikte` command both run.
|
||||||
|
|
||||||
|
The file is also executed by path, because that is what a desktop shortcut and
|
||||||
|
the launcher in ~/.local/bin do: neither knows a working directory to be in.
|
||||||
|
Run that way there is no package around it, so the checkout has to be put on
|
||||||
|
the import path here, before the first line of the application is imported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if not __package__:
|
||||||
|
# realpath, because the launcher is a symlink into the checkout: what has to
|
||||||
|
# end up on the path is the checkout, not ~/.local/bin. The directory this
|
||||||
|
# file is in comes off the path in exchange, so that a module beside it is
|
||||||
|
# only ever reachable as part of the package.
|
||||||
|
here = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
sys.path[:] = [p for p in sys.path if os.path.realpath(p or ".") != here]
|
||||||
|
sys.path.insert(0, os.path.dirname(here))
|
||||||
|
|
||||||
|
from dikte.app import main # noqa: E402
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -22,8 +22,8 @@ import threading
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
APP_URL = "https://github.com/yusufipk/dikte"
|
APP_URL = "https://github.com/yusufipk/dikte"
|
||||||
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
||||||
Executable → Regular
+18
-21
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Dikte: press Ctrl+Space, talk, press again to transcribe, clean up and paste.
|
"""Dikte: press Ctrl+Space, talk, press again to transcribe, clean up and paste.
|
||||||
|
|
||||||
This is the application: the tray icon, the state machine, and the socket the
|
This is the application: the tray icon, the state machine, and the socket the
|
||||||
terminal talks to. Every verb it answers is in cli.py, which is also what runs
|
terminal talks to. Every verb it answers is in cli.py, which is also what runs
|
||||||
`dikte.py --help`; the only argument handled here is --gui, which is how the
|
`dikte --help`; the only argument handled here is --gui, which is how the
|
||||||
command line says "there is no instance to talk to, so be one".
|
command line says "there is no instance to talk to, so be one".
|
||||||
|
|
||||||
|
Nothing runs this file directly; __main__.py is what the launchers start.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -36,21 +37,21 @@ from PyQt6.QtGui import QAction, QIcon # noqa: E402
|
|||||||
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
|
from PyQt6.QtNetwork import QLocalServer, QLocalSocket # noqa: E402
|
||||||
from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon # noqa: E402
|
from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon # noqa: E402
|
||||||
|
|
||||||
import assistant # noqa: E402
|
from . import assistant # noqa: E402
|
||||||
import audio # noqa: E402
|
from . import audio # noqa: E402
|
||||||
import cli # noqa: E402
|
from . import cli # noqa: E402
|
||||||
import config as cfg # noqa: E402
|
from . import config as cfg # noqa: E402
|
||||||
import ggml # noqa: E402
|
from . import ggml # noqa: E402
|
||||||
import hotkey # noqa: E402
|
from . import hotkey # noqa: E402
|
||||||
import i18n # noqa: E402
|
from . import i18n # noqa: E402
|
||||||
import ipc # noqa: E402
|
from . import ipc # noqa: E402
|
||||||
import meeting # noqa: E402
|
from . import meeting # noqa: E402
|
||||||
import trayicon # noqa: E402
|
from . import trayicon # noqa: E402
|
||||||
from i18n import t # noqa: E402
|
from .i18n import t # noqa: E402
|
||||||
from meeting import MeetingPipeline # noqa: E402
|
from .meeting import MeetingPipeline # noqa: E402
|
||||||
from overlay import Overlay # noqa: E402
|
from .overlay import Overlay # noqa: E402
|
||||||
from settings_ui import SettingsWindow # noqa: E402
|
from .settings_ui import SettingsWindow # noqa: E402
|
||||||
from worker import Pipeline # noqa: E402
|
from .worker import Pipeline # noqa: E402
|
||||||
|
|
||||||
SERVER_NAME = ipc.SERVER_NAME
|
SERVER_NAME = ipc.SERVER_NAME
|
||||||
IDLE, RECORDING, BUSY = "idle", "recording", "busy"
|
IDLE, RECORDING, BUSY = "idle", "recording", "busy"
|
||||||
@@ -1168,7 +1169,3 @@ def run_app(args):
|
|||||||
QTimer.singleShot(0, dikte.toggle_meeting)
|
QTimer.singleShot(0, dikte.toggle_meeting)
|
||||||
|
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -29,9 +29,9 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
SESSION_FILE = cfg.DATA_DIR / "assistant.json"
|
||||||
PROVIDERS = ("claude", "codex", "openrouter")
|
PROVIDERS = ("claude", "codex", "openrouter")
|
||||||
@@ -31,7 +31,7 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
RATE = 16000
|
RATE = 16000
|
||||||
CHANNELS = 1
|
CHANNELS = 1
|
||||||
@@ -18,10 +18,10 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
PROVIDERS = ("openrouter", "local", "claude", "codex")
|
PROVIDERS = ("openrouter", "local", "claude", "codex")
|
||||||
|
|
||||||
+10
-10
@@ -22,16 +22,16 @@ import time
|
|||||||
|
|
||||||
from PyQt6.QtCore import QCoreApplication, QTimer
|
from PyQt6.QtCore import QCoreApplication, QTimer
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import hotkey
|
from . import hotkey
|
||||||
import ipc
|
from . import ipc
|
||||||
import meeting
|
from . import meeting
|
||||||
import paste
|
from . import paste
|
||||||
|
|
||||||
NOT_RUNNING = 3
|
NOT_RUNNING = 3
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import ggml
|
from . import ggml
|
||||||
import i18n
|
from . import i18n
|
||||||
import paste
|
from . import paste
|
||||||
import paths
|
from . import paths
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
|
|
||||||
_MACOS = sys.platform == "darwin"
|
_MACOS = sys.platform == "darwin"
|
||||||
@@ -23,10 +23,10 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import ggml
|
from . import ggml
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
UPLOAD_LIMIT = 24 * 1024 * 1024 # the APIs take 25 MB; leave the form its room
|
UPLOAD_LIMIT = 24 * 1024 * 1024 # the APIs take 25 MB; leave the form its room
|
||||||
MP3_BITRATE = "48k" # mono speech at 16 kHz: whisper hears nothing less
|
MP3_BITRATE = "48k" # mono speech at 16 kHz: whisper hears nothing less
|
||||||
@@ -44,9 +44,9 @@ import time
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import hub
|
from . import hub
|
||||||
import paths
|
from . import paths
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
# The path api.py asks for, so its URL and the server's line up.
|
# The path api.py asks for, so its URL and the server's line up.
|
||||||
@@ -30,7 +30,7 @@ import threading
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
from i18n import t
|
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"
|
||||||
@@ -25,7 +25,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
GITHUB_API = "https://api.github.com"
|
GITHUB_API = "https://api.github.com"
|
||||||
HF_API = "https://huggingface.co/api"
|
HF_API = "https://huggingface.co/api"
|
||||||
@@ -22,8 +22,13 @@ CONNECT_MS = 800
|
|||||||
|
|
||||||
|
|
||||||
def script_path():
|
def script_path():
|
||||||
|
"""The package entry point, as a path.
|
||||||
|
|
||||||
|
A shortcut and a relaunch both start a second process, and neither has a
|
||||||
|
working directory to run `-m dikte` from, so the file is named outright.
|
||||||
|
"""
|
||||||
return os.path.realpath(
|
return os.path.realpath(
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "dikte.py")
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "__main__.py")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,13 +24,13 @@ import wave
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import vad
|
from . import vad
|
||||||
from filetranscribe import Cancelled, format_timestamp
|
from .filetranscribe import Cancelled, format_timestamp
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
# Where the document stops being prose and starts being the transcript. It is a
|
# Where the document stops being prose and starts being the transcript. It is a
|
||||||
# comment, so it never shows up in a rendered document, and it is what a retry
|
# comment, so it never shows up in a rendered document, and it is what a retry
|
||||||
@@ -19,7 +19,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
# Linux input event codes (linux/input-event-codes.h), which is what ydotool
|
# Linux input event codes (linux/input-event-codes.h), which is what ydotool
|
||||||
# takes. They are also the list of keys a paste shortcut may be built from, so
|
# takes. They are also the list of keys a paste shortcut may be built from, so
|
||||||
@@ -13,19 +13,19 @@ from PyQt6.QtWidgets import (
|
|||||||
QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import filetranscribe
|
from . import filetranscribe
|
||||||
import ggml
|
from . import ggml
|
||||||
import hotkey
|
from . import hotkey
|
||||||
import ipc
|
from . import ipc
|
||||||
import meeting
|
from . import meeting
|
||||||
import paste
|
from . import paste
|
||||||
from filetranscribe import FileTranscriber
|
from .filetranscribe import FileTranscriber
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
UI_LANGUAGES = [("Automatic (system)", "auto"), ("Turkish", "tr"), ("English", "en")]
|
UI_LANGUAGES = [("Automatic (system)", "auto"), ("Turkish", "tr"), ("English", "en")]
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
@@ -118,7 +118,7 @@ def _working(painter, size, ink):
|
|||||||
painter.drawPath(head)
|
painter.drawPath(head)
|
||||||
|
|
||||||
|
|
||||||
# The names Linux themes use, which are what dikte.py asks for either way.
|
# The names Linux themes use, which are what app.py asks for either way.
|
||||||
SHAPES = {
|
SHAPES = {
|
||||||
"audio-input-microphone": _microphone,
|
"audio-input-microphone": _microphone,
|
||||||
"media-record": _record,
|
"media-record": _record,
|
||||||
@@ -15,15 +15,15 @@ import traceback
|
|||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
import api
|
from . import api
|
||||||
import assistant
|
from . import assistant
|
||||||
import audio
|
from . import audio
|
||||||
import cleanup
|
from . import cleanup
|
||||||
import config as cfg
|
from . import config as cfg
|
||||||
import i18n
|
from . import i18n
|
||||||
import paste
|
from . import paste
|
||||||
import vad
|
from . import vad
|
||||||
from i18n import t
|
from .i18n import t
|
||||||
|
|
||||||
CHUNK_SECONDS = audio.CHUNK_FRAMES / audio.RATE
|
CHUNK_SECONDS = audio.CHUNK_FRAMES / audio.RATE
|
||||||
|
|
||||||
+43
-14
@@ -9,18 +9,35 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
# path. That is a second script rather than a branch through this one, and
|
# path. That is a second script rather than a branch through this one, and
|
||||||
# update.sh reaches it through here without having to know which it is on.
|
# update.sh reaches it through here without having to know which it is on.
|
||||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
exec "$DIR/install-mac.sh" "$@"
|
exec "$DIR/scripts/install-mac.sh" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PY="$(command -v python3)"
|
PY="$(command -v python3)"
|
||||||
|
# The one file that starts the application, whoever is asking: the launcher
|
||||||
|
# below, both .desktop files, and every shortcut Dikte registers.
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
APP_DIR="$HOME/.local/share/applications"
|
APP_DIR="$HOME/.local/share/applications"
|
||||||
AUTOSTART_DIR="$HOME/.config/autostart"
|
AUTOSTART_DIR="$HOME/.config/autostart"
|
||||||
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
|
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
|
||||||
SHORTCUT="${1:-Ctrl+Space}"
|
# Only the one: the discard key's default is the settings' own, read back below.
|
||||||
# Without the colon, so that a second argument given as "" stays empty. That is
|
DEFAULT_SHORTCUT="Ctrl+Space"
|
||||||
# how update.sh says "this one was turned off", as against not saying anything.
|
# Given as arguments, or asked of the settings further down. An installer run
|
||||||
CANCEL_SHORTCUT="${2-Ctrl+Alt+Space}"
|
# again, which is what every update does, must not undo a key you chose in
|
||||||
|
# Settings, so silence here means "keep whatever is there".
|
||||||
|
SHORTCUT="${1:-}"
|
||||||
|
CANCEL_SHORTCUT="${2-}"
|
||||||
|
# Passed as "" means the discard key is off, which is not the same answer as
|
||||||
|
# not being passed at all.
|
||||||
|
CANCEL_GIVEN=$(( $# >= 2 ))
|
||||||
|
# One caller says both without meaning either: an updater from before Dikte
|
||||||
|
# became a package looks for the settings at a path that no longer exists, and
|
||||||
|
# so passes the default key and an empty discard key rather than yours. This
|
||||||
|
# can go once nobody is updating across that commit any more.
|
||||||
|
if [[ "$SHORTCUT" == "$DEFAULT_SHORTCUT" && $CANCEL_GIVEN == 1 && -z "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
SHORTCUT=""
|
||||||
|
CANCEL_GIVEN=0
|
||||||
|
fi
|
||||||
|
|
||||||
say() { printf ' %s\n' "$1"; }
|
say() { printf ' %s\n' "$1"; }
|
||||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
||||||
@@ -83,8 +100,8 @@ fi
|
|||||||
|
|
||||||
# 3. Launchers -------------------------------------------------------------
|
# 3. Launchers -------------------------------------------------------------
|
||||||
mkdir -p "$BIN_DIR" "$APP_DIR" "$AUTOSTART_DIR" "$ICON_DIR"
|
mkdir -p "$BIN_DIR" "$APP_DIR" "$AUTOSTART_DIR" "$ICON_DIR"
|
||||||
ln -sf "$DIR/dikte.py" "$BIN_DIR/dikte"
|
ln -sf "$ENTRY" "$BIN_DIR/dikte"
|
||||||
chmod +x "$DIR/dikte.py"
|
chmod +x "$ENTRY"
|
||||||
ok "Command installed: $BIN_DIR/dikte"
|
ok "Command installed: $BIN_DIR/dikte"
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BIN_DIR:"*) ;;
|
*":$BIN_DIR:"*) ;;
|
||||||
@@ -97,7 +114,7 @@ esac
|
|||||||
# bare X11 login Qt is left with hicolor, which has no such name, and the entry
|
# bare X11 login Qt is left with hicolor, which has no such name, and the entry
|
||||||
# comes out blank. hicolor is also where this goes, since it is the theme every
|
# comes out blank. hicolor is also where this goes, since it is the theme every
|
||||||
# desktop must fall back to.
|
# desktop must fall back to.
|
||||||
if "$PY" "$DIR/trayicon.py" --hicolor "$ICON_DIR" >/dev/null 2>&1; then
|
if PYTHONPATH="$DIR" "$PY" -m dikte.trayicon --hicolor "$ICON_DIR" >/dev/null 2>&1; then
|
||||||
ICON=dikte
|
ICON=dikte
|
||||||
# Only GTK reads a cache, and only if one is already there; a stale cache
|
# Only GTK reads a cache, and only if one is already there; a stale cache
|
||||||
# would otherwise hide the file we just wrote.
|
# would otherwise hide the file we just wrote.
|
||||||
@@ -116,7 +133,7 @@ cat > "$APP_DIR/dikte.desktop" <<EOF
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=Dikte
|
Name=Dikte
|
||||||
Comment=Voice dictation: record, transcribe, clean up, paste
|
Comment=Voice dictation: record, transcribe, clean up, paste
|
||||||
Exec=$PY $DIR/dikte.py
|
Exec=$PY $ENTRY
|
||||||
Icon=$ICON
|
Icon=$ICON
|
||||||
Categories=Utility;AudioVideo;
|
Categories=Utility;AudioVideo;
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
@@ -127,7 +144,7 @@ cat > "$AUTOSTART_DIR/dikte.desktop" <<EOF
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Dikte
|
Name=Dikte
|
||||||
Exec=$PY $DIR/dikte.py
|
Exec=$PY $ENTRY
|
||||||
Icon=$ICON
|
Icon=$ICON
|
||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
@@ -143,14 +160,26 @@ ok "Will start automatically on login"
|
|||||||
# knows which desktop it is on, and it stores the combination in the settings
|
# knows which desktop it is on, and it stores the combination in the settings
|
||||||
# as well, which is where the built-in listener reads it from. A key written
|
# as well, which is where the built-in listener reads it from. A key written
|
||||||
# to only one of the two places is a key that half works.
|
# to only one of the two places is a key that half works.
|
||||||
if [[ "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
# What was not asked for is read back out of the settings, which is where Dikte
|
||||||
warn "Both arguments are $SHORTCUT, so the discard key was left out."
|
# keeps the keys and so what a second run of this script has to leave alone.
|
||||||
|
stored() { PYTHONPATH="$DIR" "$PY" -m dikte config get "$1" 2>/dev/null || true; }
|
||||||
|
if [[ -z "$SHORTCUT" ]]; then
|
||||||
|
SHORTCUT="$(stored shortcut)"
|
||||||
|
SHORTCUT="${SHORTCUT:-$DEFAULT_SHORTCUT}"
|
||||||
|
fi
|
||||||
|
if [[ $CANCEL_GIVEN == 0 ]]; then
|
||||||
|
# An empty answer here is a discard key that was turned off, and it stays off.
|
||||||
|
CANCEL_SHORTCUT="$(stored cancel_shortcut)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CANCEL_SHORTCUT" && "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
warn "Both keys are $SHORTCUT, so the discard key was left out."
|
||||||
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
||||||
CANCEL_SHORTCUT=""
|
CANCEL_SHORTCUT=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
register() { # which combination label
|
register() { # which combination label
|
||||||
if out="$("$PY" "$DIR/dikte.py" shortcut install "$1" --combo "$2" 2>&1)"; then
|
if out="$("$PY" "$ENTRY" shortcut install "$1" --combo "$2" 2>&1)"; then
|
||||||
ok "$3: $2"
|
ok "$3: $2"
|
||||||
else
|
else
|
||||||
# One line: the rest of what it has to say about KWin is printed below.
|
# One line: the rest of what it has to say about KWin is printed below.
|
||||||
@@ -167,7 +196,7 @@ if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
|||||||
# not this script's. Guessing from XDG_CURRENT_DESKTOP here is how every
|
# not this script's. Guessing from XDG_CURRENT_DESKTOP here is how every
|
||||||
# session that was neither GNOME nor KDE used to be promised a KWin that was
|
# session that was neither GNOME nor KDE used to be promised a KWin that was
|
||||||
# never running.
|
# never running.
|
||||||
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
case "$(PYTHONPATH="$DIR" "$PY" -c 'from dikte import hotkey; print(hotkey.backend())' 2>/dev/null)" in
|
||||||
kde)
|
kde)
|
||||||
warn "KWin only reads these at startup, so they go live after your next"
|
warn "KWin only reads these at startup, so they go live after your next"
|
||||||
say "login. Until then open Settings → Shortcuts and turn on the"
|
say "login. Until then open Settings → Shortcuts and turn on the"
|
||||||
|
|||||||
@@ -11,19 +11,38 @@
|
|||||||
# Homebrew moves that copy.
|
# Homebrew moves that copy.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# The one file that starts the application, whoever is asking: the wrapper in
|
||||||
|
# ~/.local/bin, the bundle, and every shortcut Dikte registers.
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
APP_DIR="$HOME/Applications"
|
APP_DIR="$HOME/Applications"
|
||||||
APP="$APP_DIR/Dikte.app"
|
APP="$APP_DIR/Dikte.app"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
AGENT_DIR="$HOME/Library/LaunchAgents"
|
AGENT_DIR="$HOME/Library/LaunchAgents"
|
||||||
AGENT_ID="io.github.yusufipk.dikte"
|
AGENT_ID="io.github.yusufipk.dikte"
|
||||||
AGENT="$AGENT_DIR/$AGENT_ID.plist"
|
AGENT="$AGENT_DIR/$AGENT_ID.plist"
|
||||||
SHORTCUT="${1:-Ctrl+Option+Space}"
|
# Only the one: the discard key's default is the settings' own, read back below.
|
||||||
# Without the colon, so that a second argument given as "" stays empty. That is
|
DEFAULT_SHORTCUT="Ctrl+Option+Space"
|
||||||
# how update.sh says "this one was turned off", as against not saying anything.
|
# Given as arguments, or asked of the settings further down. An installer run
|
||||||
CANCEL_SHORTCUT="${2-Ctrl+Option+D}"
|
# again, which is what every update does, must not undo a key you chose in
|
||||||
|
# Settings, so silence here means "keep whatever is there".
|
||||||
|
SHORTCUT="${1:-}"
|
||||||
|
CANCEL_SHORTCUT="${2-}"
|
||||||
|
# Passed as "" means the discard key is off, which is not the same answer as
|
||||||
|
# not being passed at all.
|
||||||
|
CANCEL_GIVEN=$(( $# >= 2 ))
|
||||||
|
# One caller says both without meaning either: an updater from before Dikte
|
||||||
|
# became a package looks for the settings at a path that no longer exists, and
|
||||||
|
# so passes the default key and an empty discard key rather than yours. This
|
||||||
|
# can go once nobody is updating across that commit any more.
|
||||||
|
if [[ "$SHORTCUT" == "$DEFAULT_SHORTCUT" && $CANCEL_GIVEN == 1 && -z "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
SHORTCUT=""
|
||||||
|
CANCEL_GIVEN=0
|
||||||
|
fi
|
||||||
|
|
||||||
# The two places Homebrew installs to, in front, for the same reason dikte.py
|
# The two places Homebrew installs to, in front, for the same reason the app
|
||||||
# puts them there: a shell that has not been logged into since Homebrew was
|
# puts them there: a shell that has not been logged into since Homebrew was
|
||||||
# installed does not have them, and this script would then report ffmpeg as
|
# installed does not have them, and this script would then report ffmpeg as
|
||||||
# missing while the application finds it perfectly well.
|
# missing while the application finds it perfectly well.
|
||||||
@@ -147,7 +166,7 @@ if [ ! -d "\$PYTHONHOME" ]; then
|
|||||||
osascript -e 'display alert "Dikte" message "The Python this was installed against is gone, most likely after a brew upgrade. Run ./install.sh again."' >/dev/null 2>&1
|
osascript -e 'display alert "Dikte" message "The Python this was installed against is gone, most likely after a brew upgrade. Run ./install.sh again."' >/dev/null 2>&1
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exec "\$HERE/python3" "$DIR/dikte.py" --gui "\$@"
|
exec "\$HERE/python3" "$ENTRY" --gui "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$APP/Contents/MacOS/Dikte"
|
chmod +x "$APP/Contents/MacOS/Dikte"
|
||||||
|
|
||||||
@@ -186,7 +205,7 @@ printf 'APPL????' > "$APP/Contents/PkgInfo"
|
|||||||
# and no second place to change what Dikte looks like. Failing to draw it is
|
# and no second place to change what Dikte looks like. Failing to draw it is
|
||||||
# not worth stopping for: a bundle with no icon gets the generic one.
|
# not worth stopping for: a bundle with no icon gets the generic one.
|
||||||
iconset="$(mktemp -d)/Dikte.iconset"
|
iconset="$(mktemp -d)/Dikte.iconset"
|
||||||
if "$PY" "$DIR/trayicon.py" "$iconset" >/dev/null 2>&1 \
|
if PYTHONPATH="$DIR" "$PY" -m dikte.trayicon "$iconset" >/dev/null 2>&1 \
|
||||||
&& iconutil -c icns "$iconset" -o "$APP/Contents/Resources/Dikte.icns" 2>/dev/null; then
|
&& iconutil -c icns "$iconset" -o "$APP/Contents/Resources/Dikte.icns" 2>/dev/null; then
|
||||||
ok "Icon drawn"
|
ok "Icon drawn"
|
||||||
else
|
else
|
||||||
@@ -212,7 +231,7 @@ fi
|
|||||||
-f "$APP" >/dev/null 2>&1 || true
|
-f "$APP" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# 4. The command ------------------------------------------------------------
|
# 4. The command ------------------------------------------------------------
|
||||||
# A wrapper, where Linux gets a symlink to dikte.py. The shebang there is
|
# A wrapper, where Linux gets a symlink to the entry point. The shebang there is
|
||||||
# `env python3`, and on a Mac that is Apple's 3.9: the symlink would resolve to
|
# `env python3`, and on a Mac that is Apple's 3.9: the symlink would resolve to
|
||||||
# the one interpreter that cannot run this. Naming the interpreter here also
|
# the one interpreter that cannot run this. Naming the interpreter here also
|
||||||
# gives update.sh and uninstall.sh somewhere to read it from, so that the three
|
# gives update.sh and uninstall.sh somewhere to read it from, so that the three
|
||||||
@@ -221,9 +240,9 @@ mkdir -p "$BIN_DIR"
|
|||||||
cat > "$BIN_DIR/dikte" <<EOF
|
cat > "$BIN_DIR/dikte" <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Written by install-mac.sh. Edit that, not this.
|
# Written by install-mac.sh. Edit that, not this.
|
||||||
exec "$PY" "$DIR/dikte.py" "\$@"
|
exec "$PY" "$ENTRY" "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$BIN_DIR/dikte" "$DIR/dikte.py"
|
chmod +x "$BIN_DIR/dikte" "$ENTRY"
|
||||||
ok "Command installed: $BIN_DIR/dikte"
|
ok "Command installed: $BIN_DIR/dikte"
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$BIN_DIR:"*) ;;
|
*":$BIN_DIR:"*) ;;
|
||||||
@@ -271,14 +290,27 @@ fi
|
|||||||
# shortcut registry at all, so "installed" means the running application is
|
# shortcut registry at all, so "installed" means the running application is
|
||||||
# holding the combination, and the settings file is where it reads it from
|
# holding the combination, and the settings file is where it reads it from
|
||||||
# after a restart. Nothing here needs a logout, unlike KDE.
|
# after a restart. Nothing here needs a logout, unlike KDE.
|
||||||
if [[ "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
#
|
||||||
warn "Both arguments are $SHORTCUT, so the discard key was left out."
|
# What was not asked for is read back out of the settings, which is what makes
|
||||||
|
# a second run of this script leave the keys you chose alone.
|
||||||
|
stored() { PYTHONPATH="$DIR" "$PY" -m dikte config get "$1" 2>/dev/null || true; }
|
||||||
|
if [[ -z "$SHORTCUT" ]]; then
|
||||||
|
SHORTCUT="$(stored shortcut)"
|
||||||
|
SHORTCUT="${SHORTCUT:-$DEFAULT_SHORTCUT}"
|
||||||
|
fi
|
||||||
|
if [[ $CANCEL_GIVEN == 0 ]]; then
|
||||||
|
# An empty answer here is a discard key that was turned off, and it stays off.
|
||||||
|
CANCEL_SHORTCUT="$(stored cancel_shortcut)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CANCEL_SHORTCUT" && "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
||||||
|
warn "Both keys are $SHORTCUT, so the discard key was left out."
|
||||||
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
||||||
CANCEL_SHORTCUT=""
|
CANCEL_SHORTCUT=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
register() { # which combination label
|
register() { # which combination label
|
||||||
if out="$("$PY" "$DIR/dikte.py" shortcut install "$1" --combo "$2" 2>&1)"; then
|
if out="$("$PY" "$ENTRY" shortcut install "$1" --combo "$2" 2>&1)"; then
|
||||||
ok "$3: $2"
|
ok "$3: $2"
|
||||||
else
|
else
|
||||||
warn "${out%%$'\n'*}"
|
warn "${out%%$'\n'*}"
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
# is the word that deletes them.
|
# is the word that deletes them.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
USER_NAME="$(id -un)"
|
USER_NAME="$(id -un)"
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ count() {
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: ./uninstall.sh [--purge] [--yes]
|
Usage: ./scripts/uninstall.sh [--purge] [--yes]
|
||||||
|
|
||||||
--purge also delete the settings ($CONFIG_DIR)
|
--purge also delete the settings ($CONFIG_DIR)
|
||||||
and the dictations, meetings and recordings ($DATA_DIR)
|
and the dictations, meetings and recordings ($DATA_DIR)
|
||||||
@@ -93,9 +96,9 @@ 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 pause 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" "$ENTRY" shortcut remove "$which" >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
case "$(PYTHONPATH="$DIR" "$PY" -c 'from dikte import hotkey; print(hotkey.backend())' 2>/dev/null)" in
|
||||||
kde)
|
kde)
|
||||||
ok "Global shortcuts unregistered"
|
ok "Global shortcuts unregistered"
|
||||||
say "KWin reads that file at startup, so the keys are free after your next login."
|
say "KWin reads that file at startup, so the keys are free after your next login."
|
||||||
@@ -111,10 +114,12 @@ fi
|
|||||||
# 2. The running instance --------------------------------------------------
|
# 2. The running instance --------------------------------------------------
|
||||||
# It holds a tray icon and a socket; asking it to quit is tidier than pulling
|
# It holds a tray icon and a socket; asking it to quit is tidier than pulling
|
||||||
# its launchers out from under it.
|
# its launchers out from under it.
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
# The pattern matches an instance started before Dikte became a package as well,
|
||||||
[[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" quit >/dev/null 2>&1 || true
|
# which is what an uninstall run straight after an update finds running.
|
||||||
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
|
[[ -n "$PY" ]] && "$PY" "$ENTRY" quit >/dev/null 2>&1 || true
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
warn "Dikte is still running; close it from the tray icon"
|
warn "Dikte is still running; close it from the tray icon"
|
||||||
else
|
else
|
||||||
ok "Stopped the running instance"
|
ok "Stopped the running instance"
|
||||||
@@ -216,11 +221,11 @@ elif [[ "$CONFIG_DIR" == "$DATA_DIR" ]]; then
|
|||||||
# macOS keeps both in the one directory a Mac user's backup already knows
|
# macOS keeps both in the one directory a Mac user's backup already knows
|
||||||
# about, so naming it twice would only look like two things were kept.
|
# about, so naming it twice would only look like two things were kept.
|
||||||
say "Settings and dictations kept: $CONFIG_DIR"
|
say "Settings and dictations kept: $CONFIG_DIR"
|
||||||
say "Delete them too with: ./uninstall.sh --purge"
|
say "Delete them too with: ./scripts/uninstall.sh --purge"
|
||||||
else
|
else
|
||||||
say "Settings kept: $CONFIG_DIR"
|
say "Settings kept: $CONFIG_DIR"
|
||||||
say "Dictations kept: $DATA_DIR"
|
say "Dictations kept: $DATA_DIR"
|
||||||
say "Delete them too with: ./uninstall.sh --purge"
|
say "Delete them too with: ./scripts/uninstall.sh --purge"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
# Dikte updater: pull, put the launchers back, restart what was running.
|
# Dikte updater: pull, put the launchers back, restart what was running.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
# The checkout, one level up: this script lives in scripts/, everything it
|
||||||
|
# touches is at the top of the tree.
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENTRY="$DIR/dikte/__main__.py"
|
||||||
USER_NAME="$(id -un)"
|
USER_NAME="$(id -un)"
|
||||||
|
|
||||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
@@ -13,10 +16,8 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|||||||
# make a missing file the end of the script rather than a question answered no.
|
# make a missing file the end of the script rather than a question answered no.
|
||||||
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$HOME/.local/bin/dikte" 2>/dev/null | head -1 || true)"
|
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$HOME/.local/bin/dikte" 2>/dev/null | head -1 || true)"
|
||||||
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
|
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
|
||||||
DEFAULT_SHORTCUT="Ctrl+Option+Space"
|
|
||||||
else
|
else
|
||||||
PY="$(command -v python3 || true)"
|
PY="$(command -v python3 || true)"
|
||||||
DEFAULT_SHORTCUT="Ctrl+Space"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
say() { printf ' %s\n' "$1"; }
|
say() { printf ' %s\n' "$1"; }
|
||||||
@@ -24,13 +25,6 @@ ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|||||||
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
||||||
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
|
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
|
||||||
|
|
||||||
# The combination stored in the settings, which is where Dikte itself reads it
|
|
||||||
# from and the one place that is the same whichever mechanism the session has.
|
|
||||||
setting() {
|
|
||||||
[[ -n "$PY" ]] || return 0
|
|
||||||
"$PY" "$DIR/dikte.py" config get "$1" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Updating Dikte"
|
echo "Updating Dikte"
|
||||||
echo "──────────────"
|
echo "──────────────"
|
||||||
@@ -90,18 +84,15 @@ echo
|
|||||||
|
|
||||||
# 4. Launchers --------------------------------------------------------------
|
# 4. Launchers --------------------------------------------------------------
|
||||||
# An update can add a dependency or move a file, so the installer runs again.
|
# An update can add a dependency or move a file, so the installer runs again.
|
||||||
# It would otherwise register its own defaults over the keys you chose, so it
|
# With no keys named it keeps the ones stored in the settings, which are the
|
||||||
# is told what those are. Read before the installer runs, since it is the one
|
# ones you chose.
|
||||||
# writing them.
|
"$DIR/install.sh"
|
||||||
shortcut="$(setting shortcut)"
|
|
||||||
cancel_shortcut="$(setting cancel_shortcut)"
|
|
||||||
# Positional, so a chosen discard key cannot be passed without the other one.
|
|
||||||
"$DIR/install.sh" "${shortcut:-$DEFAULT_SHORTCUT}" "${cancel_shortcut:-}"
|
|
||||||
|
|
||||||
# 5. The running instance ---------------------------------------------------
|
# 5. The running instance ---------------------------------------------------
|
||||||
# It is still running the code from before the pull.
|
# It is still running the code from before the pull, which on an update across
|
||||||
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
# the move into a package is a process still named after the old entry point.
|
||||||
if [[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" restart >/dev/null 2>&1; then
|
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
|
||||||
|
if [[ -n "$PY" ]] && "$PY" "$ENTRY" restart >/dev/null 2>&1; then
|
||||||
ok "Restarted, so the new version is the one running"
|
ok "Restarted, so the new version is the one running"
|
||||||
else
|
else
|
||||||
warn "Could not restart it; use the tray menu → Restart"
|
warn "Could not restart it; use the tray menu → Restart"
|
||||||
+3
-3
@@ -22,9 +22,9 @@ import urllib.request
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import assistant
|
from dikte import assistant
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import i18n
|
from dikte import i18n
|
||||||
|
|
||||||
# What the application is, rather than what it does: PipeWire, wl-clipboard,
|
# What the application is, rather than what it does: PipeWire, wl-clipboard,
|
||||||
# ydotool, KDE's shortcut file, /dev/input. A port to another desktop replaces
|
# ydotool, KDE's shortcut file, /dev/input. A port to another desktop replaces
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import ggml
|
from dikte import ggml
|
||||||
from tests.support import (
|
from tests.support import (
|
||||||
DikteTest,
|
DikteTest,
|
||||||
fake_urlopen,
|
fake_urlopen,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import time
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import assistant
|
from dikte import assistant
|
||||||
from tests.support import DikteTest, fake_urlopen, only_these_tools
|
from tests.support import DikteTest, fake_urlopen, only_these_tools
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import audio
|
from dikte import audio
|
||||||
from tests.support import (
|
from tests.support import (
|
||||||
DikteTest,
|
DikteTest,
|
||||||
FakeCompleted,
|
FakeCompleted,
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import ggml
|
from dikte import ggml
|
||||||
from tests.support import DikteTest, fake_urlopen, sent_json, url_error
|
from tests.support import DikteTest, fake_urlopen, sent_json, url_error
|
||||||
from tests.test_api import FakeServer, chat_reply
|
from tests.test_api import FakeServer, chat_reply
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -12,10 +12,10 @@ import json
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import cli
|
from dikte import cli
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
import ipc
|
from dikte import ipc
|
||||||
from tests.support import DikteTest, fake_urlopen
|
from tests.support import DikteTest, fake_urlopen
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import i18n
|
from dikte import i18n
|
||||||
import paste
|
from dikte import paste
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import filetranscribe as ft
|
from dikte import filetranscribe as ft
|
||||||
from tests.support import DikteTest, make_wav, silence, tone
|
from tests.support import DikteTest, make_wav, silence, tone
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import hub
|
from dikte import hub
|
||||||
from tests.support import (DikteTest, fake_urlopen, http_error, json_body,
|
from tests.support import (DikteTest, fake_urlopen, http_error, json_body,
|
||||||
linux_only, url_error)
|
linux_only, url_error)
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
from tests.support import DikteTest, FakeCompleted, linux_only
|
from tests.support import DikteTest, FakeCompleted, linux_only
|
||||||
|
|
||||||
SHORTCUTS_RC = """[services][dikte-toggle.desktop]
|
SHORTCUTS_RC = """[services][dikte-toggle.desktop]
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import hub
|
from dikte import hub
|
||||||
from tests.support import DikteTest, fake_urlopen, http_error, url_error
|
from tests.support import DikteTest, fake_urlopen, http_error, url_error
|
||||||
|
|
||||||
RELEASE = {
|
RELEASE = {
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import string
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import i18n
|
from dikte import i18n
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ipc
|
from dikte import ipc
|
||||||
|
|
||||||
|
|
||||||
class FakeSocket:
|
class FakeSocket:
|
||||||
@@ -57,7 +57,7 @@ class FakeSocket:
|
|||||||
|
|
||||||
class Paths(unittest.TestCase):
|
class Paths(unittest.TestCase):
|
||||||
def test_script_path_points_at_dikte(self):
|
def test_script_path_points_at_dikte(self):
|
||||||
self.assertTrue(ipc.script_path().endswith("dikte.py"))
|
self.assertTrue(ipc.script_path().endswith("dikte/__main__.py"))
|
||||||
self.assertTrue(os.path.exists(ipc.script_path()))
|
self.assertTrue(os.path.exists(ipc.script_path()))
|
||||||
|
|
||||||
def test_the_shortcut_command_runs_it_with_this_interpreter(self):
|
def test_the_shortcut_command_runs_it_with_this_interpreter(self):
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import unittest
|
|||||||
import wave
|
import wave
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import meeting
|
from dikte import meeting
|
||||||
from tests.support import DikteTest, make_wav, silence, speech, stereo, tone
|
from tests.support import DikteTest, make_wav, silence, speech, stereo, tone
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ import unittest
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import paste
|
from dikte import paste
|
||||||
from tests.support import DikteTest, FakeCompleted, only_these_tools
|
from tests.support import DikteTest, FakeCompleted, only_these_tools
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,9 +9,9 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import ggml
|
from dikte import ggml
|
||||||
import paths
|
from dikte import paths
|
||||||
|
|
||||||
|
|
||||||
class Directories(unittest.TestCase):
|
class Directories(unittest.TestCase):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from unittest import mock
|
|||||||
from PyQt6.QtGui import QImage
|
from PyQt6.QtGui import QImage
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
import trayicon
|
from dikte import trayicon
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
# One application for the whole run; Qt allows no second one.
|
# One application for the whole run; Qt allows no second one.
|
||||||
@@ -66,7 +66,7 @@ class Tray(DikteTest):
|
|||||||
self.patch_attr(trayicon, "_cache", {})
|
self.patch_attr(trayicon, "_cache", {})
|
||||||
|
|
||||||
def test_a_name_we_do_not_draw_is_a_null_icon(self):
|
def test_a_name_we_do_not_draw_is_a_null_icon(self):
|
||||||
# dikte.py asks the theme first and falls through to here, so anything
|
# app.py asks the theme first and falls through to here, so anything
|
||||||
# answered with a picture would be one the theme should have given.
|
# answered with a picture would be one the theme should have given.
|
||||||
self.assertTrue(trayicon.icon("emblem-important").isNull())
|
self.assertTrue(trayicon.icon("emblem-important").isNull())
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -16,12 +16,12 @@ from PyQt6.QtCore import QPoint, QPointF, Qt
|
|||||||
from PyQt6.QtGui import QWheelEvent
|
from PyQt6.QtGui import QWheelEvent
|
||||||
from PyQt6.QtWidgets import QApplication, QMessageBox
|
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
import cleanup
|
from dikte import cleanup
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import hotkey
|
from dikte import hotkey
|
||||||
import overlay as overlay_module
|
from dikte import overlay as overlay_module
|
||||||
import paste
|
from dikte import paste
|
||||||
import settings_ui
|
from dikte import settings_ui
|
||||||
from tests.support import DikteTest, only_these_tools
|
from tests.support import DikteTest, only_these_tools
|
||||||
|
|
||||||
# One application for the whole run; Qt allows no second one.
|
# One application for the whole run; Qt allows no second one.
|
||||||
@@ -281,7 +281,7 @@ class Settings(DikteTest):
|
|||||||
text = self.shortcut_tab_text(window)
|
text = self.shortcut_tab_text(window)
|
||||||
self.assertIn("i3 keeps no shortcut registry", text)
|
self.assertIn("i3 keeps no shortcut registry", text)
|
||||||
self.assertNotIn("KWin", text)
|
self.assertNotIn("KWin", text)
|
||||||
self.assertIn("dikte.py toggle", text)
|
self.assertIn("__main__.py toggle", text)
|
||||||
# Not a choice to offer where it is the only mechanism there is.
|
# Not a choice to offer where it is the only mechanism there is.
|
||||||
self.assertTrue(window.evdev_enabled.isHidden())
|
self.assertTrue(window.evdev_enabled.isHidden())
|
||||||
self.assertFalse([button for button in
|
self.assertFalse([button for button in
|
||||||
@@ -458,7 +458,7 @@ class MacSettings(Settings):
|
|||||||
text = self.shortcut_tab_text(window)
|
text = self.shortcut_tab_text(window)
|
||||||
self.assertIn("Dikte asks macOS for these combinations", text)
|
self.assertIn("Dikte asks macOS for these combinations", text)
|
||||||
self.assertNotIn("KWin", text)
|
self.assertNotIn("KWin", text)
|
||||||
self.assertNotIn("dikte.py toggle", text)
|
self.assertNotIn("__main__.py toggle", text)
|
||||||
|
|
||||||
def test_the_paste_keys_on_offer_are_the_ones_a_mac_uses(self):
|
def test_the_paste_keys_on_offer_are_the_ones_a_mac_uses(self):
|
||||||
window = self.window(cfg.Config())
|
window = self.window(cfg.Config())
|
||||||
@@ -482,7 +482,7 @@ class KdeSettings(Settings):
|
|||||||
self.assertIn("KWin only reads shortcut settings at startup", text)
|
self.assertIn("KWin only reads shortcut settings at startup", text)
|
||||||
self.assertIn("Install as a KDE shortcut", text)
|
self.assertIn("Install as a KDE shortcut", text)
|
||||||
self.assertNotIn("keeps no shortcut registry", text)
|
self.assertNotIn("keeps no shortcut registry", text)
|
||||||
self.assertNotIn("dikte.py toggle", text)
|
self.assertNotIn("__main__.py toggle", text)
|
||||||
# Here it is a choice: the wait for the next login, or the key press
|
# Here it is a choice: the wait for the next login, or the key press
|
||||||
# reaching the focused application as well.
|
# reaching the focused application as well.
|
||||||
self.assertFalse(window.evdev_enabled.isHidden())
|
self.assertFalse(window.evdev_enabled.isHidden())
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import vad
|
from dikte import vad
|
||||||
from tests.support import DikteTest
|
from tests.support import DikteTest
|
||||||
|
|
||||||
CHUNK = 1024 / 16000 # what worker.py feeds it: one chunk of the level meter
|
CHUNK = 1024 / 16000 # what worker.py feeds it: one chunk of the level meter
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import api
|
from dikte import api
|
||||||
import assistant
|
from dikte import assistant
|
||||||
import config as cfg
|
from dikte import config as cfg
|
||||||
import paste
|
from dikte import paste
|
||||||
import worker
|
from dikte import worker
|
||||||
from tests.support import DikteTest, make_wav, speech
|
from tests.support import DikteTest, make_wav, speech
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user