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 Banner

Blitztext Linux

-

Dein lokaler KI-Sprachassistent für KDE Plasma & Wayland

+

Dein lokaler KI-Sprachassistent für Linux-Desktops unter Wayland

Webseite @@ -11,7 +11,7 @@

Blitztext Linux CI Lizenz: MIT - Plattform + Plattform

🇬🇧 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 Banner

Blitztext Linux

-

Your local AI voice assistant for KDE Plasma & Wayland

+

Your local AI voice assistant for Linux desktops on Wayland

Website @@ -11,7 +11,7 @@

Blitztext Linux CI License: MIT - Platform + Platform

🇬🇧 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)