diff --git a/.github/workflows/blitztext-linux-ci.yml b/.github/workflows/blitztext-linux-ci.yml
index 0953c40..6b57430 100644
--- a/.github/workflows/blitztext-linux-ci.yml
+++ b/.github/workflows/blitztext-linux-ci.yml
@@ -46,7 +46,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.11", "3.12"]
+ python-version: ["3.11", "3.12", "3.14"]
env:
# Qt headless: Offscreen-Plattform, damit GUI-Tests ohne Display laufen.
QT_QPA_PLATFORM: offscreen
diff --git a/README.de.md b/README.de.md
index 043f17e..92044c9 100644
--- a/README.de.md
+++ b/README.de.md
@@ -2,7 +2,7 @@
Blitztext Linux
- Dein lokaler KI-Sprachassistent für KDE Plasma & Wayland
+ Dein lokaler KI-Sprachassistent für Linux-Desktops unter Wayland
@@ -11,7 +11,7 @@
-
+
🇬🇧 English | 🇩🇪 Deutsch
Sprache per Hotkey aufnehmen, lokal oder online transkribieren, optional per LLM umschreiben und direkt in die aktive Anwendung einfügen.
@@ -25,7 +25,7 @@
> [!IMPORTANT]
-> **Eigenständiger Linux-Port:** Dieses Repository enthält ausschließlich den Linux-Port von Blitztext – eine eigenständige Python 3/PyQt6-Implementierung optimiert für **Kubuntu/Ubuntu unter KDE Plasma mit Wayland**. Für die originale macOS-Version besuche bitte das [offizielle Haupt-Repository](https://github.com/cmagnussen/blitztext-app).
+> **Eigenständiger Linux-Port:** Dieses Repository enthält ausschließlich den Linux-Port von Blitztext – eine eigenständige Python 3/PyQt6-Implementierung, die unter **KDE Plasma mit Wayland** entwickelt und zusätzlich nativ unter **Ubuntu 26.04 GNOME mit Wayland** verifiziert wurde. Für die originale macOS-Version besuche bitte das [offizielle Haupt-Repository](https://github.com/cmagnussen/blitztext-app).
---
@@ -71,6 +71,16 @@ Die KI-Workflows helfen bei Formulierung, Ton und Emojis. Die passenden Einstell
+> [!IMPORTANT]
+> **Das Feld „API-Key-Umgebung“ ist kein Eingabefeld für den geheimen
+> API-Key.** Trage dort nur den Namen der Umgebungsvariable ein, für OpenAI
+> beispielsweise `OPENAI_API_KEY` und für OpenRouter `OPENROUTER_API_KEY`.
+> Der tatsächliche Schlüssel wird beim Speichern der Einstellungen weder in
+> `config.json` noch automatisch in eine andere Datei geschrieben. Lege ihn
+> separat in `~/.config/blitztext-linux/secrets.env` ab; `./run.sh` und der
+> systemd-User-Service laden diese Datei beim nächsten Start. Eine vollständige
+> Anleitung mit Dateiformat und Berechtigungen steht unter [Secrets](#secrets).
+
**LLM-Anbieter.** Blitztext unterstützt drei Anbieter-Modi, wählbar unter **Einstellungen → KI-Workflows → „LLM-Anbieter"**:
| Anbieter | Wann verwenden |
@@ -238,7 +248,8 @@ Es ist idempotent (mehrfach ausführbar) und erledigt alles vollautomatisch:
2. Installiert fehlende Systempakete (inkl. `pipx`).
3. Fragt den Betriebsmodus ab: globale Hotkeys mit `input`-Gruppe oder nur Fenster/Tray ohne globale Hotkeys.
4. Richtet eine `.venv` Umgebung ein und installiert `openai-whisper`/`faster-whisper`.
-5. Bereitet `ydotool.service` und den systemd-User-Service vor.
+5. Bereitet `ydotool.service` vor, installiert den systemd-User-Service und
+ aktiviert ihn für den Autostart, ohne ihn sofort zu starten.
### Nach der Installation
@@ -251,11 +262,21 @@ Es ist idempotent (mehrfach ausführbar) und erledigt alles vollautomatisch:
./run.sh
```
*(Erscheint das Tray-Symbol und reagieren die Hotkeys? Dann lief alles glatt!)*
-3. **Autostart aktivieren:**
+3. **Den bereits aktivierten Autostart-Dienst jetzt starten:**
```bash
systemctl --user start blitztext-linux
```
+> **Stand Ubuntu 26.04 GNOME / Wayland (verifiziert am 10.08.2026):** Eine
+> saubere Installation mit Python 3.14.4 bestand `scripts/verify.sh` vor der
+> ersten Benutzerkonfiguration mit 17 PASS, 0 FAIL, 0 WARN und danach mit
+> 18 PASS, 0 FAIL, 0 WARN. Hauptfenster, GNOME-AppIndicator-Tray, Audioaufnahme,
+> lokale `openai-whisper`-Transkription, globaler Left-Alt-Hotkey,
+> Wayland-Zwischenablage, `ydotool`-Auto-Paste und der Autostart als
+> systemd-User-Service wurden in der echten Desktop-Sitzung ausgeführt. Diese
+> Verifikation gilt für die native Installation, nicht für das experimentelle
+> Flatpak-Manifest.
+
Autostart wieder deaktivieren
@@ -300,9 +321,11 @@ pip install PyQt6 evdev openai pytest openai-whisper faster-whisper
```
**4. Whisper-Engine als Alternative via pipx**
-Falls du `openai-whisper` losgelöst von der venv installieren möchtest (umgeht Versionskonflikte auf neueren Ubuntu-Setups durch Python 3.11):
+Falls du `openai-whisper` losgelöst von der venv installieren möchtest, nutze
+den vorhandenen System-Interpreter. Der verifizierte Ubuntu-26.04-Pfad verwendet
+die Projekt-venv mit Python 3.14.4; pipx ist dort nicht erforderlich:
```bash
-pipx install --python "$(command -v python3.11)" openai-whisper
+pipx install --python "$(command -v python3)" openai-whisper
pipx inject openai-whisper faster-whisper # optional, für beschleunigte Ausführung
```
diff --git a/README.md b/README.md
index fdaad4f..179b1dc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Blitztext Linux
- Your local AI voice assistant for KDE Plasma & Wayland
+ Your local AI voice assistant for Linux desktops on Wayland
@@ -11,7 +11,7 @@
-
+
🇬🇧 English | 🇩🇪 Deutsch
Record speech via hotkey, transcribe locally or online, optionally rewrite it with an LLM, and paste it directly into the active application.
@@ -25,7 +25,7 @@
> [!IMPORTANT]
-> **Standalone Linux port:** This repository contains exclusively the Linux port of Blitztext – a standalone Python 3/PyQt6 implementation optimized for **Kubuntu/Ubuntu running KDE Plasma with Wayland**. For the original macOS version, please visit the [official main repository](https://github.com/cmagnussen/blitztext-app).
+> **Standalone Linux port:** This repository contains exclusively the Linux port of Blitztext – a standalone Python 3/PyQt6 implementation developed on **KDE Plasma with Wayland** and also verified natively on **Ubuntu 26.04 GNOME with Wayland**. For the original macOS version, please visit the [official main repository](https://github.com/cmagnussen/blitztext-app).
---
@@ -71,6 +71,15 @@ The AI workflows help with phrasing, tone, and emojis. You'll find the relevant
+> [!IMPORTANT]
+> **“API key environment” is not an input field for the secret API key.** Enter
+> only the environment-variable name there, such as `OPENAI_API_KEY` for OpenAI
+> or `OPENROUTER_API_KEY` for OpenRouter. Saving the settings does not write the
+> actual key to `config.json` or automatically create another file for it.
+> Store the key separately in `~/.config/blitztext-linux/secrets.env`; `./run.sh`
+> and the systemd user service load that file on the next start. See
+> [Secrets](#secrets) for the complete file format and permission instructions.
+
**LLM providers.** Blitztext supports three provider modes, selectable under **Settings → AI Workflows → "LLM provider"**:
| Provider | When to use |
@@ -238,7 +247,8 @@ It is idempotent (safe to run repeatedly) and handles everything fully automatic
2. Installs missing system packages (incl. `pipx`).
3. Prompts for the operating mode: global hotkeys with `input` group, or window/tray only without global hotkeys.
4. Sets up a `.venv` environment and installs `openai-whisper`/`faster-whisper`.
-5. Prepares `ydotool.service` and the systemd user service.
+5. Prepares `ydotool.service`, installs the systemd user service, and enables it
+ for autostart without starting it immediately.
### After installation
@@ -251,11 +261,20 @@ It is idempotent (safe to run repeatedly) and handles everything fully automatic
./run.sh
```
*(Does the tray icon appear and do the hotkeys respond? Then everything went smoothly!)*
-3. **Enable autostart:**
+3. **Start the already enabled autostart service now:**
```bash
systemctl --user start blitztext-linux
```
+> **Ubuntu 26.04 GNOME / Wayland status (verified 2026-08-10):** A clean
+> installation on Python 3.14.4 passed `scripts/verify.sh` with 17 PASS, 0 FAIL,
+> 0 WARN before the first user configuration and 18 PASS, 0 FAIL, 0 WARN
+> afterwards. The main window, GNOME AppIndicator tray, audio recording, local
+> `openai-whisper` transcription, the global Left-Alt hotkey, Wayland clipboard,
+> `ydotool` auto-paste, and systemd user-service autostart were all exercised in
+> the real desktop session. This verification applies to the native install,
+> not the experimental Flatpak manifest.
+
Disable autostart again
@@ -300,9 +319,11 @@ pip install PyQt6 evdev openai pytest openai-whisper faster-whisper
```
**4. Whisper engine as an alternative via pipx**
-If you want to install `openai-whisper` decoupled from the venv (avoids version conflicts on newer Ubuntu setups due to Python 3.11):
+If you want to install `openai-whisper` decoupled from the venv, use the
+available system interpreter. The verified Ubuntu 26.04 path uses the project
+venv with Python 3.14.4, so pipx is not required there:
```bash
-pipx install --python "$(command -v python3.11)" openai-whisper
+pipx install --python "$(command -v python3)" openai-whisper
pipx inject openai-whisper faster-whisper # optional, for accelerated execution
```
diff --git a/app/history_panel.py b/app/history_panel.py
index 57cdaf6..d54837f 100644
--- a/app/history_panel.py
+++ b/app/history_panel.py
@@ -165,6 +165,16 @@ def __init__(self, entry: HistoryEntry, parent: Optional[QWidget] = None) -> Non
"border: 1px solid palette(mid); margin: 1px 0; }"
)
+ # Diese Timer muessen dem Widget gehoeren. Ein statischer
+ # QTimer.singleShot mit Lambda kann nach deleteLater() noch feuern und
+ # dann auf das bereits zerstoerte C++-Objekt zugreifen.
+ self._copy_reset_timer = QTimer(self)
+ self._copy_reset_timer.setSingleShot(True)
+ self._copy_reset_timer.timeout.connect(self._reset_copy_button)
+ self._highlight_reset_timer = QTimer(self)
+ self._highlight_reset_timer.setSingleShot(True)
+ self._highlight_reset_timer.timeout.connect(self._reset_highlight)
+
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 6, 8, 6)
layout.setSpacing(3)
@@ -200,7 +210,7 @@ def _copy_to_clipboard(self) -> None:
_clipboard_write(self.entry.text)
self._btn_copy.setText("✓")
self._btn_copy.setStyleSheet("color: #4caf50; font-weight: bold;")
- QTimer.singleShot(1500, self._reset_copy_button)
+ self._copy_reset_timer.start(1500)
def _reset_copy_button(self) -> None:
self._btn_copy.setText("\U0001f4cb")
@@ -211,10 +221,13 @@ def highlight(self) -> None:
"HistoryEntryWidget { background: #fff3cd; border-radius: 4px; "
"border: 1px solid #ffc107; margin: 1px 0; }"
)
- QTimer.singleShot(800, lambda: self.setStyleSheet(
+ self._highlight_reset_timer.start(800)
+
+ def _reset_highlight(self) -> None:
+ self.setStyleSheet(
"HistoryEntryWidget { background: palette(base); border-radius: 4px; "
"border: 1px solid palette(mid); margin: 1px 0; }"
- ))
+ )
class HistoryPanel(QWidget):
@@ -236,6 +249,17 @@ def __init__(
self._entry_widgets: List[HistoryEntryWidget] = []
self._clear_armed = False
self._setup_ui()
+ self._scroll_reset_timer = QTimer(self)
+ self._scroll_reset_timer.setSingleShot(True)
+ self._scroll_reset_timer.timeout.connect(
+ lambda: self._scroll.verticalScrollBar().setValue(0)
+ )
+ self._clear_disarm_timer = QTimer(self)
+ self._clear_disarm_timer.setSingleShot(True)
+ self._clear_disarm_timer.timeout.connect(self._disarm_clear)
+ self._merge_reset_timer = QTimer(self)
+ self._merge_reset_timer.setSingleShot(True)
+ self._merge_reset_timer.timeout.connect(self._reset_merge_button)
def _setup_ui(self) -> None:
layout = QVBoxLayout(self)
@@ -293,7 +317,7 @@ def add_entry(self, text: str, is_dictation: bool = False, source: Optional[str]
self._list_layout.insertWidget(0, widget)
widget.highlight()
- QTimer.singleShot(50, lambda: self._scroll.verticalScrollBar().setValue(0))
+ self._scroll_reset_timer.start(50)
self._update_header()
self._update_merge_button()
@@ -320,7 +344,7 @@ def _on_clear_clicked(self) -> None:
self._clear_armed = True
self._btn_clear.setText(t("history.button.confirm_clear"))
self._btn_clear.setStyleSheet("color: #f44336; font-weight: bold;")
- QTimer.singleShot(3000, self._disarm_clear)
+ self._clear_disarm_timer.start(3000)
else:
self.clear_all()
@@ -353,7 +377,7 @@ def _merge_dictation(self) -> None:
self._btn_merge.setText(t("history.status.saved") if path else t("history.status.copied"))
self._btn_merge.setStyleSheet("color: #4caf50; font-weight: bold;")
- QTimer.singleShot(2500, self._reset_merge_button)
+ self._merge_reset_timer.start(2500)
def _reset_merge_button(self) -> None:
self._btn_merge.setText(t("history.button.merge"))
diff --git a/docs/setup.md b/docs/setup.md
index aac51b9..27fb62d 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -9,7 +9,8 @@ bash scripts/install.sh
## After installation
-1. Restart or log out and back in so the `input` group is active.
+1. If you selected global hotkeys, restart or log out and back in so the
+ `input` group is active. Window/tray-only mode does not require this.
2. Run the verification script:
```bash
@@ -22,7 +23,7 @@ bash scripts/install.sh
./run.sh
```
-4. Enable autostart if you want it on every login:
+4. Start the autostart service that `install.sh` already enabled:
```bash
systemctl --user start blitztext-linux
@@ -39,16 +40,18 @@ systemctl --user disable blitztext-linux
## Compatibility matrix (diagnostic, not a support promise)
-BlitztextLinux is developed and tested on **Kubuntu (KDE Plasma, Wayland)**.
-The systems below share the Ubuntu/Debian package base, so `install.sh` runs on
-them, but only Kubuntu is systematically tested. This table documents expected
-behavior and known risks — it is **not** an official support statement.
+BlitztextLinux is developed and tested on **Kubuntu (KDE Plasma, Wayland)** and
+has also completed a native clean-install and live functional pass on **Ubuntu
+26.04 (GNOME, Wayland)**. The other systems below share the Ubuntu/Debian
+package base, so `install.sh` runs on them, but they are not systematically
+tested. This table documents observed or expected behavior and known risks — it
+is **not** an official support statement.
| Target system | Expected session | Main risks | Status |
| :--- | :--- | :--- | :--- |
| Kubuntu 26.04, Plasma | Wayland | ydotool ≥ 1.0 via apt available; lowest risk | tested |
| Ubuntu 24.04, GNOME | Wayland | apt ydotool 0.1.x is client-only → no auto-paste (clipboard mode works); GNOME tray needs the AppIndicator extension | untested |
-| Ubuntu 26.04, GNOME | Wayland | GNOME tray needs the AppIndicator extension; Python 3.13/3.14: torch/openai-whisper wheels may lag behind new Python releases | untested |
+| Ubuntu 26.04, GNOME | Wayland | GNOME tray needs the AppIndicator extension; ydotool 1.0.4 is available via apt | tested 2026-08-10: Python 3.14.4, verify 17/0/0 before first config and 18/0/0 afterwards; window, tray, recording, local Whisper, hotkey, clipboard, auto-paste, autostart |
| Linux Mint 22.x, Cinnamon | X11 (default) | `xclip` is the required clipboard backend, not `wl-copy`; apt ydotool 0.1.x → no auto-paste | untested |
| Lubuntu 24.04, LXQt | X11 (default) | same as Mint: `xclip` required; Qt tray via StatusNotifier usually fine | untested |
| Lubuntu 26.04, LXQt | X11 or Wayland | session type decides the clipboard backend — run `scripts/verify.sh` to see which one applies | untested |
@@ -56,7 +59,7 @@ behavior and known risks — it is **not** an official support statement.
Xfce and MATE are expected to behave like the X11 rows above, but are not
tracked here.
-What works everywhere, regardless of session type:
+Base functions when the listed session prerequisites are present:
- Transcription and clipboard copy (no `ydotool`, no `input` group needed).
- Window/tray operation without global hotkeys (`install.sh` offers this mode;
@@ -71,8 +74,9 @@ What is environment-dependent:
## Desktop session notes
-BlitztextLinux is developed for KDE Plasma on Wayland, with X11 fallbacks where the
-underlying tools support them.
+BlitztextLinux is developed for KDE Plasma on Wayland and additionally verified
+on Ubuntu 26.04 GNOME/Wayland, with X11 fallbacks where the underlying tools
+support them.
- GUI startup needs a real desktop session: either a usable `WAYLAND_DISPLAY` socket
or `DISPLAY` must be available. In headless shells, `scripts/verify.sh` can report
@@ -80,6 +84,9 @@ underlying tools support them.
- Qt prefers Wayland when `WAYLAND_DISPLAY` points to an existing socket. If that
variable is stale but `DISPLAY` is set, the launcher falls back to X11.
- Clipboard support uses `wl-copy`/`wl-paste` on Wayland and `xclip` on X11.
+- GNOME needs an AppIndicator/StatusNotifier extension for the tray icon. Ubuntu
+ 26.04's default GNOME session provided a working StatusNotifier host in the
+ verified native-install test.
- Auto-paste uses `ydotool`; terminal windows may need `Ctrl+Shift+V` instead of
`Ctrl+V`, so the app detects known terminal window classes when possible.
- Global hotkeys still use `evdev`/the `input` group. A future desktop-native XDG
@@ -116,6 +123,7 @@ sudo usermod -aG input $USER
```bash
python3 -m venv .venv
source .venv/bin/activate
+pip install --index-url https://download.pytorch.org/whl/cpu torch
pip install PyQt6 evdev openai pytest openai-whisper faster-whisper
```
@@ -124,7 +132,7 @@ pip install PyQt6 evdev openai pytest openai-whisper faster-whisper
If you want to install `openai-whisper` outside the venv:
```bash
-pipx install --python "$(command -v python3.11)" openai-whisper
+pipx install --python "$(command -v python3)" openai-whisper
pipx inject openai-whisper faster-whisper # optional
```
diff --git a/scripts/install.sh b/scripts/install.sh
index ec6c1a4..8f010ac 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -360,7 +360,8 @@ print_summary() {
echo ""
echo " (Falls Sie bereits im Projektverzeichnis sind, reicht: ./run.sh)"
echo ""
- echo -e " ${YELLOW}3.${RESET} ${BOLD}Wenn alles funktioniert — Autostart aktivieren:${RESET}"
+ echo -e " ${YELLOW}3.${RESET} ${BOLD}Wenn alles funktioniert — Dienst jetzt starten:${RESET}"
+ echo " (Der Autostart wurde bereits durch den Installer aktiviert.)"
echo " systemctl --user start blitztext-linux"
echo ""
echo -e " ${YELLOW}4.${RESET} ${BOLD}Diagnose bei Problemen:${RESET}"
diff --git a/tests/test_state_machine.py b/tests/test_state_machine.py
index 3cfd7b1..d3de2f7 100644
--- a/tests/test_state_machine.py
+++ b/tests/test_state_machine.py
@@ -534,6 +534,36 @@ def test_text_edit_action_fits_compact_main_window(self, gui_app):
assert button_rect.bottom() < history_rect.top()
assert text_width < button_rect.width()
+ def test_history_reset_timers_are_owned_by_their_widgets(self, gui_app):
+ from PyQt6.QtCore import QCoreApplication, QEvent
+ from PyQt6.QtTest import QTest
+ from app.history_panel import HistoryEntry, HistoryEntryWidget, HistoryPanel
+
+ entry_widget = HistoryEntryWidget(HistoryEntry("Testtext"))
+ entry_widget.highlight()
+
+ assert entry_widget._highlight_reset_timer.parent() is entry_widget
+ assert entry_widget._highlight_reset_timer.isSingleShot() is True
+ assert entry_widget._highlight_reset_timer.isActive() is True
+ assert entry_widget._copy_reset_timer.parent() is entry_widget
+
+ panel = HistoryPanel()
+ assert panel._scroll_reset_timer.parent() is panel
+ assert panel._clear_disarm_timer.parent() is panel
+ assert panel._merge_reset_timer.parent() is panel
+
+ # Kurze echte Timeouts wuerden bei kontextlosen singleShot-Lambdas nach
+ # der C++-Zerstoerung einen Prozess-Abort ausloesen.
+ entry_widget._copy_reset_timer.start(1)
+ entry_widget._highlight_reset_timer.start(1)
+ panel._scroll_reset_timer.start(1)
+ panel._clear_disarm_timer.start(1)
+ panel._merge_reset_timer.start(1)
+ entry_widget.deleteLater()
+ panel.deleteLater()
+ QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
+ QTest.qWait(20)
+
def test_dictation_mode_syncs_window_and_tray(self, gui_app):
win = gui_app._ensure_main_window()
gui_app.set_dictation_mode(True)