diff --git a/io.github.FaithlifeCommunity.OuDedetai.yml b/io.github.FaithlifeCommunity.OuDedetai.yml index c8ae5fe7..2162811e 100644 --- a/io.github.FaithlifeCommunity.OuDedetai.yml +++ b/io.github.FaithlifeCommunity.OuDedetai.yml @@ -99,6 +99,7 @@ modules: - type: file path: ./snap/gui/verbum.png dest-filename: verbum.png + # FIXME: Also support logos4 and libronixdls URL Schemes - type: inline dest-filename: oudedetai.desktop contents: | diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index d88fa18e..3ad3d29d 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -502,6 +502,9 @@ def get_logos_user_id( logos_appdata_dir: str ) -> Optional[str]: logos_data_path = Path(logos_appdata_dir) / "Data" + # If somehow the directory does not exist - like if the user removed it manually while trying to reset their data. + if not logos_data_path.exists(): + return None contents = os.listdir(logos_data_path) children = [logos_data_path / child for child in contents] file_children = [child for child in children if child.is_dir()] @@ -523,7 +526,8 @@ class Config: # prefix with app_ if it's ours (and otherwise not clear) # prefix with wine_ if it's theirs # suffix with _binary if it's a linux binary - # suffix with _exe if it's a windows binary + # suffix with _exe if it's a windows binary structured as a linux path + # suffix with _exe_windows_path if it's a windows binary structured as a windows path # suffix with _path if it's a file path # suffix with _file_name if it's a file's name (with extension) @@ -1161,25 +1165,42 @@ def wine_user(self) -> Optional[str]: return self._wine_user @property - def logos_cef_exe(self) -> Optional[str]: - if self.wine_user is not None: - # This name is the same even in Verbum - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\{self.faithlife_product}\\System\\LogosCEF.exe' - return None + def logos_appdata_windows_path(self) -> Optional[str]: + """Path to the Logos appdata dir within windows. + + Structured like: C:\\Users\\user\\AppData\\Local... + """ + if self.wine_user is None: + return None + # We don't want to prompt here. + if self._raw.faithlife_product is None: + return None + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\{self._raw.faithlife_product}' @property - def logos_indexer_exe(self) -> Optional[str]: - if self.wine_user is not None: - return (f'C:\\users\\{self.wine_user}\\AppData\\Local\\{self.faithlife_product}\\System\\' - f'{self.faithlife_product}Indexer.exe') - return None + def logos_exe_windows_path(self) -> Optional[str]: + if self.logos_appdata_windows_path is None or self._raw.faithlife_product is None: + return None + return f'{self.logos_appdata_windows_path}\\{self._raw.faithlife_product}.exe' @property - def logos_login_exe(self) -> Optional[str]: - if self.wine_user is not None: - return (f'C:\\users\\{self.wine_user}\\AppData\\Local\\{self.faithlife_product}\\System\\' - f'{self.faithlife_product}.exe') - return None + def logos_cef_exe_windows_path(self) -> Optional[str]: + if self.logos_appdata_windows_path is None: + return None + # This name is the same even in Verbum + return f'{self.logos_appdata_windows_path}\\System\\LogosCEF.exe' + + @property + def logos_indexer_exe_windows_path(self) -> Optional[str]: + if self.logos_appdata_windows_path is None or self._raw.faithlife_product is None: + return None + return f'{self.logos_appdata_windows_path}\\System\\{self._raw.faithlife_product}Indexer.exe' + + @property + def logos_system_exe_windows_path(self) -> Optional[str]: + if self.logos_appdata_windows_path is None or self._raw.faithlife_product is None: + return None + return f'{self.logos_appdata_windows_path}\\System\\{self._raw.faithlife_product}.exe' @property def log_level(self) -> str | int: diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 0042f295..3a7cbe05 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -1,8 +1,10 @@ import logging import os import shutil +import subprocess import sys from pathlib import Path +from typing import Optional from ou_dedetai import system from ou_dedetai.app import App, UserExitedFromAsk @@ -340,24 +342,37 @@ def create_wine_appimage_symlinks(app: App): def create_desktop_file( filename: str, app_name: str, - generic_name: str, - comment: str, exec_cmd: str, - icon_path: str | Path, - wm_class: str, + generic_name: str | None = None, + comment: str | None = None, + icon_path: str | Path | None = None, + wm_class: str | None = None, + additional_keywords: list[str] | None = None, + mime_types: list[str] | None = None, + terminal: Optional[bool] = None ): contents = f"""[Desktop Entry] Name={app_name} -GenericName={generic_name} -Comment={comment} -Exec={exec_cmd} -Icon={icon_path} -Terminal=false Type=Application -StartupWMClass={wm_class} +Exec={exec_cmd} Categories=Education;Spirituality;Languages;Literature;Maps; -Keywords=Logos;Verbum;FaithLife;Bible;Control;Christianity;Jesus; +Keywords=Logos;Verbum;FaithLife;Bible;Control;Christianity;Jesus;{";".join(additional_keywords or [])} """ + contents += f"Terminal={"true" if terminal is True else "false"}\n" + if generic_name: + contents += f"GenericName={generic_name}\n" + if comment: + contents += f"Comment={comment}\n" + if icon_path: + contents += f"Icon={icon_path}\n" + if mime_types: + contents += f"MimeType={";".join(mime_types)}\n" + + if wm_class: + contents += f"StartupWMClass={wm_class}\n" + else: + contents += "StartupNotify=false\n" + local_share = Path.home() / '.local' / 'share' xdg_data_home = Path(os.getenv('XDG_DATA_HOME', local_share)) launcher_path = xdg_data_home / 'applications' / filename @@ -388,7 +403,7 @@ def create_launcher_shortcuts(app: App): app_icon_path = app_dir / app_icon_src.name if constants.RUNMODE == 'binary': - lli_executable = f"{installdir}/{constants.BINARY_NAME}" + oudedetai_executable = f"{installdir}/{constants.BINARY_NAME}" elif constants.RUNMODE == "source": script = Path(sys.argv[0]).expanduser().resolve() repo_dir = None @@ -403,7 +418,7 @@ def create_launcher_shortcuts(app: App): py_bin = next(repo_dir.glob('*/bin/python')) if not py_bin.is_file(): app.exit("Could not locate python binary in virtual environment.") - lli_executable = f"env DIALOG=tk {py_bin} {script}" + oudedetai_executable = f"env DIALOG=tk {py_bin} {script}" elif constants.RUNMODE in ["snap", "flatpak"]: logging.info(f"Not creating launcher shortcuts, {constants.RUNMODE} already handles this") return @@ -417,23 +432,72 @@ def create_launcher_shortcuts(app: App): # Create Logos/Verbum desktop file. logos_path = create_desktop_file( - f"{flproduct}Bible.desktop", - f"{flproduct}", - "Bible", - "Runs Faithlife Bible Software via Wine (snap). Community supported.", - f"{lli_executable} --run-installed-app", - logos_icon_path, - f"{flproduct.lower()}.exe", + filename=f"{flproduct}Bible.desktop", + app_name=f"{flproduct}", + generic_name="Bible", + comment="Runs Faithlife Bible Software via Wine (snap). Community supported.", + exec_cmd=f"{oudedetai_executable} --run-installed-app", + icon_path=logos_icon_path, + wm_class=f"{flproduct.lower()}.exe", + additional_keywords=["Catholic"] if flproduct == "Verbum" else None ) logging.debug(f"> File exists?: {logos_path}: {logos_path.is_file()}") # Create Ou Dedetai desktop file. app_path = create_desktop_file( - f"{constants.BINARY_NAME}.desktop", - constants.APP_NAME, - "FaithLife App Installer", - "Installs and manages either Logos or Verbum via wine. Community supported.", - lli_executable, - app_icon_path, - constants.BINARY_NAME, + filename=f"{constants.BINARY_NAME}.desktop", + app_name=constants.APP_NAME, + generic_name="FaithLife App Installer", + comment="Installs and manages either Logos or Verbum via wine. Community supported.", + exec_cmd=oudedetai_executable, + icon_path=app_icon_path, + wm_class=constants.BINARY_NAME, ) logging.debug(f"> File exists?: {app_path}: {app_path.is_file()}") + + # Register URL scheme handlers: + # logos4 - to facilitate Logos 40.1+ login OAuth flow + # libronixdls - allows opening of bible links from the browser + + if not app.conf.logos_exe_windows_path: + logging.error("Failed to register MIME types with system due to missing wine exe path") + return + + url_handler_desktop_filename = f"{flproduct}-url-handler.desktop" + # Create the desktop file to register the MIME types. + app_path = create_desktop_file( + filename=url_handler_desktop_filename, + app_name=f"{flproduct} URL Handler", + comment="Handles logos4: and libronixdls: URL Schemes", + exec_cmd=f"{oudedetai_executable} --wine '{app.conf.logos_exe_windows_path.replace('\\','\\\\')}' '%u'", + icon_path=app_icon_path, + mime_types=["x-scheme-handler/logos4","x-scheme-handler/libronixdls"], + terminal=True + ) + # For most users Logos will be "installed" at this point, if we fail here there is no easy + # way in the current flow to re-apply these - and this isn't required for Logos to function, + # more of a nice to have. While it would be nice for support reasons not to branch here - + # which is more important: a passing exit code doing what we could to setup Logos, or + # everything that we do - even that which is optional such as this - passes? + + # On most systems these commands have the effect of adding the following to ~/.config/mimetypes: + # ``` + # [Default Applications] + # x-scheme-handler/logos4=logos4.desktop + # x-scheme-handler/libronixdls=libronixdls.desktop + # ``` + try: + system.run_command([ + "xdg-mime", + "default", + url_handler_desktop_filename, + "x-scheme-handler/logos4" + ]) + system.run_command([ + "xdg-mime", + "default", + url_handler_desktop_filename, + "x-scheme-handler/libronixdls" + ]) + except subprocess.CalledProcessError: + logging.exception("Failed to register MIME types with system") + diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 269f854e..f017b664 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -34,8 +34,8 @@ def __init__(self, app: App): """These are processes we discovered already running""" def monitor_indexing(self): - if self.app.conf.logos_indexer_exe in self.existing_processes: - indexer = self.existing_processes.get(self.app.conf.logos_indexer_exe) + if self.app.conf.logos_indexer_exe_windows_path in self.existing_processes: + indexer = self.existing_processes.get(self.app.conf.logos_indexer_exe_windows_path) if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): self.indexing_state = State.RUNNING else: @@ -47,10 +47,10 @@ def monitor_logos(self): cef = [] if self.app.conf.logos_exe: splash = self.existing_processes.get(self.app.conf.logos_exe, []) - if self.app.conf.logos_login_exe: - login = self.existing_processes.get(self.app.conf.logos_login_exe, []) - if self.app.conf.logos_cef_exe: - cef = self.existing_processes.get(self.app.conf.logos_cef_exe, []) + if self.app.conf.logos_system_exe_windows_path: + login = self.existing_processes.get(self.app.conf.logos_system_exe_windows_path, []) + if self.app.conf.logos_cef_exe_windows_path: + cef = self.existing_processes.get(self.app.conf.logos_cef_exe_windows_path, []) splash_running = splash[0].is_running() if splash else False login_running = login[0].is_running() if login else False @@ -78,19 +78,14 @@ def get_logos_pids(self): app = self.app # FIXME: consider refactoring to make one call to get a system pids # Currently this gets all system pids 4 times - if app.conf.logos_exe: - self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) - if app.conf.wine_user: - # Also look for the system's Logos.exe (this may be the login window) - logos_system_exe = ( - f"C:\\users\\{app.conf.wine_user}\\AppData\\Local\\{app.conf.faithlife_product}\\System\\" - f"{app.conf.faithlife_product}.exe" - ) - self.existing_processes[logos_system_exe] = system.get_pids(logos_system_exe) - if app.conf.logos_indexer_exe: - self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) - if app.conf.logos_cef_exe: - self.existing_processes[app.conf.logos_cef_exe] = system.get_pids(app.conf.logos_cef_exe) + for exe_path in [ + app.conf.logos_exe, + app.conf.logos_system_exe_windows_path, + app.conf.logos_indexer_exe_windows_path, + app.conf.logos_cef_exe_windows_path + ]: + if exe_path: + self.existing_processes[exe_path] = system.get_pids(exe_path) def monitor(self): if self.app.is_installed(): @@ -257,15 +252,15 @@ def index(self): index_finished = threading.Event() def run_indexing(): - if not self.app.conf.logos_indexer_exe: + if not self.app.conf.logos_indexer_exe_windows_path: raise ValueError("Cannot find installed indexer") process = wine.run_wine_application( app=self.app, wine_binary=self.app.conf.wine_binary, - exe=self.app.conf.logos_indexer_exe + exe=self.app.conf.logos_indexer_exe_windows_path ) if process is not None: - self.processes[self.app.conf.logos_indexer_exe] = process + self.processes[self.app.conf.logos_indexer_exe_windows_path] = process def check_if_indexing(process: threading.Thread): start_time = time.time() @@ -305,7 +300,7 @@ def stop_indexing(self): self.indexing_state = State.STOPPING if self.app: pids = [] - for process_name in [self.app.conf.logos_indexer_exe]: + for process_name in [self.app.conf.logos_indexer_exe_windows_path]: if process_name is None: continue process = self.processes.get(process_name) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 683ac247..f2b68ec4 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -420,6 +420,7 @@ def get_package_manager() -> PackageManager | None: "libfuse2 " # appimages "binutils wget winbind " # wine "p7zip-full cabextract " # winetricks + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) # NOTE: Package names changed together for Ubuntu 24+, Debian 13+, and # derivatives. This does not include an exhaustive list of distros that @@ -459,6 +460,7 @@ def get_package_manager() -> PackageManager | None: "fuse fuse-libs " # appimages "mod_auth_ntlm_winbind samba-winbind samba-winbind-clients " # wine "cabextract " # winetricks + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('zypper') is not None: # OpenSUSE @@ -473,6 +475,7 @@ def get_package_manager() -> PackageManager | None: "samba wget " # wine "curl gawk grep " # other "7zip cabextract " # winetricks + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('apk') is not None: # alpine @@ -488,6 +491,7 @@ def get_package_manager() -> PackageManager | None: "wget curl " # network "7zip cabextract " # winetricks "samba sed grep gawk bash bash-completion " # other + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('pamac') is not None: # manjaro @@ -501,6 +505,7 @@ def get_package_manager() -> PackageManager | None: "samba wget " # wine "curl gawk grep " # other "7zip cabextract " # winetricks (7zip used to be called p7zip) + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('pacman') is not None: # arch, steamOS @@ -520,7 +525,8 @@ def get_package_manager() -> PackageManager | None: "lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite " "libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses " "lib32-ncurses ocl-icd lib32-ocl-icd libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 " - "gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader" + "gst-plugins-base-libs lib32-gst-plugins-base-libs vulkan-icd-loader lib32-vulkan-icd-loader " + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) else: # arch packages = ( @@ -532,6 +538,7 @@ def get_package_manager() -> PackageManager | None: "alsa-plugins gst-plugins-base-libs libpulse openal " # audio "libva mpg123 v4l-utils " # video "libxslt sqlite " # misc + "xdg-utils " # For xdg-mime needed for custom url scheme registration ) incompatible_packages = "" # appimagelauncher handled separately elif os_name == "org.freedesktop.platform": diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5aa0a46a..c0d75a24 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -47,6 +47,8 @@ environment: # https://forum.snapcraft.io/t/libpxbackend-1-0-so-cannot-open-shared-object-file-no-such-file-or-directory/44263/2 LD_LIBRARY_PATH: $SNAP/usr/lib:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR +# FIXME: Also support logos4 and libronixdls URL Schemes by adding a new .desktop file with MimeType. It may just work from there. + apps: oudedetai: extensions: [gnome]