fix(appimage): smoke-test fixes for tray, labels, and packaging (#602)
* fix(ui): AppImage smoke fixes for tray icons, labels, and identity Replace placeholder mic tray SVGs with opaque branded icons, show whisper.cpp instead of Whisper_cpp, set GLib/AppIndicator identity to Vocalinux (not main.py), and give About dialog buttons enough padding so labels are not clipped under Adwaita. * fix(packaging): harden AppImage build for Wayland and tray deps Pin the build interpreter, install onnxruntime into the bundle, set desktop Exec=AppRun + StartupWMClass, prune typelibs to an allowlist after linuxdeploy, and stop forcing GDK_BACKEND=x11 on Wayland (VOCALINUX_FORCE_X11=1 remains as an escape hatch). CI AppImage jobs also pull Ayatana AppIndicator and librsvg SVG loader packages. * fix(packaging): bundle full stdlib from base_prefix in AppImage When build.sh runs under a venv, sys.prefix only has site-packages, so the AppImage was missing encodings and crashed on start. Copy the interpreter and stdlib from sys.base_prefix, force pip --ignore-installed into AppDir, set Gdk program class to Vocalinux, and tone down the muted tray slash so small StatusNotifier icons stay slate. * fix(tray): use absolute icon paths so stale hicolor icons lose XFCE StatusNotifier resolves IconName through the system theme and can ignore IconThemePath. A leftover ~/.local/share/icons placeholder (vocalinux-microphone-off.svg filled #e74c3c) was winning over the AppImage's bundled icons. Outside Flatpak, pass absolute SVG paths to set_icon_full / new_with_path instead of bare theme names.
@@ -42,7 +42,10 @@ jobs:
|
||||
gobject-introspection \
|
||||
gir1.2-gtk-3.0 \
|
||||
gir1.2-appindicator3-0.1 \
|
||||
gir1.2-ibus-1.0
|
||||
gir1.2-ayatanaappindicator3-0.1 \
|
||||
gir1.2-ibus-1.0 \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-common
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
@@ -284,7 +287,10 @@ jobs:
|
||||
gobject-introspection \
|
||||
gir1.2-gtk-3.0 \
|
||||
gir1.2-appindicator3-0.1 \
|
||||
gir1.2-ibus-1.0
|
||||
gir1.2-ayatanaappindicator3-0.1 \
|
||||
gir1.2-ibus-1.0 \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-common
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
|
||||
@@ -42,7 +42,10 @@ jobs:
|
||||
gobject-introspection \
|
||||
gir1.2-gtk-3.0 \
|
||||
gir1.2-appindicator3-0.1 \
|
||||
gir1.2-ibus-1.0
|
||||
gir1.2-ayatanaappindicator3-0.1 \
|
||||
gir1.2-ibus-1.0 \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-common
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
@@ -163,7 +166,10 @@ jobs:
|
||||
gobject-introspection \
|
||||
gir1.2-gtk-3.0 \
|
||||
gir1.2-appindicator3-0.1 \
|
||||
gir1.2-ibus-1.0
|
||||
gir1.2-ayatanaappindicator3-0.1 \
|
||||
gir1.2-ibus-1.0 \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-common
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
|
||||
@@ -198,7 +198,10 @@ jobs:
|
||||
sudo apt-get install -y \
|
||||
xdotool \
|
||||
gir1.2-appindicator3-0.1 \
|
||||
gir1.2-ayatanaappindicator3-0.1 \
|
||||
gir1.2-ibus-1.0 \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-common \
|
||||
libcairo2-dev \
|
||||
pkg-config \
|
||||
python3-dev \
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
# ponytail: text-injection CLI tools (xdotool/wtype/ydotool) are not
|
||||
# bundled, same runtime prerequisite as the PyPI install path documented
|
||||
# in docs/INSTALL.md. Add bundling if users hit missing-binary complaints.
|
||||
#
|
||||
# GPU note: the pip pywhispercpp wheel is CPU-only. Vulkan/CUDA rebuilds
|
||||
# stay on install.sh for now; the AppImage logs when GPU libs are absent.
|
||||
set -euo pipefail
|
||||
|
||||
WHEEL="$1"
|
||||
@@ -21,18 +24,28 @@ VERSION="$2"
|
||||
OUTDIR="${3:-dist}"
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ARCH="$(uname -m)"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64|aarch64) ;;
|
||||
*) echo "Unsupported architecture: $ARCH (need x86_64 or aarch64)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
TYPELIBS=(
|
||||
Gtk-3.0 Gdk-3.0 GdkX11-3.0 GdkPixbuf-2.0 GLib-2.0 GObject-2.0 Gio-2.0
|
||||
Pango-1.0 PangoCairo-1.0 cairo-1.0 HarfBuzz-0.0 Atk-1.0 freetype2-2.0
|
||||
AppIndicator3-0.1 AyatanaAppIndicator3-0.1 Notify-0.7 IBus-1.0 Rsvg-2.0
|
||||
)
|
||||
|
||||
WORKDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
APPDIR="$WORKDIR/AppDir"
|
||||
TOOLDIR="$WORKDIR/tools"
|
||||
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" "$TOOLDIR" "$OUTDIR"
|
||||
|
||||
# Nested AppImages need extract-and-run on hosts without usable FUSE.
|
||||
export APPIMAGE_EXTRACT_AND_RUN="${APPIMAGE_EXTRACT_AND_RUN:-1}"
|
||||
|
||||
echo "== Fetching AppImage tooling ($ARCH) =="
|
||||
curl -fsSL -o "$TOOLDIR/linuxdeploy" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
|
||||
@@ -42,31 +55,52 @@ curl -fsSL -o "$TOOLDIR/appimagetool" \
|
||||
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage"
|
||||
chmod +x "$TOOLDIR/linuxdeploy" "$TOOLDIR/linuxdeploy-plugin-gtk.sh" "$TOOLDIR/appimagetool"
|
||||
|
||||
echo "== Bundling Python runtime =="
|
||||
PY_PREFIX="$(python3 -c 'import sys; print(sys.prefix)')"
|
||||
PY_VER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
||||
cp -L "$(command -v python3)" "$APPDIR/usr/bin/python3"
|
||||
echo "== Bundling Python runtime ($PYTHON) =="
|
||||
# Use base_prefix so a venv builder still ships a full stdlib (encodings, etc.).
|
||||
PY_PREFIX="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')"
|
||||
PY_VER="$("$PYTHON" -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
||||
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')/bin/python${PY_VER}"
|
||||
if [ ! -x "$PY_BIN" ]; then
|
||||
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')/bin/python3"
|
||||
fi
|
||||
if [ ! -x "$PY_BIN" ]; then
|
||||
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.executable)')"
|
||||
fi
|
||||
cp -L "$PY_BIN" "$APPDIR/usr/bin/python3"
|
||||
cp -r "$PY_PREFIX/lib/python${PY_VER}" "$APPDIR/usr/lib/python${PY_VER}"
|
||||
rm -rf "$APPDIR/usr/lib/python${PY_VER}/site-packages"
|
||||
|
||||
echo "== Installing Vocalinux + PyGObject into the bundle =="
|
||||
python3 -m pip install --no-cache-dir --prefix "$APPDIR/usr" "$WHEEL" PyGObject pycairo
|
||||
echo "== Installing Vocalinux + runtime deps into the bundle =="
|
||||
# --ignore-installed: pip otherwise treats the builder env's packages as
|
||||
# satisfying deps and skips copying vosk/pywhispercpp/etc. into AppDir.
|
||||
"$PYTHON" -m pip install --no-cache-dir --ignore-installed --prefix "$APPDIR/usr" \
|
||||
"$WHEEL" PyGObject pycairo onnxruntime
|
||||
|
||||
echo "== Adding desktop entry + icon =="
|
||||
install -Dm644 "$REPO_ROOT/vocalinux.desktop" "$APPDIR/usr/share/applications/vocalinux.desktop"
|
||||
# AppImage desktop integration expects Exec=AppRun; set WM class for the tray app.
|
||||
sed -i \
|
||||
-e 's|^Exec=.*|Exec=AppRun|' \
|
||||
-e '/^StartupWMClass=/d' \
|
||||
"$APPDIR/usr/share/applications/vocalinux.desktop"
|
||||
printf 'StartupWMClass=vocalinux\n' >> "$APPDIR/usr/share/applications/vocalinux.desktop"
|
||||
install -Dm644 "$REPO_ROOT/resources/icons/scalable/vocalinux.svg" \
|
||||
"$APPDIR/usr/share/icons/hicolor/scalable/apps/vocalinux.svg"
|
||||
|
||||
copy_typelibs() {
|
||||
local dest="$1"
|
||||
mkdir -p "$dest"
|
||||
local typelib found
|
||||
for typelib in "${TYPELIBS[@]}"; do
|
||||
found="$(find /usr/lib -name "${typelib}.typelib" 2>/dev/null | head -1 || true)"
|
||||
if [ -n "$found" ]; then
|
||||
cp "$found" "$dest/"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "== Copying GObject-Introspection typelibs (not handled by linuxdeploy-plugin-gtk) =="
|
||||
mkdir -p "$APPDIR/usr/lib/girepository-1.0"
|
||||
for typelib in Gtk-3.0 Gdk-3.0 GdkX11-3.0 GdkPixbuf-2.0 GLib-2.0 GObject-2.0 Gio-2.0 \
|
||||
Pango-1.0 PangoCairo-1.0 cairo-1.0 HarfBuzz-0.0 Atk-1.0 freetype2-2.0 \
|
||||
AppIndicator3-0.1 AyatanaAppIndicator3-0.1 Notify-0.7 IBus-1.0; do
|
||||
found="$(find /usr/lib -name "${typelib}.typelib" 2>/dev/null | head -1)"
|
||||
if [ -n "$found" ]; then
|
||||
cp "$found" "$APPDIR/usr/lib/girepository-1.0/"
|
||||
fi
|
||||
done
|
||||
copy_typelibs "$APPDIR/usr/lib/girepository-1.0"
|
||||
|
||||
echo "== Writing AppRun =="
|
||||
cat > "$APPDIR/AppRun" << 'APPRUN'
|
||||
@@ -81,7 +115,7 @@ exec "$HERE/usr/bin/python3" -m vocalinux.main "$@"
|
||||
APPRUN
|
||||
# PYTHONPATH above uses a version-agnostic symlink so AppRun doesn't need
|
||||
# to know the exact interpreter minor version at runtime.
|
||||
ln -s "python${PY_VER}" "$APPDIR/usr/lib/python3"
|
||||
ln -sfn "python${PY_VER}" "$APPDIR/usr/lib/python3"
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
echo "== Running linuxdeploy (resolves the shared-library closure + GTK theming) =="
|
||||
@@ -92,6 +126,46 @@ export DEPLOY_GTK_VERSION=3
|
||||
-d "$APPDIR/usr/share/applications/vocalinux.desktop" \
|
||||
-i "$APPDIR/usr/share/icons/hicolor/scalable/apps/vocalinux.svg"
|
||||
|
||||
echo "== Pruning typelibs back to the allowlist (linuxdeploy copies the host set) =="
|
||||
rm -rf "$APPDIR/usr/lib/girepository-1.0"
|
||||
copy_typelibs "$APPDIR/usr/lib/girepository-1.0"
|
||||
|
||||
echo "== Patching linuxdeploy GTK AppRun hook (Wayland-friendly GDK backend) =="
|
||||
GTK_HOOK="$APPDIR/apprun-hooks/linuxdeploy-plugin-gtk.sh"
|
||||
if [ -f "$GTK_HOOK" ]; then
|
||||
# Upstream forces GDK_BACKEND=x11 even on Wayland, which breaks hover/focus
|
||||
# for GTK widgets under Plasma (XWayland). Prefer native Wayland unless the
|
||||
# user opts into X11 via VOCALINUX_FORCE_X11=1.
|
||||
python3 - "$GTK_HOOK" <<'PY'
|
||||
import pathlib, re, sys
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
replacement = (
|
||||
'# Prefer Wayland when available; force X11 only when requested.\n'
|
||||
'if [ "${VOCALINUX_FORCE_X11:-0}" = "1" ]; then\n'
|
||||
' export GDK_BACKEND=x11\n'
|
||||
'elif [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then\n'
|
||||
' export GDK_BACKEND=wayland\n'
|
||||
'else\n'
|
||||
' export GDK_BACKEND=x11\n'
|
||||
'fi'
|
||||
)
|
||||
new, n = re.subn(
|
||||
r'^[ \t]*export GDK_BACKEND=x11[^\n]*$',
|
||||
replacement,
|
||||
text,
|
||||
count=1,
|
||||
flags=re.M,
|
||||
)
|
||||
if n != 1:
|
||||
raise SystemExit(f"failed to patch GDK_BACKEND in {path} (matches={n})")
|
||||
path.write_text(new)
|
||||
print(f"Patched {path}")
|
||||
PY
|
||||
else
|
||||
echo "Warning: GTK AppRun hook not found at $GTK_HOOK" >&2
|
||||
fi
|
||||
|
||||
echo "== Packaging AppImage =="
|
||||
OUTPUT="$OUTDIR/Vocalinux-${VERSION}-${ARCH}.AppImage"
|
||||
ARCH="$ARCH" "$TOOLDIR/appimagetool" "$APPDIR" "$OUTPUT"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone off icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#e74c3c" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#7f8c8d" />
|
||||
<line x1="40" y1="88" x2="88" y2="40" stroke="#e74c3c" stroke-width="8" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Idle / muted: slate disc; keep slash subtle so 22px trays don't look like a red badge -->
|
||||
<circle cx="64" cy="64" r="60" fill="#1e293b"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#94a3b8"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Mute slash: light outline only so the slate disc stays the dominant color -->
|
||||
<line x1="36" y1="96" x2="92" y2="32" stroke="#f8fafc" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 869 B |
@@ -1,16 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone processing icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#f39c12" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<circle cx="64" cy="40" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="40;50;40" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="48" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="80" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Processing: amber disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#d97706"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#1c1917"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Processing dots -->
|
||||
<circle cx="44" cy="52" r="6" fill="#fffbeb"/>
|
||||
<circle cx="64" cy="44" r="6" fill="#fffbeb"/>
|
||||
<circle cx="84" cy="52" r="6" fill="#fffbeb"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 829 B |
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone on/active icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#2ecc71" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<path d="M 40,78 Q 64,88 88,78" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<path d="M 40,88 Q 64,98 88,88" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Active / listening: branded teal disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#0d9488"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#0f172a"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Sound waves -->
|
||||
<path d="M86 48c4 4 6 9 6 16s-2 12-6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M96 40c7 7 11 15 11 24s-4 17-11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M42 48c-4 4-6 9-6 16s2 12 6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M32 40c-7 7-11 15-11 24s4 17 11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 1.1 KiB |
@@ -263,6 +263,22 @@ def main():
|
||||
logger.error("Cannot start Vocalinux due to missing dependencies")
|
||||
sys.exit(1)
|
||||
|
||||
# Identity for WM class / AppIndicator title (otherwise shows as main.py)
|
||||
import gi
|
||||
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import GLib
|
||||
|
||||
GLib.set_prgname("vocalinux")
|
||||
GLib.set_application_name("Vocalinux")
|
||||
try:
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
Gdk.set_program_class("Vocalinux")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check if display is available before creating any GTK widgets
|
||||
if not check_display_available():
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone off icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#e74c3c" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#7f8c8d" />
|
||||
<line x1="40" y1="88" x2="88" y2="40" stroke="#e74c3c" stroke-width="8" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Idle / muted: slate disc; keep slash subtle so 22px trays don't look like a red badge -->
|
||||
<circle cx="64" cy="64" r="60" fill="#1e293b"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#94a3b8"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Mute slash: light outline only so the slate disc stays the dominant color -->
|
||||
<line x1="36" y1="96" x2="92" y2="32" stroke="#f8fafc" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 869 B |
@@ -1,16 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone processing icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#f39c12" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<circle cx="64" cy="40" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="40;50;40" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="48" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="80" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Processing: amber disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#d97706"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#1c1917"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Processing dots -->
|
||||
<circle cx="44" cy="52" r="6" fill="#fffbeb"/>
|
||||
<circle cx="64" cy="44" r="6" fill="#fffbeb"/>
|
||||
<circle cx="84" cy="52" r="6" fill="#fffbeb"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 829 B |
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone on/active icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#2ecc71" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<path d="M 40,78 Q 64,88 88,78" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<path d="M 40,88 Q 64,98 88,88" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Active / listening: branded teal disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#0d9488"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#0f172a"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Sound waves -->
|
||||
<path d="M86 48c4 4 6 9 6 16s-2 12-6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M96 40c7 7 11 15 11 24s-4 17-11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M42 48c-4 4-6 9-6 16s2 12 6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M32 40c-7 7-11 15-11 24s4 17 11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 1.1 KiB |
@@ -186,6 +186,11 @@ class AboutDialog(Gtk.Dialog):
|
||||
color: @theme_unfocused_fg_color;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.about-dialog button {
|
||||
min-height: 32px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Gtk.Window = None):
|
||||
|
||||
@@ -89,7 +89,7 @@ ENGINE_MODELS = {
|
||||
ENGINE_DISPLAY_NAMES = {
|
||||
"vosk": "Vosk",
|
||||
"whisper": "Whisper",
|
||||
"whisper_cpp": "Whisper_cpp",
|
||||
"whisper_cpp": "whisper.cpp",
|
||||
"remote_api": "Remote API",
|
||||
}
|
||||
|
||||
@@ -3657,8 +3657,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
def _update_advanced_tab_sensitivity(self):
|
||||
"""Enable or disable advanced settings based on selected engine."""
|
||||
engine_text = self.engine_combo.get_active_text()
|
||||
is_whispercpp = engine_text and engine_text.lower() == "whisper_cpp"
|
||||
is_whispercpp = self._get_selected_engine() == "whisper_cpp"
|
||||
|
||||
widgets = [
|
||||
self.advanced_no_timestamps_switch,
|
||||
|
||||
@@ -117,6 +117,18 @@ class TrayIndicator:
|
||||
}
|
||||
self.icon_names = _themed_icon_names()
|
||||
|
||||
# Prefer absolute icon paths outside Flatpak. StatusNotifier hosts often
|
||||
# resolve IconName via the system hicolor theme and ignore IconThemePath,
|
||||
# so a stale ~/.local/share/icons copy of vocalinux-microphone-off.svg
|
||||
# (the old red placeholder) would win over the AppImage's bundled icons.
|
||||
self._icon_keys = {
|
||||
"default": (self.icon_names["default"] if FLATPAK_ID else self.icon_paths["default"]),
|
||||
"active": (self.icon_names["active"] if FLATPAK_ID else self.icon_paths["active"]),
|
||||
"processing": (
|
||||
self.icon_names["processing"] if FLATPAK_ID else self.icon_paths["processing"]
|
||||
),
|
||||
}
|
||||
|
||||
# Register for speech recognition state changes
|
||||
self.speech_engine.register_state_callback(self._on_recognition_state_changed)
|
||||
|
||||
@@ -222,7 +234,7 @@ class TrayIndicator:
|
||||
exists = os.path.exists(path)
|
||||
logger.info(f"Icon '{name}' ({path}): {'exists' if exists else 'missing'}")
|
||||
|
||||
initial_icon = _themed_icon_names()["default"]
|
||||
initial_icon = self._icon_keys["default"]
|
||||
try:
|
||||
if FLATPAK_ID:
|
||||
self.indicator = AppIndicator3.Indicator.new(
|
||||
@@ -239,6 +251,7 @@ class TrayIndicator:
|
||||
)
|
||||
self.indicator.set_icon_theme_path(ICON_DIR)
|
||||
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
self.indicator.set_title("Vocalinux")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create AppIndicator: {e}")
|
||||
GLib.idle_add(self._show_appindicator_error_dialog, str(e))
|
||||
@@ -463,19 +476,19 @@ class TrayIndicator:
|
||||
return False
|
||||
|
||||
if state == RecognitionState.IDLE:
|
||||
self.indicator.set_icon_full(self.icon_names["default"], "Microphone off")
|
||||
self.indicator.set_icon_full(self._icon_keys["default"], "Microphone off")
|
||||
self._set_menu_item_enabled("Start Voice Typing", True)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", False)
|
||||
elif state == RecognitionState.LISTENING:
|
||||
self.indicator.set_icon_full(self.icon_names["active"], "Microphone on")
|
||||
self.indicator.set_icon_full(self._icon_keys["active"], "Microphone on")
|
||||
self._set_menu_item_enabled("Start Voice Typing", False)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", True)
|
||||
elif state == RecognitionState.PROCESSING:
|
||||
self.indicator.set_icon_full(self.icon_names["processing"], "Processing speech")
|
||||
self.indicator.set_icon_full(self._icon_keys["processing"], "Processing speech")
|
||||
self._set_menu_item_enabled("Start Voice Typing", False)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", True)
|
||||
elif state == RecognitionState.ERROR:
|
||||
self.indicator.set_icon_full(self.icon_names["default"], "Error")
|
||||
self.indicator.set_icon_full(self._icon_keys["default"], "Error")
|
||||
self._set_menu_item_enabled("Start Voice Typing", True)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", False)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Tests for speech-engine display name helpers."""
|
||||
|
||||
from vocalinux.ui.settings_dialog import (
|
||||
ENGINE_DISPLAY_NAMES,
|
||||
_engine_display_name,
|
||||
_engine_from_display,
|
||||
)
|
||||
|
||||
|
||||
def test_whisper_cpp_display_name():
|
||||
assert ENGINE_DISPLAY_NAMES["whisper_cpp"] == "whisper.cpp"
|
||||
assert _engine_display_name("whisper_cpp") == "whisper.cpp"
|
||||
assert _engine_from_display("whisper.cpp") == "whisper_cpp"
|
||||
|
||||
|
||||
def test_engine_display_roundtrip():
|
||||
for engine_id in ENGINE_DISPLAY_NAMES:
|
||||
assert _engine_from_display(_engine_display_name(engine_id)) == engine_id
|
||||
@@ -323,7 +323,7 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
result = self.tray_indicator._update_ui(self.RecognitionState.LISTENING)
|
||||
|
||||
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
||||
"vocalinux-microphone", "Microphone on"
|
||||
self.tray_indicator._icon_keys["active"], "Microphone on"
|
||||
)
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@@ -340,7 +340,7 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
result = self.tray_indicator._update_ui(self.RecognitionState.PROCESSING)
|
||||
|
||||
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
||||
"vocalinux-microphone-process", "Processing speech"
|
||||
self.tray_indicator._icon_keys["processing"], "Processing speech"
|
||||
)
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@@ -357,7 +357,7 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
result = self.tray_indicator._update_ui(self.RecognitionState.ERROR)
|
||||
|
||||
self.tray_indicator.indicator.set_icon_full.assert_called_once_with(
|
||||
"vocalinux-microphone-off", "Error"
|
||||
self.tray_indicator._icon_keys["default"], "Error"
|
||||
)
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@@ -515,7 +515,12 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
tray = TrayIndicator.__new__(TrayIndicator)
|
||||
tray.icon_paths = {}
|
||||
tray.icon_paths = {
|
||||
"default": "/tmp/fake-off.svg",
|
||||
"active": "/tmp/fake-on.svg",
|
||||
"processing": "/tmp/fake-process.svg",
|
||||
}
|
||||
tray._icon_keys = dict(tray.icon_paths)
|
||||
tray._show_appindicator_error_dialog = MagicMock(return_value=False)
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.os.path.exists", return_value=False):
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone off icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#e74c3c" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#7f8c8d" />
|
||||
<line x1="40" y1="88" x2="88" y2="40" stroke="#e74c3c" stroke-width="8" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Idle / muted: slate disc; keep slash subtle so 22px trays don't look like a red badge -->
|
||||
<circle cx="64" cy="64" r="60" fill="#1e293b"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#94a3b8"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Mute slash: light outline only so the slate disc stays the dominant color -->
|
||||
<line x1="36" y1="96" x2="92" y2="32" stroke="#f8fafc" stroke-width="8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 869 B |
@@ -1,16 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone processing icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#f39c12" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<circle cx="64" cy="40" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="40;50;40" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="48" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="80" cy="60" r="8" fill="#f39c12">
|
||||
<animate attributeName="cy" values="60;70;60" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Processing: amber disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#d97706"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#1c1917"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#1c1917" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Processing dots -->
|
||||
<circle cx="44" cy="52" r="6" fill="#fffbeb"/>
|
||||
<circle cx="64" cy="44" r="6" fill="#fffbeb"/>
|
||||
<circle cx="84" cy="52" r="6" fill="#fffbeb"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 829 B |
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<!-- Placeholder for microphone on/active icon -->
|
||||
<circle cx="64" cy="64" r="60" fill="#2ecc71" />
|
||||
<rect x="44" y="40" width="40" height="60" rx="20" fill="#2c3e50" />
|
||||
<path d="M 40,78 Q 64,88 88,78" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<path d="M 40,88 Q 64,98 88,88" stroke="#2ecc71" stroke-width="4" fill="none" />
|
||||
<!-- This is a placeholder. Replace with actual custom design. -->
|
||||
<!-- Active / listening: branded teal disc, opaque for StatusNotifier trays -->
|
||||
<circle cx="64" cy="64" r="60" fill="#0d9488"/>
|
||||
<!-- Mic body -->
|
||||
<rect x="52" y="28" width="24" height="40" rx="12" fill="#0f172a"/>
|
||||
<!-- Mic stand -->
|
||||
<path d="M40 68c0 13.255 10.745 24 24 24s24-10.745 24-24" fill="none" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="64" y1="92" x2="64" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="50" y1="102" x2="78" y2="102" stroke="#0f172a" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Sound waves -->
|
||||
<path d="M86 48c4 4 6 9 6 16s-2 12-6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M96 40c7 7 11 15 11 24s-4 17-11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M42 48c-4 4-6 9-6 16s2 12 6 16" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M32 40c-7 7-11 15-11 24s4 17 11 24" fill="none" stroke="#ecfdf5" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 1.1 KiB |