diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..f6b759a --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,157 @@ +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Builds Flow's Linux packages (AppImage, .deb, .rpm) and uploads them as +# workflow artifacts. Separate from and does not touch the existing +# (manual, Windows-only) release process. +# +# Runs on ubuntu-22.04 deliberately, not a newer Ubuntu: linuxdeploy's +# bundled `strip` doesn't understand the RELR relocations that newer +# glibc/binutils toolchains emit by default, which breaks AppImage bundling +# entirely on more recent distros. This was reproduced and confirmed by +# hand during Phase 6.1 of the Linux port. +# +# The AppImage build additionally works around two more environment- +# specific issues also found and fixed by hand in Phase 6.1: +# - linuxdeploy's own AppImage tries to FUSE-mount itself, which sandboxed/ +# containerized CI runners commonly can't do; APPIMAGE_EXTRACT_AND_RUN=1 +# makes every AppImage runtime involved (including nested plugin +# AppImages) fall back to extracting instead. +# - linuxdeploy's dependency scanner runs `ldd` on the compiled binary to +# decide what to bundle, but can't resolve the vendored (not system- +# installed) libvosk.so unless it's on LD_LIBRARY_PATH at bundle time. +# The binary's own runtime resolution goes through its RUNPATH, not +# this variable - this is only needed to help linuxdeploy find the file +# to copy in the first place. +# +# packaging/linux/fix-appimage-libs.sh removes a handful of bundled Wayland/ +# XCB client libraries that linuxdeploy-plugin-gtk pulls in but which must +# come from the host's own compositor/X server to avoid version-mismatch +# EGL failures (also found and fixed by hand in Phase 6.1, reproduced on a +# real desktop after building on Ubuntu 22.04). +# +# TAURI_SIGNING_PRIVATE_KEY is expected to be the same key already used for +# the existing Windows updater artifacts (see Phase 6.4 - the plan calls +# for reusing it, not creating a new one). If it isn't configured as a +# repository secret yet, the AppImage is still built, just without updater +# artifacts, so this workflow doesn't hard-depend on Phase 6.4 being done +# first. + +name: Build Linux + +on: + push: + branches: + - linux-native-build + paths: + - "src/**" + - "src-tauri/**" + - "packaging/linux/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/build-linux.yml" + pull_request: + paths: + - "src/**" + - "src-tauri/**" + - "packaging/linux/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/build-linux.yml" + workflow_dispatch: + +jobs: + build: + name: Build AppImage / deb / rpm + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install system build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libasound2-dev \ + libssl-dev \ + pkg-config \ + xdg-utils \ + unzip \ + file + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Fetch vendored Vosk library + run: bash packaging/linux/fetch-vosk.sh + + - name: Build AppImage + env: + FLOW_LINUX_PACKAGE: appimage + APPIMAGE_EXTRACT_AND_RUN: "1" + LD_LIBRARY_PATH: ${{ github.workspace }}/src-tauri/resources/vosk/linux-x64 + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + if [ -n "${TAURI_SIGNING_PRIVATE_KEY}" ]; then + echo "Signing key configured - building AppImage with updater artifacts" + npx tauri build \ + --config src-tauri/tauri.linux.conf.json \ + --config src-tauri/tauri.linux.appimage.conf.json \ + --bundles appimage + else + echo "::warning::TAURI_SIGNING_PRIVATE_KEY not configured - building AppImage without updater artifacts" + npx tauri build \ + --config src-tauri/tauri.linux.conf.json \ + --bundles appimage + fi + + - name: Fix bundled Wayland/XCB libraries in AppImage + run: | + APPIMAGE_PATH="$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage')" + bash packaging/linux/fix-appimage-libs.sh "${APPIMAGE_PATH}" + + - name: Build .deb + env: + FLOW_LINUX_PACKAGE: deb + run: npx tauri build --config src-tauri/tauri.linux.conf.json --bundles deb + + - name: Build .rpm + env: + FLOW_LINUX_PACKAGE: rpm + run: npx tauri build --config src-tauri/tauri.linux.conf.json --bundles rpm + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: flow-linux-x86_64 + path: | + src-tauri/target/release/bundle/appimage/*.AppImage + src-tauri/target/release/bundle/appimage/*.AppImage.sig + src-tauri/target/release/bundle/deb/*.deb + src-tauri/target/release/bundle/rpm/*.rpm + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index 97eef33..f140c20 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist-ssr *.local src-tauri/target/ src-tauri/gen/ +src-tauri/resources/vosk/linux-x64/*.so tools/ tmp/ src/pro diff --git a/BUILDING-LINUX.md b/BUILDING-LINUX.md new file mode 100644 index 0000000..05bbb44 --- /dev/null +++ b/BUILDING-LINUX.md @@ -0,0 +1,178 @@ + + +# Building Flow on Linux + +Flow is primarily a Windows application, but this branch adds a native +Linux build (AppImage, `.deb`, `.rpm`, and an Arch `PKGBUILD`). This +document covers everything needed to build and run it from source on +Linux, plus the known platform limitations. + +## Prerequisites + +You need Rust (stable), Node.js 20+, and the Tauri v2 native +dependencies for your distro. + +### Debian / Ubuntu (22.04+) + +```bash +sudo apt-get update +sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libasound2-dev \ + libssl-dev \ + pkg-config \ + xdg-utils \ + unzip \ + file +``` + +(`unzip` and `file` are only needed to run `fetch-vosk.sh` and to build +an AppImage; skip them if you only want a debug build for development.) + +### Arch Linux + +```bash +sudo pacman -S --needed webkit2gtk-4.1 gtk3 libayatana-appindicator \ + alsa-lib vosk-api hicolor-icon-theme gtk-update-icon-cache \ + desktop-file-utils rust nodejs npm pkgconf +``` + +Arch's `vosk-api` package is used directly (see [Speech recognition +library (Vosk)](#speech-recognition-library-vosk) below) — you do not +need `fetch-vosk.sh` on Arch. A ready-to-use `PKGBUILD` is provided at +[packaging/arch/PKGBUILD](packaging/arch/PKGBUILD). + +### Fedora / openSUSE (rpm-based) + +Package names vary by distro/version; the runtime dependencies a built +`.rpm` declares are `webkit2gtk4.1`, `gtk3`, `libayatana-appindicator- +gtk3`, and `alsa-lib` (see +[tauri.linux.conf.json](src-tauri/tauri.linux.conf.json)) — install +their `-devel` equivalents to build from source. + +## Speech recognition library (Vosk) + +Flow's voice engine links against `libvosk.so` at runtime. Two ways to +provide it: + +- **System package** (Arch): install the distro's `vosk-api` package; + `FLOW_LINUX_PACKAGE=pacman` tells the build to expect it on the + system instead of vendoring a copy (see + [packaging/arch/PKGBUILD](packaging/arch/PKGBUILD)). +- **Vendored copy** (AppImage/deb/rpm, or plain development builds on + distros without a `vosk-api` package): run + [packaging/linux/fetch-vosk.sh](packaging/linux/fetch-vosk.sh) once. + It downloads the official prebuilt `libvosk.so` (pinned to v0.3.45 — + the last version with published binary release assets, see the + script's comments) into + `src-tauri/resources/vosk/linux-x64/libvosk.so`, verifying its + SHA-256 against a hardcoded value. `src-tauri/build.rs` picks it up + automatically once present. + +## Building + +```bash +npm ci +bash packaging/linux/fetch-vosk.sh # skip on Arch if using system vosk-api +``` + +**Development build (debug, runs in place):** + +```bash +cd src-tauri && cargo build +./packaging/linux/flow-launcher.sh # or run target/debug/flow directly +``` + +**Packaged builds**, each via `tauri build` with the Linux config +overlay (`tauri.linux.conf.json` merges automatically-shared settings +from `tauri.conf.json`; nothing there needs editing per-package): + +```bash +# AppImage +FLOW_LINUX_PACKAGE=appimage npx tauri build \ + --config src-tauri/tauri.linux.conf.json \ + --bundles appimage +bash packaging/linux/fix-appimage-libs.sh \ + src-tauri/target/release/bundle/appimage/*.AppImage + +# .deb +FLOW_LINUX_PACKAGE=deb npx tauri build \ + --config src-tauri/tauri.linux.conf.json --bundles deb + +# .rpm +FLOW_LINUX_PACKAGE=rpm npx tauri build \ + --config src-tauri/tauri.linux.conf.json --bundles rpm +``` + +`fix-appimage-libs.sh` is **required** after every AppImage build — see +[Known issues](#known-issues) below for why. It re-signs the AppImage +if a `.sig` file (updater artifact) exists next to it and +`TAURI_SIGNING_PRIVATE_KEY` is set in the environment, since repacking +changes the file's signature-relevant bytes. + +To also produce a signed updater artifact (`.AppImage.sig`), add +`--config src-tauri/tauri.linux.appimage.conf.json` as a second +`--config` flag to the AppImage build above, with +`TAURI_SIGNING_PRIVATE_KEY` (and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`, +even if empty) set — this is the same signing key already used for +Windows releases, not a separate one. + +[.github/workflows/build-linux.yml](.github/workflows/build-linux.yml) +runs this exact sequence in CI (on `ubuntu-22.04` — see the workflow's +own comments for why not a newer Ubuntu) and uploads all three package +formats as artifacts on every push/PR to `linux-native-build`. + +## Known issues and platform limitations + +- **Wayland**: Flow is an always-on-top overlay that relies on absolute + window positioning, always-on-top, and global hotkeys. Native + Wayland's security model doesn't allow any of these for regular + applications, and neither GTK3 nor `tao` (Tauri's windowing layer on + Linux) work around that. [flow-launcher.sh](packaging/linux/flow-launcher.sh) + therefore forces the app onto XWayland by default + (`GDK_BACKEND=x11`); set `FLOW_ALLOW_WAYLAND=1` to opt out and run + under native Wayland, with the caveat that window positioning, + always-on-top, and global hotkeys will not work correctly. +- **Blank/black window (DMA-BUF renderer)**: on some AMD/Mesa driver + combinations, WebKitGTK's DMA-BUF renderer fails to initialize the + window contents. `flow-launcher.sh` sets + `WEBKIT_DISABLE_DMABUF_RENDERER=1` by default to work around this; + unset it explicitly if you've verified your system doesn't need it. +- **Screen-capture protection is not available on Linux.** The + Windows build can hide the teleprompter window from screen + recording/sharing (via a Windows-only API); there is no equivalent + mechanism on X11 or Wayland, so this feature is disabled on Linux. +- **Remote Control needs a firewall exception.** The "Remote Control" + feature runs a small local HTTP relay on TCP port **43127** so + another device on the same network can send text to Flow directly + (see `REMOTE_RELAY_PORT` in + [src-tauri/src/lib.rs](src-tauri/src/lib.rs)). Most desktop Linux + distros ship a default-deny firewall (`ufw`, `firewalld`) with no + interactive per-app prompt like Windows Defender Firewall shows on + first launch — you need to open the port yourself if you want to use + this feature from another device on your LAN, e.g.: + ```bash + sudo ufw allow 43127/tcp # ufw (Debian/Ubuntu) + sudo firewall-cmd --add-port=43127/tcp --permanent && sudo firewall-cmd --reload # firewalld (Fedora) + ``` +- **AppImage and bundled Wayland/XCB libraries**: `linuxdeploy-plugin- + gtk` bundles Wayland/X11/XCB client libraries and `libxkbcommon` + into the AppImage, but these must match the exact protocol/ABI + version of the host's own compositor or X server. A version mismatch + causes WebKitGTK to fail EGL/GL context creation entirely (`Could + not create default EGL display: EGL_BAD_PARAMETER`). + [fix-appimage-libs.sh](packaging/linux/fix-appimage-libs.sh) strips + these specific libraries out and repacks the AppImage so the dynamic + linker falls through to the host's own copies instead — this is why + the script must run after every AppImage build, not just optionally. diff --git a/README.ar.md b/README.ar.md index 1faa1af..04bd314 100644 --- a/README.ar.md +++ b/README.ar.md @@ -38,6 +38,7 @@ SourceForge Downloads Windows + Linux (community port) Tauri + Rust GPLv3 License

@@ -227,6 +228,21 @@ npm run tauri build --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## الخصوصية - يتم تخزين معظم البيانات محلياً على جهازك. diff --git a/README.de.md b/README.de.md index 0451891..495e2d1 100644 --- a/README.de.md +++ b/README.de.md @@ -38,6 +38,7 @@ SourceForge Downloads Windows + Linux (Community-Portierung) Tauri + Rust GPLv3-Lizenz

@@ -209,6 +210,21 @@ Die `.sig`-Datei wird zusammen mit der MSI zur Referenz generiert, während `lat --- +## Linux (Community-Portierung) + +Flow lässt sich auch nativ unter Linux bauen — als AppImage, `.deb`, `.rpm` oder über ein Arch-`PKGBUILD`. Diese Portierung wird von der Community gepflegt; Windows bleibt die primäre, offiziell unterstützte Plattform. Voraussetzungen und Build-Anleitung stehen in [BUILDING-LINUX.md](BUILDING-LINUX.md) — ein offizielles, fertig gebautes Release gibt es noch nicht, daher ist aktuell ein Build aus dem Quellcode nötig (oder ein Artefakt aus dem [Linux-CI-Workflow](.github/workflows/build-linux.yml)). + +Ehrliche Übersicht der Einschränkungen gegenüber Windows: + +| Funktion | Linux | +| --- | --- | +| Schutz vor Bildschirmaufnahme | Nicht verfügbar — die zugrunde liegende API ist Windows-exklusiv, ein Äquivalent für X11/Wayland gibt es nicht. | +| Wayland | Läuft standardmäßig über XWayland (Always-on-Top, absolute Fensterpositionierung und globale Hotkeys benötigen es); natives Wayland ist optional aktivierbar, bricht dabei aber genau diese drei Funktionen. | +| Fernsteuerung (Remote Control) | Funktioniert, benötigt aber einmalig eine manuelle Firewall-Freigabe für TCP-Port 43127 (die meisten Linux-Desktops blockieren eingehende Verbindungen standardmäßig, anders als der Erstlaunch-Dialog unter Windows). | +| Automatisches In-App-Update | Funktioniert bei der AppImage; `.deb`/`.rpm`/`PKGBUILD`-Installationen werden stattdessen über den Paketmanager der Distribution aktualisiert. | + +--- + ## Datenschutz - Die meisten Daten werden lokal auf dem Gerät gespeichert. diff --git a/README.es.md b/README.es.md index 5fafd9b..dcbc8ed 100644 --- a/README.es.md +++ b/README.es.md @@ -38,6 +38,7 @@ Descargas en SourceForge Windows + Linux (community port) Tauri + Rust Licencia GPLv3

@@ -209,6 +210,21 @@ El archivo `.sig` se genera junto con el instalador MSI como referencia, mientra --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Privacidad - La mayoría de los datos se almacenan localmente en el dispositivo. diff --git a/README.fi.md b/README.fi.md index 6d52a21..d88c4fb 100644 --- a/README.fi.md +++ b/README.fi.md @@ -38,6 +38,7 @@ SourceForge-lataukset Windows + Linux (community port) Tauri + Rust GPLv3-lisenssi

@@ -209,6 +210,21 @@ Julkaise nämä tiedostot hakemistosta `src-tauri/target/release/bundle` GitHub- --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Tietosuoja - Suurin osa tiedoista tallennetaan paikallisesti laitteellesi. diff --git a/README.fr.md b/README.fr.md index cec4eb3..3a1ecc0 100644 --- a/README.fr.md +++ b/README.fr.md @@ -38,6 +38,7 @@ Téléchargements SourceForge Windows + Linux (community port) Tauri + Rust Licence GPLv3

@@ -209,6 +210,21 @@ Le fichier `.sig` est généré aux côtés du MSI à titre de référence, tand --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Confidentialité - La majorité des données est stockée localement sur votre appareil. diff --git a/README.md b/README.md index 7928b48..7c47cce 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ SourceForge Downloads Windows + Linux (community port) Tauri + Rust GPLv3 License

@@ -209,6 +210,21 @@ The `.sig` file is generated alongside the MSI for reference, while `latest.json --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Privacy - Most data is stored locally on the device. diff --git a/README.pt.md b/README.pt.md index 26aefb1..e341b18 100644 --- a/README.pt.md +++ b/README.pt.md @@ -38,6 +38,7 @@ Downloads no SourceForge Windows + Linux (community port) Tauri + Rust Licença GPLv3

@@ -209,6 +210,21 @@ O arquivo `.sig` é gerado junto com o MSI para referência, enquanto o `latest. --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Privacidade - A maior parte dos dados é armazenada localmente no dispositivo. diff --git a/README.tr.md b/README.tr.md index 50e7af0..47d1161 100644 --- a/README.tr.md +++ b/README.tr.md @@ -38,6 +38,7 @@ SourceForge İndirmeleri Windows + Linux (community port) Tauri + Rust GPLv3 Lisansı

@@ -209,6 +210,21 @@ npm run tauri build --- +## Linux (community port) + +Flow also builds natively on Linux, as an AppImage, `.deb`, `.rpm`, or an Arch `PKGBUILD`. This is a community-maintained port; Windows remains the primary, officially supported platform. See [BUILDING-LINUX.md](BUILDING-LINUX.md) for prerequisites and build instructions — there is no official pre-built binary release yet, so building from source (or grabbing an artifact from the [Linux CI workflow](.github/workflows/build-linux.yml)) is currently required. + +Honest limitations compared to Windows: + +| Feature | Linux | +| --- | --- | +| Screen-capture protection | Not available — the underlying API is Windows-only, with no X11/Wayland equivalent. | +| Wayland | Runs via XWayland by default (always-on-top, absolute positioning, and global hotkeys need it); native Wayland is opt-in but breaks those three. | +| Remote Control | Works, but needs a one-time manual firewall exception for TCP port 43127 (most Linux desktops default-deny inbound connections, unlike Windows' first-launch prompt). | +| In-app auto-update | Works for the AppImage; `.deb`/`.rpm`/`PKGBUILD` installs update through your distro's package manager instead. | + +--- + ## Gizlilik - Çoğu veri yerel olarak cihazda depolanır. diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..e15c381 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,74 @@ +# Maintainer: Emanuel Halbwachs +# +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +pkgname=flow-teleprompter +pkgver=2.0.0 +pkgrel=1 +pkgdesc="A modern desktop teleprompter for smooth reading, quick editing, voice controls, and remote message injection" +arch=('x86_64') +url="https://github.com/LumoRez07/Flow" +license=('GPL-3.0-or-later') +depends=('webkit2gtk-4.1' 'gtk3' 'libayatana-appindicator' 'alsa-lib' 'vosk-api' 'hicolor-icon-theme' + 'gtk-update-icon-cache' 'desktop-file-utils') +makedepends=('rust' 'cargo' 'nodejs' 'npm' 'pkgconf') +provides=('flow-teleprompter-bin') +conflicts=('flow-teleprompter-bin') +options=('!lto') +install="$pkgname.install" +source=("$pkgname-$pkgver.tar.gz::https://github.com/LumoRez07/Flow/archive/refs/tags/v$pkgver.tar.gz") +# TODO: replace with the real checksum once v$pkgver is tagged upstream and +# the tarball above actually exists (this Linux port has not been released +# yet — see plan/01-SONNET-TASKS.md Phase 7.3 "Upstream-PR"). +sha256sums=('SKIP') + +build() { + cd "Flow-$pkgver" + + export FLOW_LINUX_PACKAGE=pacman + export CARGO_HOME="$srcdir/cargo-home" + + # tauri-build automatically merges tauri.linux.conf.json into every Linux + # build (not just `tauri build`), and its `bundle.resources` entry requires + # resources/vosk/linux-x64/libvosk.so to exist on disk at build time even + # though this package depends on the system vosk-api package at runtime. + # Stage the system lib into the vendored path instead of vendoring/ + # downloading a redundant copy — the `vosk-api` depends entry already + # guarantees it exists. + local system_libvosk + system_libvosk="$(ldconfig -p | awk '/libvosk\.so/ { print $NF; exit }')" + if [[ -z "$system_libvosk" ]]; then + echo "libvosk.so not found via ldconfig; is vosk-api installed?" >&2 + return 1 + fi + install -Dm644 "$system_libvosk" "src-tauri/resources/vosk/linux-x64/libvosk.so" + + npm ci --no-audit --no-fund + cargo build --release --manifest-path=src-tauri/Cargo.toml +} + +package() { + cd "Flow-$pkgver" + + install -Dm755 "src-tauri/target/release/flow" "$pkgdir/usr/lib/flow/flow" + install -Dm755 "packaging/linux/flow-launcher.sh" "$pkgdir/usr/bin/flow" + + install -Dm644 "packaging/linux/app.flowteleprompter.Flow.desktop" \ + "$pkgdir/usr/share/applications/app.flowteleprompter.Flow.desktop" + install -Dm644 "packaging/linux/app.flowteleprompter.Flow.metainfo.xml" \ + "$pkgdir/usr/share/metainfo/app.flowteleprompter.Flow.metainfo.xml" + + for size in 32x32 64x64 128x128 256x256 512x512; do + install -Dm644 "packaging/linux/icons/hicolor/$size/apps/app.flowteleprompter.Flow.png" \ + "$pkgdir/usr/share/icons/hicolor/$size/apps/app.flowteleprompter.Flow.png" + done + + install -Dm644 "LICENSE" \ + "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/packaging/arch/PKGBUILD-bin b/packaging/arch/PKGBUILD-bin new file mode 100644 index 0000000..97cf364 --- /dev/null +++ b/packaging/arch/PKGBUILD-bin @@ -0,0 +1,59 @@ +# Maintainer: Emanuel Halbwachs +# +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Binary counterpart to flow-teleprompter (packaging/arch/PKGBUILD): skips +# compiling from source and instead installs a prebuilt release binary. +# Desktop file / icons / metainfo / license are still pulled from the +# lightweight source tag tarball rather than duplicated into the binary +# release asset. +# +# NOTE: the second source entry's filename/URL must match whatever the +# Linux release workflow (plan/01-SONNET-TASKS.md Phase 6.3, not yet +# implemented) actually publishes as a release asset. Update it here once +# that workflow exists and its real asset name is known. + +pkgname=flow-teleprompter-bin +pkgver=2.0.0 +pkgrel=1 +pkgdesc="A modern desktop teleprompter for smooth reading, quick editing, voice controls, and remote message injection (prebuilt binary)" +arch=('x86_64') +url="https://github.com/LumoRez07/Flow" +license=('GPL-3.0-or-later') +depends=('webkit2gtk-4.1' 'gtk3' 'libayatana-appindicator' 'alsa-lib' 'vosk-api' 'hicolor-icon-theme' + 'gtk-update-icon-cache' 'desktop-file-utils') +provides=('flow-teleprompter') +conflicts=('flow-teleprompter') +options=('!strip') +install="flow-teleprompter.install" +source=("flow-src-$pkgver.tar.gz::https://github.com/LumoRez07/Flow/archive/refs/tags/v$pkgver.tar.gz" + "flow-linux-$CARCH-$pkgver.tar.gz::https://github.com/LumoRez07/Flow/releases/download/v$pkgver/flow-linux-$CARCH.tar.gz") +# TODO: replace with real checksums once v$pkgver is tagged/released +# upstream (this Linux port has not been released yet — see +# plan/01-SONNET-TASKS.md Phase 7.3 "Upstream-PR"). +sha256sums=('SKIP' + 'SKIP') + +package() { + install -Dm755 "flow-linux-$CARCH/flow" "$pkgdir/usr/lib/flow/flow" + install -Dm755 "Flow-$pkgver/packaging/linux/flow-launcher.sh" "$pkgdir/usr/bin/flow" + + install -Dm644 "Flow-$pkgver/packaging/linux/app.flowteleprompter.Flow.desktop" \ + "$pkgdir/usr/share/applications/app.flowteleprompter.Flow.desktop" + install -Dm644 "Flow-$pkgver/packaging/linux/app.flowteleprompter.Flow.metainfo.xml" \ + "$pkgdir/usr/share/metainfo/app.flowteleprompter.Flow.metainfo.xml" + + for size in 32x32 64x64 128x128 256x256 512x512; do + install -Dm644 "Flow-$pkgver/packaging/linux/icons/hicolor/$size/apps/app.flowteleprompter.Flow.png" \ + "$pkgdir/usr/share/icons/hicolor/$size/apps/app.flowteleprompter.Flow.png" + done + + install -Dm644 "Flow-$pkgver/LICENSE" \ + "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/packaging/arch/flow-teleprompter.install b/packaging/arch/flow-teleprompter.install new file mode 100644 index 0000000..fae5cce --- /dev/null +++ b/packaging/arch/flow-teleprompter.install @@ -0,0 +1,33 @@ +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pacman's built-in gtk-update-icon-cache hook only fires when a whole new +# top-level icon theme directory appears; it does not cover files an +# application package adds into the already-installed shared "hicolor" +# theme. Without this, the icon cache stays stale and app launchers/menus +# fall back to a generic icon until something else happens to rebuild it. +# Confirmed by hand on a KDE Plasma system: after a plain `pacman -U`, the +# app menu kept showing a generic icon until kbuildsycoca6 (KDE's own +# service/desktop-file cache, separate from the GTK icon cache) was rebuilt +# too. kbuildsycoca6 is guarded with `command -v` since it comes from KDE +# Frameworks (kservice) and must not become a hard dependency for users on +# GNOME/XFCE/etc. + +post_install() { + gtk-update-icon-cache -q -t -f usr/share/icons/hicolor &>/dev/null || true + update-desktop-database -q &>/dev/null || true + command -v kbuildsycoca6 &>/dev/null && kbuildsycoca6 --noincremental &>/dev/null || true +} + +post_upgrade() { + post_install +} + +post_remove() { + post_install +} diff --git a/packaging/linux/app.flowteleprompter.Flow.desktop b/packaging/linux/app.flowteleprompter.Flow.desktop new file mode 100644 index 0000000..57a471d --- /dev/null +++ b/packaging/linux/app.flowteleprompter.Flow.desktop @@ -0,0 +1,19 @@ +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +[Desktop Entry] +Type=Application +Name=Flow +GenericName=Teleprompter +Comment=A modern desktop teleprompter for smooth reading, quick editing, voice controls, and remote message injection +Exec=flow %U +Icon=app.flowteleprompter.Flow +Terminal=false +Categories=AudioVideo;Utility; +StartupWMClass=flow +StartupNotify=true diff --git a/packaging/linux/app.flowteleprompter.Flow.metainfo.xml b/packaging/linux/app.flowteleprompter.Flow.metainfo.xml new file mode 100644 index 0000000..5ef4ad3 --- /dev/null +++ b/packaging/linux/app.flowteleprompter.Flow.metainfo.xml @@ -0,0 +1,48 @@ + + + + app.flowteleprompter.Flow + CC0-1.0 + GPL-3.0-or-later + Flow + A modern desktop teleprompter for smooth reading and voice-controlled scripts + app.flowteleprompter.Flow.desktop + https://flowteleprompter.vercel.app/ + https://github.com/LumoRez07/Flow/issues + https://github.com/LumoRez07/Flow + + LumoRez + + +

+ Flow is a teleprompter app built with web technologies and Tauri. It's + designed to be simple, lightweight, and customizable. +

+

+ Flow includes script editing, multiple playback modes, voice tracking, + AI-assisted drafting, remote message injection, and tray controls, made + for creators, educators, students, and everyone who wants to read off + their screen while looking at the camera. +

+
+ + AudioVideo + Utility + + + + + +

Native Linux build of Flow, alongside the existing Windows release.

+
+
+
+
diff --git a/packaging/linux/fetch-vosk.sh b/packaging/linux/fetch-vosk.sh new file mode 100755 index 0000000..f277e91 --- /dev/null +++ b/packaging/linux/fetch-vosk.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Fetches the official prebuilt Vosk API shared library for Linux x86_64 +# and stages it under src-tauri/resources/vosk/linux-x64/, so that +# src-tauri/build.rs can pick it up as a vendored fallback for packaged +# builds (AppImage/deb/rpm) that should not depend on a system-installed +# vosk-api package. +# +# The Vosk project stopped publishing prebuilt release assets after +# v0.3.45 (v0.3.50, the version Arch Linux's `vosk-api` package builds +# from source, has no binary release assets) - see +# https://github.com/alphacep/vosk-api/releases. v0.3.45 is therefore +# pinned deliberately, not because it is "latest". + +set -euo pipefail + +VOSK_VERSION="0.3.45" +ARCHIVE_NAME="vosk-linux-x86_64-${VOSK_VERSION}.zip" +DOWNLOAD_URL="https://github.com/alphacep/vosk-api/releases/download/v${VOSK_VERSION}/${ARCHIVE_NAME}" +# Verified by hand against the published GitHub release asset: +# curl -sL "$DOWNLOAD_URL" | sha256sum +EXPECTED_SHA256="bbdc8ed85c43979f6443142889770ea95cbfbc56cffb5c5dcd73afa875c5fbb2" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DEST_DIR="${REPO_ROOT}/src-tauri/resources/vosk/linux-x64" +DEST_LIB="${DEST_DIR}/libvosk.so" + +if [[ -f "${DEST_LIB}" ]]; then + echo "libvosk.so already present at ${DEST_LIB}, skipping download." + echo "Delete it and re-run this script to force a re-fetch." + exit 0 +fi + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT + +ARCHIVE_PATH="${WORK_DIR}/${ARCHIVE_NAME}" + +echo "Downloading Vosk API ${VOSK_VERSION} for linux-x86_64..." +curl -fL --retry 3 -o "${ARCHIVE_PATH}" "${DOWNLOAD_URL}" + +echo "Verifying checksum..." +ACTUAL_SHA256="$(sha256sum "${ARCHIVE_PATH}" | cut -d' ' -f1)" +if [[ "${ACTUAL_SHA256}" != "${EXPECTED_SHA256}" ]]; then + echo "ERROR: checksum mismatch for ${ARCHIVE_NAME}" >&2 + echo " expected: ${EXPECTED_SHA256}" >&2 + echo " actual: ${ACTUAL_SHA256}" >&2 + exit 1 +fi + +echo "Extracting libvosk.so..." +unzip -q -j "${ARCHIVE_PATH}" "vosk-linux-x86_64-${VOSK_VERSION}/libvosk.so" -d "${WORK_DIR}" + +mkdir -p "${DEST_DIR}" +mv "${WORK_DIR}/libvosk.so" "${DEST_LIB}" +chmod 0644 "${DEST_LIB}" + +echo "Staged $(du -h "${DEST_LIB}" | cut -f1) libvosk.so at ${DEST_LIB}" diff --git a/packaging/linux/fix-appimage-libs.sh b/packaging/linux/fix-appimage-libs.sh new file mode 100755 index 0000000..e089e81 --- /dev/null +++ b/packaging/linux/fix-appimage-libs.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Tauri's AppImage bundling (via linuxdeploy + linuxdeploy-plugin-gtk) pulls +# in the full GTK dependency chain, including Wayland/X11 protocol client +# libraries that must match the exact protocol/ABI version of whatever +# compositor or X server the AppImage actually runs against. Bundling them +# is a well-known linuxdeploy-plugin-gtk anti-pattern: the AppImage's own +# (older, build-host-vintage) copies get preferred over the running +# system's copies via RUNPATH, and a mismatch there causes WebKitGTK to +# fail EGL/GL context creation entirely ("Could not create default EGL +# display: EGL_BAD_PARAMETER. Aborting...") even though every other part +# of the app is fine. +# +# Verified by hand: building the AppImage on Ubuntu 22.04 (per the CI +# environment used to sidestep unrelated RELR-relocation/strip +# incompatibilities with newer distros) and running the result on an +# unrelated, much newer Arch Linux desktop reproduced the EGL failure +# every time these libraries were present, and fixed it every time they +# were removed and the AppImage was repacked, letting the dynamic linker +# fall through to the host's own (correct, matching) copies instead. +# +# Usage: fix-appimage-libs.sh +# Rewrites the given AppImage in place. Run from the repository root (needs +# `npx tauri` on PATH via package.json) if a pre-existing .sig should +# be re-signed after repacking -- see below. +# +# Repacking changes the AppImage's bytes, which invalidates any updater +# signature (.sig) generated for the original file before this script +# ran. If TAURI_SIGNING_PRIVATE_KEY is set and a stale .sig is found next to +# the input file, this script re-signs the repacked file so the two stay +# consistent. Found by hand while testing Phase 6.4's update flow: the +# updater's signature verification failed until the .sig was regenerated to +# match the post-fix file. + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +APPIMAGE_PATH="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +INVOKED_FROM_DIR="$(pwd)" + +# Libraries that must come from the host system, never be bundled: Wayland/ +# X11/XCB client protocol libraries and their EGL glue. libxkbcommon is +# included because a mismatched keymap-compiler ABI causes similar (if +# rarer) failures. +LIBS_TO_REMOVE=( + libwayland-client.so.0 + libwayland-server.so.0 + libwayland-egl.so.1 + libwayland-cursor.so.0 + libxcb-randr.so.0 + libxcb-render.so.0 + libxcb-shm.so.0 + libxkbcommon.so.0 +) + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT + +echo "Extracting ${APPIMAGE_PATH}..." +cd "${WORK_DIR}" +APPIMAGE_EXTRACT_AND_RUN=1 "${APPIMAGE_PATH}" --appimage-extract >/dev/null + +REMOVED_ANY=0 +for lib in "${LIBS_TO_REMOVE[@]}"; do + LIB_PATH="squashfs-root/usr/lib/${lib}" + if [[ -f "${LIB_PATH}" ]]; then + echo "Removing bundled ${lib} (must come from the host system)" + rm -f "${LIB_PATH}" + REMOVED_ANY=1 + fi +done + +if [[ "${REMOVED_ANY}" -eq 0 ]]; then + echo "No known-problematic libraries were bundled; nothing to do." + exit 0 +fi + +echo "Repacking AppImage..." +LINUXDEPLOY_PLUGIN_APPIMAGE="${LINUXDEPLOY_PLUGIN_APPIMAGE:-${HOME}/.cache/tauri/linuxdeploy-plugin-appimage.AppImage}" +if [[ ! -x "${LINUXDEPLOY_PLUGIN_APPIMAGE}" ]]; then + echo "linuxdeploy-plugin-appimage.AppImage not found at ${LINUXDEPLOY_PLUGIN_APPIMAGE}" >&2 + echo "(set LINUXDEPLOY_PLUGIN_APPIMAGE to override, or run a tauri appimage build once first so it gets cached)" >&2 + exit 1 +fi + +APPIMAGE_EXTRACT_AND_RUN=1 "${LINUXDEPLOY_PLUGIN_APPIMAGE}" --appdir=squashfs-root + +REBUILT="$(find . -maxdepth 1 -name '*.AppImage' -not -path "$(basename "${APPIMAGE_PATH}")")" +if [[ -z "${REBUILT}" ]]; then + echo "Repacking did not produce a new .AppImage file" >&2 + exit 1 +fi + +mv "${REBUILT}" "${APPIMAGE_PATH}" +chmod +x "${APPIMAGE_PATH}" +echo "Fixed AppImage written to ${APPIMAGE_PATH}" + +SIG_PATH="${APPIMAGE_PATH}.sig" +if [[ -f "${SIG_PATH}" ]]; then + if [[ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]; then + echo "Re-signing repacked AppImage (existing .sig is now stale)..." + (cd "${INVOKED_FROM_DIR}" && npx tauri signer sign "${APPIMAGE_PATH}") + else + echo "WARNING: ${SIG_PATH} exists but TAURI_SIGNING_PRIVATE_KEY is not set;" >&2 + echo "it is now stale and will fail update verification. Re-run with the" >&2 + echo "signing key available, or delete it if this build isn't meant to be signed." >&2 + exit 1 + fi +fi diff --git a/packaging/linux/flow-launcher.sh b/packaging/linux/flow-launcher.sh new file mode 100755 index 0000000..befad58 --- /dev/null +++ b/packaging/linux/flow-launcher.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Flow - A high-performance teleprompter for Windows. +# Copyright (C) 2026 Waled Alturkmani (LumoRez07) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Launcher wrapper for the Flow teleprompter on Linux. +# +# Flow is an always-on-top overlay: it needs absolute window positioning, +# always-on-top, and global hotkeys to work the way it does on Windows. +# Neither GTK3 nor the windowing toolkit Tauri uses on Linux (tao) support +# those under native Wayland, so this wrapper forces the app onto X11 via +# XWayland by default. Set FLOW_ALLOW_WAYLAND=1 to opt out and run under +# native Wayland instead (window positioning, always-on-top and global +# hotkeys will not work correctly in that mode). +# +# WEBKIT_DISABLE_DMABUF_RENDERER=1 works around the most common cause of a +# blank/black WebKitGTK window on Linux (DMA-BUF renderer issues, seen +# especially on some AMD/Mesa driver combinations). It is on by default; +# unset it explicitly if you have verified your system does not need it. + +set -euo pipefail + +if [[ -z "${FLOW_ALLOW_WAYLAND:-}" ]]; then + export GDK_BACKEND=x11 +fi + +export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Installed layout: binary lives at /usr/lib/flow/flow next to this wrapper's +# install location (/usr/bin/flow). Development layout: fall back to the +# debug binary built by `cargo build` inside the repository. +if [[ -x "${SCRIPT_DIR}/../lib/flow/flow" ]]; then + BINARY="${SCRIPT_DIR}/../lib/flow/flow" +elif [[ -x "${SCRIPT_DIR}/../../src-tauri/target/debug/flow" ]]; then + BINARY="${SCRIPT_DIR}/../../src-tauri/target/debug/flow" +elif [[ -x "${SCRIPT_DIR}/../../src-tauri/target/release/flow" ]]; then + BINARY="${SCRIPT_DIR}/../../src-tauri/target/release/flow" +else + echo "flow-launcher.sh: could not locate the flow binary" >&2 + exit 1 +fi + +exec "${BINARY}" "$@" diff --git a/packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png b/packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png new file mode 100644 index 0000000..fdd3f8c Binary files /dev/null and b/packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png differ diff --git a/packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png b/packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png new file mode 100644 index 0000000..1e1ba35 Binary files /dev/null and b/packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png differ diff --git a/packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png b/packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png new file mode 100644 index 0000000..ce68970 Binary files /dev/null and b/packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png differ diff --git a/packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png b/packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png new file mode 100644 index 0000000..e58211d Binary files /dev/null and b/packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png differ diff --git a/packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png b/packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png new file mode 100644 index 0000000..bc0bf2c Binary files /dev/null and b/packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 847cf82..4d4fd05 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1211,12 +1211,14 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-opener", "tauri-plugin-prevent-default", + "tauri-plugin-single-instance", "tauri-plugin-updater", "tokio", "uuid", "vosk", "windows 0.61.3", "winreg 0.52.0", + "zbus", "zip 0.6.6", ] @@ -4659,6 +4661,22 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tokio", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.10.1" @@ -4922,8 +4940,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -6464,6 +6484,7 @@ dependencies = [ "rustix", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "uuid", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e91dd89..cf528c0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,11 +32,17 @@ tauri-plugin-prevent-default = "4" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] } uuid = { version = "1", features = ["serde", "v4"] } vosk = "0.3.1" -windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Power", "Win32_UI_WindowsAndMessaging"] } futures-util = "0.3" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } zip = { version = "0.6", default-features = false, features = ["deflate"] } [target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Power", "Win32_UI_WindowsAndMessaging"] } winreg = "0.52" +[target.'cfg(unix)'.dependencies] +tauri-plugin-single-instance = "2" + +[target.'cfg(target_os = "linux")'.dependencies] +zbus = { version = "5", default-features = false, features = ["tokio", "blocking-api"] } + diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 5901c1e..1e726a8 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -60,10 +60,14 @@ fn panic_message(payload: Box) -> String { } fn stage_vosk_runtime() -> Result<(), String> { - if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() != Some("windows") { - return Ok(()); + match env::var("CARGO_CFG_TARGET_OS").ok().as_deref() { + Some("windows") => stage_vosk_windows(), + Some("linux") => stage_vosk_linux(), + _ => Ok(()), } +} +fn stage_vosk_windows() -> Result<(), String> { let runtime_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|error| error.to_string())?) .join("resources") @@ -109,6 +113,102 @@ fn stage_vosk_runtime() -> Result<(), String> { Ok(()) } +fn stage_vosk_linux() -> Result<(), String> { + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|error| error.to_string())?); + let vendored_dir = manifest_dir.join("resources").join("vosk").join("linux-x64"); + let vendored_lib = vendored_dir.join("libvosk.so"); + + println!("cargo:rerun-if-env-changed=FLOW_VOSK_LIB_DIR"); + println!("cargo:rerun-if-changed={}", vendored_dir.display()); + + // Resolution order: explicit override, then a vendored copy (AppImage/deb/rpm + // packaging), then whatever the system linker can already find (the Arch path, + // where `vosk-api` installs libvosk.so into /usr/lib). + if let Ok(override_dir) = env::var("FLOW_VOSK_LIB_DIR") { + let override_path = PathBuf::from(&override_dir); + let override_lib = override_path.join("libvosk.so"); + + if !override_lib.is_file() { + return Err(format!( + "FLOW_VOSK_LIB_DIR is set to '{}' but no libvosk.so was found there.\n\ + Install the Vosk API runtime (e.g. `sudo pacman -S vosk-api` on Arch \ + Linux) or point FLOW_VOSK_LIB_DIR at a directory containing libvosk.so.", + override_path.display() + )); + } + + emit_vosk_link_directives(&override_path); + return Ok(()); + } + + if vendored_lib.is_file() { + emit_vosk_link_directives(&vendored_dir); + + let profile_dir = resolve_profile_dir()?; + fs::create_dir_all(&profile_dir).map_err(|error| error.to_string())?; + let destination = profile_dir.join("libvosk.so"); + copy_if_changed(&vendored_lib, &destination)?; + + return Ok(()); + } + + if let Some(system_dir) = find_system_vosk_lib_dir() { + emit_vosk_link_directives(&system_dir); + return Ok(()); + } + + Err( + "Could not find libvosk.so for the Linux build.\n\ + Install the Vosk API runtime, e.g. `sudo pacman -S vosk-api` on Arch Linux,\n\ + or place a vendored copy at src-tauri/resources/vosk/linux-x64/libvosk.so\n\ + (see packaging/linux/fetch-vosk.sh), or set FLOW_VOSK_LIB_DIR to a directory\n\ + containing libvosk.so." + .to_string(), + ) +} + +fn emit_vosk_link_directives(dir: &Path) { + println!("cargo:rustc-link-search=native={}", dir.display()); + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib/flow"); +} + +fn find_system_vosk_lib_dir() -> Option { + let output = process::Command::new("ldconfig").arg("-p").output().ok()?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().find_map(|line| { + let line = line.trim(); + if !line.starts_with("libvosk.so") { + return None; + } + + let resolved_path = line.rsplit("=> ").next()?.trim(); + PathBuf::from(resolved_path) + .parent() + .map(Path::to_path_buf) + }) +} + +fn copy_if_changed(source: &Path, destination: &Path) -> Result<(), String> { + if destination.is_file() { + let source_metadata = fs::metadata(source).map_err(|error| error.to_string())?; + let destination_metadata = fs::metadata(destination).map_err(|error| error.to_string())?; + + if source_metadata.len() == destination_metadata.len() { + return Ok(()); + } + } + + fs::copy(source, destination) + .map(|_| ()) + .map_err(|error| error.to_string()) +} + fn stage_runtime_file(source: &Path, destination: &Path) -> Result<(), String> { if destination.is_file() { let source_metadata = fs::metadata(source).map_err(|error| error.to_string())?; diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index bc37b97..c335fd3 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,6 +8,7 @@ "opener:default", "updater:default", "core:window:allow-close", + "core:window:allow-start-dragging", "core:window:allow-set-position", "core:window:allow-set-size", "core:window:allow-current-monitor", diff --git a/src-tauri/resources/vosk/linux-x64/.gitkeep b/src-tauri/resources/vosk/linux-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6d7508a..ec6dce6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -43,6 +43,7 @@ use uuid::Uuid; use vosk::{set_log_level, LogLevel}; use zip::ZipArchive; +mod platform; mod voice_engine; #[cfg(windows)] @@ -1135,7 +1136,12 @@ fn set_sleep_prevention(enabled: bool) { let _ = unsafe { SetThreadExecutionState(flags) }; } -#[cfg(not(windows))] +#[cfg(target_os = "linux")] +fn set_sleep_prevention(enabled: bool) { + platform::set_sleep_prevention(enabled); +} + +#[cfg(all(not(windows), not(target_os = "linux")))] fn set_sleep_prevention(_enabled: bool) {} fn load_dev_tray_icon() -> tauri::Result> { @@ -1167,7 +1173,12 @@ fn show_startup_error_dialog(message: &str) { }; } -#[cfg(not(windows))] +#[cfg(target_os = "linux")] +fn show_startup_error_dialog(message: &str) { + platform::show_startup_error_dialog(message); +} + +#[cfg(all(not(windows), not(target_os = "linux")))] fn show_startup_error_dialog(_message: &str) {} #[cfg(windows)] @@ -1266,6 +1277,24 @@ fn ensure_webview2_runtime_available() -> bool { true } +#[cfg(target_os = "linux")] +fn format_startup_error_message(error: &tauri::Error) -> String { + format!( + concat!( + "Flow could not start the desktop runtime.\n\n", + "Common causes on Linux:\n", + "- The WebKitGTK runtime (webkit2gtk-4.1) is missing or too old.\n", + "- A GPU/driver DMA-BUF rendering issue is blocking the embedded webview \n", + " (try relaunching with WEBKIT_DISABLE_DMABUF_RENDERER=1).\n", + "- The user profile or app data location (~/.local/share) is unavailable.\n\n", + "Install webkit2gtk-4.1 via your distribution's package manager, then try again.\n\n", + "Technical details:\n{}" + ), + error + ) +} + +#[cfg(not(target_os = "linux"))] fn format_startup_error_message(error: &tauri::Error) -> String { format!( concat!( @@ -1374,7 +1403,16 @@ fn ensure_window( if label == "remote-inbox" { if let Some(main) = app.get_webview_window("main") { - builder = builder.owner(&main)?; + // `owner()` is Windows-only in Tauri; `parent()` is the cross-platform + // equivalent (GTK `set_transient_for` on Linux, child window on macOS). + #[cfg(windows)] + { + builder = builder.owner(&main)?; + } + #[cfg(not(windows))] + { + builder = builder.parent(&main)?; + } } } @@ -1523,15 +1561,22 @@ fn exit_app(app: &tauri::AppHandle) { } fn center_aux_window_on_active_monitor(window: &WebviewWindow, anchor: &WebviewWindow) { - if let Ok(Some(monitor)) = anchor.current_monitor() { - if let Ok(window_size) = window.outer_size() { - let monitor_pos = monitor.position(); - let monitor_size = monitor.size(); - let x = monitor_pos.x + ((monitor_size.width as i32 - window_size.width as i32) / 2).max(0); - let y = monitor_pos.y + ((monitor_size.height as i32 - window_size.height as i32) / 2).max(0); - let _ = window.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y })); - return; - } + // Center over the anchor window's own bounds rather than querying monitor + // geometry. On X11/XWayland with multiple monitors of differing + // resolutions, `Window::current_monitor()` has been observed to report + // stale/incorrect geometry, which places the aux window off in an + // unrelated corner of the desktop instead of centering it. Anchoring to + // the main window's outer position/size avoids monitor enumeration + // entirely and is also more intuitive (dialog-style centering). + if let (Ok(anchor_pos), Ok(anchor_size), Ok(window_size)) = ( + anchor.outer_position(), + anchor.outer_size(), + window.outer_size(), + ) { + let x = anchor_pos.x + (anchor_size.width as i32 - window_size.width as i32) / 2; + let y = anchor_pos.y + (anchor_size.height as i32 - window_size.height as i32) / 2; + let _ = window.set_position(tauri::Position::Physical(tauri::PhysicalPosition { x, y })); + return; } let _ = window.center(); } @@ -1694,6 +1739,29 @@ fn toggle_clickthrough_impl( set_clickthrough_impl(app, desktop, !current) } +/// Lets a user on native Wayland (where global hotkeys can't be grabbed by +/// the app itself, see `platform::session_type`) bind desktop-environment +/// shortcuts to launching `flow --toggle-clickthrough` / `--show` / `--hide`. +/// The single-instance plugin forwards the second invocation's argv here +/// instead of starting a second app instance. +#[cfg(unix)] +fn handle_secondary_instance_cli_args(app: &tauri::AppHandle, argv: &[String]) { + let Some(desktop) = app.try_state::() else { + return; + }; + + for arg in argv.iter().skip(1) { + match arg.as_str() { + "--toggle-clickthrough" => { + let _ = toggle_clickthrough_impl(app, &desktop); + } + "--show" => show_main_window(app), + "--hide" => hide_main_window_impl(app), + _ => {} + } + } +} + #[tauri::command] fn close_app(app: tauri::AppHandle) { exit_app(&app); @@ -1919,6 +1987,72 @@ fn get_distribution_channel() -> String { .to_string() } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct PlatformCapabilities { + os: &'static str, + session: String, + capture_protection: bool, + global_shortcuts: bool, + always_on_top: bool, + window_positioning: bool, + in_app_updater: bool, + package_source: String, +} + +#[cfg(target_os = "linux")] +fn platform_session_type() -> String { + platform::session_type() +} + +#[cfg(not(target_os = "linux"))] +fn platform_session_type() -> String { + "unknown".to_string() +} + +#[cfg(target_os = "linux")] +fn platform_package_source() -> String { + option_env!("FLOW_LINUX_PACKAGE") + .unwrap_or("unknown") + .to_string() +} + +#[cfg(not(target_os = "linux"))] +fn platform_package_source() -> String { + "unknown".to_string() +} + +#[tauri::command] +fn get_platform_capabilities() -> PlatformCapabilities { + let session = platform_session_type(); + let package_source = platform_package_source(); + + // On Linux, always-on-top / absolute positioning / global shortcuts all + // rely on the same thing: an X11 server tao/global-hotkey can talk to. + // That's present under X11 and under XWayland, but not under native + // Wayland (see plan.md E2 and Task 3.6's verification). + let has_x11_window_management = cfg!(target_os = "linux").then(|| session == "x11").unwrap_or(true); + + PlatformCapabilities { + os: if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "macos") { + "macos" + } else { + "unknown" + }, + session, + capture_protection: cfg!(windows), + global_shortcuts: has_x11_window_management, + always_on_top: has_x11_window_management, + window_positioning: has_x11_window_management, + in_app_updater: cfg!(windows) || package_source == "appimage", + package_source, + } +} + #[tauri::command] async fn download_voice_model( app: tauri::AppHandle, @@ -2802,7 +2936,16 @@ pub fn run() { let voice_model_downloads = VoiceModelDownloads::default(); let voice_engine_state = voice_engine::VoiceEngineState::default(); - let result = tauri::Builder::default() + let mut builder = tauri::Builder::default(); + + #[cfg(unix)] + { + builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { + handle_secondary_instance_cli_args(app, &argv); + })); + } + + let result = builder .manage(relay.clone()) .manage(desktop_state) .manage(voice_model_downloads) @@ -2833,6 +2976,7 @@ pub fn run() { save_persisted_app_state, save_persisted_voice_model_registry, get_distribution_channel, + get_platform_capabilities, fetch_updater_feed_metadata, list_voice_models, get_voice_model_status, diff --git a/src-tauri/src/platform/linux.rs b/src-tauri/src/platform/linux.rs new file mode 100644 index 0000000..52b2977 --- /dev/null +++ b/src-tauri/src/platform/linux.rs @@ -0,0 +1,202 @@ +/* + * Flow - A high-performance teleprompter for Windows. + * Copyright (C) 2026 Waled Alturkmani (LumoRez07) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +use std::env; +use std::process::Command; +use std::sync::Mutex; + +use zbus::blocking::Connection; +use zbus::zvariant::OwnedFd; + +const STARTUP_ERROR_TITLE: &str = "Flow startup error"; + +/// Shows a startup error dialog on Linux, mirroring Windows' `MessageBoxW` +/// use in lib.rs. Tries native dialog tools in order of how commonly they're +/// preinstalled, since at this point in startup nothing GTK-based can be +/// assumed to work yet (that's exactly what failed) - `kdialog`/`zenity`/ +/// `notify-send` are separate processes, so they don't depend on our own +/// (broken) webview or GTK context. +pub fn show_startup_error_dialog(message: &str) { + if try_command_dialog("kdialog", &["--error", message, "--title", STARTUP_ERROR_TITLE]) { + return; + } + + if try_command_dialog( + "zenity", + &[ + "--error", + &format!("--title={STARTUP_ERROR_TITLE}"), + &format!("--text={message}"), + ], + ) { + return; + } + + if try_command_dialog( + "notify-send", + &["--urgency=critical", STARTUP_ERROR_TITLE, message], + ) { + return; + } + + eprintln!("[flow] {STARTUP_ERROR_TITLE}: {message}"); +} + +fn try_command_dialog(program: &str, args: &[&str]) -> bool { + Command::new(program) + .args(args) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +const INHIBIT_APP_NAME: &str = "Flow"; +const INHIBIT_REASON: &str = "Teleprompter is actively being used"; + +/// Holds whatever handles are needed to release an active sleep/screensaver +/// inhibitor later. Dropping the logind file descriptor releases that +/// inhibitor automatically; the ScreenSaver one needs an explicit UnInhibit +/// call on the same connection that requested it. +struct SleepInhibitor { + screensaver: Option<(Connection, u32)>, + // Only ever dropped, never read: closing the fd is what releases the + // logind inhibitor lock. + #[allow(dead_code)] + logind_fd: Option, +} + +static SLEEP_INHIBITOR: Mutex> = Mutex::new(None); + +/// Prevents (or re-allows) the system from sleeping/screen-locking while +/// Flow is actively being used, mirroring Windows' `SetThreadExecutionState` +/// use in lib.rs. Two independent D-Bus mechanisms are used together since +/// desktop environments vary in which one they honor: +/// - `org.freedesktop.ScreenSaver.Inhibit` blocks the screensaver/DPMS +/// (supported by GNOME, KDE/kscreenlocker, and most other DEs). +/// - `org.freedesktop.login1.Manager.Inhibit("idle", ...)` blocks systemd- +/// logind from suspending the system due to idle timeout. +/// Failures are logged but never fatal - a teleprompter that occasionally +/// lets the screen sleep is a much smaller problem than one that crashes. +pub fn set_sleep_prevention(enabled: bool) { + let mut guard = SLEEP_INHIBITOR + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + + if enabled { + if guard.is_some() { + return; + } + *guard = Some(acquire_sleep_inhibitor()); + } else if let Some(inhibitor) = guard.take() { + release_sleep_inhibitor(inhibitor); + } +} + +fn acquire_sleep_inhibitor() -> SleepInhibitor { + let screensaver = match inhibit_screensaver() { + Ok(inhibitor) => Some(inhibitor), + Err(error) => { + crate::log_backend_error("org.freedesktop.ScreenSaver.Inhibit", &error); + None + } + }; + + let logind_fd = match inhibit_logind() { + Ok(fd) => Some(fd), + Err(error) => { + crate::log_backend_error("org.freedesktop.login1.Manager.Inhibit", &error); + None + } + }; + + SleepInhibitor { + screensaver, + logind_fd, + } +} + +fn release_sleep_inhibitor(inhibitor: SleepInhibitor) { + if let Some((connection, cookie)) = inhibitor.screensaver { + let result = connection.call_method( + Some("org.freedesktop.ScreenSaver"), + "/org/freedesktop/ScreenSaver", + Some("org.freedesktop.ScreenSaver"), + "UnInhibit", + &(cookie,), + ); + if let Err(error) = result { + crate::log_backend_error("org.freedesktop.ScreenSaver.UnInhibit", &error); + } + } + // `inhibitor.logind_fd` (if any) is dropped here, closing the file + // descriptor and releasing the logind idle inhibitor lock. +} + +fn inhibit_screensaver() -> zbus::Result<(Connection, u32)> { + let connection = Connection::session()?; + let reply = connection.call_method( + Some("org.freedesktop.ScreenSaver"), + "/org/freedesktop/ScreenSaver", + Some("org.freedesktop.ScreenSaver"), + "Inhibit", + &(INHIBIT_APP_NAME, INHIBIT_REASON), + )?; + let cookie: u32 = reply.body().deserialize()?; + Ok((connection, cookie)) +} + +fn inhibit_logind() -> zbus::Result { + let connection = Connection::system()?; + let reply = connection.call_method( + Some("org.freedesktop.login1"), + "/org/freedesktop/login1", + Some("org.freedesktop.login1.Manager"), + "Inhibit", + &("idle", INHIBIT_APP_NAME, INHIBIT_REASON, "block"), + )?; + let fd: OwnedFd = reply.body().deserialize()?; + Ok(fd) +} + +/// Returns "x11", "wayland", or "unknown" for the windowing backend GTK/tao +/// actually uses - not necessarily the compositor's own session type. +/// +/// `GDK_BACKEND` is checked first and wins whenever set: our own launcher +/// (packaging/linux/flow-launcher.sh) sets `GDK_BACKEND=x11` by default so +/// the app runs under XWayland even in a `XDG_SESSION_TYPE=wayland` session, +/// since always-on-top/window positioning/global shortcuts only work that +/// way (see plan.md E2). Reporting the compositor's session type instead +/// would make `get_platform_capabilities` claim those features are +/// unavailable on a system where they actually work fine under XWayland. +pub fn session_type() -> String { + if let Ok(value) = env::var("GDK_BACKEND") { + let normalized = value.to_lowercase(); + if normalized == "x11" || normalized == "wayland" { + return normalized; + } + } + + if let Ok(value) = env::var("XDG_SESSION_TYPE") { + let normalized = value.to_lowercase(); + if normalized == "x11" || normalized == "wayland" { + return normalized; + } + } + + if env::var("WAYLAND_DISPLAY").is_ok() { + return "wayland".to_string(); + } + + if env::var("DISPLAY").is_ok() { + return "x11".to_string(); + } + + "unknown".to_string() +} diff --git a/src-tauri/src/platform/mod.rs b/src-tauri/src/platform/mod.rs new file mode 100644 index 0000000..46a1aa6 --- /dev/null +++ b/src-tauri/src/platform/mod.rs @@ -0,0 +1,20 @@ +/* + * Flow - A high-performance teleprompter for Windows. + * Copyright (C) 2026 Waled Alturkmani (LumoRez07) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +pub use linux::{session_type, set_sleep_prevention, show_startup_error_dialog}; + +#[cfg(not(target_os = "linux"))] +pub fn session_type() -> String { + "unknown".to_string() +} diff --git a/src-tauri/src/voice_engine.rs b/src-tauri/src/voice_engine.rs index 2d27305..af4c5c2 100644 --- a/src-tauri/src/voice_engine.rs +++ b/src-tauri/src/voice_engine.rs @@ -275,12 +275,75 @@ pub(crate) fn list_input_devices() -> Result, String> .input_devices() .map_err(|error| format!("Failed to enumerate microphones: {error}"))?; - Ok(devices + #[allow(unused_mut)] + let mut devices: Vec = devices .filter_map(|device| { let label = device.name().unwrap_or_default().trim().to_string(); (!label.is_empty()).then_some(VoiceInputDeviceInfo { label }) }) - .collect()) + .collect(); + + #[cfg(target_os = "linux")] + { + devices.retain(|device| is_selectable_linux_input_device(&device.label)); + devices.sort_by(|a, b| linux_input_device_sort_key(&a.label).cmp(&linux_input_device_sort_key(&b.label))); + } + + Ok(devices) +} + +/// ALSA enumerates every PCM name a card's plugin chain defines, not just the +/// ones that make sense for microphone capture. On a typical Linux desktop +/// (verified against a live PipeWire-routed ALSA stack) this includes +/// resampler/plugin building blocks (`speexrate`, `lavrate`, `jack`, `upmix`, +/// ...) and output-routing topology variants (`front:`, `surroundNN:`, +/// `iec958:`) that were never meant to be picked directly as an input device. +/// This filters those out so the Settings UI shows a short, meaningful list. +#[cfg(target_os = "linux")] +const ALSA_NON_DEVICE_PLUGIN_NAMES: &[&str] = &[ + "lavrate", + "samplerate", + "speexrate", + "speex", + "jack", + "upmix", + "vdownmix", +]; + +#[cfg(target_os = "linux")] +fn is_selectable_linux_input_device(label: &str) -> bool { + if ALSA_NON_DEVICE_PLUGIN_NAMES.contains(&label) { + return false; + } + + let is_output_topology_variant = label.starts_with("front:") + || label.starts_with("surround") + || label.starts_with("iec958:"); + + !is_output_topology_variant +} + +/// Ranks the common cross-desktop virtual devices first (in the order a user +/// is most likely to want them), then per-card `sysdefault:CARD=...` entries, +/// then anything else (e.g. raw `hw:`/`plughw:` devices on setups without a +/// sound server) last. +#[cfg(target_os = "linux")] +const ALSA_PREFERRED_DEVICE_ORDER: &[&str] = &["default", "pipewire", "pulse"]; + +#[cfg(target_os = "linux")] +fn linux_input_device_sort_key(label: &str) -> (u8, String) { + if let Some(position) = ALSA_PREFERRED_DEVICE_ORDER + .iter() + .position(|preferred| *preferred == label) + { + return (0, format!("{position}")); + } + + if label.starts_with("sysdefault:") { + return (1, label.to_lowercase()); + } + + (2, label.to_lowercase()) } impl VoiceEngine { @@ -1230,3 +1293,4 @@ fn emit_stream_error(app: &AppHandle, error: String) { fn emit_native_voice_event(app: &AppHandle, payload: NativeVoiceEvent) { let _ = app.emit(NATIVE_VOICE_EVENT, payload); } + diff --git a/src-tauri/tauri.linux.appimage.conf.json b/src-tauri/tauri.linux.appimage.conf.json new file mode 100644 index 0000000..d50663d --- /dev/null +++ b/src-tauri/tauri.linux.appimage.conf.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "createUpdaterArtifacts": true + } +} diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..1ac8b2b --- /dev/null +++ b/src-tauri/tauri.linux.conf.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "targets": ["appimage", "deb", "rpm"], + "createUpdaterArtifacts": false, + "linux": { + "appimage": { + "files": { + "/usr/share/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png", + "/usr/share/metainfo/app.flowteleprompter.Flow.metainfo.xml": "../packaging/linux/app.flowteleprompter.Flow.metainfo.xml" + } + }, + "deb": { + "depends": [ + "libwebkit2gtk-4.1-0", + "libgtk-3-0", + "libayatana-appindicator3-1", + "libasound2t64" + ], + "desktopTemplate": "../packaging/linux/app.flowteleprompter.Flow.desktop", + "files": { + "/usr/lib/flow/libvosk.so": "resources/vosk/linux-x64/libvosk.so", + "/usr/share/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png", + "/usr/share/metainfo/app.flowteleprompter.Flow.metainfo.xml": "../packaging/linux/app.flowteleprompter.Flow.metainfo.xml" + } + }, + "rpm": { + "depends": [ + "webkit2gtk4.1", + "gtk3", + "libayatana-appindicator-gtk3", + "alsa-lib" + ], + "desktopTemplate": "../packaging/linux/app.flowteleprompter.Flow.desktop", + "files": { + "/usr/lib/flow/libvosk.so": "resources/vosk/linux-x64/libvosk.so", + "/usr/share/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/32x32/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/64x64/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/128x128/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/256x256/apps/app.flowteleprompter.Flow.png", + "/usr/share/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png": "../packaging/linux/icons/hicolor/512x512/apps/app.flowteleprompter.Flow.png", + "/usr/share/metainfo/app.flowteleprompter.Flow.metainfo.xml": "../packaging/linux/app.flowteleprompter.Flow.metainfo.xml" + } + } + }, + "resources": { + "resources/vosk/linux-x64/libvosk.so": "lib/" + } + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/LumoRez07/Flow/releases/latest/download/latest.json" + ] + } + } +} diff --git a/src/about.html b/src/about.html index 993f836..8dc2dcb 100644 --- a/src/about.html +++ b/src/about.html @@ -19,10 +19,10 @@
-