mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Merge pull request #36 from yusufipk/generic-desktop-shortcuts
Stop telling every desktop that it is KDE
This commit is contained in:
@@ -5,7 +5,8 @@ machine by default, a model cleans it up (dropping the *uh*s, the restarts, the
|
|||||||
missing punctuation), and the result lands in your clipboard and is pasted into
|
missing punctuation), and the result lands in your clipboard and is pasted into
|
||||||
whatever window you were typing in.
|
whatever window you were typing in.
|
||||||
|
|
||||||
Built for KDE Plasma 6 on Wayland, and runs on GNOME X11 and macOS too. No
|
Built for KDE Plasma 6 on Wayland, and runs on GNOME X11, macOS and any other
|
||||||
|
Linux desktop that will let it read the keyboard. No
|
||||||
dependencies beyond system packages: just the Python standard library, 3.11 or
|
dependencies beyond system packages: just the Python standard library, 3.11 or
|
||||||
newer, and PyQt6.
|
newer, and PyQt6.
|
||||||
|
|
||||||
@@ -184,14 +185,19 @@ running.
|
|||||||
right-click to delete.
|
right-click to delete.
|
||||||
- **Turkish and English interface**, following the system locale by default.
|
- **Turkish and English interface**, following the system locale by default.
|
||||||
|
|
||||||
## The global shortcuts need one logout
|
## The global shortcuts, and the logout KDE needs
|
||||||
|
|
||||||
KWin only reads `kglobalshortcutsrc` at startup, so the shortcuts `install.sh`
|
KWin only reads `kglobalshortcutsrc` at startup, so the shortcuts `install.sh`
|
||||||
writes will not fire until you log out and back in. Until then, Settings →
|
writes will not fire until you log out and back in. Until then, Settings →
|
||||||
Shortcuts → **built-in listener** reads `/dev/input` and catches the combination
|
Shortcuts → **built-in listener** reads `/dev/input` and catches the combination
|
||||||
itself. The difference: it does not swallow the key, so `Ctrl+Space` also reaches
|
itself. The difference: it does not swallow the key, so `Ctrl+Space` also reaches
|
||||||
the focused application (some editors will pop up autocomplete). The listener
|
the focused application (some editors will pop up autocomplete). The listener
|
||||||
needs your user in the `input` group: `sudo usermod -aG input $USER`.
|
needs your user in the `input` group: `sudo usermod -aG input $USER`. On GNOME
|
||||||
|
the shortcut works the moment it is installed, and on a desktop that keeps no
|
||||||
|
registry at all (i3, XFCE, sway and most others) the listener is the whole
|
||||||
|
mechanism: nothing is installed, nothing waits for a logout, and Settings →
|
||||||
|
Shortcuts shows the command to bind if you would rather your desktop owned the
|
||||||
|
keys.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -211,7 +217,7 @@ vad.py deciding whether a recording holds speech at all
|
|||||||
filetranscribe.py file transcription: ffmpeg, chunking, timestamps
|
filetranscribe.py file transcription: ffmpeg, chunking, timestamps
|
||||||
overlay.py the corner indicator
|
overlay.py the corner indicator
|
||||||
settings_ui.py settings window
|
settings_ui.py settings window
|
||||||
hotkey.py KDE shortcut installation, the evdev listener, Carbon on a Mac
|
hotkey.py the desktop's shortcut registry, the evdev listener, Carbon on a Mac
|
||||||
paste.py wl-clipboard and ydotool wrappers, pbcopy and CoreGraphics
|
paste.py wl-clipboard and ydotool wrappers, pbcopy and CoreGraphics
|
||||||
trayicon.py the tray icons, drawn where there is no icon theme
|
trayicon.py the tray icons, drawn where there is no icon theme
|
||||||
i18n.py the string table
|
i18n.py the string table
|
||||||
|
|||||||
+9
-4
@@ -4,7 +4,8 @@
|
|||||||
çevrilir, bir model transkripti temizler (ıı'lar, tekrarlar, eksik noktalama),
|
çevrilir, bir model transkripti temizler (ıı'lar, tekrarlar, eksik noktalama),
|
||||||
sonuç panoya kopyalanır ve o an yazdığın pencereye yapıştırılır.
|
sonuç panoya kopyalanır ve o an yazdığın pencereye yapıştırılır.
|
||||||
|
|
||||||
KDE Plasma 6 / Wayland için yazıldı, GNOME X11 ve macOS'ta da çalışır. Sistem
|
KDE Plasma 6 / Wayland için yazıldı; GNOME X11'de, macOS'ta ve klavyeyi
|
||||||
|
okumasına izin veren diğer Linux masaüstlerinde de çalışır. Sistem
|
||||||
paketleri dışında bağımlılığı yok: sadece Python standart kütüphanesi (3.11 veya
|
paketleri dışında bağımlılığı yok: sadece Python standart kütüphanesi (3.11 veya
|
||||||
üstü) ve PyQt6.
|
üstü) ve PyQt6.
|
||||||
|
|
||||||
@@ -182,14 +183,18 @@ olmasını ister.
|
|||||||
silebilirsin.
|
silebilirsin.
|
||||||
- **Türkçe ve İngilizce arayüz**, varsayılan olarak sistem dilini izler.
|
- **Türkçe ve İngilizce arayüz**, varsayılan olarak sistem dilini izler.
|
||||||
|
|
||||||
## Global kısayollar için bir kez oturum kapatmak gerekir
|
## Global kısayollar ve KDE'nin istediği oturum kapatma
|
||||||
|
|
||||||
KWin `kglobalshortcutsrc` dosyasını yalnızca açılışta okur, yani `install.sh`'ın
|
KWin `kglobalshortcutsrc` dosyasını yalnızca açılışta okur, yani `install.sh`'ın
|
||||||
yazdığı kısayollar oturumu yeniden açana kadar tetiklenmez. O zamana kadar Ayarlar →
|
yazdığı kısayollar oturumu yeniden açana kadar tetiklenmez. O zamana kadar Ayarlar →
|
||||||
Kısayollar → **yerleşik dinleyici** `/dev/input` üzerinden kombinasyonu kendisi
|
Kısayollar → **yerleşik dinleyici** `/dev/input` üzerinden kombinasyonu kendisi
|
||||||
yakalar. Tek farkı: tuşu yutmaz, yani `Ctrl+Space` odaktaki uygulamaya da iletilir
|
yakalar. Tek farkı: tuşu yutmaz, yani `Ctrl+Space` odaktaki uygulamaya da iletilir
|
||||||
(bazı editörlerde otomatik tamamlama açılabilir). Dinleyici kullanıcının `input`
|
(bazı editörlerde otomatik tamamlama açılabilir). Dinleyici kullanıcının `input`
|
||||||
grubunda olmasını gerektirir: `sudo usermod -aG input $USER`.
|
grubunda olmasını gerektirir: `sudo usermod -aG input $USER`. GNOME'da kısayol
|
||||||
|
kurulduğu anda çalışır; hiç kayıt defteri tutmayan masaüstlerinde (i3, XFCE,
|
||||||
|
sway ve çoğu diğeri) dinleyici mekanizmanın kendisidir: hiçbir şey kurulmaz,
|
||||||
|
oturum kapatmak gerekmez, tuşları masaüstünün sahiplenmesini istersen Ayarlar →
|
||||||
|
Kısayollar sekmesi bağlanacak komutu gösterir.
|
||||||
|
|
||||||
## Dosyalar
|
## Dosyalar
|
||||||
|
|
||||||
@@ -209,7 +214,7 @@ vad.py kayıtta gerçekten konuşma var mı kararı
|
|||||||
filetranscribe.py dosyadan transkript: ffmpeg, parçalama, zaman damgaları
|
filetranscribe.py dosyadan transkript: ffmpeg, parçalama, zaman damgaları
|
||||||
overlay.py köşedeki gösterge
|
overlay.py köşedeki gösterge
|
||||||
settings_ui.py ayarlar penceresi
|
settings_ui.py ayarlar penceresi
|
||||||
hotkey.py KDE kısayol kurulumu, evdev dinleyici, Mac'te Carbon
|
hotkey.py masaüstünün kısayol kaydı, evdev dinleyici, Mac'te Carbon
|
||||||
paste.py wl-clipboard ve ydotool sarmalayıcıları, pbcopy ve CoreGraphics
|
paste.py wl-clipboard ve ydotool sarmalayıcıları, pbcopy ve CoreGraphics
|
||||||
trayicon.py tepsi simgeleri, ikon teması olmayan yerler için çizilmiş
|
trayicon.py tepsi simgeleri, ikon teması olmayan yerler için çizilmiş
|
||||||
i18n.py metin tablosu
|
i18n.py metin tablosu
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import paste
|
|||||||
|
|
||||||
NOT_RUNNING = 3
|
NOT_RUNNING = 3
|
||||||
|
|
||||||
# Verbs that start the application when none is running, which is what the KDE
|
# Verbs that start the application when none is running, which is what a
|
||||||
# shortcut has always relied on: press the key on a fresh login and Dikte comes
|
# shortcut registered with the desktop has always relied on: press the key on a
|
||||||
# up recording.
|
# fresh login and Dikte comes up recording.
|
||||||
GUI_VERBS = {"", "settings", "toggle", "ask", "meeting"}
|
GUI_VERBS = {"", "settings", "toggle", "ask", "meeting"}
|
||||||
|
|
||||||
# Asking a process that is not there to stop, cancel or quit is not a failure;
|
# Asking a process that is not there to stop, cancel or quit is not a failure;
|
||||||
|
|||||||
@@ -365,8 +365,8 @@ class Dikte:
|
|||||||
|
|
||||||
def _external(self, name, handler):
|
def _external(self, name, handler):
|
||||||
# The built-in listener sees the key press the instant it happens, so a
|
# The built-in listener sees the key press the instant it happens, so a
|
||||||
# toggle arriving right behind one is the KDE shortcut catching up on
|
# toggle arriving right behind one is the desktop's own shortcut catching
|
||||||
# that same press. Its lateness is also the proof we were waiting for
|
# up on that same press. Its lateness is also the proof we were waiting for
|
||||||
# that the shortcut is live, which leaves the listener with nothing to
|
# that the shortcut is live, which leaves the listener with nothing to
|
||||||
# do but double every press.
|
# do but double every press.
|
||||||
# Where nothing was installed there is no shortcut to catch up, and
|
# Where nothing was installed there is no shortcut to catch up, and
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
"""Global shortcuts: the desktop's own registry, plus a listener of our own.
|
"""Global shortcuts: the desktop's own registry, plus a listener of our own.
|
||||||
|
|
||||||
Two things have to happen for a key combination to reach Dikte. Somewhere has
|
Two things have to happen for a key combination to reach Dikte. Somewhere has
|
||||||
to be told about it, and something has to be listening. On Linux that is the
|
to be told about it, and something has to be listening. Two desktops keep a
|
||||||
desktop's shortcut registry (KDE's file, GNOME's gsettings) and a reader of
|
registry we can write into and read back, and something outside Dikte acts on
|
||||||
/dev/input for the wait until the registry is live. macOS has no registry to
|
it: KDE has a file, GNOME has gsettings. There the /dev/input listener only
|
||||||
write into: the application asks Carbon for the combination while it runs, so
|
covers the wait until the registry is live.
|
||||||
there the listener is not a fallback but the whole mechanism.
|
|
||||||
|
Everywhere else there is nothing to write into, so Dikte holds the combination
|
||||||
|
itself for as long as it runs: macOS asks Carbon for it, and every other Linux
|
||||||
|
session (i3, XFCE, Cinnamon, sway, whatever the session calls itself) leans on
|
||||||
|
the /dev/input listener. That is not a fallback there, it is the mechanism, so
|
||||||
|
nothing about installing or removing a registry entry should be offered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
@@ -105,13 +110,23 @@ def parse_shortcut(text):
|
|||||||
return mods, key
|
return mods, key
|
||||||
|
|
||||||
|
|
||||||
|
# What the running listener holds. Where there is no registry this is the whole
|
||||||
|
# of "installed", and it lasts as long as the process does: there is no file,
|
||||||
|
# and no other program to read one. Written by the listener that is in use,
|
||||||
|
# read by the status line, so what Settings shows is what is actually being
|
||||||
|
# listened for.
|
||||||
|
_REGISTERED = {}
|
||||||
|
|
||||||
|
|
||||||
# --- built-in listener ----------------------------------------------------
|
# --- built-in listener ----------------------------------------------------
|
||||||
|
|
||||||
class EvdevHotkey(QObject):
|
class EvdevHotkey(QObject):
|
||||||
"""Catches global shortcuts by reading /dev/input directly.
|
"""Catches global shortcuts by reading /dev/input directly.
|
||||||
|
|
||||||
It does not swallow the key; the focused application sees the combination
|
It does not swallow the key; the focused application sees the combination
|
||||||
too. This is the fallback that works before the KDE shortcut goes live.
|
too. On KDE that is the price of not waiting for the next login, and the
|
||||||
|
registry takes over once it is live. On a desktop with no registry at all
|
||||||
|
it is the only way the keys arrive, and the price is permanent.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
triggered = pyqtSignal(str) # the name the binding was registered under
|
triggered = pyqtSignal(str) # the name the binding was registered under
|
||||||
@@ -154,6 +169,10 @@ class EvdevHotkey(QObject):
|
|||||||
))
|
))
|
||||||
return False
|
return False
|
||||||
self._bindings = parsed
|
self._bindings = parsed
|
||||||
|
for name, shortcut in bindings.items():
|
||||||
|
spec = SHORTCUTS.get(name)
|
||||||
|
if spec and shortcut:
|
||||||
|
_REGISTERED[spec.desktop_id] = shortcut
|
||||||
self._stop.clear()
|
self._stop.clear()
|
||||||
self._thread = threading.Thread(target=self._loop, args=(devices,), daemon=True)
|
self._thread = threading.Thread(target=self._loop, args=(devices,), daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
@@ -164,6 +183,7 @@ class EvdevHotkey(QObject):
|
|||||||
if self._thread:
|
if self._thread:
|
||||||
self._thread.join(timeout=1.5)
|
self._thread.join(timeout=1.5)
|
||||||
self._thread = None
|
self._thread = None
|
||||||
|
_REGISTERED.clear()
|
||||||
|
|
||||||
def _open_devices(self):
|
def _open_devices(self):
|
||||||
fds = []
|
fds = []
|
||||||
@@ -246,12 +266,6 @@ HOTKEY_PRESSED = 5 # kEventHotKeyPressed
|
|||||||
PARAMETER_ANY = "----" # kEventParamDirectObject / typeWildCard
|
PARAMETER_ANY = "----" # kEventParamDirectObject / typeWildCard
|
||||||
HOTKEY_ID_PARAMETER = "hkid"
|
HOTKEY_ID_PARAMETER = "hkid"
|
||||||
|
|
||||||
# What the running listener holds. This is the whole of "installed" on macOS,
|
|
||||||
# and it lasts as long as the process does: there is no file, and no other
|
|
||||||
# program to read one. Written by CarbonHotkey.start(), read by the status
|
|
||||||
# line, so what Settings shows is what the Mac actually gave us.
|
|
||||||
_REGISTERED = {}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_macos_shortcut(text):
|
def parse_macos_shortcut(text):
|
||||||
"""'Cmd+Space' -> (256, 49), or (None, None) when unusable."""
|
"""'Cmd+Space' -> (256, 49), or (None, None) when unusable."""
|
||||||
@@ -428,13 +442,39 @@ def _carbon():
|
|||||||
|
|
||||||
# --- the desktop's own shortcut -------------------------------------------
|
# --- the desktop's own shortcut -------------------------------------------
|
||||||
|
|
||||||
|
# The four ways a combination can reach Dikte. Everything below asks backend()
|
||||||
|
# rather than looking at the session itself, so the name shown, the status read
|
||||||
|
# back, what Install writes and what the installer promises cannot disagree
|
||||||
|
# about which one this session got.
|
||||||
|
KDE = "kde"
|
||||||
|
GNOME = "gnome"
|
||||||
|
MACOS = "macos"
|
||||||
|
LISTENER = "listener"
|
||||||
|
|
||||||
|
|
||||||
def _macos():
|
def _macos():
|
||||||
return sys.platform == "darwin"
|
return sys.platform == "darwin"
|
||||||
|
|
||||||
|
|
||||||
def _gnome():
|
def backend():
|
||||||
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
"""Which shortcut mechanism this session has.
|
||||||
return "gnome" in desktop and shutil.which("gsettings") is not None
|
|
||||||
|
A desktop only counts when the program that writes its registry is
|
||||||
|
installed too: a GNOME session without gsettings, or a Plasma one without
|
||||||
|
kwriteconfig6, has nothing we can register into. Anything unrecognised is
|
||||||
|
the listener's, which is every other Linux desktop and needs nothing from
|
||||||
|
the session at all.
|
||||||
|
"""
|
||||||
|
if _macos():
|
||||||
|
return MACOS
|
||||||
|
names = os.environ.get("XDG_CURRENT_DESKTOP", "").lower().split(":")
|
||||||
|
names = [name.strip() for name in names if name.strip()]
|
||||||
|
if any("gnome" in name for name in names) and shutil.which("gsettings"):
|
||||||
|
return GNOME
|
||||||
|
if (any("kde" in name or "plasma" in name for name in names)
|
||||||
|
and shutil.which("kwriteconfig6")):
|
||||||
|
return KDE
|
||||||
|
return LISTENER
|
||||||
|
|
||||||
|
|
||||||
def _gnome_path(desktop_id):
|
def _gnome_path(desktop_id):
|
||||||
@@ -581,55 +621,77 @@ def installs_shortcuts():
|
|||||||
"""Whether this system keeps a shortcut registry to write into.
|
"""Whether this system keeps a shortcut registry to write into.
|
||||||
|
|
||||||
KDE and GNOME do, and something outside Dikte reads it, so the combination
|
KDE and GNOME do, and something outside Dikte reads it, so the combination
|
||||||
survives Dikte being closed. macOS does not: there is nothing to install,
|
survives Dikte being closed. macOS and the plain listener do not: there is
|
||||||
nothing to remove, and Settings should not offer either.
|
nothing to install, nothing to remove, and Settings should not offer either.
|
||||||
"""
|
"""
|
||||||
return not _macos()
|
return backend() in (KDE, GNOME)
|
||||||
|
|
||||||
|
|
||||||
def shortcut_needs_restart():
|
def shortcut_needs_restart():
|
||||||
"""Whether an installed shortcut waits for the next login before it works.
|
"""Whether an installed shortcut waits for the next login before it works.
|
||||||
|
|
||||||
KWin reads kglobalshortcutsrc once, when it starts. GNOME picks a binding
|
KWin reads kglobalshortcutsrc once, when it starts. GNOME picks a binding
|
||||||
up as it is written, and macOS never had one to write.
|
up as it is written, and the other two never had one to write.
|
||||||
"""
|
"""
|
||||||
return not _macos() and not _gnome()
|
return backend() == KDE
|
||||||
|
|
||||||
|
|
||||||
def install_shortcut(shortcut, exec_command, name="Dikte: start/stop recording",
|
def install_shortcut(shortcut, exec_command, name="Dikte: start/stop recording",
|
||||||
desktop_id=DESKTOP_ID):
|
desktop_id=DESKTOP_ID):
|
||||||
if _macos():
|
which = backend()
|
||||||
_REGISTERED[desktop_id] = shortcut
|
if which == GNOME:
|
||||||
|
return install_gnome_shortcut(shortcut, exec_command, name, desktop_id)
|
||||||
|
if which == KDE:
|
||||||
|
return install_kde_shortcut(shortcut, exec_command, name, desktop_id)
|
||||||
|
_REGISTERED[desktop_id] = shortcut
|
||||||
|
if which == MACOS:
|
||||||
return True, t(
|
return True, t(
|
||||||
"Shortcut saved: {shortcut}\nDikte holds this one itself while it "
|
"Shortcut saved: {shortcut}\nDikte holds this one itself while it "
|
||||||
"is running, so it works as soon as the settings are saved.",
|
"is running, so it works as soon as the settings are saved.",
|
||||||
shortcut=shortcut,
|
shortcut=shortcut,
|
||||||
)
|
)
|
||||||
if _gnome():
|
return True, t(
|
||||||
return install_gnome_shortcut(shortcut, exec_command, name, desktop_id)
|
"Shortcut saved: {shortcut}\n{desktop} has no shortcut registry to "
|
||||||
return install_kde_shortcut(shortcut, exec_command, name, desktop_id)
|
"install into, so Dikte listens for this one itself while it is "
|
||||||
|
"running. It works as soon as the settings are saved.",
|
||||||
|
shortcut=shortcut, desktop=desktop_name(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_shortcut(desktop_id=DESKTOP_ID):
|
def remove_shortcut(desktop_id=DESKTOP_ID):
|
||||||
if _macos():
|
which = backend()
|
||||||
_REGISTERED.pop(desktop_id, None)
|
if which == GNOME:
|
||||||
elif _gnome():
|
|
||||||
remove_gnome_shortcut(desktop_id)
|
remove_gnome_shortcut(desktop_id)
|
||||||
else:
|
elif which == KDE:
|
||||||
remove_kde_shortcut(desktop_id)
|
remove_kde_shortcut(desktop_id)
|
||||||
|
else:
|
||||||
|
_REGISTERED.pop(desktop_id, None)
|
||||||
|
|
||||||
|
|
||||||
def shortcut_status(desktop_id=DESKTOP_ID):
|
def shortcut_status(desktop_id=DESKTOP_ID):
|
||||||
if _macos():
|
which = backend()
|
||||||
return _REGISTERED.get(desktop_id)
|
if which == GNOME:
|
||||||
return (gnome_shortcut_status(desktop_id) if _gnome()
|
return gnome_shortcut_status(desktop_id)
|
||||||
else kde_shortcut_status(desktop_id))
|
if which == KDE:
|
||||||
|
return kde_shortcut_status(desktop_id)
|
||||||
|
return _REGISTERED.get(desktop_id)
|
||||||
|
|
||||||
|
|
||||||
def desktop_name():
|
def desktop_name():
|
||||||
if _macos():
|
"""What to call this session in the interface.
|
||||||
|
|
||||||
|
The listener's desktops get the name the session gave itself, so an i3 user
|
||||||
|
is told about i3 rather than about a KDE that is not running.
|
||||||
|
"""
|
||||||
|
which = backend()
|
||||||
|
if which == MACOS:
|
||||||
return "macOS"
|
return "macOS"
|
||||||
return "GNOME" if _gnome() else "KDE"
|
if which == GNOME:
|
||||||
|
return "GNOME"
|
||||||
|
if which == KDE:
|
||||||
|
return "KDE"
|
||||||
|
name = os.environ.get("XDG_CURRENT_DESKTOP", "").split(":")[0].strip()
|
||||||
|
return name or "This desktop"
|
||||||
|
|
||||||
|
|
||||||
# --- KDE ------------------------------------------------------------------
|
# --- KDE ------------------------------------------------------------------
|
||||||
@@ -713,9 +775,12 @@ def kde_shortcut_status(desktop_id=DESKTOP_ID):
|
|||||||
|
|
||||||
def conflicting_shortcuts(shortcut, desktop_id=DESKTOP_ID):
|
def conflicting_shortcuts(shortcut, desktop_id=DESKTOP_ID):
|
||||||
"""Names of other KDE entries bound to the same combination."""
|
"""Names of other KDE entries bound to the same combination."""
|
||||||
if _macos():
|
if backend() != KDE:
|
||||||
# There is no list to read: macOS answers the question by refusing the
|
# Nowhere else has a list to read. macOS answers the question by
|
||||||
# registration, which CarbonHotkey reports when it asks for the key.
|
# refusing the registration, which CarbonHotkey reports when it asks
|
||||||
|
# for the key; the other two would only be reading a file their session
|
||||||
|
# never looks at, and a leftover one from a Plasma install the user has
|
||||||
|
# since left would refuse perfectly good combinations.
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
text = SHORTCUTS_FILE.read_text(encoding="utf-8")
|
text = SHORTCUTS_FILE.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -308,6 +308,8 @@ TR = {
|
|||||||
"Registered in KDE: {shortcut}": "KDE'de kayıtlı: {shortcut}",
|
"Registered in KDE: {shortcut}": "KDE'de kayıtlı: {shortcut}",
|
||||||
"No KDE shortcut installed.": "KDE kısayolu kurulu değil.",
|
"No KDE shortcut installed.": "KDE kısayolu kurulu değil.",
|
||||||
"Registered in {desktop}: {shortcut}": "{desktop}'da kayıtlı: {shortcut}",
|
"Registered in {desktop}: {shortcut}": "{desktop}'da kayıtlı: {shortcut}",
|
||||||
|
"Held by Dikte while it runs: {shortcut}":
|
||||||
|
"Dikte çalıştığı sürece tutuyor: {shortcut}",
|
||||||
"No global shortcut installed.": "Global kısayol kurulu değil.",
|
"No global shortcut installed.": "Global kısayol kurulu değil.",
|
||||||
"No global shortcut installed. The tray menu starts a meeting too.":
|
"No global shortcut installed. The tray menu starts a meeting too.":
|
||||||
"Global kısayol kurulu değil. Toplantı tepsi menüsünden de başlatılabilir.",
|
"Global kısayol kurulu değil. Toplantı tepsi menüsünden de başlatılabilir.",
|
||||||
@@ -351,6 +353,18 @@ TR = {
|
|||||||
"meantime.":
|
"meantime.":
|
||||||
"Dikte bu kombinasyonları çalışırken macOS'tan kendisi ister. Hiçbir şey "
|
"Dikte bu kombinasyonları çalışırken macOS'tan kendisi ister. Hiçbir şey "
|
||||||
"kurulmaz ve o sırada başka hiçbir uygulama bu tuşları almaz.",
|
"kurulmaz ve o sırada başka hiçbir uygulama bu tuşları almaz.",
|
||||||
|
"{desktop} keeps no shortcut registry, so Dikte listens for these "
|
||||||
|
"combinations itself while it is running. Your user has to be able to read "
|
||||||
|
"/dev/input for that, and the focused application receives the keys as "
|
||||||
|
"well. To have the desktop own them instead, bind this command in its own "
|
||||||
|
"configuration, with the last word swapped for pause, cancel, ask or "
|
||||||
|
"meeting:":
|
||||||
|
"{desktop} kısayol kaydı tutmaz, bu yüzden Dikte bu kombinasyonları "
|
||||||
|
"çalıştığı sürece kendisi dinler. Bunun için kullanıcının /dev/input'u "
|
||||||
|
"okuyabilmesi gerekir, ayrıca tuşlar odaktaki uygulamaya da iletilir. "
|
||||||
|
"Tuşları masaüstünün sahiplenmesini istersen, son kelimeyi pause, "
|
||||||
|
"cancel, ask veya meeting ile değiştirerek şu komutu kendi "
|
||||||
|
"yapılandırmasında bir tuşa bağla:",
|
||||||
"Shortcut conflict": "Kısayol çakışması",
|
"Shortcut conflict": "Kısayol çakışması",
|
||||||
"{shortcut} is also used by:\n\n{list}\n\nInstall anyway?":
|
"{shortcut} is also used by:\n\n{list}\n\nInstall anyway?":
|
||||||
"{shortcut} şu girdilerde de kullanılıyor:\n\n{list}\n\nYine de kurulsun mu?",
|
"{shortcut} şu girdilerde de kullanılıyor:\n\n{list}\n\nYine de kurulsun mu?",
|
||||||
@@ -367,6 +381,12 @@ TR = {
|
|||||||
"running, so it works as soon as the settings are saved.":
|
"running, so it works as soon as the settings are saved.":
|
||||||
"Kısayol kaydedildi: {shortcut}\nDikte bunu çalıştığı sürece kendisi "
|
"Kısayol kaydedildi: {shortcut}\nDikte bunu çalıştığı sürece kendisi "
|
||||||
"tutar, yani ayarlar kaydedilir kaydedilmez çalışır.",
|
"tutar, yani ayarlar kaydedilir kaydedilmez çalışır.",
|
||||||
|
"Shortcut saved: {shortcut}\n{desktop} has no shortcut registry to install "
|
||||||
|
"into, so Dikte listens for this one itself while it is running. It works "
|
||||||
|
"as soon as the settings are saved.":
|
||||||
|
"Kısayol kaydedildi: {shortcut}\n{desktop} kurulacak bir kısayol kaydı "
|
||||||
|
"tutmadığı için Dikte bunu çalıştığı sürece kendisi dinler. Ayarlar "
|
||||||
|
"kaydedilir kaydedilmez çalışır.",
|
||||||
"Could not reach the macOS shortcut service: {error}":
|
"Could not reach the macOS shortcut service: {error}":
|
||||||
"macOS kısayol servisine ulaşılamadı: {error}",
|
"macOS kısayol servisine ulaşılamadı: {error}",
|
||||||
"macOS would not give Dikte {shortcut}; another application already holds it.":
|
"macOS would not give Dikte {shortcut}; another application already holds it.":
|
||||||
|
|||||||
+22
-5
@@ -163,11 +163,28 @@ if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
|||||||
if [[ -n "$CANCEL_SHORTCUT" ]]; then
|
if [[ -n "$CANCEL_SHORTCUT" ]]; then
|
||||||
register cancel "$CANCEL_SHORTCUT" "Discard the recording"
|
register cancel "$CANCEL_SHORTCUT" "Discard the recording"
|
||||||
fi
|
fi
|
||||||
if [[ "${XDG_CURRENT_DESKTOP:-}" != *[Gg][Nn][Oo][Mm][Ee]* ]]; then
|
# Which of the three mechanisms this session got is Dikte's answer to give,
|
||||||
warn "KWin only reads these at startup, so they go live after your next"
|
# not this script's. Guessing from XDG_CURRENT_DESKTOP here is how every
|
||||||
say "login. Until then open Settings → Shortcuts and turn on the"
|
# session that was neither GNOME nor KDE used to be promised a KWin that was
|
||||||
say "built-in listener to use them right away."
|
# never running.
|
||||||
fi
|
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
||||||
|
kde)
|
||||||
|
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 "built-in listener to use them right away."
|
||||||
|
;;
|
||||||
|
gnome) ;;
|
||||||
|
*)
|
||||||
|
if id -nG 2>/dev/null | tr ' ' '\n' | grep -qx input; then
|
||||||
|
say "Your desktop keeps no shortcut registry, so Dikte listens for these"
|
||||||
|
say "keys itself while it is running."
|
||||||
|
else
|
||||||
|
warn "Your desktop keeps no shortcut registry, so Dikte listens for these"
|
||||||
|
say "keys itself, and it cannot read /dev/input yet:"
|
||||||
|
say " sudo usermod -aG input $(id -un) (then log out and back in)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
else
|
else
|
||||||
warn "PyQt6 is missing, so no shortcut was registered. Install it, then run:"
|
warn "PyQt6 is missing, so no shortcut was registered. Install it, then run:"
|
||||||
say "dikte shortcut install toggle --combo '$SHORTCUT'"
|
say "dikte shortcut install toggle --combo '$SHORTCUT'"
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ def script_path():
|
|||||||
|
|
||||||
|
|
||||||
def command_for(verb):
|
def command_for(verb):
|
||||||
"""The command line a KDE shortcut runs for one of the verbs."""
|
"""The command line a desktop's shortcut runs for one of the verbs.
|
||||||
|
|
||||||
|
Also what Settings shows an i3 or XFCE user to paste into their own
|
||||||
|
configuration, since there is no registry there for Dikte to write into.
|
||||||
|
"""
|
||||||
return f"{sys.executable} {script_path()} {verb}"
|
return f"{sys.executable} {script_path()} {verb}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+34
-9
@@ -1363,15 +1363,34 @@ class SettingsWindow(QDialog):
|
|||||||
)
|
)
|
||||||
elif hotkey.installs_shortcuts():
|
elif hotkey.installs_shortcuts():
|
||||||
explanation = t("The shortcut starts working as soon as it is installed.")
|
explanation = t("The shortcut starts working as soon as it is installed.")
|
||||||
else:
|
elif hotkey.backend() == hotkey.MACOS:
|
||||||
explanation = t(
|
explanation = t(
|
||||||
"Dikte asks macOS for these combinations itself, while it is "
|
"Dikte asks macOS for these combinations itself, while it is "
|
||||||
"running. Nothing is installed, and no other application receives "
|
"running. Nothing is installed, and no other application receives "
|
||||||
"them in the meantime."
|
"them in the meantime."
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# The desktops nobody writes a backend for. Saying "installed" here
|
||||||
|
# would be the old bug in words: there is no registry, the listener
|
||||||
|
# is the whole mechanism, and both of its costs are permanent
|
||||||
|
# rather than lasting until the next login.
|
||||||
|
explanation = t(
|
||||||
|
"{desktop} keeps no shortcut registry, so Dikte listens for "
|
||||||
|
"these combinations itself while it is running. Your user has "
|
||||||
|
"to be able to read /dev/input for that, and the focused "
|
||||||
|
"application receives the keys as well. To have the desktop own "
|
||||||
|
"them instead, bind this command in its own configuration, with "
|
||||||
|
"the last word swapped for pause, cancel, ask or meeting:",
|
||||||
|
desktop=hotkey.desktop_name(),
|
||||||
|
)
|
||||||
note = QLabel(explanation)
|
note = QLabel(explanation)
|
||||||
note.setWordWrap(True)
|
note.setWordWrap(True)
|
||||||
layout.addWidget(note)
|
layout.addWidget(note)
|
||||||
|
if hotkey.backend() == hotkey.LISTENER:
|
||||||
|
command = QLineEdit(ipc.command_for("toggle"))
|
||||||
|
command.setReadOnly(True)
|
||||||
|
command.setCursorPosition(0)
|
||||||
|
layout.addWidget(command)
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
@@ -1442,7 +1461,7 @@ class SettingsWindow(QDialog):
|
|||||||
"""The field a global shortcut is typed or picked in."""
|
"""The field a global shortcut is typed or picked in."""
|
||||||
box = QComboBox()
|
box = QComboBox()
|
||||||
box.setEditable(True)
|
box.setEditable(True)
|
||||||
box.addItems(MAC_SHORTCUTS if hotkey.desktop_name() == "macOS"
|
box.addItems(MAC_SHORTCUTS if hotkey.backend() == hotkey.MACOS
|
||||||
else SHORTCUTS)
|
else SHORTCUTS)
|
||||||
box.setCurrentText("")
|
box.setCurrentText("")
|
||||||
if placeholder:
|
if placeholder:
|
||||||
@@ -1491,8 +1510,9 @@ class SettingsWindow(QDialog):
|
|||||||
def _install_buttons(install_handler, remove_handler):
|
def _install_buttons(install_handler, remove_handler):
|
||||||
"""Install and Remove, where this system has somewhere to install into.
|
"""Install and Remove, where this system has somewhere to install into.
|
||||||
|
|
||||||
macOS has not: Dikte asks for the combination itself while it runs, so
|
macOS has not, and neither has a Linux desktop that keeps no registry:
|
||||||
there is nothing to write down and nothing to take back out.
|
Dikte holds the combination itself while it runs, so there is nothing
|
||||||
|
to write down and nothing to take back out.
|
||||||
"""
|
"""
|
||||||
if not hotkey.installs_shortcuts():
|
if not hotkey.installs_shortcuts():
|
||||||
return []
|
return []
|
||||||
@@ -1990,11 +2010,16 @@ class SettingsWindow(QDialog):
|
|||||||
def _refresh_shortcut_status(self, which):
|
def _refresh_shortcut_status(self, which):
|
||||||
_box, status, missing = self._shortcut_rows[which]
|
_box, status, missing = self._shortcut_rows[which]
|
||||||
current = hotkey.shortcut_status(hotkey.SHORTCUTS[which].desktop_id)
|
current = hotkey.shortcut_status(hotkey.SHORTCUTS[which].desktop_id)
|
||||||
status.setText(
|
if not current:
|
||||||
t("Registered in {desktop}: {shortcut}",
|
status.setText(missing)
|
||||||
desktop=hotkey.desktop_name(), shortcut=current) if current
|
elif hotkey.installs_shortcuts():
|
||||||
else missing
|
status.setText(t("Registered in {desktop}: {shortcut}",
|
||||||
)
|
desktop=hotkey.desktop_name(), shortcut=current))
|
||||||
|
else:
|
||||||
|
# Nothing was written anywhere: this is the combination the running
|
||||||
|
# process is holding, which is the only sense in which it exists.
|
||||||
|
status.setText(t("Held by Dikte while it runs: {shortcut}",
|
||||||
|
shortcut=current))
|
||||||
|
|
||||||
def _cleanup_provider_changed(self):
|
def _cleanup_provider_changed(self):
|
||||||
provider = self.cleanup_provider.currentData() or "openrouter"
|
provider = self.cleanup_provider.currentData() or "openrouter"
|
||||||
|
|||||||
+73
-11
@@ -174,38 +174,69 @@ class Bindings(DikteTest):
|
|||||||
|
|
||||||
|
|
||||||
class Chooser(DikteTest):
|
class Chooser(DikteTest):
|
||||||
"""Which desktop is asked to register the shortcut."""
|
"""Which mechanism the session gets, and everything keyed off that."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.patch_attr(hotkey.sys, "platform", "linux")
|
self.patch_attr(hotkey.sys, "platform", "linux")
|
||||||
|
self.addCleanup(hotkey._REGISTERED.clear)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def under(self, desktop, has_gsettings=True):
|
def under(self, desktop, tools=True):
|
||||||
"""A session that says it is this desktop, with or without gsettings."""
|
"""A session that says it is this desktop, with or without its tools."""
|
||||||
with mock.patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": desktop}), \
|
with mock.patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": desktop}), \
|
||||||
mock.patch.object(hotkey.shutil, "which",
|
mock.patch.object(hotkey.shutil, "which",
|
||||||
return_value="/usr/bin/gsettings"
|
return_value="/usr/bin/tool" if tools else None):
|
||||||
if has_gsettings else None):
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def test_gnome_when_the_session_says_so_and_gsettings_is_there(self):
|
def test_gnome_when_the_session_says_so_and_gsettings_is_there(self):
|
||||||
with self.under("GNOME"):
|
with self.under("GNOME"):
|
||||||
|
self.assertEqual(hotkey.backend(), hotkey.GNOME)
|
||||||
self.assertEqual(hotkey.desktop_name(), "GNOME")
|
self.assertEqual(hotkey.desktop_name(), "GNOME")
|
||||||
|
|
||||||
def test_kde_otherwise(self):
|
def test_kde_when_the_session_says_so_and_kwriteconfig_is_there(self):
|
||||||
with self.under("KDE"):
|
with self.under("KDE"):
|
||||||
|
self.assertEqual(hotkey.backend(), hotkey.KDE)
|
||||||
self.assertEqual(hotkey.desktop_name(), "KDE")
|
self.assertEqual(hotkey.desktop_name(), "KDE")
|
||||||
|
|
||||||
def test_a_gnome_session_with_no_gsettings_falls_back(self):
|
def test_a_desktop_with_no_registry_is_the_listeners(self):
|
||||||
"""Nothing to write the binding with, so KDE's file is the only try."""
|
"""The bug this replaced: i3 was told KDE, and KWin was not running."""
|
||||||
with self.under("GNOME", has_gsettings=False):
|
for desktop in ("i3", "XFCE", "X-Cinnamon", "sway", "MATE", ""):
|
||||||
self.assertEqual(hotkey.desktop_name(), "KDE")
|
with self.subTest(desktop=desktop), self.under(desktop):
|
||||||
|
self.assertEqual(hotkey.backend(), hotkey.LISTENER)
|
||||||
|
|
||||||
|
def test_the_desktop_that_has_no_registry_is_called_by_its_own_name(self):
|
||||||
|
with self.under("i3"):
|
||||||
|
self.assertEqual(hotkey.desktop_name(), "i3")
|
||||||
|
with self.under("XFCE:GNOME-Flashback", tools=False):
|
||||||
|
self.assertEqual(hotkey.desktop_name(), "XFCE")
|
||||||
|
with self.under(""):
|
||||||
|
self.assertEqual(hotkey.desktop_name(), "This desktop")
|
||||||
|
|
||||||
|
def test_a_gnome_session_with_no_gsettings_falls_back_to_the_listener(self):
|
||||||
|
"""Nothing to write the binding with, and KDE's file is not an answer:
|
||||||
|
KWin is no more running here than it is on i3."""
|
||||||
|
with self.under("GNOME", tools=False):
|
||||||
|
self.assertEqual(hotkey.backend(), hotkey.LISTENER)
|
||||||
|
|
||||||
def test_the_desktop_is_matched_loosely(self):
|
def test_the_desktop_is_matched_loosely(self):
|
||||||
for desktop in ("GNOME", "ubuntu:GNOME", "gnome"):
|
for desktop in ("GNOME", "ubuntu:GNOME", "gnome"):
|
||||||
with self.subTest(desktop=desktop), self.under(desktop):
|
with self.subTest(desktop=desktop), self.under(desktop):
|
||||||
self.assertEqual(hotkey.desktop_name(), "GNOME")
|
self.assertEqual(hotkey.backend(), hotkey.GNOME)
|
||||||
|
for desktop in ("KDE", "KDE:plasma", "plasma"):
|
||||||
|
with self.subTest(desktop=desktop), self.under(desktop):
|
||||||
|
self.assertEqual(hotkey.backend(), hotkey.KDE)
|
||||||
|
|
||||||
|
def test_only_a_registry_is_installed_into_and_only_kwin_waits(self):
|
||||||
|
with self.under("KDE"):
|
||||||
|
self.assertTrue(hotkey.installs_shortcuts())
|
||||||
|
self.assertTrue(hotkey.shortcut_needs_restart())
|
||||||
|
with self.under("GNOME"):
|
||||||
|
self.assertTrue(hotkey.installs_shortcuts())
|
||||||
|
self.assertFalse(hotkey.shortcut_needs_restart())
|
||||||
|
with self.under("i3"):
|
||||||
|
self.assertFalse(hotkey.installs_shortcuts())
|
||||||
|
self.assertFalse(hotkey.shortcut_needs_restart())
|
||||||
|
|
||||||
def test_installing_goes_to_whichever_it_is(self):
|
def test_installing_goes_to_whichever_it_is(self):
|
||||||
with self.under("GNOME"), \
|
with self.under("GNOME"), \
|
||||||
@@ -220,6 +251,20 @@ class Chooser(DikteTest):
|
|||||||
hotkey.install_shortcut("Ctrl+Space", "dikte toggle")
|
hotkey.install_shortcut("Ctrl+Space", "dikte toggle")
|
||||||
kde.assert_called_once()
|
kde.assert_called_once()
|
||||||
|
|
||||||
|
def test_a_desktop_with_no_registry_installs_nothing_anywhere(self):
|
||||||
|
with self.under("i3"), \
|
||||||
|
mock.patch.object(hotkey, "install_kde_shortcut") as kde, \
|
||||||
|
mock.patch.object(hotkey, "install_gnome_shortcut") as gnome:
|
||||||
|
ok, message = hotkey.install_shortcut("Ctrl+Space", "dikte toggle")
|
||||||
|
self.assertEqual(hotkey.shortcut_status(), "Ctrl+Space")
|
||||||
|
hotkey.remove_shortcut()
|
||||||
|
self.assertIsNone(hotkey.shortcut_status())
|
||||||
|
kde.assert_not_called()
|
||||||
|
gnome.assert_not_called()
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertIn("i3", message)
|
||||||
|
self.assertNotIn("log out", message)
|
||||||
|
|
||||||
def test_removing_and_reading_back_go_to_the_same_one(self):
|
def test_removing_and_reading_back_go_to_the_same_one(self):
|
||||||
with self.under("GNOME"), \
|
with self.under("GNOME"), \
|
||||||
mock.patch.object(hotkey, "remove_gnome_shortcut") as remove, \
|
mock.patch.object(hotkey, "remove_gnome_shortcut") as remove, \
|
||||||
@@ -230,6 +275,17 @@ class Chooser(DikteTest):
|
|||||||
remove.assert_called_once()
|
remove.assert_called_once()
|
||||||
status.assert_called_once()
|
status.assert_called_once()
|
||||||
|
|
||||||
|
def test_only_kde_has_a_list_of_conflicts_to_read(self):
|
||||||
|
"""A leftover kglobalshortcutsrc from a Plasma the user has since left
|
||||||
|
would otherwise refuse combinations nothing is holding."""
|
||||||
|
rc = self.path("kglobalshortcutsrc")
|
||||||
|
rc.write_text(SHORTCUTS_RC, encoding="utf-8")
|
||||||
|
self.patch_attr(hotkey, "SHORTCUTS_FILE", rc)
|
||||||
|
with self.under("KDE"):
|
||||||
|
self.assertTrue(hotkey.conflicting_shortcuts("Meta+W"))
|
||||||
|
with self.under("i3"):
|
||||||
|
self.assertEqual(hotkey.conflicting_shortcuts("Meta+W"), [])
|
||||||
|
|
||||||
|
|
||||||
@linux_only
|
@linux_only
|
||||||
class GnomeAccelerator(DikteTest):
|
class GnomeAccelerator(DikteTest):
|
||||||
@@ -372,6 +428,12 @@ class KdeShortcut(DikteTest):
|
|||||||
self.rc = self.path("kglobalshortcutsrc")
|
self.rc = self.path("kglobalshortcutsrc")
|
||||||
self.patch_attr(hotkey, "APPLICATIONS_DIR", self.apps)
|
self.patch_attr(hotkey, "APPLICATIONS_DIR", self.apps)
|
||||||
self.patch_attr(hotkey, "SHORTCUTS_FILE", self.rc)
|
self.patch_attr(hotkey, "SHORTCUTS_FILE", self.rc)
|
||||||
|
# A Plasma session with kwriteconfig6 on it, whatever the machine
|
||||||
|
# running the suite happens to be logged into.
|
||||||
|
session = mock.patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": "KDE"})
|
||||||
|
session.start()
|
||||||
|
self.addCleanup(session.stop)
|
||||||
|
self.patch_attr(hotkey.shutil, "which", lambda _name: "/usr/bin/tool")
|
||||||
|
|
||||||
def test_installing_writes_a_desktop_file_kwin_will_launch(self):
|
def test_installing_writes_a_desktop_file_kwin_will_launch(self):
|
||||||
with mock.patch.object(subprocess, "run", return_value=FakeCompleted()):
|
with mock.patch.object(subprocess, "run", return_value=FakeCompleted()):
|
||||||
|
|||||||
+66
-2
@@ -6,6 +6,7 @@ save, so a setting added to one half and not the other is silently reset the
|
|||||||
next time anybody presses Save. That is the failure this catches.
|
next time anybody presses Save. That is the failure this catches.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
@@ -107,13 +108,21 @@ class Settings(DikteTest):
|
|||||||
# one. Everything else about the window is the same on both.
|
# one. Everything else about the window is the same on both.
|
||||||
changed = CHANGED
|
changed = CHANGED
|
||||||
platform = "linux"
|
platform = "linux"
|
||||||
|
# A session with no shortcut registry, which is what most Linux desktops
|
||||||
|
# are. The subclasses below stand on the other two. Pinned rather than
|
||||||
|
# inherited from whatever the machine running the suite is logged into,
|
||||||
|
# since half the shortcut tab is built from the answer.
|
||||||
|
desktop = "i3"
|
||||||
|
tools: ClassVar[tuple] = ()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# No pactl, no model lists over the network, and no modal dialogue
|
# No pactl, no model lists over the network, and no modal dialogue
|
||||||
# waiting for somebody to press OK.
|
# waiting for somebody to press OK.
|
||||||
self.enterContext(mock.patch.object(sys, "platform", self.platform))
|
self.enterContext(mock.patch.object(sys, "platform", self.platform))
|
||||||
self.enterContext(only_these_tools())
|
self.enterContext(only_these_tools(*self.tools))
|
||||||
|
self.enterContext(mock.patch.dict(
|
||||||
|
os.environ, {"XDG_CURRENT_DESKTOP": self.desktop}))
|
||||||
self.enterContext(mock.patch.object(QMessageBox, "information"))
|
self.enterContext(mock.patch.object(QMessageBox, "information"))
|
||||||
self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
|
self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
|
||||||
"_load_models"))
|
"_load_models"))
|
||||||
@@ -254,6 +263,31 @@ class Settings(DikteTest):
|
|||||||
window = self.window(cfg.Config())
|
window = self.window(cfg.Config())
|
||||||
self.assertEqual(set(window._shortcut_rows), set(hotkey.SHORTCUTS))
|
self.assertEqual(set(window._shortcut_rows), set(hotkey.SHORTCUTS))
|
||||||
|
|
||||||
|
def shortcut_tab_text(self, window):
|
||||||
|
"""Everything the shortcut tab says, as one string."""
|
||||||
|
return "\n".join(
|
||||||
|
widget.text() for widget in
|
||||||
|
window.findChildren(settings_ui.QLabel)
|
||||||
|
+ window.findChildren(settings_ui.QLineEdit)
|
||||||
|
+ window.findChildren(settings_ui.QPushButton)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_the_shortcut_tab_talks_about_this_session_and_no_other(self):
|
||||||
|
"""A desktop with no registry is told the truth: nothing is installed
|
||||||
|
anywhere, Dikte is listening, and here is the command to bind if the
|
||||||
|
desktop should own the keys instead. It used to be promised a KWin that
|
||||||
|
was not running."""
|
||||||
|
window = self.window(cfg.Config())
|
||||||
|
text = self.shortcut_tab_text(window)
|
||||||
|
self.assertIn("i3 keeps no shortcut registry", text)
|
||||||
|
self.assertNotIn("KWin", text)
|
||||||
|
self.assertIn("dikte.py toggle", text)
|
||||||
|
# Not a choice to offer where it is the only mechanism there is.
|
||||||
|
self.assertTrue(window.evdev_enabled.isHidden())
|
||||||
|
self.assertFalse([button for button in
|
||||||
|
window.findChildren(settings_ui.QPushButton)
|
||||||
|
if "install" in button.text().lower()])
|
||||||
|
|
||||||
def test_emptying_a_shortcut_turns_it_off_but_not_the_toggle(self):
|
def test_emptying_a_shortcut_turns_it_off_but_not_the_toggle(self):
|
||||||
"""The application is unusable without the toggle, so that one box
|
"""The application is unusable without the toggle, so that one box
|
||||||
falls back. The rest stay empty, which is how they are switched off.
|
falls back. The rest stay empty, which is how they are switched off.
|
||||||
@@ -415,7 +449,16 @@ class MacSettings(Settings):
|
|||||||
def test_the_listener_is_not_offered_as_a_choice(self):
|
def test_the_listener_is_not_offered_as_a_choice(self):
|
||||||
"""It is the whole mechanism there; turning it off would leave nothing."""
|
"""It is the whole mechanism there; turning it off would leave nothing."""
|
||||||
window = self.window(cfg.Config())
|
window = self.window(cfg.Config())
|
||||||
self.assertFalse(window.evdev_enabled.isVisible())
|
self.assertTrue(window.evdev_enabled.isHidden())
|
||||||
|
|
||||||
|
def test_the_shortcut_tab_talks_about_this_session_and_no_other(self):
|
||||||
|
"""Carbon holds the keys here, so there is no command to bind and no
|
||||||
|
/dev/input to be let into."""
|
||||||
|
window = self.window(cfg.Config())
|
||||||
|
text = self.shortcut_tab_text(window)
|
||||||
|
self.assertIn("Dikte asks macOS for these combinations", text)
|
||||||
|
self.assertNotIn("KWin", text)
|
||||||
|
self.assertNotIn("dikte.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())
|
||||||
@@ -424,6 +467,27 @@ class MacSettings(Settings):
|
|||||||
self.assertEqual(offered, paste.MACOS.shortcuts)
|
self.assertEqual(offered, paste.MACOS.shortcuts)
|
||||||
|
|
||||||
|
|
||||||
|
class KdeSettings(Settings):
|
||||||
|
"""The same window on the one desktop that keeps a registry and makes you
|
||||||
|
wait for it. Nothing here is about KDE: it is the rest of the window,
|
||||||
|
checked on the platform where Install, Remove and the listener's own
|
||||||
|
checkbox are all on screen."""
|
||||||
|
|
||||||
|
desktop = "KDE"
|
||||||
|
tools: ClassVar[tuple] = ("kwriteconfig6",)
|
||||||
|
|
||||||
|
def test_the_shortcut_tab_talks_about_this_session_and_no_other(self):
|
||||||
|
window = self.window(cfg.Config())
|
||||||
|
text = self.shortcut_tab_text(window)
|
||||||
|
self.assertIn("KWin only reads shortcut settings at startup", text)
|
||||||
|
self.assertIn("Install as a KDE shortcut", text)
|
||||||
|
self.assertNotIn("keeps no shortcut registry", text)
|
||||||
|
self.assertNotIn("dikte.py toggle", text)
|
||||||
|
# Here it is a choice: the wait for the next login, or the key press
|
||||||
|
# reaching the focused application as well.
|
||||||
|
self.assertFalse(window.evdev_enabled.isHidden())
|
||||||
|
|
||||||
|
|
||||||
class Overlay(DikteTest):
|
class Overlay(DikteTest):
|
||||||
def overlay(self, **kwargs):
|
def overlay(self, **kwargs):
|
||||||
widget = overlay_module.Overlay(**kwargs)
|
widget = overlay_module.Overlay(**kwargs)
|
||||||
|
|||||||
+12
-5
@@ -85,17 +85,24 @@ echo "──────────────────"
|
|||||||
|
|
||||||
# 1. Global shortcuts ------------------------------------------------------
|
# 1. Global shortcuts ------------------------------------------------------
|
||||||
# Handed to Dikte while it can still run, because it is the half that knows
|
# Handed to Dikte while it can still run, because it is the half that knows
|
||||||
# whether they went into KDE's kglobalshortcutsrc or GNOME's gsettings. macOS
|
# whether they went into KDE's kglobalshortcutsrc, GNOME's gsettings, or
|
||||||
# keeps no registry: the combinations are held by the running process and are
|
# nowhere at all. macOS and the desktops with no registry hold the combinations
|
||||||
# gone the moment it stops, so there is nothing here to take back.
|
# in the running process, where they are gone the moment it stops, so there is
|
||||||
|
# nothing there to take back.
|
||||||
if ((MACOS)); then
|
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" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
ok "Global shortcuts unregistered"
|
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 2>/dev/null)" in
|
||||||
say "KWin reads that file at startup, so the keys are free after your next login."
|
kde)
|
||||||
|
ok "Global shortcuts unregistered"
|
||||||
|
say "KWin reads that file at startup, so the keys are free after your next login."
|
||||||
|
;;
|
||||||
|
gnome) ok "Global shortcuts unregistered" ;;
|
||||||
|
*) say "Nothing to unregister: Dikte listened for the keys itself, and they stop with it." ;;
|
||||||
|
esac
|
||||||
else
|
else
|
||||||
warn "PyQt6 is missing, so the shortcuts were left registered."
|
warn "PyQt6 is missing, so the shortcuts were left registered."
|
||||||
say "Remove them in your desktop's shortcut settings."
|
say "Remove them in your desktop's shortcut settings."
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ 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
|
# The combination stored in the settings, which is where Dikte itself reads it
|
||||||
# from and the one place that is the same on KDE and on GNOME.
|
# from and the one place that is the same whichever mechanism the session has.
|
||||||
setting() {
|
setting() {
|
||||||
[[ -n "$PY" ]] || return 0
|
[[ -n "$PY" ]] || return 0
|
||||||
"$PY" "$DIR/dikte.py" config get "$1" 2>/dev/null || true
|
"$PY" "$DIR/dikte.py" config get "$1" 2>/dev/null || true
|
||||||
|
|||||||
Reference in New Issue
Block a user