Merge feature/17 into integration/spec16 (ticket #17: centralise app identity)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""Central user-visible application identity for Vocalinux.
|
||||
|
||||
Ticket #17 prefactor: every user-visible identity string that is hardcoded in
|
||||
more than one place is driven from this single module. No value changes here;
|
||||
the rename ticket (#18) flips them in one place instead of a scavenger hunt.
|
||||
|
||||
Retained internal names (the ``vocalinux`` Python package, upstream URLs, and
|
||||
changelog history) are recorded in :data:`RETAINED_SURFACES`, not renamed.
|
||||
"""
|
||||
|
||||
APP_NAME: str = "Vocalinux"
|
||||
APP_ID: str = "vocalinux"
|
||||
APP_DIR_NAME: str = "vocalinux"
|
||||
CLI_PROG: str = "vocalinux"
|
||||
|
||||
BUS_NAME: str = "com.vocalinux.Vocalinux"
|
||||
OBJECT_PATH: str = "/com/vocalinux/Vocalinux"
|
||||
INTERFACE_NAME: str = "com.vocalinux.Vocalinux"
|
||||
|
||||
DESKTOP_FILENAME: str = "vocalinux.desktop"
|
||||
DESKTOP_ICON_NAME: str = "vocalinux"
|
||||
|
||||
TRAY_TITLE: str = APP_NAME
|
||||
ICON_DEFAULT: str = "vocalinux-microphone-off"
|
||||
ICON_ACTIVE: str = "vocalinux-microphone"
|
||||
ICON_PROCESSING: str = "vocalinux-microphone-process"
|
||||
ICON_BASE_NAMES: tuple[str, ...] = (
|
||||
"vocalinux",
|
||||
ICON_ACTIVE,
|
||||
ICON_DEFAULT,
|
||||
ICON_PROCESSING,
|
||||
)
|
||||
|
||||
ENGINE_NAME: str = "vocalinux"
|
||||
ENGINE_LONGNAME: str = "Vocalinux"
|
||||
ENGINE_DESCRIPTION: str = "Vocalinux voice dictation (use as default input method)"
|
||||
COMPONENT_NAME: str = "org.freedesktop.IBus.Vocalinux"
|
||||
IBUS_DIR_NAME: str = "vocalinux-ibus"
|
||||
IBUS_TEXTDOMAIN: str = "vocalinux"
|
||||
|
||||
UPDATE_CHECKER_USER_AGENT: str = "Vocalinux-UpdateChecker"
|
||||
|
||||
RESOURCE_DIR_NAME: str = "vocalinux"
|
||||
|
||||
VERIFICATION_STAMP_NAME: str = ".vocalinux_verified"
|
||||
LOG_FILE_PREFIX: str = "vocalinux_logs_"
|
||||
PORTAL_TOKEN_PREFIX: str = "vocalinux_"
|
||||
|
||||
ENV_FORCE_BACKEND: str = "VOCALINUX_FORCE_BACKEND"
|
||||
ENV_INJECT_MODIFIER_WAIT: str = "VOCALINUX_INJECT_MODIFIER_WAIT"
|
||||
ENV_YDOTOOL_KEY_DELAY: str = "VOCALINUX_YDOTOOL_KEY_DELAY"
|
||||
|
||||
SITE_URL: str = "https://vocalinux.com"
|
||||
|
||||
#: Files the rename ticket (#18) must flip. Checked by
|
||||
#: ``tests/test_app_identity.py`` so the list cannot drift silently.
|
||||
RENAME_SURFACES: tuple[str, ...] = (
|
||||
"pyproject.toml",
|
||||
"vocalinux.desktop",
|
||||
"snap/gui/vocalinux.desktop",
|
||||
"snap/snapcraft.yaml",
|
||||
"packaging/flatpak/com.vocalinux.Vocalinux.desktop",
|
||||
"packaging/flatpak/com.vocalinux.Vocalinux.metainfo.xml",
|
||||
"packaging/flatpak/com.vocalinux.Vocalinux.yml",
|
||||
"packaging/aur/vocalinux-bin/vocalinux.desktop",
|
||||
"web/src/app/page.tsx",
|
||||
"web/package.json",
|
||||
"README.md",
|
||||
"docs/INSTALL.md",
|
||||
)
|
||||
|
||||
#: Surfaces deliberately never renamed (internal module name, upstream URLs,
|
||||
#: history). Recorded here so the rename ticket does not touch them.
|
||||
RETAINED_SURFACES: tuple[str, ...] = (
|
||||
"src/vocalinux (Python import package name)",
|
||||
"https://github.com/VocaHQ/vocalinux (upstream URLs and provenance)",
|
||||
"CHANGELOG.md (historical changelog entries)",
|
||||
)
|
||||
@@ -14,11 +14,9 @@ from typing import Callable, Optional
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .app_identity import BUS_NAME, INTERFACE_NAME, OBJECT_PATH
|
||||
|
||||
BUS_NAME = "com.vocalinux.Vocalinux"
|
||||
OBJECT_PATH = "/com/vocalinux/Vocalinux"
|
||||
INTERFACE_NAME = "com.vocalinux.Vocalinux"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Maps CLI trigger names to the D-Bus method names exposed below.
|
||||
COMMAND_METHODS = {
|
||||
@@ -27,9 +25,9 @@ COMMAND_METHODS = {
|
||||
"stop": "Stop",
|
||||
}
|
||||
|
||||
_INTROSPECTION_XML = """
|
||||
_INTROSPECTION_XML = f"""
|
||||
<node>
|
||||
<interface name="com.vocalinux.Vocalinux">
|
||||
<interface name="{INTERFACE_NAME}">
|
||||
<method name="Toggle"/>
|
||||
<method name="Start"/>
|
||||
<method name="Stop"/>
|
||||
|
||||
+10
-9
@@ -13,6 +13,7 @@ import time
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .app_identity import APP_ID, APP_NAME, CLI_PROG
|
||||
from .utils.host_process import running_from_appimage
|
||||
from .utils.vosk_model_info import SUPPORTED_LANGUAGES
|
||||
from .version import __version__
|
||||
@@ -58,12 +59,12 @@ def _should_append_trailing_space() -> bool:
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(prog="vocalinux", description="Vocalinux")
|
||||
parser = argparse.ArgumentParser(prog=CLI_PROG, description=APP_NAME)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}",
|
||||
help="Show the installed Vocalinux version and exit",
|
||||
help=f"Show the installed {APP_NAME} version and exit",
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
||||
# default model, language and engine are loaded from default config
|
||||
@@ -174,13 +175,13 @@ def _run_file_transcription(path: str) -> int:
|
||||
blocks = transcribe_audio_file(path)
|
||||
except (OSError, ValueError, RuntimeError) as error:
|
||||
print(
|
||||
f"vocalinux: could not transcribe {path}: {type(error).__name__}: {error}",
|
||||
f"{CLI_PROG}: could not transcribe {path}: {type(error).__name__}: {error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
transcript = format_transcript(blocks)
|
||||
print(transcript if transcript else "vocalinux: no speech detected")
|
||||
print(transcript if transcript else f"{CLI_PROG}: no speech detected")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -376,9 +377,9 @@ def main():
|
||||
try:
|
||||
from gi.repository import Notify
|
||||
|
||||
Notify.init("Vocalinux")
|
||||
Notify.init(APP_NAME)
|
||||
notification = Notify.Notification.new(
|
||||
"Vocalinux",
|
||||
APP_NAME,
|
||||
"Another instance is already running. Only one instance is allowed at a time.",
|
||||
"dialog-error",
|
||||
)
|
||||
@@ -404,13 +405,13 @@ def main():
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import GLib
|
||||
|
||||
GLib.set_prgname("vocalinux")
|
||||
GLib.set_application_name("Vocalinux")
|
||||
GLib.set_prgname(APP_ID)
|
||||
GLib.set_application_name(APP_NAME)
|
||||
try:
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
Gdk.set_program_class("Vocalinux")
|
||||
Gdk.set_program_class(APP_NAME)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
from ..app_identity import APP_NAME, RESOURCE_DIR_NAME
|
||||
from ..audio.capture import ( # noqa: F401 (re-exported for compatibility)
|
||||
_STICKY_LOCK_MIN_MEAN_SQUARE,
|
||||
CaptureStalledError,
|
||||
@@ -365,7 +366,7 @@ def _show_notification(title: str, message: str, icon: str = "dialog-warning"):
|
||||
|
||||
# Use notify-send which is available on most Linux desktops
|
||||
subprocess.Popen(
|
||||
["notify-send", "-i", icon, "-a", "Vocalinux", title, message],
|
||||
["notify-send", "-i", icon, "-a", APP_NAME, title, message],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env=host_env(),
|
||||
@@ -394,7 +395,7 @@ def _get_system_model_paths() -> list:
|
||||
xdg_data = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
|
||||
for base in xdg_data.split(":"):
|
||||
if base: # Skip empty strings
|
||||
paths.append(os.path.join(base, "vocalinux", "models"))
|
||||
paths.append(os.path.join(base, RESOURCE_DIR_NAME, "models"))
|
||||
|
||||
# Distribution-specific paths
|
||||
# Try to detect the distribution from /etc/os-release
|
||||
@@ -407,12 +408,12 @@ def _get_system_model_paths() -> list:
|
||||
id in os_release
|
||||
for id in ["fedora", "rhel", "centos", "rocky", "almalinux", "red hat"]
|
||||
):
|
||||
paths.append("/usr/lib64/vocalinux/models")
|
||||
paths.append("/usr/lib/vocalinux/models")
|
||||
paths.append(f"/usr/lib64/{RESOURCE_DIR_NAME}/models")
|
||||
paths.append(f"/usr/lib/{RESOURCE_DIR_NAME}/models")
|
||||
|
||||
# Arch Linux doesn't use /usr/local
|
||||
if "arch" in os_release:
|
||||
local_share_path = "/usr/local/share/vocalinux/models"
|
||||
local_share_path = f"/usr/local/share/{RESOURCE_DIR_NAME}/models"
|
||||
if local_share_path in paths:
|
||||
paths.remove(local_share_path)
|
||||
|
||||
@@ -421,10 +422,10 @@ def _get_system_model_paths() -> list:
|
||||
|
||||
# Add common fallback paths that might be used
|
||||
additional_paths = [
|
||||
"/usr/local/lib/vocalinux/models",
|
||||
"/usr/lib/vocalinux/models",
|
||||
"/usr/lib64/vocalinux/models",
|
||||
"/opt/vocalinux/models", # Some distros use /opt
|
||||
f"/usr/local/lib/{RESOURCE_DIR_NAME}/models",
|
||||
f"/usr/lib/{RESOURCE_DIR_NAME}/models",
|
||||
f"/usr/lib64/{RESOURCE_DIR_NAME}/models",
|
||||
f"/opt/{RESOURCE_DIR_NAME}/models", # Some distros use /opt
|
||||
]
|
||||
|
||||
for path in additional_paths:
|
||||
|
||||
@@ -30,15 +30,42 @@ from typing import Any, Callable, Mapping, Optional
|
||||
# Package import when loaded normally; absolute / inline fallbacks when this
|
||||
# file is executed by path (Vocalinux start_engine_process / IBus component exec).
|
||||
try:
|
||||
from ..app_identity import (
|
||||
APP_NAME,
|
||||
COMPONENT_NAME,
|
||||
ENGINE_DESCRIPTION,
|
||||
ENGINE_LONGNAME,
|
||||
ENGINE_NAME,
|
||||
IBUS_DIR_NAME,
|
||||
IBUS_TEXTDOMAIN,
|
||||
)
|
||||
from ..utils.host_process import host_env
|
||||
from ..utils.paths import xdg_data_home
|
||||
except ImportError: # pragma: no cover - exercised via script-path subprocess
|
||||
try:
|
||||
from vocalinux.app_identity import (
|
||||
APP_NAME,
|
||||
COMPONENT_NAME,
|
||||
ENGINE_DESCRIPTION,
|
||||
ENGINE_LONGNAME,
|
||||
ENGINE_NAME,
|
||||
IBUS_DIR_NAME,
|
||||
IBUS_TEXTDOMAIN,
|
||||
)
|
||||
from vocalinux.utils.host_process import host_env
|
||||
from vocalinux.utils.paths import xdg_data_home
|
||||
except ImportError:
|
||||
# Last resort: same semantics as the vocalinux.utils originals, except
|
||||
# that host_env cannot strip a bundle it can no longer import.
|
||||
# Identity literals below keep in step with vocalinux/app_identity.py.
|
||||
APP_NAME = "Vocalinux"
|
||||
COMPONENT_NAME = "org.freedesktop.IBus.Vocalinux"
|
||||
ENGINE_DESCRIPTION = "Vocalinux voice dictation (use as default input method)"
|
||||
ENGINE_LONGNAME = "Vocalinux"
|
||||
ENGINE_NAME = "vocalinux"
|
||||
IBUS_DIR_NAME = "vocalinux-ibus"
|
||||
IBUS_TEXTDOMAIN = "vocalinux"
|
||||
|
||||
def host_env(base: Optional[Mapping[str, str]] = None) -> dict[str, str]:
|
||||
return dict(os.environ if base is None else base)
|
||||
|
||||
@@ -77,33 +104,29 @@ except (ImportError, ValueError) as e:
|
||||
GObject = None
|
||||
|
||||
|
||||
# File paths for communication ($XDG_DATA_HOME, defaults to ~/.local/share)
|
||||
VOCALINUX_IBUS_DIR = Path(xdg_data_home()) / "vocalinux-ibus"
|
||||
# File paths for communication ($XDG_DATA_HOME, defaults to ~/.local/share).
|
||||
# Identity strings come from vocalinux/app_identity.py (see the import above);
|
||||
# the homepage below is the retained upstream URL, never renamed.
|
||||
VOCALINUX_IBUS_DIR = Path(xdg_data_home()) / IBUS_DIR_NAME
|
||||
SOCKET_PATH = VOCALINUX_IBUS_DIR / "inject.sock"
|
||||
PID_FILE = VOCALINUX_IBUS_DIR / "engine.pid"
|
||||
|
||||
# Engine identification
|
||||
ENGINE_NAME = "vocalinux"
|
||||
ENGINE_LONGNAME = "Vocalinux"
|
||||
ENGINE_DESCRIPTION = "Vocalinux voice dictation (use as default input method)"
|
||||
COMPONENT_NAME = "org.freedesktop.IBus.Vocalinux"
|
||||
|
||||
# Shared metadata used by component XML, --xml output, and runtime registration.
|
||||
# Kept in one place to prevent drift between the three consumers.
|
||||
ENGINE_RANK = 50
|
||||
_ENGINE_META = {
|
||||
"language": "other",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Vocalinux",
|
||||
"author": APP_NAME,
|
||||
"icon": "audio-input-microphone",
|
||||
"layout": "default",
|
||||
}
|
||||
_COMPONENT_META = {
|
||||
"version": "1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Vocalinux",
|
||||
"author": APP_NAME,
|
||||
"homepage": "https://github.com/VocaHQ/vocalinux",
|
||||
"textdomain": "vocalinux",
|
||||
"textdomain": IBUS_TEXTDOMAIN,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import threading
|
||||
import time
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from ..app_identity import PORTAL_TOKEN_PREFIX
|
||||
from ..utils.paths import data_dir
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -196,7 +197,7 @@ def _save_restore_token(token: str) -> None:
|
||||
|
||||
def _new_token() -> str:
|
||||
"""Return a unique D-Bus handle token."""
|
||||
return f"vocalinux_{secrets.token_hex(8)}"
|
||||
return f"{PORTAL_TOKEN_PREFIX}{secrets.token_hex(8)}"
|
||||
|
||||
|
||||
def _predicted_request_path(unique_name: str, token: str) -> str:
|
||||
|
||||
@@ -16,6 +16,12 @@ import time
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Tuple, cast
|
||||
|
||||
from ..app_identity import (
|
||||
APP_NAME,
|
||||
ENV_FORCE_BACKEND,
|
||||
ENV_INJECT_MODIFIER_WAIT,
|
||||
ENV_YDOTOOL_KEY_DELAY,
|
||||
)
|
||||
from ..utils.host_process import host_env
|
||||
from ..utils.paths import config_dir
|
||||
from .focused_window import (
|
||||
@@ -582,7 +588,7 @@ class TextInjector:
|
||||
Accepts ``ibus``, ``portal``, ``wtype``, ``ydotool``, ``xdotool`` or
|
||||
``auto``.
|
||||
"""
|
||||
raw = os.environ.get("VOCALINUX_FORCE_BACKEND")
|
||||
raw = os.environ.get(ENV_FORCE_BACKEND)
|
||||
if raw is None:
|
||||
return None
|
||||
value = raw.strip().lower()
|
||||
@@ -781,7 +787,7 @@ class TextInjector:
|
||||
"""
|
||||
env = TextInjector._forced_backend_setting()
|
||||
if env is not None:
|
||||
return env, "VOCALINUX_FORCE_BACKEND"
|
||||
return env, ENV_FORCE_BACKEND
|
||||
configured = TextInjector._configured_backend()
|
||||
if configured != "auto":
|
||||
return configured, "text_injection.backend"
|
||||
@@ -1661,7 +1667,7 @@ class TextInjector:
|
||||
"-i",
|
||||
"edit-paste",
|
||||
"-a",
|
||||
"Vocalinux",
|
||||
APP_NAME,
|
||||
"Text copied to clipboard",
|
||||
"Text injection failed — paste with Ctrl+V " "(Ctrl+Shift+V in a terminal)",
|
||||
],
|
||||
@@ -2778,7 +2784,7 @@ class TextInjector:
|
||||
would make the wait deadline infinite and block injection forever while a
|
||||
modifier stays held.
|
||||
"""
|
||||
raw = os.environ.get("VOCALINUX_INJECT_MODIFIER_WAIT")
|
||||
raw = os.environ.get(ENV_INJECT_MODIFIER_WAIT)
|
||||
if raw is None:
|
||||
return self._DEFAULT_INJECT_MODIFIER_WAIT
|
||||
try:
|
||||
@@ -2927,7 +2933,7 @@ class TextInjector:
|
||||
else: # ydotool
|
||||
# Keep key-delay > 0 to avoid Shift-leak ("Can you" -> "CAN YOu").
|
||||
# Low delay so fallback typing finishes quickly for long phrases.
|
||||
key_delay = os.environ.get("VOCALINUX_YDOTOOL_KEY_DELAY", "2")
|
||||
key_delay = os.environ.get(ENV_YDOTOOL_KEY_DELAY, "2")
|
||||
# ``--`` ends ydotool's option parsing for the same reason.
|
||||
cmd = ["ydotool", "type", "--key-delay", key_delay, "--", chunk]
|
||||
type_timeout = max(5, len(chunk) * self._key_delay_seconds(key_delay) * 4)
|
||||
@@ -3372,7 +3378,7 @@ class TextInjector:
|
||||
# 400-character deletion would otherwise take ~9.6s. Match the delay
|
||||
# `ydotool type` already uses, and scale the budget with the count.
|
||||
code = self._KEYCODES["backspace"]
|
||||
delay = os.environ.get("VOCALINUX_YDOTOOL_KEY_DELAY", "2")
|
||||
delay = os.environ.get(ENV_YDOTOOL_KEY_DELAY, "2")
|
||||
cmd = ["ydotool", "key", "--key-delay", delay] + [f"{code}:1", f"{code}:0"] * count
|
||||
timeout = max(3, count * 2 * self._key_delay_seconds(delay) * 3)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import wave
|
||||
from pathlib import Path # noqa: F401
|
||||
from typing import Optional
|
||||
|
||||
from ..app_identity import APP_DIR_NAME
|
||||
from .config_manager import DEFAULT_SOUND_EFFECT_TONE, normalize_sound_effect_tone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -113,7 +114,7 @@ def _pcm_silence(nframes: int, nchannels: int, sampwidth: int) -> bytes:
|
||||
|
||||
def _preroll_cache_dir() -> str:
|
||||
cache_home = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
||||
path = os.path.join(cache_home, "vocalinux", "audio-preroll")
|
||||
path = os.path.join(cache_home, APP_DIR_NAME, "audio-preroll")
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..app_identity import APP_ID, APP_NAME, DESKTOP_FILENAME
|
||||
|
||||
DESKTOP_FILENAME = "vocalinux.desktop"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_autostart_dir() -> Path:
|
||||
@@ -34,7 +34,7 @@ def get_exec_command() -> str:
|
||||
if flatpak_id:
|
||||
return f"flatpak run {flatpak_id} --start-minimized"
|
||||
|
||||
installed_command = shutil.which("vocalinux")
|
||||
installed_command = shutil.which(APP_ID)
|
||||
if installed_command:
|
||||
return f"{shlex.quote(installed_command)} --start-minimized"
|
||||
|
||||
@@ -62,9 +62,9 @@ def enable_autostart() -> bool:
|
||||
desktop_entry = f"""[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Vocalinux
|
||||
Name={APP_NAME}
|
||||
Exec={exec_command}
|
||||
Icon=vocalinux
|
||||
Icon={APP_ID}
|
||||
Comment=Voice dictation for Linux
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Callable, Optional
|
||||
if TYPE_CHECKING:
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
from ..app_identity import APP_NAME
|
||||
from ..common_types import RecognitionState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -176,7 +177,7 @@ class DictationOverlay:
|
||||
# keyboard focus so the active editor keeps receiving injected text.
|
||||
# Size/position are applied after map via _layout_bottom_strip.
|
||||
window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
|
||||
window.set_title("Vocalinux Dictation")
|
||||
window.set_title(f"{APP_NAME} Dictation")
|
||||
window.set_decorated(False)
|
||||
window.set_resizable(False)
|
||||
window.set_accept_focus(False)
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from ...app_identity import BUS_NAME, INTERFACE_NAME, OBJECT_PATH
|
||||
from .base import (
|
||||
DEFAULT_SHORTCUT,
|
||||
DEFAULT_SHORTCUT_MODE,
|
||||
@@ -98,9 +99,9 @@ def _log_flatpak_input_hint() -> None:
|
||||
logger.warning(
|
||||
"Grant --device=input (Flatpak 1.15.6+) or --device=all, or enable "
|
||||
"External Activation and bind a desktop shortcut to: %s",
|
||||
"gdbus call --session --dest com.vocalinux.Vocalinux "
|
||||
"--object-path /com/vocalinux/Vocalinux "
|
||||
"--method com.vocalinux.Vocalinux.Toggle",
|
||||
f"gdbus call --session --dest {BUS_NAME} "
|
||||
f"--object-path {OBJECT_PATH} "
|
||||
f"--method {INTERFACE_NAME}.Toggle",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
|
||||
|
||||
from ..app_identity import LOG_FILE_PREFIX # noqa: E402
|
||||
from ..utils.paths import collapse_repeated_extension # noqa: E402
|
||||
from .logging_manager import LogRecord, get_logging_manager # noqa: E402
|
||||
|
||||
@@ -650,7 +651,7 @@ class LoggingDialog(Gtk.Dialog):
|
||||
|
||||
# Set default filename
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
default_filename = f"vocalinux_logs_{timestamp}.txt"
|
||||
default_filename = f"{LOG_FILE_PREFIX}{timestamp}.txt"
|
||||
file_dialog.set_current_name(default_filename)
|
||||
|
||||
# Add file filter
|
||||
|
||||
@@ -10,12 +10,11 @@ import logging
|
||||
import subprocess
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ..app_identity import APP_NAME
|
||||
from ..utils.host_process import host_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
APP_NAME = "Vocalinux"
|
||||
|
||||
# Notifications waiting for their action to be clicked. Without a reference the
|
||||
# notification can be garbage collected before the user answers it, and the
|
||||
# callback then never runs.
|
||||
|
||||
@@ -36,6 +36,7 @@ gi.require_version("Gdk", "3.0")
|
||||
# Need GLib for idle_add
|
||||
from gi.repository import Gdk, GLib, GObject, Gtk, Pango # noqa: E402
|
||||
|
||||
from ..app_identity import APP_ID, APP_NAME, RESOURCE_DIR_NAME # noqa: E402
|
||||
from ..audio.pulse_sources import ( # noqa: E402
|
||||
PULSE_INDEX_BASE,
|
||||
PulseSource,
|
||||
@@ -717,8 +718,8 @@ def _model_specialization_tooltip(model_name: str) -> str:
|
||||
# Models directory
|
||||
MODELS_DIR = models_dir()
|
||||
SYSTEM_MODELS_DIRS = [
|
||||
"/usr/local/share/vocalinux/models",
|
||||
"/usr/share/vocalinux/models",
|
||||
f"/usr/local/share/{RESOURCE_DIR_NAME}/models",
|
||||
f"/usr/share/{RESOURCE_DIR_NAME}/models",
|
||||
]
|
||||
|
||||
# CSS for modern styling
|
||||
@@ -2534,7 +2535,7 @@ class SettingsDialog(AboutSectionMixin):
|
||||
hotkey_passthrough_update_callback: Optional[Callable[[bool], None]] = None,
|
||||
dictation_pad_open_callback: Optional[Callable[[], None]] = None,
|
||||
) -> None:
|
||||
super().__init__(title="Vocalinux Settings", transient_for=parent, flags=0)
|
||||
super().__init__(title=f"{APP_NAME} Settings", transient_for=parent, flags=0)
|
||||
# Force window decorations (title-bar close) on all WMs. An in-window
|
||||
# Close button also lives in the sidebar footer so the dialog always
|
||||
# has a visible way to dismiss it, even on window managers that hide
|
||||
@@ -6460,7 +6461,7 @@ class SettingsDialog(AboutSectionMixin):
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://huggingface.co/api/models/ggerganov/whisper.cpp",
|
||||
headers={"User-Agent": f"vocalinux/{__version__}"},
|
||||
headers={"User-Agent": f"{APP_ID}/{__version__}"},
|
||||
timeout=10,
|
||||
proxies=requests_proxies(config),
|
||||
)
|
||||
|
||||
@@ -54,6 +54,14 @@ if not INDICATOR_USES_GMENU:
|
||||
|
||||
from gi.repository import GdkPixbuf, Gio, GLib, GObject, Gtk
|
||||
|
||||
from ..app_identity import APP_ID as CENTRAL_APP_ID
|
||||
from ..app_identity import (
|
||||
ICON_ACTIVE,
|
||||
ICON_DEFAULT,
|
||||
ICON_PROCESSING,
|
||||
TRAY_TITLE,
|
||||
)
|
||||
|
||||
# Import local modules - Use protocols to avoid circular imports
|
||||
from ..auto_pause_monitor import DEFAULT_POLL_INTERVAL_SECONDS, AutoPauseMonitor
|
||||
from ..common_types import RecognitionState, SpeechRecognitionManagerProtocol, TextInjectorProtocol
|
||||
@@ -93,7 +101,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Flatpak StatusNotifier host resolves exported <app-id>-* icons, not /app paths.
|
||||
FLATPAK_ID = os.environ.get("FLATPAK_ID")
|
||||
APP_ID = FLATPAK_ID or "vocalinux"
|
||||
APP_ID = FLATPAK_ID or CENTRAL_APP_ID
|
||||
|
||||
# Initialize resource manager
|
||||
_resource_manager = ResourceManager()
|
||||
@@ -119,9 +127,9 @@ def _idle_once(func, *args):
|
||||
|
||||
|
||||
# Bundled icon file names (paths under ICON_DIR).
|
||||
DEFAULT_ICON = "vocalinux-microphone-off"
|
||||
ACTIVE_ICON = "vocalinux-microphone"
|
||||
PROCESSING_ICON = "vocalinux-microphone-process"
|
||||
DEFAULT_ICON = ICON_DEFAULT
|
||||
ACTIVE_ICON = ICON_ACTIVE
|
||||
PROCESSING_ICON = ICON_PROCESSING
|
||||
|
||||
|
||||
def _themed_icon_names() -> dict:
|
||||
@@ -159,7 +167,7 @@ class TrayIndicator:
|
||||
# Tests build TrayIndicator stubs with __new__ that skip __init__; keep a
|
||||
# class-level default so teardown paths like _quit() still find the list.
|
||||
_language_shortcut_managers: list = []
|
||||
_indicator_title: str = "Vocalinux"
|
||||
_indicator_title: str = TRAY_TITLE
|
||||
|
||||
#: Same for the push-to-talk owner slot: a __new__-built stub must answer
|
||||
#: "no binding owns the live session" instead of raising on release paths.
|
||||
@@ -264,7 +272,7 @@ class TrayIndicator:
|
||||
# IconThemePath so gray→red→gray→red actually turns red again.
|
||||
self._last_icon_key: Optional[str] = None
|
||||
self._icon_theme_nudge = False
|
||||
self._indicator_title = "Vocalinux"
|
||||
self._indicator_title = TRAY_TITLE
|
||||
|
||||
# Register for speech recognition state changes
|
||||
self.speech_engine.register_state_callback(self._on_recognition_state_changed)
|
||||
@@ -601,8 +609,8 @@ class TrayIndicator:
|
||||
indicator.set_icon_theme_path(ICON_DIR)
|
||||
self.indicator = GMenuIndicator(indicator, Gio) if INDICATOR_USES_GMENU else indicator
|
||||
self.indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE)
|
||||
self.indicator.set_title("Vocalinux")
|
||||
self._indicator_title = "Vocalinux"
|
||||
self.indicator.set_title(TRAY_TITLE)
|
||||
self._indicator_title = TRAY_TITLE
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create AppIndicator: {e}")
|
||||
GLib.idle_add(self._show_appindicator_error_dialog, str(e))
|
||||
@@ -1263,7 +1271,7 @@ class TrayIndicator:
|
||||
self._last_icon_key = icon_key
|
||||
|
||||
def _set_indicator_title(self, title: str) -> None:
|
||||
if getattr(self, "_indicator_title", "Vocalinux") == title:
|
||||
if getattr(self, "_indicator_title", TRAY_TITLE) == title:
|
||||
return
|
||||
self.indicator.set_title(title)
|
||||
self._indicator_title = title
|
||||
@@ -1297,12 +1305,12 @@ class TrayIndicator:
|
||||
self._set_cancel_menu_item_visible(show_cancel)
|
||||
|
||||
if state == RecognitionState.IDLE:
|
||||
self._set_indicator_title("Vocalinux")
|
||||
self._set_indicator_title(TRAY_TITLE)
|
||||
self._set_indicator_icon(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._set_indicator_title("Vocalinux")
|
||||
self._set_indicator_title(TRAY_TITLE)
|
||||
self._set_indicator_icon(self._icon_keys["active"], "Microphone on")
|
||||
self._set_menu_item_enabled("Start Voice Typing", False)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", True)
|
||||
@@ -1315,12 +1323,12 @@ class TrayIndicator:
|
||||
description = f"Transcribing, press {name} again to cancel"
|
||||
else:
|
||||
description = "Processing speech"
|
||||
self._set_indicator_title(description if transcribing else "Vocalinux")
|
||||
self._set_indicator_title(description if transcribing else TRAY_TITLE)
|
||||
self._set_indicator_icon(self._icon_keys["processing"], description)
|
||||
self._set_menu_item_enabled("Start Voice Typing", False)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", True)
|
||||
elif state == RecognitionState.ERROR:
|
||||
self._set_indicator_title("Vocalinux")
|
||||
self._set_indicator_title(TRAY_TITLE)
|
||||
self._set_indicator_icon(self._icon_keys["default"], "Error")
|
||||
self._set_menu_item_enabled("Start Voice Typing", True)
|
||||
self._set_menu_item_enabled("Stop Voice Typing", False)
|
||||
@@ -1578,8 +1586,8 @@ class TrayIndicator:
|
||||
"-i",
|
||||
"software-update-available",
|
||||
"-a",
|
||||
"Vocalinux",
|
||||
"Vocalinux update available",
|
||||
TRAY_TITLE,
|
||||
f"{TRAY_TITLE} update available",
|
||||
f"{release.tag_name} is ready. Open the tray menu or "
|
||||
"Settings → About for release notes.",
|
||||
],
|
||||
|
||||
@@ -34,6 +34,8 @@ import re
|
||||
from functools import lru_cache
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from ..app_identity import VERIFICATION_STAMP_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_MANIFEST_NAME = "model_checksums.txt"
|
||||
@@ -42,7 +44,7 @@ _MANIFEST_NAME = "model_checksums.txt"
|
||||
# came out of. A directory has no hash of its own and the zip is deleted after
|
||||
# unpacking, so this file is the only evidence the tree was ever verified.
|
||||
# install.sh reads it under the same name; keep the two in step.
|
||||
VERIFICATION_STAMP_NAME = ".vocalinux_verified"
|
||||
# (Name itself lives in vocalinux/app_identity.py so the rename flips it once.)
|
||||
|
||||
# Hash of a 2GB model read in one go would sit in RAM; stream it instead.
|
||||
_CHUNK_SIZE = 1024 * 1024
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
APP_DIR_NAME = "vocalinux"
|
||||
from ..app_identity import APP_DIR_NAME
|
||||
|
||||
|
||||
def xdg_config_home() -> str:
|
||||
|
||||
@@ -11,6 +11,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, cast
|
||||
|
||||
from ..app_identity import ICON_BASE_NAMES, RESOURCE_DIR_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -55,7 +57,7 @@ class ResourceManager:
|
||||
# For direct repository execution (go up from src/vocalinux/utils to root)
|
||||
module_dir.parent.parent.parent / "resources",
|
||||
# For installed package or virtual environment (install.sh copies here)
|
||||
Path(sys.prefix) / "share" / "vocalinux" / "resources",
|
||||
Path(sys.prefix) / "share" / RESOURCE_DIR_NAME / "resources",
|
||||
# For development in virtual environment
|
||||
Path(sys.prefix).parent / "resources",
|
||||
]
|
||||
@@ -64,19 +66,19 @@ class ResourceManager:
|
||||
xdg_data = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
|
||||
for base in xdg_data.split(":"):
|
||||
if base: # Skip empty strings
|
||||
candidates.append(Path(base) / "vocalinux" / "resources")
|
||||
candidates.append(Path(base) / RESOURCE_DIR_NAME / "resources")
|
||||
|
||||
# Add distribution-specific standard paths
|
||||
additional_paths = [
|
||||
Path("/usr/local/share/vocalinux/resources"),
|
||||
Path("/usr/share/vocalinux/resources"),
|
||||
Path(f"/usr/local/share/{RESOURCE_DIR_NAME}/resources"),
|
||||
Path(f"/usr/share/{RESOURCE_DIR_NAME}/resources"),
|
||||
# Some distros use lib64 or lib for package data
|
||||
Path("/usr/local/lib/vocalinux/resources"),
|
||||
Path("/usr/lib/vocalinux/resources"),
|
||||
Path("/usr/local/lib64/vocalinux/resources"),
|
||||
Path("/usr/lib64/vocalinux/resources"),
|
||||
Path(f"/usr/local/lib/{RESOURCE_DIR_NAME}/resources"),
|
||||
Path(f"/usr/lib/{RESOURCE_DIR_NAME}/resources"),
|
||||
Path(f"/usr/local/lib64/{RESOURCE_DIR_NAME}/resources"),
|
||||
Path(f"/usr/lib64/{RESOURCE_DIR_NAME}/resources"),
|
||||
# Flatpak specific path
|
||||
Path("/app/share/vocalinux/resources"),
|
||||
Path(f"/app/share/{RESOURCE_DIR_NAME}/resources"),
|
||||
]
|
||||
|
||||
for path in additional_paths:
|
||||
@@ -103,12 +105,7 @@ class ResourceManager:
|
||||
if not candidate.exists():
|
||||
return -1
|
||||
|
||||
expected_icons = [
|
||||
"vocalinux",
|
||||
"vocalinux-microphone",
|
||||
"vocalinux-microphone-off",
|
||||
"vocalinux-microphone-process",
|
||||
]
|
||||
expected_icons = list(ICON_BASE_NAMES)
|
||||
expected_sounds = ["start_recording", "stop_recording", "error"]
|
||||
|
||||
icon_score = sum(
|
||||
@@ -201,12 +198,7 @@ class ResourceManager:
|
||||
}
|
||||
|
||||
# Expected icons
|
||||
expected_icons = [
|
||||
"vocalinux",
|
||||
"vocalinux-microphone",
|
||||
"vocalinux-microphone-off",
|
||||
"vocalinux-microphone-process",
|
||||
]
|
||||
expected_icons = list(ICON_BASE_NAMES)
|
||||
|
||||
for icon in expected_icons:
|
||||
icon_path = self.get_icon_path(icon)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import version as _version_module
|
||||
from ..app_identity import UPDATE_CHECKER_USER_AGENT
|
||||
from ..version import __url__
|
||||
from .proxy import requests_proxies
|
||||
|
||||
@@ -222,7 +223,7 @@ def _fetch_stable_from_releases_page(
|
||||
url = f"https://github.com/{owner}/{repo}/releases/latest"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "Vocalinux-UpdateChecker",
|
||||
"User-Agent": UPDATE_CHECKER_USER_AGENT,
|
||||
}
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=timeout, proxies=requests_proxies())
|
||||
@@ -263,7 +264,7 @@ def fetch_latest_release(
|
||||
api_base = _repo_api_base(repo_url)
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "Vocalinux-UpdateChecker",
|
||||
"User-Agent": UPDATE_CHECKER_USER_AGENT,
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Ticket #17 prefactor inventory: central identity + rename surfaces.
|
||||
|
||||
Seams under test (per spec Testing Decisions):
|
||||
- Seam 1 (Python behaviour suite): central constants hold current values and
|
||||
every previously duplicated consumer resolves through the shared definition.
|
||||
- Seam 2 (offline policy test): rename/retained inventories enumerate files
|
||||
the rename ticket must flip (or deliberately keep) without network access.
|
||||
|
||||
No user-visible behaviour, name, path, or string changes: all asserted values
|
||||
are the pre-rename Vocalinux/vocalinux literals.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = REPO_ROOT / "src" / "vocalinux"
|
||||
|
||||
|
||||
def _load_identity():
|
||||
from vocalinux import app_identity
|
||||
|
||||
return app_identity
|
||||
|
||||
|
||||
def test_central_values_are_current_names():
|
||||
"""Shared definition still carries the pre-rename literals."""
|
||||
identity = _load_identity()
|
||||
assert identity.APP_NAME == "Vocalinux"
|
||||
assert identity.APP_ID == "vocalinux"
|
||||
assert identity.APP_DIR_NAME == "vocalinux"
|
||||
assert identity.CLI_PROG == "vocalinux"
|
||||
assert identity.BUS_NAME == "com.vocalinux.Vocalinux"
|
||||
assert identity.OBJECT_PATH == "/com/vocalinux/Vocalinux"
|
||||
assert identity.INTERFACE_NAME == "com.vocalinux.Vocalinux"
|
||||
assert identity.DESKTOP_FILENAME == "vocalinux.desktop"
|
||||
assert identity.DESKTOP_ICON_NAME == "vocalinux"
|
||||
assert identity.ENGINE_NAME == "vocalinux"
|
||||
assert identity.ENGINE_LONGNAME == "Vocalinux"
|
||||
assert identity.COMPONENT_NAME == "org.freedesktop.IBus.Vocalinux"
|
||||
assert identity.IBUS_DIR_NAME == "vocalinux-ibus"
|
||||
assert identity.UPDATE_CHECKER_USER_AGENT == "Vocalinux-UpdateChecker"
|
||||
assert identity.RESOURCE_DIR_NAME == "vocalinux"
|
||||
assert identity.VERIFICATION_STAMP_NAME == ".vocalinux_verified"
|
||||
assert identity.LOG_FILE_PREFIX == "vocalinux_logs_"
|
||||
assert identity.PORTAL_TOKEN_PREFIX == "vocalinux_"
|
||||
assert identity.ENV_FORCE_BACKEND == "VOCALINUX_FORCE_BACKEND"
|
||||
assert identity.ENV_INJECT_MODIFIER_WAIT == "VOCALINUX_INJECT_MODIFIER_WAIT"
|
||||
assert identity.ENV_YDOTOOL_KEY_DELAY == "VOCALINUX_YDOTOOL_KEY_DELAY"
|
||||
|
||||
|
||||
def _imports_app_identity(path: Path) -> bool:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.module and node.module.endswith("app_identity"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_consumers_resolve_through_shared_definition():
|
||||
"""Previously duplicated surfaces import the shared definition (AST, offline)."""
|
||||
consumers = [
|
||||
SRC_ROOT / "utils" / "paths.py",
|
||||
SRC_ROOT / "ui" / "notifications.py",
|
||||
SRC_ROOT / "ui" / "tray_indicator.py",
|
||||
SRC_ROOT / "dbus_service.py",
|
||||
SRC_ROOT / "ui" / "autostart_manager.py",
|
||||
SRC_ROOT / "text_injection" / "ibus_engine.py",
|
||||
SRC_ROOT / "utils" / "update_checker.py",
|
||||
SRC_ROOT / "utils" / "resource_manager.py",
|
||||
SRC_ROOT / "main.py",
|
||||
SRC_ROOT / "text_injection" / "text_injector.py",
|
||||
SRC_ROOT / "text_injection" / "remote_desktop_portal.py",
|
||||
SRC_ROOT / "ui" / "logging_dialog.py",
|
||||
SRC_ROOT / "utils" / "model_checksums.py",
|
||||
SRC_ROOT / "speech_recognition" / "recognition_manager.py",
|
||||
SRC_ROOT / "ui" / "settings_dialog.py",
|
||||
SRC_ROOT / "ui" / "dictation_overlay.py",
|
||||
SRC_ROOT / "ui" / "audio_feedback.py",
|
||||
SRC_ROOT / "ui" / "keyboard_backends" / "__init__.py",
|
||||
]
|
||||
missing = [str(p) for p in consumers if not p.exists()]
|
||||
assert not missing, f"consumer files missing: {missing}"
|
||||
not_centralised = [str(p) for p in consumers if not _imports_app_identity(p)]
|
||||
assert not_centralised == [], f"consumers not resolving through app_identity: {not_centralised}"
|
||||
|
||||
|
||||
def test_lightweight_consumers_match_shared_values_at_runtime():
|
||||
"""Runtime check where importing the consumer needs no display server."""
|
||||
identity = _load_identity()
|
||||
from vocalinux.ui import autostart_manager, notifications
|
||||
from vocalinux.utils import paths
|
||||
|
||||
assert paths.APP_DIR_NAME == identity.APP_DIR_NAME
|
||||
assert autostart_manager.DESKTOP_FILENAME == identity.DESKTOP_FILENAME
|
||||
assert notifications.APP_NAME == identity.APP_NAME
|
||||
|
||||
|
||||
def test_source_pinned_literals_reserved_for_rename():
|
||||
"""Literals existing source-shape tests pin stay literal until the rename.
|
||||
|
||||
``tests/test_settings_dialog.py`` (About page) and
|
||||
``tests/test_dictation_pad.py`` (shelving) read module source or exec
|
||||
function bodies in isolation, so ``settings_about.py`` and
|
||||
``dictation_pad.py`` keep their current literals. They are recorded here
|
||||
as checked rename surfaces for ticket #18 rather than centralised twice.
|
||||
"""
|
||||
about = (SRC_ROOT / "ui" / "settings_about.py").read_text(encoding="utf-8")
|
||||
for literal in (
|
||||
'VOCALINUX_SITE_URL = "https://vocalinux.com"',
|
||||
'title="Vocalinux"',
|
||||
'get_icon_path("vocalinux")',
|
||||
):
|
||||
assert literal in about, literal
|
||||
pad = (SRC_ROOT / "ui" / "dictation_pad.py").read_text(encoding="utf-8")
|
||||
assert 'window.set_title("Vocalinux Dictation Pad")' in pad
|
||||
|
||||
|
||||
def test_rename_inventory_files_exist():
|
||||
"""Every rename surface the rename ticket must flip is a real checked file."""
|
||||
identity = _load_identity()
|
||||
assert identity.RENAME_SURFACES, "rename inventory must not be empty"
|
||||
missing = [rel for rel in identity.RENAME_SURFACES if not (REPO_ROOT / rel).exists()]
|
||||
assert missing == [], f"rename inventory entries missing on disk: {missing}"
|
||||
|
||||
|
||||
def test_retained_inventory_records_internal_names():
|
||||
"""Internal module name and upstream URLs stay, by record not by accident."""
|
||||
identity = _load_identity()
|
||||
assert any("src/vocalinux" in entry for entry in identity.RETAINED_SURFACES)
|
||||
assert any("github.com/VocaHQ/vocalinux" in entry for entry in identity.RETAINED_SURFACES)
|
||||
# The retained Python package must still import under its current name.
|
||||
import vocalinux
|
||||
|
||||
assert vocalinux.__name__ == "vocalinux"
|
||||
Reference in New Issue
Block a user