From 82d0c94ef0fa9177e870d79641c75df476ab12c5 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 24 Oct 2024 00:30:57 -0700 Subject: [PATCH 001/137] feat: generic prompts Works on all three UIs offers a generic function to ask a question that platform independent. If the user fails to offer a response, the installer will terminate. In the GUI this still works, however it may not be desirable to prompt the user for each question. So long as we don't attempt to access the variable before the user has had a chance to put in their preferences it will not prompt them Changed the GUI to gray out the other widgets if the product is not selected. start_ensure_config is called AFTER product is set, if it's called before it attempts to figure out which platform it's on, prompting the user with an additional dialog (not ideal, but acceptable) --- ou_dedetai/app.py | 59 ++++++++++++++++++++ ou_dedetai/cli.py | 22 +++++++- ou_dedetai/gui.py | 31 +++++++++++ ou_dedetai/gui_app.py | 117 +++++++++++++++++++++++++++++++++------- ou_dedetai/installer.py | 36 ++++--------- ou_dedetai/tui_app.py | 58 +++++++++----------- 6 files changed, 243 insertions(+), 80 deletions(-) create mode 100644 ou_dedetai/app.py diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py new file mode 100644 index 00000000..713eeb54 --- /dev/null +++ b/ou_dedetai/app.py @@ -0,0 +1,59 @@ +import abc +from typing import Optional + +from ou_dedetai import config + + +class App(abc.ABC): + def __init__(self, **kwargs) -> None: + self.conf = Config(self) + + def ask(self, question: str, options: list[str]) -> str: + """Askes the user a question with a list of supplied options + + Returns the option the user picked. + + If the internal ask function returns None, the process will exit with an error code 1 + """ + if options is not None and self._exit_option is not None: + options += [self._exit_option] + answer = self._ask(question, options) + if answer == self._exit_option: + answer = None + + if answer is None: + exit(1) + + return answer + + _exit_option: Optional[str] = "Exit" + + @abc.abstractmethod + def _ask(self, question: str, options: list[str] = None) -> Optional[str]: + """Implementation for asking a question pre-front end + + If you would otherwise return None, consider shutting down cleanly, + the calling function will exit the process with an error code of one + if this function returns None + """ + raise NotImplementedError() + + def _hook_product_update(self, product: Optional[str]): + """A hook for any changes the individual apps want to do when a platform changes""" + pass + +class Config: + def __init__(self, app: App) -> None: + self.app = app + + @property + def faithlife_product(self) -> str: + """Wrapper function that ensures that ensures the product is set + + if it's not then the user is prompted to choose one.""" + if not config.FLPRODUCT: + question = "Choose which FaithLife product the script should install: " # noqa: E501 + options = ["Logos", "Verbum"] + config.FLPRODUCT = self.app.ask(question, options) + self.app._hook_product_update(config.FLPRODUCT) + return config.FLPRODUCT \ No newline at end of file diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index af17cc20..e85cd994 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -1,5 +1,8 @@ import queue import threading +from typing import Optional + +from ou_dedetai.app import App from . import control from . import installer @@ -8,8 +11,9 @@ from . import utils -class CLI: +class CLI(App): def __init__(self): + super().__init__() self.running = True self.choice_q = queue.Queue() self.input_q = queue.Queue() @@ -88,6 +92,20 @@ def winetricks(self): import config wine.run_winetricks_cmd(*config.winetricks_args) + _exit_option: str = "Exit" + + def _ask(self, question: str, options: list[str]) -> str: + """Passes the user input to the user_input_processor thread + + The user_input_processor is running on the thread that the user's stdin/stdout is attached to + This function is being called from another thread so we need to pass the information between threads using a queue/event + """ + self.input_q.put((question, options)) + self.input_event.set() + self.choice_event.wait() + self.choice_event.clear() + return self.choice_q.get() + def user_input_processor(self, evt=None): while self.running: prompt = None @@ -111,7 +129,7 @@ def user_input_processor(self, evt=None): choice = input(f"{question}: {optstr}: ") if len(choice) == 0: choice = default - if choice is not None and choice.lower() == 'exit': + if choice is not None and choice == self._exit_option: self.running = False if choice is not None: self.choice_q.put(choice) diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index a370744c..80b04588 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -18,6 +18,37 @@ from . import utils +class ChoiceGui(Frame): + _default_prompt: str = "Choose…" + + def __init__(self, root, question: str, options: list[str], **kwargs): + super(ChoiceGui, self).__init__(root, **kwargs) + self.italic = font.Font(slant='italic') + self.config(padding=5) + self.grid(row=0, column=0, sticky='nwes') + + # Label Row + self.question_label = Label(self, text=question) + # drop-down menu + self.answer_var = StringVar(value=self._default_prompt) + self.answer_dropdown = Combobox(self, textvariable=self.answer_var) + self.answer_dropdown['values'] = options + if len(options) > 0: + self.answer_dropdown.set(options[0]) + + # Cancel/Okay buttons row. + self.cancel_button = Button(self, text="Cancel") + self.okay_button = Button(self, text="Confirm") + + # Place widgets. + row = 0 + self.question_label.grid(column=0, row=row, sticky='nws', pady=2) + self.answer_dropdown.grid(column=1, row=row, sticky='w', pady=2) + row += 1 + self.cancel_button.grid(column=3, row=row, sticky='e', pady=2) + self.okay_button.grid(column=4, row=row, sticky='e', pady=2) + + class InstallerGui(Frame): def __init__(self, root, **kwargs): super(InstallerGui, self).__init__(root, **kwargs) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 7436cd17..b5f3113f 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -7,11 +7,15 @@ from pathlib import Path from queue import Queue +from threading import Event from tkinter import PhotoImage from tkinter import Tk from tkinter import Toplevel from tkinter import filedialog as fd from tkinter.ttk import Style +from typing import Optional + +from ou_dedetai.app import App from . import config from . import control @@ -23,6 +27,33 @@ from . import utils from . import wine +class GuiApp(App): + """Implements the App interface for all windows""" + + _exit_option: Optional[str] = None + + def __init__(self, root: "Root", **kwargs): + super().__init__() + self.root_to_destory_on_none = root + + def _ask(self, question: str, options: list[str] = None) -> Optional[str]: + answer_q = Queue() + answer_event = Event() + def spawn_dialog(): + # Create a new popup (with it's own event loop) + pop_up = ChoicePopUp(question, options, answer_q, answer_event) + + # Run the mainloop in this thread + pop_up.mainloop() + + utils.start_thread(spawn_dialog) + + answer_event.wait() + answer = answer_q.get() + if answer is None: + self.root_to_destory_on_none.destroy() + return None + return answer class Root(Tk): def __init__(self, *args, **kwargs): @@ -82,8 +113,45 @@ def __init__(self, *args, **kwargs): self.iconphoto(False, self.pi) -class InstallerWindow(): - def __init__(self, new_win, root, **kwargs): +class ChoicePopUp(Tk): + """Creates a pop-up with a choice""" + def __init__(self, question: str, options: list[str], answer_q: Queue, answer_event: Event, **kwargs): + # Set root parameters. + super().__init__() + self.title(f"Quesiton: {question.strip().strip(':')}") + self.resizable(False, False) + self.gui = gui.ChoiceGui(self, question, options) + # Set root widget event bindings. + self.bind( + "", + self.on_confirm_choice + ) + self.bind( + "", + self.on_cancel_released + ) + self.gui.cancel_button.config(command=self.on_cancel_released) + self.gui.okay_button.config(command=self.on_confirm_choice) + self.answer_q = answer_q + self.answer_event = answer_event + + def on_confirm_choice(self, evt=None): + if self.gui.answer_dropdown.get() == gui.ChoiceGui._default_prompt: + return + answer = self.gui.answer_dropdown.get() + self.answer_q.put(answer) + self.answer_event.set() + self.destroy() + + def on_cancel_released(self, evt=None): + self.answer_q.put(None) + self.answer_event.set() + self.destroy() + + +class InstallerWindow(GuiApp): + def __init__(self, new_win, root: Root, **kwargs): + super().__init__(root) # Set root parameters. self.win = new_win self.root = root @@ -177,7 +245,29 @@ def __init__(self, new_win, root, **kwargs): # Run commands. self.get_winetricks_options() - self.start_ensure_config() + self.grey_out_others_if_faithlife_product_is_not_selected() + + def grey_out_others_if_faithlife_product_is_not_selected(self): + if not config.FLPRODUCT: + # Disable all input widgets after Version. + widgets = [ + self.gui.version_dropdown, + self.gui.release_dropdown, + self.gui.release_check_button, + self.gui.wine_dropdown, + self.gui.wine_check_button, + self.gui.okay_button, + ] + self.set_input_widgets_state('disabled', widgets=widgets) + if not self.gui.productvar.get(): + self.gui.productvar.set(self.gui.product_dropdown['values'][0]) + # This is started in a new thread because it blocks and was called form the constructor + utils.start_thread(self.set_product) + + def _hook_product_update(self, product: Optional[str]): + if product is not None: + self.gui.productvar.set(product) + self.gui.product_dropdown.set(product) def start_ensure_config(self): # Ensure progress counter is reset. @@ -222,21 +312,7 @@ def todo(self, evt=None, task=None): else: return self.set_input_widgets_state('enabled') - if task == 'FLPRODUCT': - # Disable all input widgets after Version. - widgets = [ - self.gui.version_dropdown, - self.gui.release_dropdown, - self.gui.release_check_button, - self.gui.wine_dropdown, - self.gui.wine_check_button, - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - if not self.gui.productvar.get(): - self.gui.productvar.set(self.gui.product_dropdown['values'][0]) - self.set_product() - elif task == 'TARGETVERSION': + if task == 'TARGETVERSION': # Disable all input widgets after Version. widgets = [ self.gui.release_dropdown, @@ -290,7 +366,7 @@ def set_product(self, evt=None): self.gui.product_dropdown.selection_clear() if evt: # manual override; reset dependent variables logging.debug(f"User changed FLPRODUCT to '{self.gui.flproduct}'") - config.FLPRODUCT = None + config.FLPRODUCT = self.gui.flproduct config.FLPRODUCTi = None config.VERBUM_PATH = None @@ -556,8 +632,9 @@ def update_install_progress(self, evt=None): return 0 -class ControlWindow(): +class ControlWindow(GuiApp): def __init__(self, root, *args, **kwargs): + super().__init__(root) # Set root parameters. self.root = root self.root.title(f"{config.name_app} Control Panel") diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 34a4eb1c..6107fe0d 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -4,6 +4,8 @@ import sys from pathlib import Path +from ou_dedetai.app import App + from . import config from . import msg from . import network @@ -12,41 +14,23 @@ from . import wine -def ensure_product_choice(app=None): +def ensure_product_choice(app: App): config.INSTALL_STEPS_COUNT += 1 update_install_feedback("Choose product…", app=app) logging.debug('- config.FLPRODUCT') logging.debug('- config.FLPRODUCTi') logging.debug('- config.VERBUM_PATH') - if not config.FLPRODUCT: - if config.DIALOG == 'cli': - app.input_q.put( - ( - "Choose which FaithLife product the script should install: ", # noqa: E501 - ["Logos", "Verbum", "Exit"] - ) - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.FLPRODUCT = app.choice_q.get() - else: - utils.send_task(app, 'FLPRODUCT') - if config.DIALOG == 'curses': - app.product_e.wait() - config.FLPRODUCT = app.product_q.get() - else: - if config.DIALOG == 'curses' and app: - app.set_product(config.FLPRODUCT) - - config.FLPRODUCTi = get_flproducti_name(config.FLPRODUCT) - if config.FLPRODUCT == 'Logos': + # accessing app.conf.faithlife_product ensures the product is selected + # Eventually we'd migrate all of these kind of variables in config to this pattern + # That require a user selection if they are found to be None + config.FLPRODUCTi = get_flproducti_name(app.conf.faithlife_product) + if app.conf.faithlife_product == 'Logos': config.VERBUM_PATH = "/" - elif config.FLPRODUCT == 'Verbum': + elif app.conf.faithlife_product == 'Verbum': config.VERBUM_PATH = "/Verbum/" - logging.debug(f"> {config.FLPRODUCT=}") + logging.debug(f"> {app.conf.faithlife_product=}") logging.debug(f"> {config.FLPRODUCTi=}") logging.debug(f"> {config.VERBUM_PATH=}") diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 14d32d81..1d6d02bc 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -6,6 +6,9 @@ import curses from pathlib import Path from queue import Queue +from typing import Optional + +from ou_dedetai.app import App from . import config from . import control @@ -23,8 +26,9 @@ # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. -class TUI: +class TUI(App): def __init__(self, stdscr): + super().__init__() self.stdscr = stdscr # if config.current_logos_version is not None: self.title = f"Welcome to {config.name_app} {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 @@ -37,6 +41,10 @@ def __init__(self, stdscr): self.logos = logos.LogosManager(app=self) self.tmp = "" + # Generic ask/response events/threads + self.ask_answer_queue = Queue() + self.ask_answer_event = threading.Event() + # Queues self.main_thread = threading.Thread() self.get_q = Queue() @@ -54,8 +62,6 @@ def __init__(self, stdscr): self.switch_q = Queue() # Install and Options - self.product_q = Queue() - self.product_e = threading.Event() self.version_q = Queue() self.version_e = threading.Event() self.releases_q = Queue() @@ -350,9 +356,7 @@ def run(self): signal.signal(signal.SIGINT, self.end) def task_processor(self, evt=None, task=None): - if task == 'FLPRODUCT': - utils.start_thread(self.get_product, config.use_python_dialog) - elif task == 'TARGETVERSION': + if task == 'TARGETVERSION': utils.start_thread(self.get_version, config.use_python_dialog) elif task == 'TARGET_RELEASE_VERSION': utils.start_thread(self.get_release, config.use_python_dialog) @@ -377,7 +381,7 @@ def choice_processor(self, stdscr, screen_id, choice): screen_actions = { 0: self.main_menu_select, 1: self.custom_appimage_select, - 2: self.product_select, + 2: self.handle_ask_response, 3: self.version_select, 4: self.release_select, 5: self.installdir_select, @@ -585,16 +589,6 @@ def custom_appimage_select(self, choice): self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) self.appimage_e.set() - def product_select(self, choice): - if choice: - if str(choice).startswith("Logos"): - config.FLPRODUCT = "Logos" - elif str(choice).startswith("Verbum"): - config.FLPRODUCT = "Verbum" - self.menu_screen.choice = "Processing" - self.product_q.put(config.FLPRODUCT) - self.product_e.set() - def version_select(self, choice): if choice: if "10" in choice: @@ -719,25 +713,25 @@ def switch_screen(self, dialog): if isinstance(self.active_screen, tui_screen.CursesScreen): self.clear() - def get_product(self, dialog): - question = "Choose which FaithLife product the script should install:" # noqa: E501 - labels = ["Logos", "Verbum", "Return to Main Menu"] - options = self.which_dialog_options(labels, dialog) + _exit_option = "Return to Main Menu" + + def _ask(self, question: str, options: list[str]) -> Optional[str]: + options = self.which_dialog_options(options, config.use_python_dialog) self.menu_options = options - self.screen_q.put(self.stack_menu(2, self.product_q, self.product_e, question, options, dialog=dialog)) + self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, options, dialog=config.use_python_dialog)) - def set_product(self, choice): - if str(choice).startswith("Logos"): - config.FLPRODUCT = "Logos" - elif str(choice).startswith("Verbum"): - config.FLPRODUCT = "Verbum" - self.menu_screen.choice = "Processing" - self.product_q.put(config.FLPRODUCT) - self.product_e.set() + # Now wait for it to complete + self.ask_answer_event.wait() + return self.ask_answer_queue.get() + + def handle_ask_response(self, choice: Optional[str]): + if choice is not None: + self.ask_answer_queue.put(choice) + self.ask_answer_event.set() + self.switch_screen(config.use_python_dialog) def get_version(self, dialog): - self.product_e.wait() - question = f"Which version of {config.FLPRODUCT} should the script install?" # noqa: E501 + question = f"Which version of {self.conf.faithlife_product} should the script install?" # noqa: E501 labels = ["10", "9", "Return to Main Menu"] options = self.which_dialog_options(labels, dialog) self.menu_options = options From 60860f821b6e157a0b29bc0be5976b7693191653 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:29:39 -0800 Subject: [PATCH 002/137] refactor: move constants to their own file --- ou_dedetai/app.py | 9 ++++++++- ou_dedetai/config.py | 30 ++++-------------------------- ou_dedetai/constants.py | 25 +++++++++++++++++++++++++ ou_dedetai/control.py | 3 ++- ou_dedetai/gui.py | 5 +++-- ou_dedetai/gui_app.py | 35 ++++++++++++++++++----------------- ou_dedetai/installer.py | 21 +++++++++++---------- ou_dedetai/main.py | 21 +++++++++++---------- ou_dedetai/msg.py | 7 ++++--- ou_dedetai/network.py | 13 +++++++------ ou_dedetai/system.py | 7 ++++--- ou_dedetai/tui_app.py | 15 ++++++++------- ou_dedetai/utils.py | 33 +++++++++++++++++---------------- ou_dedetai/wine.py | 3 ++- pyproject.toml | 2 +- 15 files changed, 125 insertions(+), 104 deletions(-) create mode 100644 ou_dedetai/constants.py diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 713eeb54..49418d6e 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -9,7 +9,7 @@ def __init__(self, **kwargs) -> None: self.conf = Config(self) def ask(self, question: str, options: list[str]) -> str: - """Askes the user a question with a list of supplied options + """Asks the user a question with a list of supplied options Returns the option the user picked. @@ -38,10 +38,17 @@ def _ask(self, question: str, options: list[str] = None) -> Optional[str]: """ raise NotImplementedError() + # XXX: should this be changed to config updates more generally? def _hook_product_update(self, product: Optional[str]): """A hook for any changes the individual apps want to do when a platform changes""" pass + # XXX: unused at present + @abc.abstractmethod + def update_progress(self, message: str, percent: Optional[int] = None): + """Updates the progress of the current operation""" + pass + class Config: def __init__(self, app: App) -> None: self.app = app diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index fb971b76..3f1e0f58 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -5,12 +5,8 @@ from datetime import datetime from typing import Optional +from . import constants -# Define app name variables. -name_app = 'Ou Dedetai' -name_binary = 'oudedetai' -name_package = 'ou_dedetai' -repo_link = "https://github.com/FaithLife-Community/LogosLinuxInstaller" # Define and set variables that are required in the config file. core_config_keys = [ @@ -34,7 +30,7 @@ 'DEBUG': False, 'DELETE_LOG': None, 'DIALOG': None, - 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{name_binary}.log"), # noqa: E501 + 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log"), # noqa: E501 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 'LOGOS_EXE': None, 'LOGOS_EXECUTABLE': None, @@ -63,33 +59,19 @@ ACTION: str = 'app' APPIMAGE_FILE_PATH: Optional[str] = None BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. -DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{name_binary}.json") # noqa: E501 FLPRODUCTi: Optional[str] = None INSTALL_STEP: int = 0 INSTALL_STEPS_COUNT: int = 0 L9PACKAGES = None -LEGACY_CONFIG_FILES = [ - os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 - os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 -] -LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-beta.4" LLI_LATEST_VERSION: Optional[str] = None -LLI_TITLE = name_app LOG_LEVEL = logging.WARNING -LOGOS_BLUE = '#0082FF' -LOGOS_GRAY = '#E7E7E7' -LOGOS_WHITE = '#FCFCFC' -# LOGOS_WHITE = '#F7F7F7' LOGOS_DIR = os.path.dirname(LOGOS_EXE) if LOGOS_EXE else None # noqa: F821 LOGOS_FORCE_ROOT: bool = False LOGOS_ICON_FILENAME: Optional[str] = None LOGOS_ICON_URL: Optional[str] = None -LOGOS_LATEST_VERSION_FILENAME = name_binary +LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None LOGOS9_RELEASES = None # used to save downloaded releases list # FIXME: not set #noqa: E501 -LOGOS9_WINE64_BOTTLE_TARGZ_NAME = "wine64_bottle.tar.gz" -LOGOS9_WINE64_BOTTLE_TARGZ_URL = f"https://github.com/ferion11/wine64_bottle_dotnet/releases/download/v5.11b/{LOGOS9_WINE64_BOTTLE_TARGZ_NAME}" # noqa: E501 LOGOS10_RELEASES = None # used to save downloaded releases list # FIXME: not set #noqa: E501 MYDOWNLOADS: Optional[str] = None # FIXME: Should this use ~/.cache? OS_NAME: Optional[str] = None @@ -99,8 +81,6 @@ PACKAGE_MANAGER_COMMAND_QUERY: Optional[list[str]] = None PACKAGES: Optional[str] = None PASSIVE: Optional[bool] = None -pid_file = f'/tmp/{name_binary}.pid' -PRESENT_WORKING_DIRECTORY: str = os.getcwd() QUERY_PREFIX: Optional[str] = None REBOOT_REQUIRED: Optional[str] = None RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: Optional[str] = None @@ -110,8 +90,6 @@ RECOMMENDED_WINE64_APPIMAGE_BRANCH: Optional[str] = None SUPERUSER_COMMAND: Optional[str] = None VERBUM_PATH: Optional[str] = None -WINETRICKS_URL = "https://raw.githubusercontent.com/Winetricks/winetricks/5904ee355e37dff4a3ab37e1573c56cffe6ce223/src/winetricks" # noqa: E501 -WINETRICKS_VERSION = '20220411' wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") install_finished = False @@ -134,7 +112,7 @@ 0: "yes", 1: "uptodate", 2: "no", - None: "config.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set.", # noqa: E501 + None: "constants.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set.", # noqa: E501 } check_if_indexing = None diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py new file mode 100644 index 00000000..b8084e4f --- /dev/null +++ b/ou_dedetai/constants.py @@ -0,0 +1,25 @@ +import logging +import os + +# Define app name variables. +APP_NAME = 'Ou Dedetai' +BINARY_NAME = 'oudedetai' +PACKAGE_NAME = 'ou_dedetai' +REPOSITORY_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller" + +# Set other run-time variables not set in the env. +DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 +LEGACY_CONFIG_FILES = [ + os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 + os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 +] +LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" +LLI_CURRENT_VERSION = "4.0.0-beta.4" +LOG_LEVEL = logging.WARNING +LOGOS_BLUE = '#0082FF' +LOGOS_GRAY = '#E7E7E7' +LOGOS_WHITE = '#FCFCFC' +LOGOS9_WINE64_BOTTLE_TARGZ_NAME = "wine64_bottle.tar.gz" +LOGOS9_WINE64_BOTTLE_TARGZ_URL = f"https://github.com/ferion11/wine64_bottle_dotnet/releases/download/v5.11b/{LOGOS9_WINE64_BOTTLE_TARGZ_NAME}" # noqa: E501 +PID_FILE = f'/tmp/{BINARY_NAME}.pid' +WINETRICKS_VERSION = '20220411' diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 10e547a4..13cf9159 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -13,6 +13,7 @@ from pathlib import Path from . import config +from . import constants from . import msg from . import network from . import system @@ -269,7 +270,7 @@ def set_winetricks(): # Check if local winetricks version is up-to-date; if so, offer to # use it or to download; else, download it. local_winetricks_version = subprocess.check_output(["winetricks", "--version"]).split()[0] # noqa: E501 - if str(local_winetricks_version) != config.WINETRICKS_VERSION: # noqa: E501 + if str(local_winetricks_version) != constants.WINETRICKS_VERSION: # noqa: E501 if config.DIALOG == 'tk': #FIXME: CLI client not considered logging.info("Setting winetricks to the local binary…") config.WINETRICKSBIN = local_winetricks_path diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index 80b04588..b0501297 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -16,6 +16,7 @@ from . import config from . import utils +from . import constants class ChoiceGui(Frame): @@ -61,7 +62,7 @@ def __init__(self, root, **kwargs): self.flproduct = config.FLPRODUCT self.targetversion = config.TARGETVERSION self.logos_release_version = config.TARGET_RELEASE_VERSION - self.default_config_path = config.DEFAULT_CONFIG_PATH + self.default_config_path = constants.DEFAULT_CONFIG_PATH self.wine_exe = utils.get_wine_exe_path() self.winetricksbin = config.WINETRICKSBIN self.skip_fonts = config.SKIP_FONTS @@ -249,7 +250,7 @@ def __init__(self, root, *args, **kwargs): self.backups_label = Label(self, text="Backup/restore data") self.backup_button = Button(self, text="Backup") self.restore_button = Button(self, text="Restore") - self.update_lli_label = Label(self, text=f"Update {config.name_app}") # noqa: E501 + self.update_lli_label = Label(self, text=f"Update {constants.APP_NAME}") # noqa: E501 self.update_lli_button = Button(self, text="Update") # AppImage buttons self.latest_appimage_label = Label( diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index b5a193ef..25ad511f 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -18,6 +18,7 @@ from ou_dedetai.app import App from . import config +from . import constants from . import control from . import gui from . import installer @@ -64,23 +65,23 @@ def __init__(self, *args, **kwargs): self.style.theme_use('alt') # Update color scheme. - self.style.configure('TCheckbutton', bordercolor=config.LOGOS_GRAY) - self.style.configure('TCombobox', bordercolor=config.LOGOS_GRAY) - self.style.configure('TCheckbutton', indicatorcolor=config.LOGOS_GRAY) - self.style.configure('TRadiobutton', indicatorcolor=config.LOGOS_GRAY) + self.style.configure('TCheckbutton', bordercolor=constants.LOGOS_GRAY) + self.style.configure('TCombobox', bordercolor=constants.LOGOS_GRAY) + self.style.configure('TCheckbutton', indicatorcolor=constants.LOGOS_GRAY) + self.style.configure('TRadiobutton', indicatorcolor=constants.LOGOS_GRAY) bg_widgets = [ 'TCheckbutton', 'TCombobox', 'TFrame', 'TLabel', 'TRadiobutton' ] fg_widgets = ['TButton', 'TSeparator'] for w in bg_widgets: - self.style.configure(w, background=config.LOGOS_WHITE) + self.style.configure(w, background=constants.LOGOS_WHITE) for w in fg_widgets: - self.style.configure(w, background=config.LOGOS_GRAY) + self.style.configure(w, background=constants.LOGOS_GRAY) self.style.configure( 'Horizontal.TProgressbar', - thickness=10, background=config.LOGOS_BLUE, - bordercolor=config.LOGOS_GRAY, - troughcolor=config.LOGOS_GRAY, + thickness=10, background=constants.LOGOS_BLUE, + bordercolor=constants.LOGOS_GRAY, + troughcolor=constants.LOGOS_GRAY, ) # Justify to the left [('Button.label', {'sticky': 'w'})] @@ -155,7 +156,7 @@ def __init__(self, new_win, root: Root, **kwargs): # Set root parameters. self.win = new_win self.root = root - self.win.title(f"{config.name_app} Installer") + self.win.title(f"{constants.APP_NAME} Installer") self.win.resizable(False, False) self.gui = gui.InstallerGui(self.win) @@ -637,14 +638,14 @@ def __init__(self, root, *args, **kwargs): super().__init__(root) # Set root parameters. self.root = root - self.root.title(f"{config.name_app} Control Panel") + self.root.title(f"{constants.APP_NAME} Control Panel") self.root.resizable(False, False) self.gui = gui.ControlGui(self.root) self.actioncmd = None self.logos = logos.LogosManager(app=self) text = self.gui.update_lli_label.cget('text') - ver = config.LLI_CURRENT_VERSION + ver = constants.LLI_CURRENT_VERSION new = config.LLI_LATEST_VERSION text = f"{text}\ncurrent: v{ver}\nlatest: v{new}" self.gui.update_lli_label.config(text=text) @@ -745,7 +746,7 @@ def configure_app_button(self, evt=None): self.gui.app_button.config(command=self.run_installer) def run_installer(self, evt=None): - classname = config.name_binary + classname = constants.BINARY_NAME self.installer_win = Toplevel() InstallerWindow(self.installer_win, self.root, class_=classname) self.root.icon = config.LOGOS_ICON_URL @@ -822,7 +823,7 @@ def open_file_dialog(self, filetype_name, filetype_extension): def update_to_latest_lli_release(self, evt=None): self.start_indeterminate_progress() - self.gui.statusvar.set(f"Updating to latest {config.name_app} version…") # noqa: E501 + self.gui.statusvar.set(f"Updating to latest {constants.APP_NAME} version…") # noqa: E501 utils.start_thread(utils.update_to_latest_lli_release, app=self) def update_to_latest_appimage(self, evt=None): @@ -904,10 +905,10 @@ def update_latest_lli_release_button(self, evt=None): state = '!disabled' elif config.logos_linux_installer_status == 1: state = 'disabled' - msg = f"This button is disabled. {config.name_app} is up-to-date." # noqa: E501 + msg = f"This button is disabled. {constants.APP_NAME} is up-to-date." # noqa: E501 elif config.logos_linux_installer_status == 2: state = 'disabled' - msg = f"This button is disabled. {config.name_app} is newer than the latest release." # noqa: E501 + msg = f"This button is disabled. {constants.APP_NAME} is newer than the latest release." # noqa: E501 if msg: gui.ToolTip(self.gui.update_lli_button, msg) self.clear_status_text() @@ -990,7 +991,7 @@ def stop_indeterminate_progress(self, evt=None): def control_panel_app(): utils.set_debug() - classname = config.name_binary + classname = constants.BINARY_NAME root = Root(className=classname) ControlWindow(root, class_=classname) root.mainloop() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 949ee10a..beff724c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -7,6 +7,7 @@ from ou_dedetai.app import App from . import config +from . import constants from . import msg from . import network from . import system @@ -426,16 +427,16 @@ def ensure_premade_winebottle_download(app=None): if config.TARGETVERSION != '9': return update_install_feedback( - f"Ensuring {config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME} bottle is downloaded…", # noqa: E501 + f"Ensuring {constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME} bottle is downloaded…", # noqa: E501 app=app ) - downloaded_file = utils.get_downloaded_file_path(config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 + downloaded_file = utils.get_downloaded_file_path(constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE network.logos_reuse_download( - config.LOGOS9_WINE64_BOTTLE_TARGZ_URL, - config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, + constants.LOGOS9_WINE64_BOTTLE_TARGZ_URL, + constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, config.MYDOWNLOADS, app=app, ) @@ -640,7 +641,7 @@ def ensure_launcher_executable(app=None): ) # Copy executable to config.INSTALLDIR. - launcher_exe = Path(f"{config.INSTALLDIR}/{config.name_binary}") + launcher_exe = Path(f"{config.INSTALLDIR}/{constants.BINARY_NAME}") if launcher_exe.is_file(): logging.debug("Removing existing launcher binary.") launcher_exe.unlink() @@ -733,7 +734,7 @@ def get_flproducti_name(product_name) -> str: def create_config_file(): - config_dir = Path(config.DEFAULT_CONFIG_PATH).parent + config_dir = Path(constants.DEFAULT_CONFIG_PATH).parent config_dir.mkdir(exist_ok=True, parents=True) if config_dir.is_dir(): utils.write_config(config.CONFIG_FILE) @@ -794,7 +795,7 @@ def create_launcher_shortcuts(): app_icon_path = app_dir / app_icon_src.name if system.get_runmode() == 'binary': - lli_executable = f"{installdir}/{config.name_binary}" + lli_executable = f"{installdir}/{constants.BINARY_NAME}" else: script = Path(sys.argv[0]).expanduser().resolve() repo_dir = None @@ -834,16 +835,16 @@ def create_launcher_shortcuts(): """ ), ( - f"{config.name_binary}.desktop", + f"{constants.BINARY_NAME}.desktop", f"""[Desktop Entry] -Name={config.name_app} +Name={constants.APP_NAME} GenericName=FaithLife Wine App Installer Comment=Manages FaithLife Bible Software via Wine Exec={lli_executable} Icon={app_icon_path} Terminal=false Type=Application -StartupWMClass={config.name_binary} +StartupWMClass={constants.BINARY_NAME} Categories=Education; Keywords={flproduct};Logos;Bible;Control; """ diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index b9fadb11..e093c136 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -13,6 +13,7 @@ from . import cli from . import config from . import control +from . import constants from . import gui_app from . import msg from . import network @@ -30,8 +31,8 @@ def get_parser(): parser.add_argument( '-v', '--version', action='version', version=( - f"{config.LLI_TITLE}, " - f"{config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}" + f"{constants.APP_NAME}, " + f"{constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}" ), ) @@ -65,7 +66,7 @@ def get_parser(): '-c', '--config', metavar='CONFIG_FILE', help=( "use a custom config file during installation " - f"[default: {config.DEFAULT_CONFIG_PATH}]" + f"[default: {constants.DEFAULT_CONFIG_PATH}]" ), ) cfg.add_argument( @@ -138,7 +139,7 @@ def get_parser(): ) cmd.add_argument( '--update-self', '-u', action='store_true', - help=f'Update {config.name_app} to the latest release.', + help=f'Update {constants.APP_NAME} to the latest release.', ) cmd.add_argument( '--update-latest-appimage', '-U', action='store_true', @@ -331,7 +332,7 @@ def set_config(): # Update config from CONFIG_FILE. if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 - for legacy_config in config.LEGACY_CONFIG_FILES: + for legacy_config in constants.LEGACY_CONFIG_FILES: if utils.file_exists(legacy_config): config.set_config_env(legacy_config) utils.write_config(config.CONFIG_FILE) @@ -388,8 +389,8 @@ def check_incompatibilities(): question_text = "Remove AppImageLauncher? A reboot will be required." secondary = ( "Your system currently has AppImageLauncher installed.\n" - f"{config.name_app} is not compatible with AppImageLauncher.\n" - f"For more information, see: {config.repo_link}/issues/114" + f"{constants.APP_NAME} is not compatible with AppImageLauncher.\n" + f"For more information, see: {constants.REPOSITORY_LINK}/issues/114" ) no_text = "User declined to remove AppImageLauncher." msg.logos_continue_question(question_text, no_text, secondary) @@ -459,7 +460,7 @@ def main(): utils.die_if_root() # Print terminal banner - logging.info(f"{config.LLI_TITLE}, {config.LLI_CURRENT_VERSION} by {config.LLI_AUTHOR}.") # noqa: E501 + logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 logging.debug(f"Installer log file: {config.LOGOS_LOG}") check_incompatibilities() @@ -470,7 +471,7 @@ def main(): def close(): - logging.debug(f"Closing {config.name_app}.") + logging.debug(f"Closing {constants.APP_NAME}.") for thread in threads: # Only wait on non-daemon threads. if not thread.daemon: @@ -481,7 +482,7 @@ def close(): wine.end_wine_processes() else: logging.debug("No extra processes found.") - logging.debug(f"Closing {config.name_app} finished.") + logging.debug(f"Closing {constants.APP_NAME} finished.") if __name__ == '__main__': diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 034a5dfc..4c87d51f 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -9,6 +9,7 @@ from pathlib import Path from . import config +from . import constants from .gui import ask_question from .gui import show_error @@ -165,7 +166,7 @@ def logos_warn(message): def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' - WIKI_LINK = f"{config.repo_link}/wiki" + WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 @@ -191,7 +192,7 @@ def ui_message(message, secondary=None, detail=None, app=None, parent=None, fata def logos_error(message, secondary=None, detail=None, app=None, parent=None): # if detail is None: # detail = '' - # WIKI_LINK = f"{config.repo_link}/wiki" + # WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" # TELEGRAM_LINK = "https://t.me/linux_logos" # MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" # help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 @@ -215,7 +216,7 @@ def logos_error(message, secondary=None, detail=None, app=None, parent=None): logging.critical(message) if secondary is None or secondary == "": try: - os.remove(config.pid_file) + os.remove(constants.PID_FILE) except FileNotFoundError: # no pid file when testing functions pass os.kill(os.getpgid(os.getpid()), signal.SIGKILL) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 1b5cd068..40fd9546 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -17,6 +17,7 @@ from ou_dedetai import wine from . import config +from . import constants from . import msg from . import utils @@ -411,7 +412,7 @@ def set_logoslinuxinstaller_latest_release_config(): json_data = get_latest_release_data(repo) logoslinuxinstaller_url = get_first_asset_url(json_data) if logoslinuxinstaller_url is None: - logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501 + logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 return config.LOGOS_LATEST_VERSION_URL = logoslinuxinstaller_url config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 @@ -565,10 +566,10 @@ def get_logos_releases(app=None): def update_lli_binary(app=None): lli_file_path = os.path.realpath(sys.argv[0]) - lli_download_path = Path(config.MYDOWNLOADS) / config.name_binary - temp_path = Path(config.MYDOWNLOADS) / f"{config.name_binary}.tmp" + lli_download_path = Path(config.MYDOWNLOADS) / constants.BINARY_NAME + temp_path = Path(config.MYDOWNLOADS) / f"{constants.BINARY_NAME}.tmp" logging.debug( - f"Updating {config.name_app} to latest version by overwriting: {lli_file_path}") # noqa: E501 + f"Updating {constants.APP_NAME} to latest version by overwriting: {lli_file_path}") # noqa: E501 # Remove existing downloaded file if different version. if lli_download_path.is_file(): @@ -581,7 +582,7 @@ def update_lli_binary(app=None): logos_reuse_download( config.LOGOS_LATEST_VERSION_URL, - config.name_binary, + constants.BINARY_NAME, config.MYDOWNLOADS, app=app, ) @@ -593,5 +594,5 @@ def update_lli_binary(app=None): return os.chmod(sys.argv[0], os.stat(sys.argv[0]).st_mode | 0o111) - logging.debug(f"Successfully updated {config.name_app}.") + logging.debug(f"Successfully updated {constants.APP_NAME}.") utils.restart_lli() diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index f6f4047c..e4b5bfa3 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -12,6 +12,7 @@ from . import config +from . import constants from . import msg from . import network @@ -711,7 +712,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 detail = ( "Please run the following command in a terminal, then restart " - f"{config.name_app}:\n{sudo_command}\n" + f"{constants.APP_NAME}:\n{sudo_command}\n" ) if config.DIALOG == "tk": if hasattr(app, 'root'): @@ -735,7 +736,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) 17, app.manualinstall_q, app.manualinstall_e, - f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{config.name_app}:\n{sudo_command}\n", # noqa: E501 + f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{constants.APP_NAME}:\n{sudo_command}\n", # noqa: E501 "User cancelled dependency installation.", # noqa: E501 message, options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 @@ -815,7 +816,7 @@ def check_libs(libraries, app=None): def install_winetricks( installdir, app=None, - version=config.WINETRICKS_VERSION, + version=constants.WINETRICKS_VERSION, ): msg.status(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 445031bd..1ebe2fa7 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -12,6 +12,7 @@ from . import config from . import control +from . import constants from . import installer from . import logos from . import msg @@ -31,10 +32,10 @@ def __init__(self, stdscr): super().__init__() self.stdscr = stdscr # if config.current_logos_version is not None: - self.title = f"Welcome to {config.name_app} {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 # else: - # self.title = f"Welcome to {config.name_app} ({config.LLI_CURRENT_VERSION})" # noqa: E501 + # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False @@ -239,7 +240,7 @@ def end(self, signal, frame): def update_main_window_contents(self): self.clear() - self.title = f"Welcome to {config.name_app} {config.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) @@ -443,7 +444,7 @@ def main_menu_select(self, choice): daemon_bool=True, app=self, ) - elif choice.startswith(f"Update {config.name_app}"): + elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release() elif choice == f"Run {config.FLPRODUCT}": self.reset_screen() @@ -535,7 +536,7 @@ def utilities_menu_select(self, choice): utils.change_logos_release_channel() self.update_main_window_contents() self.go_to_main_menu() - elif choice == f"Change {config.name_app} Release Channel": + elif choice == f"Change {constants.APP_NAME} Release Channel": self.reset_screen() utils.change_lli_release_channel() network.set_logoslinuxinstaller_latest_release_config() @@ -916,7 +917,7 @@ def set_tui_menu_options(self, dialog=False): status = config.logos_linux_installer_status error_message = config.logos_linux_installer_status_info.get(status) # noqa: E501 if status == 0: - labels.append(f"Update {config.name_app}") + labels.append(f"Update {constants.APP_NAME}") elif status == 1: # logging.debug("Logos Linux Installer is up-to-date.") pass @@ -1031,7 +1032,7 @@ def set_utilities_menu_options(self, dialog=False): if utils.file_exists(config.LOGOS_EXE): labels_utils_installed = [ "Change Logos Release Channel", - f"Change {config.name_app} Release Channel", + f"Change {constants.APP_NAME} Release Channel", # "Back Up Data", # "Restore Data" ] diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 97dce407..6dc18e49 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -20,6 +20,7 @@ from typing import List, Optional, Union from . import config +from . import constants from . import msg from . import network from . import system @@ -55,7 +56,7 @@ def set_default_config(): system.get_superuser_command() system.get_package_manager() if config.CONFIG_FILE is None: - config.CONFIG_FILE = config.DEFAULT_CONFIG_PATH + config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH config.PRESENT_WORKING_DIRECTORY = os.getcwd() config.MYDOWNLOADS = get_user_downloads_dir() os.makedirs(os.path.dirname(config.LOGOS_LOG), exist_ok=True) @@ -125,11 +126,11 @@ def update_config_file(config_file_path, key, value): def die_if_running(): def remove_pid_file(): - if os.path.exists(config.pid_file): - os.remove(config.pid_file) + if os.path.exists(constants.PID_FILE): + os.remove(constants.PID_FILE) - if os.path.isfile(config.pid_file): - with open(config.pid_file, 'r') as f: + if os.path.isfile(constants.PID_FILE): + with open(constants.PID_FILE, 'r') as f: pid = f.read().strip() message = f"The script is already running on PID {pid}. Should it be killed to allow this instance to run?" # noqa: E501 if config.DIALOG == "tk": @@ -150,7 +151,7 @@ def remove_pid_file(): os.kill(int(pid), signal.SIGKILL) atexit.register(remove_pid_file) - with open(config.pid_file, 'w') as f: + with open(constants.PID_FILE, 'w') as f: f.write(str(os.getpid())) @@ -165,8 +166,8 @@ def die(message): def restart_lli(): - logging.debug(f"Restarting {config.name_app}.") - pidfile = Path(config.pid_file) + logging.debug(f"Restarting {constants.APP_NAME}.") + pidfile = Path(constants.PID_FILE) if pidfile.is_file(): pidfile.unlink() os.execv(sys.executable, [sys.executable]) @@ -187,7 +188,7 @@ def clean_all(): logging.info("Cleaning all temp files…") os.system("rm -fr /tmp/LBS.*") os.system(f"rm -fr {config.WORKDIR}") - os.system(f"rm -f {config.PRESENT_WORKING_DIRECTORY}/wget-log*") + os.system(f"rm -f {os.getcwd()}/wget-log*") logging.info("done") @@ -442,7 +443,7 @@ def get_winetricks_options(): # Check if local winetricks version is up-to-date. cmd = ["winetricks", "--version"] local_winetricks_version = subprocess.check_output(cmd).split()[0] - if str(local_winetricks_version) != config.WINETRICKS_VERSION: #noqa: E501 + if str(local_winetricks_version) != constants.WINETRICKS_VERSION: #noqa: E501 winetricks_options.insert(0, local_winetricks_path) else: logging.info("Local winetricks is too old.") @@ -567,15 +568,15 @@ def get_latest_folder(folder_path): def install_premade_wine_bottle(srcdir, appdir): - msg.status(f"Extracting: '{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 + msg.status(f"Extracting: '{constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 shutil.unpack_archive( - f"{srcdir}/{config.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}", + f"{srcdir}/{constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}", appdir ) def compare_logos_linux_installer_version( - current=config.LLI_CURRENT_VERSION, + current=constants.LLI_CURRENT_VERSION, latest=config.LLI_LATEST_VERSION, ): # NOTE: The above params evaluate the variables when the module is @@ -848,13 +849,13 @@ def update_to_latest_lli_release(app=None): status, _ = compare_logos_linux_installer_version() if system.get_runmode() != 'binary': - logging.error(f"Can't update {config.name_app} when run as a script.") + logging.error(f"Can't update {constants.APP_NAME} when run as a script.") elif status == 0: network.update_lli_binary(app=app) elif status == 1: - logging.debug(f"{config.LLI_TITLE} is already at the latest version.") + logging.debug(f"{constants.APP_NAME} is already at the latest version.") elif status == 2: - logging.debug(f"{config.LLI_TITLE} is at a newer version than the latest.") # noqa: 501 + logging.debug(f"{constants.APP_NAME} is at a newer version than the latest.") # noqa: 501 def update_to_latest_recommended_appimage(): diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 68ae1ca1..343e6470 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -8,6 +8,7 @@ from typing import Optional from . import config +from . import constants from . import msg from . import network from . import system @@ -442,7 +443,7 @@ def enforce_icu_data_files(app=None): icu_latest_version = network.get_tag_name(json_data).lstrip('v') if icu_url is None: - logging.critical(f"Unable to set {config.name_app} release without URL.") # noqa: E501 + logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 return icu_filename = os.path.basename(icu_url).removesuffix(".tar.gz") # Append the version to the file name so it doesn't collide with previous versions diff --git a/pyproject.toml b/pyproject.toml index 766f400c..1082744c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ oudedetai = "ou_dedetai.main:main" [tool.setuptools.dynamic] readme = {file = ["README.md"], content-type = "text/plain"} -version = {attr = "ou_dedetai.config.LLI_CURRENT_VERSION"} +version = {attr = "ou_dedetai.constants.LLI_CURRENT_VERSION"} [tool.setuptools.packages.find] where = ["."] From a3244a78334dde79a4da5b530d47d0066a0b9759 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:32:12 -0800 Subject: [PATCH 003/137] refactor: move function from config to utils --- .gitignore | 3 ++- ou_dedetai/config.py | 3 --- ou_dedetai/control.py | 2 +- ou_dedetai/msg.py | 3 ++- ou_dedetai/utils.py | 4 ++++ ou_dedetai/wine.py | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6c1324ac..a76dd021 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ env/ venv/ .venv/ .idea/ -*.egg-info \ No newline at end of file +*.egg-info +.vscode/ \ No newline at end of file diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 3f1e0f58..a01ac496 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -179,6 +179,3 @@ def get_env_config(): logging.info(f"Setting '{var}' to '{val}'") globals()[var] = val - -def get_timestamp(): - return datetime.today().strftime('%Y-%m-%dT%H%M%S') diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 13cf9159..f210a932 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -154,7 +154,7 @@ def backup_and_restore(mode='backup', app=None): if dst.is_dir(): shutil.rmtree(dst) else: # backup mode - timestamp = config.get_timestamp().replace('-', '') + timestamp = utils.get_timestamp().replace('-', '') current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 dst_dir = backup_dir / current_backup_name logging.debug(f"Backup directory path: \"{dst_dir}\".") diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 4c87d51f..042c2cad 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -10,6 +10,7 @@ from . import config from . import constants +from . import utils from .gui import ask_question from .gui import show_error @@ -329,7 +330,7 @@ def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] - timestamp = config.get_timestamp() + timestamp = utils.get_timestamp() """Handles status messages for both TUI and GUI.""" if app is not None: if config.DIALOG == 'tk': diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 6dc18e49..ae3c90ea 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -1,4 +1,5 @@ import atexit +from datetime import datetime import glob import inspect import json @@ -1012,3 +1013,6 @@ def stopwatch(start_time=None, interval=10.0): return True, last_log_time else: return False, start_time + +def get_timestamp(): + return datetime.today().strftime('%Y-%m-%dT%H%M%S') diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 343e6470..ca4535b2 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -327,7 +327,7 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): cmd = f"subprocess cmd: '{' '.join(command)}'" with open(config.wine_log, 'a') as wine_log: - print(f"{config.get_timestamp()}: {cmd}", file=wine_log) + print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) logging.debug(cmd) try: with open(config.wine_log, 'a') as wine_log: From 4676033a5be86cdee914a398f6e0812cc714e9e7 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:58:00 -0800 Subject: [PATCH 004/137] feat(wip): move the install prompts into the abstract class ignore the todos for now --- ou_dedetai/app.py | 87 +++++++++++++++++--- ou_dedetai/gui_app.py | 72 +++-------------- ou_dedetai/installer.py | 171 +++++++--------------------------------- ou_dedetai/network.py | 14 +--- ou_dedetai/tui_app.py | 162 +------------------------------------ ou_dedetai/utils.py | 1 + 6 files changed, 119 insertions(+), 388 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 49418d6e..5f250ded 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,7 +1,9 @@ import abc +from pathlib import Path from typing import Optional -from ou_dedetai import config +from ou_dedetai import config, network, utils +from ou_dedetai.installer import update_install_feedback class App(abc.ABC): @@ -38,9 +40,8 @@ def _ask(self, question: str, options: list[str] = None) -> Optional[str]: """ raise NotImplementedError() - # XXX: should this be changed to config updates more generally? - def _hook_product_update(self, product: Optional[str]): - """A hook for any changes the individual apps want to do when a platform changes""" + def _hook(self): + """A hook for any changes the individual apps want to do when the config changes""" pass # XXX: unused at present @@ -50,17 +51,77 @@ def update_progress(self, message: str, percent: Optional[int] = None): pass class Config: + """Set of configuration values. + + If the user hasn't selected a particular value yet, they will be prompted in their UI.""" + def __init__(self, app: App) -> None: self.app = app + + def _ask_if_not_found(self, config_key: str, question: str, options: list[str], dependent_config_keys: Optional[list[str]] = None) -> str: + # XXX: should this also update the feedback? + if not getattr(config, config_key): + if dependent_config_keys is not None: + for dependent_config_key in dependent_config_keys: + setattr(config, dependent_config_key, None) + setattr(config, config_key, self.app.ask(question, options)) + self.app._hook() + return getattr(config, config_key) @property def faithlife_product(self) -> str: - """Wrapper function that ensures that ensures the product is set - - if it's not then the user is prompted to choose one.""" - if not config.FLPRODUCT: - question = "Choose which FaithLife product the script should install: " # noqa: E501 - options = ["Logos", "Verbum"] - config.FLPRODUCT = self.app.ask(question, options) - self.app._hook_product_update(config.FLPRODUCT) - return config.FLPRODUCT \ No newline at end of file + question = "Choose which FaithLife product the script should install: " # noqa: E501 + options = ["Logos", "Verbum"] + return self._ask_if_not_found("FLPRODUCT", question, options, ["TARGETVERSION", "TARGET_RELEASE_VERSION"]) + + @property + def faithlife_product_version(self) -> str: + question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 + options = ["10", "9"] + return self._ask_if_not_found("FLPRODUCT", question, options, ["TARGET_RELEASE_VERSION"]) + + @property + def faithlife_product_release(self) -> str: + question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 + options = network.get_logos_releases(None) + return self._ask_if_not_found("TARGET_RELEASE_VERSION", question, options) + + # FIXME: should this just ensure that winetricks is installed and in the right location? That isn't really a matter for a config... + @property + def winetricks_binary(self) -> str: + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux.", # noqa: E501 + options = utils.get_winetricks_options() + return self._ask_if_not_found("WINETRICKSBIN", question, options) + + @property + def install_dir(self) -> str: + default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 + question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 + options = [default] + # XXX: This also needs to allow the user to put in their own custom path + output = self._ask_if_not_found("INSTALLDIR", question, options) + # XXX: Why is this stored separately if it's always at this relative path? Shouldn't this relative string be a constant? + config.APPDIR_BINDIR = f"{output}/data/bin" + return output + + @property + def wine_binary(self) -> str: + if not config.WINE_EXE: + question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 + network.set_recommended_appimage_config() + options = utils.get_wine_options( + utils.find_appimage_files(config.TARGET_RELEASE_VERSION), + utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) + ) + + choice = self.app.ask(question, options) + + # Make the possibly relative path absolute before storing + config.WINE_EXE = utils.get_relative_path( + utils.get_config_var(choice), + self.install_dir + ) + self.app._hook() + return config.WINE_EXE + + \ No newline at end of file diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 25ad511f..3ebdfa95 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -246,29 +246,17 @@ def __init__(self, new_win, root: Root, **kwargs): # Run commands. self.get_winetricks_options() - self.grey_out_others_if_faithlife_product_is_not_selected() - - def grey_out_others_if_faithlife_product_is_not_selected(self): - if not config.FLPRODUCT: - # Disable all input widgets after Version. - widgets = [ - self.gui.version_dropdown, - self.gui.release_dropdown, - self.gui.release_check_button, - self.gui.wine_dropdown, - self.gui.wine_check_button, - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - if not self.gui.productvar.get(): - self.gui.productvar.set(self.gui.product_dropdown['values'][0]) - # This is started in a new thread because it blocks and was called form the constructor - utils.start_thread(self.set_product) - - def _hook_product_update(self, product: Optional[str]): - if product is not None: - self.gui.productvar.set(product) - self.gui.product_dropdown.set(product) + + def _hook(self): + """Update the GUI to reflect changes in the configuration if they were prompted separately""" + # The configuration enforces dependencies, if FLPRODUCT is unset, so will it's dependents (TARGETVERSION and TARGET_RELEASE_VERSION) + # XXX: test this hook. Interesting thing is, this may never be called in production, as it's only called (presently) when the separate prompt returns + # Returns either from config or the dropdown + self.gui.productvar.set(config.FLPRODUCT or self.gui.product_dropdown['values'][0]) + self.gui.versionvar.set(config.TARGETVERSION or self.gui.version_dropdown['values'][-1]) + self.gui.releasevar.set(config.TARGET_RELEASE_VERSION or self.gui.release_dropdown['values'][0]) + # Returns either WINE_EXE if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' + self.gui.winevar.set(config.WINE_EXE or next(iter(self.gui.wine_dropdown['values']), '')) def start_ensure_config(self): # Ensure progress counter is reset. @@ -313,43 +301,7 @@ def todo(self, evt=None, task=None): else: return self.set_input_widgets_state('enabled') - if task == 'TARGETVERSION': - # Disable all input widgets after Version. - widgets = [ - self.gui.release_dropdown, - self.gui.release_check_button, - self.gui.wine_dropdown, - self.gui.wine_check_button, - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - if not self.gui.versionvar.get(): - self.gui.versionvar.set(self.gui.version_dropdown['values'][1]) - self.set_version() - elif task == 'TARGET_RELEASE_VERSION': - # Disable all input widgets after Release. - widgets = [ - self.gui.wine_dropdown, - self.gui.wine_check_button, - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - self.start_releases_check() - elif task == 'WINE_EXE': - # Disable all input widgets after Wine Exe. - widgets = [ - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) - elif task == 'WINETRICKSBIN': - # Disable all input widgets after Winetricks. - widgets = [ - self.gui.okay_button, - ] - self.set_input_widgets_state('disabled', widgets=widgets) - self.set_winetricks() - elif task == 'INSTALL': + if task == 'INSTALL': self.gui.statusvar.set('Ready to install!') self.gui.progressvar.set(0) elif task == 'INSTALLING': diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index beff724c..70f93fdd 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -15,6 +15,7 @@ from . import wine +# XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses def ensure_product_choice(app: App): config.INSTALL_STEPS_COUNT += 1 update_install_feedback("Choose product…", app=app) @@ -36,108 +37,43 @@ def ensure_product_choice(app: App): logging.debug(f"> {config.VERBUM_PATH=}") -def ensure_version_choice(app=None): +# XXX: we don't need this install step anymore +def ensure_version_choice(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_product_choice(app=app) config.INSTALL_STEP += 1 update_install_feedback("Choose version…", app=app) logging.debug('- config.TARGETVERSION') - if not config.TARGETVERSION: - if config.DIALOG == 'cli': - app.input_q.put( - ( - f"Which version of {config.FLPRODUCT} should the script install?: ", # noqa: E501 - ["10", "9", "Exit"] - ) - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.TARGETVERSION = app.choice_q.get() - else: - utils.send_task(app, 'TARGETVERSION') - if config.DIALOG == 'curses': - app.version_e.wait() - config.TARGETVERSION = app.version_q.get() - else: - if config.DIALOG == 'curses' and app: - app.set_version(config.TARGETVERSION) - + # Accessing this ensures it's set + app.conf.faithlife_product_version logging.debug(f"> {config.TARGETVERSION=}") -def ensure_release_choice(app=None): +# XXX: no longer needed +def ensure_release_choice(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_version_choice(app=app) config.INSTALL_STEP += 1 update_install_feedback("Choose product release…", app=app) logging.debug('- config.TARGET_RELEASE_VERSION') - - if not config.TARGET_RELEASE_VERSION: - if config.DIALOG == 'cli': - utils.start_thread( - network.get_logos_releases, - daemon_bool=True, - app=app - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.TARGET_RELEASE_VERSION = app.choice_q.get() - else: - utils.send_task(app, 'TARGET_RELEASE_VERSION') - if config.DIALOG == 'curses': - app.release_e.wait() - config.TARGET_RELEASE_VERSION = app.release_q.get() - logging.debug(f"{config.TARGET_RELEASE_VERSION=}") - else: - if config.DIALOG == 'curses' and app: - app.set_release(config.TARGET_RELEASE_VERSION) - + # accessing this sets the config + app.conf.faithlife_product_release logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") -def ensure_install_dir_choice(app=None): +def ensure_install_dir_choice(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_release_choice(app=app) config.INSTALL_STEP += 1 - update_install_feedback( - "Choose installation folder…", - app=app - ) + update_install_feedback("Choose installation folder…", app=app) logging.debug('- config.INSTALLDIR') - - default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - if not config.INSTALLDIR: - if config.DIALOG == 'cli': - default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - question = f"Where should {config.FLPRODUCT} files be installed to?: " # noqa: E501 - app.input_q.put( - ( - question, - [default, "Type your own custom path", "Exit"] - ) - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.INSTALLDIR = app.choice_q.get() - elif config.DIALOG == 'tk': - config.INSTALLDIR = default - elif config.DIALOG == 'curses': - utils.send_task(app, 'INSTALLDIR') - app.installdir_e.wait() - config.INSTALLDIR = app.installdir_q.get() - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - else: - if config.DIALOG == 'curses' and app: - app.set_installdir(config.INSTALLDIR) - + # Accessing this sets install_dir and bin_dir + app.conf.install_dir logging.debug(f"> {config.INSTALLDIR=}") logging.debug(f"> {config.APPDIR_BINDIR=}") -def ensure_wine_choice(app=None): +def ensure_wine_choice(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_install_dir_choice(app=app) config.INSTALL_STEP += 1 @@ -149,39 +85,8 @@ def ensure_wine_choice(app=None): logging.debug('- config.WINE_EXE') logging.debug('- config.WINEBIN_CODE') - if utils.get_wine_exe_path() is None: - network.set_recommended_appimage_config() - if config.DIALOG == 'cli': - options = utils.get_wine_options( - utils.find_appimage_files(config.TARGET_RELEASE_VERSION), - utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) - ) - app.input_q.put( - ( - f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?: ", # noqa: E501 - options - ) - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - config.WINE_EXE = utils.get_relative_path( - utils.get_config_var(app.choice_q.get()), - config.INSTALLDIR - ) - else: - utils.send_task(app, 'WINE_EXE') - if config.DIALOG == 'curses': - app.wine_e.wait() - config.WINE_EXE = app.wines_q.get() - # GUI uses app.wines_q for list of available, then app.wine_q - # for the user's choice of specific binary. - elif config.DIALOG == 'tk': - config.WINE_EXE = app.wine_q.get() - - else: - if config.DIALOG == 'curses' and app: - app.set_wine(utils.get_wine_exe_path()) + # This sets config.WINE_EXE + app.conf.wine_binary # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. m = f"Preparing to process WINE_EXE. Currently set to: {utils.get_wine_exe_path()}." # noqa: E501 @@ -199,44 +104,19 @@ def ensure_wine_choice(app=None): logging.debug(f"> {utils.get_wine_exe_path()=}") -def ensure_winetricks_choice(app=None): +# XXX: this isn't needed anymore +def ensure_winetricks_choice(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_wine_choice(app=app) config.INSTALL_STEP += 1 update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') - - if config.WINETRICKSBIN is None: - # Check if local winetricks version available; else, download it. - config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" - - winetricks_options = utils.get_winetricks_options() - - if config.DIALOG == 'cli': - app.input_q.put( - ( - f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux.", # noqa: E501 - winetricks_options - ) - ) - app.input_event.set() - app.choice_event.wait() - app.choice_event.clear() - winetricksbin = app.choice_q.get() - else: - utils.send_task(app, 'WINETRICKSBIN') - if config.DIALOG == 'curses': - app.tricksbin_e.wait() - winetricksbin = app.tricksbin_q.get() - - if not winetricksbin.startswith('Download'): - config.WINETRICKSBIN = winetricksbin - else: - config.WINETRICKSBIN = winetricks_options[0] - + # Accessing the winetricks_binary variable will do this. + app.conf.winetricks_binary logging.debug(f"> {config.WINETRICKSBIN=}") +# XXX: huh? What does this do? def ensure_install_fonts_choice(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_winetricks_choice(app=app) @@ -247,6 +127,7 @@ def ensure_install_fonts_choice(app=None): logging.debug(f"> {config.SKIP_FONTS=}") +# XXX: huh? What does this do? def ensure_check_sys_deps_choice(app=None): config.INSTALL_STEPS_COUNT += 1 ensure_install_fonts_choice(app=app) @@ -271,6 +152,9 @@ def ensure_installation_config(app=None): logging.debug('- config.LOGOS64_MSI') logging.debug('- config.LOGOS64_URL') + # XXX: This doesn't prompt the user for anything, all values are derived from other user-supplied values + # these "config" values probably don't need to be stored independently of the values they're derived from + # Set icon variables. app_dir = Path(__file__).parent flproducti = get_flproducti_name(config.FLPRODUCT) @@ -288,6 +172,7 @@ def ensure_installation_config(app=None): logging.debug(f"> {config.LOGOS64_MSI=}") logging.debug(f"> {config.LOGOS64_URL=}") + # XXX: What does the install task do? Shouldn't that logic be here? if config.DIALOG in ['curses', 'dialog', 'tk']: utils.send_task(app, 'INSTALL') else: @@ -323,6 +208,7 @@ def ensure_install_dirs(app=None): logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") logging.debug(f"> {config.WINEPREFIX=}") + # XXX: what does this task do? Shouldn't that logic be here? if config.DIALOG in ['curses', 'dialog', 'tk']: utils.send_task(app, 'INSTALLING') @@ -602,6 +488,8 @@ def ensure_config_file(app=None): config.INSTALL_STEP += 1 update_install_feedback("Ensuring config file is up-to-date…", app=app) + # XXX: Why the platform specific logic? + if not Path(config.CONFIG_FILE).is_file(): logging.info(f"No config file at {config.CONFIG_FILE}") create_config_file() @@ -670,6 +558,7 @@ def ensure_launcher_shortcuts(app=None): app=app ) + # XXX: why only for this dialog? if config.DIALOG == 'cli': # Signal CLI.user_input_processor to stop. app.input_q.put(None) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 40fd9546..eaa11aa5 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -490,8 +490,7 @@ def get_recommended_appimage(): config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, config.APPDIR_BINDIR) - -def get_logos_releases(app=None): +def get_logos_releases(app=None) -> list[str]: # Use already-downloaded list if requested again. downloaded_releases = None if config.TARGETVERSION == '9' and config.LOGOS9_RELEASES: @@ -550,17 +549,6 @@ def get_logos_releases(app=None): if config.DIALOG == 'tk': app.releases_q.put(filtered_releases) app.root.event_generate(app.release_evt) - elif config.DIALOG == 'curses': - app.releases_q.put(filtered_releases) - app.releases_e.set() - elif config.DIALOG == 'cli': - app.input_q.put( - ( - f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?: ", # noqa: E501 - filtered_releases - ) - ) - app.input_event.set() return filtered_releases diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 1ebe2fa7..7982f0d3 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -63,22 +63,10 @@ def __init__(self, stdscr): self.switch_q = Queue() # Install and Options - self.version_q = Queue() - self.version_e = threading.Event() - self.releases_q = Queue() - self.releases_e = threading.Event() - self.release_q = Queue() - self.release_e = threading.Event() self.manualinstall_q = Queue() self.manualinstall_e = threading.Event() self.installdeps_q = Queue() self.installdeps_e = threading.Event() - self.installdir_q = Queue() - self.installdir_e = threading.Event() - self.wines_q = Queue() - self.wine_e = threading.Event() - self.tricksbin_q = Queue() - self.tricksbin_e = threading.Event() self.deps_q = Queue() self.deps_e = threading.Event() self.finished_q = Queue() @@ -357,17 +345,7 @@ def run(self): signal.signal(signal.SIGINT, self.end) def task_processor(self, evt=None, task=None): - if task == 'TARGETVERSION': - utils.start_thread(self.get_version, config.use_python_dialog) - elif task == 'TARGET_RELEASE_VERSION': - utils.start_thread(self.get_release, config.use_python_dialog) - elif task == 'INSTALLDIR': - utils.start_thread(self.get_installdir, config.use_python_dialog) - elif task == 'WINE_EXE': - utils.start_thread(self.get_wine, config.use_python_dialog) - elif task == 'WINETRICKSBIN': - utils.start_thread(self.get_winetricksbin, config.use_python_dialog) - elif task == 'INSTALL' or task == 'INSTALLING': + if task == 'INSTALL' or task == 'INSTALLING': utils.start_thread(self.get_waiting, config.use_python_dialog) elif task == 'INSTALLING_PW': utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) @@ -383,11 +361,6 @@ def choice_processor(self, stdscr, screen_id, choice): 0: self.main_menu_select, 1: self.custom_appimage_select, 2: self.handle_ask_response, - 3: self.version_select, - 4: self.release_select, - 5: self.installdir_select, - 6: self.wine_select, - 7: self.winetricksbin_select, 8: self.waiting, 9: self.config_update_select, 10: self.waiting_releases, @@ -590,50 +563,6 @@ def custom_appimage_select(self, choice): self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) self.appimage_e.set() - def version_select(self, choice): - if choice: - if "10" in choice: - config.TARGETVERSION = "10" - elif "9" in choice: - config.TARGETVERSION = "9" - self.menu_screen.choice = "Processing" - self.version_q.put(config.TARGETVERSION) - self.version_e.set() - - def release_select(self, choice): - if choice: - config.TARGET_RELEASE_VERSION = choice - self.menu_screen.choice = "Processing" - self.release_q.put(config.TARGET_RELEASE_VERSION) - self.release_e.set() - - def installdir_select(self, choice): - if choice: - config.INSTALLDIR = choice - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - self.menu_screen.choice = "Processing" - self.installdir_q.put(config.INSTALLDIR) - self.installdir_e.set() - - def wine_select(self, choice): - config.WINE_EXE = choice - if choice: - self.menu_screen.choice = "Processing" - self.wines_q.put(config.WINE_EXE) - self.wine_e.set() - - def winetricksbin_select(self, choice): - winetricks_options = utils.get_winetricks_options() - if choice.startswith("Download"): - self.menu_screen.choice = "Processing" - self.tricksbin_q.put("Download") - self.tricksbin_e.set() - else: - self.menu_screen.choice = "Processing" - config.WINETRICKSBIN = winetricks_options[0] - self.tricksbin_q.put(config.WINETRICKSBIN) - self.tricksbin_e.set() - def waiting(self, choice): pass @@ -731,95 +660,6 @@ def handle_ask_response(self, choice: Optional[str]): self.ask_answer_event.set() self.switch_screen(config.use_python_dialog) - def get_version(self, dialog): - question = f"Which version of {self.conf.faithlife_product} should the script install?" # noqa: E501 - labels = ["10", "9", "Return to Main Menu"] - options = self.which_dialog_options(labels, dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(3, self.version_q, self.version_e, question, options, dialog=dialog)) - - def set_version(self, choice): - if "10" in choice: - config.TARGETVERSION = "10" - elif "9" in choice: - config.TARGETVERSION = "9" - self.menu_screen.choice = "Processing" - self.version_q.put(config.TARGETVERSION) - self.version_e.set() - - def get_release(self, dialog): - labels = [] - self.screen_q.put(self.stack_text(10, self.version_q, self.version_e, "Waiting to acquire Logos versions…", wait=True, dialog=dialog)) - self.version_e.wait() - question = f"Which version of {config.FLPRODUCT} {config.TARGETVERSION} do you want to install?" # noqa: E501 - utils.start_thread(network.get_logos_releases, daemon_bool=True, app=self) - self.releases_e.wait() - - labels = self.releases_q.get() - - if labels is None: - msg.logos_error("Failed to fetch TARGET_RELEASE_VERSION.") - labels.append("Return to Main Menu") - options = self.which_dialog_options(labels, dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(4, self.release_q, self.release_e, question, options, dialog=dialog)) - - def set_release(self, choice): - config.TARGET_RELEASE_VERSION = choice - self.menu_screen.choice = "Processing" - self.release_q.put(config.TARGET_RELEASE_VERSION) - self.release_e.set() - - def get_installdir(self, dialog): - self.release_e.wait() - default = f"{str(Path.home())}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - question = f"Where should {config.FLPRODUCT} files be installed to? [{default}]: " # noqa: E501 - self.screen_q.put(self.stack_input(5, self.installdir_q, self.installdir_e, question, default, dialog=dialog)) - - def set_installdir(self, choice): - config.INSTALLDIR = choice - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - self.menu_screen.choice = "Processing" - self.installdir_q.put(config.INSTALLDIR) - self.installdir_e.set() - - def get_wine(self, dialog): - self.installdir_e.wait() - self.screen_q.put(self.stack_text(10, self.wines_q, self.wine_e, "Waiting to acquire available Wine binaries…", wait=True, dialog=dialog)) - question = f"Which Wine AppImage or binary should the script use to install {config.FLPRODUCT} v{config.TARGET_RELEASE_VERSION} in {config.INSTALLDIR}?" # noqa: E501 - labels = utils.get_wine_options( - utils.find_appimage_files(config.TARGET_RELEASE_VERSION), - utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) - ) - labels.append("Return to Main Menu") - max_length = max(len(label) for label in labels) - max_length += len(str(len(labels))) + 10 - options = self.which_dialog_options(labels, dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(6, self.wines_q, self.wine_e, question, options, width=max_length, dialog=dialog)) - - def set_wine(self, choice): - self.wines_q.put(utils.get_relative_path(utils.get_config_var(choice), config.INSTALLDIR)) - self.menu_screen.choice = "Processing" - self.wine_e.set() - - def get_winetricksbin(self, dialog): - self.wine_e.wait() - winetricks_options = utils.get_winetricks_options() - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {config.FLPRODUCT} requires on Linux." # noqa: E501 - options = self.which_dialog_options(winetricks_options, dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(7, self.tricksbin_q, self.tricksbin_e, question, options, dialog=dialog)) - - def set_winetricksbin(self, choice): - if choice.startswith("Download"): - self.tricksbin_q.put("Download") - else: - winetricks_options = utils.get_winetricks_options() - self.tricksbin_q.put(winetricks_options[0]) - self.menu_screen.choice = "Processing" - self.tricksbin_e.set() - def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index ae3c90ea..3191a8f6 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -982,6 +982,7 @@ def get_config_var(var): return None +# XXX: this function is never called with an argument def get_wine_exe_path(path=None): if path is not None: path = get_relative_path(get_config_var(path), config.INSTALLDIR) From 923cbbc7b43062e7743fa1906dac8dbbe313796c Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:58:48 -0800 Subject: [PATCH 005/137] refactor: function never called with an argument --- ou_dedetai/utils.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 3191a8f6..0324021a 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -982,24 +982,17 @@ def get_config_var(var): return None -# XXX: this function is never called with an argument -def get_wine_exe_path(path=None): - if path is not None: - path = get_relative_path(get_config_var(path), config.INSTALLDIR) +def get_wine_exe_path(): + if config.WINE_EXE is not None: + path = get_relative_path( + get_config_var(config.WINE_EXE), + config.INSTALLDIR + ) wine_exe_path = Path(create_dynamic_path(path, config.INSTALLDIR)) logging.debug(f"{wine_exe_path=}") return wine_exe_path else: - if config.WINE_EXE is not None: - path = get_relative_path( - get_config_var(config.WINE_EXE), - config.INSTALLDIR - ) - wine_exe_path = Path(create_dynamic_path(path, config.INSTALLDIR)) - logging.debug(f"{wine_exe_path=}") - return wine_exe_path - else: - return None + return None def stopwatch(start_time=None, interval=10.0): From c97b5f5a84bf027769efb523ff8d8b2fb71d3947 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:43:29 -0800 Subject: [PATCH 006/137] fix(wip): add prompting and started new storage implementation --- ou_dedetai/app.py | 181 +++++++++++++++++++++++++++++++++++++++- ou_dedetai/cli.py | 1 + ou_dedetai/config.py | 7 +- ou_dedetai/constants.py | 1 + ou_dedetai/control.py | 8 +- ou_dedetai/gui_app.py | 14 +++- ou_dedetai/tui_app.py | 26 +++++- ou_dedetai/utils.py | 1 - 8 files changed, 226 insertions(+), 13 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 5f250ded..fad8b332 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,10 +1,18 @@ import abc +from dataclasses import dataclass +from datetime import datetime +import enum +import json +import logging +import os from pathlib import Path from typing import Optional -from ou_dedetai import config, network, utils -from ou_dedetai.installer import update_install_feedback +from ou_dedetai import config, network, utils, constants +# Strings for choosing a follow up file or directory +PROMPT_OPTION_DIRECTORY = "Choose Directory" +PROMPT_OPTION_FILE = "Choose File" class App(abc.ABC): def __init__(self, **kwargs) -> None: @@ -34,6 +42,9 @@ def ask(self, question: str, options: list[str]) -> str: def _ask(self, question: str, options: list[str] = None) -> Optional[str]: """Implementation for asking a question pre-front end + Options may include ability to prompt for an additional value. + Implementations MUST handle the follow up prompt before returning + If you would otherwise return None, consider shutting down cleanly, the calling function will exit the process with an error code of one if this function returns None @@ -50,13 +61,174 @@ def update_progress(self, message: str, percent: Optional[int] = None): """Updates the progress of the current operation""" pass + +@dataclass +class LegacyConfiguration: + """Configuration and it's keys from before the user configuration class existed. + + Useful for one directional compatibility""" + FLPRODUCT: Optional[str] = None + TARGETVERSION: Optional[str] = None + TARGET_RELEASE_VERSION: Optional[str] = None + current_logos_version: Optional[str] = None + curses_colors: Optional[str] = None + INSTALLDIR: Optional[str] = None + WINETRICKSBIN: Optional[str] = None + WINEBIN_CODE: Optional[str] = None + WINE_EXE: Optional[str] = None + WINECMD_ENCODING: Optional[str] = None + LOGS: Optional[str] = None + BACKUPDIR: Optional[str] = None + LAST_UPDATED: Optional[str] = None + RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None + LLI_LATEST_VERSION: Optional[str] = None + logos_release_channel: Optional[str] = None + lli_release_channel: Optional[str] = None + + @classmethod + def config_file_path() -> str: + return os.getenv(constants.CONFIG_FILE_ENV) or constants.DEFAULT_CONFIG_PATH + + @classmethod + def from_file_and_env() -> "LegacyConfiguration": + config_file_path = LegacyConfiguration.config_file_path() + config_dict = LegacyConfiguration() + if config_file_path.endswith('.json'): + try: + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) + + for key, value in cfg.items(): + config_dict[key] = value + except TypeError as e: + logging.error("Error opening Config file.") + logging.error(e) + raise e + except FileNotFoundError: + logging.info(f"No config file not found at {config_file_path}") + except json.JSONDecodeError as e: + logging.error("Config file could not be read.") + logging.error(e) + raise e + elif config_file_path.endswith('.conf'): + # Legacy config from bash script. + logging.info("Reading from legacy config file.") + with open(config_file_path, 'r') as config_file: + for line in config_file: + line = line.strip() + if len(line) == 0: # skip blank lines + continue + if line[0] == '#': # skip commented lines + continue + parts = line.split('=') + if len(parts) == 2: + value = parts[1].strip('"').strip("'") # remove quotes + vparts = value.split('#') # get rid of potential comment + if len(vparts) > 1: + value = vparts[0].strip().strip('"').strip("'") + config_dict[parts[0]] = value + + # Now update from ENV + for var in LegacyConfiguration().__dict__.keys(): + if os.getenv(var) is not None: + config_dict[var] = os.getenv(var) + + return config_dict + + +@dataclass +class UserConfiguration: + """This is the class that actually stores the values. + + Normally shouldn't be used directly, as it's types may be None + + Easy reading to/from JSON and supports legacy keys""" + faithlife_product: Optional[str] = None + faithlife_product_version: Optional[str] = None + faithlife_product_release: Optional[str] = None + install_dir: Optional[Path] = None + winetricks_binary: Optional[Path] = None + wine_binary: Optional[Path] = None + # This is where to search for wine + wine_binary_code: Optional[str] = None + backup_directory: Optional[Path] = None + + # Color to use in curses. Either "Logos", "Light", or "Dark" + curses_colors: str = "Logos" + # Faithlife's release channel. Either "stable" or "beta" + faithlife_product_release_channel: str = "stable" + # The Installer's release channel. Either "stable" or "beta" + installer_release_channel: str = "stable" + + @classmethod + def read_from_file_and_env() -> "UserConfiguration": + # First read in the legacy configuration + new_config = UserConfiguration.from_legacy(LegacyConfiguration.from_file_and_env()) + # Then read the file again this time looking for the new keys + config_file_path = LegacyConfiguration.config_file_path() + + new_keys = UserConfiguration().__dict__.keys() + + if config_file_path.endswith('.json'): + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) + + for key, value in cfg.items(): + if key in new_keys: + new_config[key] = value + else: + logging.info("Not reading new values from non-json config") + + return new_config + + @classmethod + def from_legacy(legacy: LegacyConfiguration) -> "UserConfiguration": + return UserConfiguration( + faithlife_product=legacy.FLPRODUCT, + backup_directory=legacy.BACKUPDIR, + curses_colors=legacy.curses_colors, + faithlife_product_release=legacy.TARGET_RELEASE_VERSION, + faithlife_product_release_channel=legacy.logos_release_channel, + faithlife_product_version=legacy.TARGETVERSION, + install_dir=legacy.INSTALLDIR, + installer_release_channel=legacy.lli_release_channel, + wine_binary=legacy.WINE_EXE, + wine_binary_code=legacy.WINEBIN_CODE, + winetricks_binary=legacy.WINETRICKSBIN + ) + + def write_config(self): + config_file_path = LegacyConfiguration.config_file_path() + with open(config_file_path, 'w') as config_file: + json.dump(self.__dict__, config_file) + +# XXX: what to do with these? +# Used to be called current_logos_version, but actually could be used in Verbium too. +installed_faithlife_product_release: Optional[str] = None +# Whether or not the installed faithlife product is configured for additional logging. +# Used to be called "LOGS" +installed_faithlife_logging: Optional[bool] = None +# Text encoding of the wine command. This calue can be retrieved from the system +winecmd_encoding: Optional[str] = None +last_updated: Optional[datetime] = None +recommended_wine_url: Optional[str] = None +latest_installer_version: Optional[str] = None + + class Config: """Set of configuration values. If the user hasn't selected a particular value yet, they will be prompted in their UI.""" + # Storage for the keys + user: UserConfiguration + def __init__(self, app: App) -> None: self.app = app + self.user = UserConfiguration.read_from_file_and_env() + logging.debug("Current persistent config:") + for k, v in self.user.__dict__.items(): + logging.debug(f"{k}: {v}") def _ask_if_not_found(self, config_key: str, question: str, options: list[str], dependent_config_keys: Optional[list[str]] = None) -> str: # XXX: should this also update the feedback? @@ -66,6 +238,8 @@ def _ask_if_not_found(self, config_key: str, question: str, options: list[str], setattr(config, dependent_config_key, None) setattr(config, config_key, self.app.ask(question, options)) self.app._hook() + # And update the file so it's always up to date. + self.user.write_config() return getattr(config, config_key) @property @@ -97,8 +271,7 @@ def winetricks_binary(self) -> str: def install_dir(self) -> str: default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 - options = [default] - # XXX: This also needs to allow the user to put in their own custom path + options = [default, PROMPT_OPTION_DIRECTORY] output = self._ask_if_not_found("INSTALLDIR", question, options) # XXX: Why is this stored separately if it's always at this relative path? Shouldn't this relative string be a constant? config.APPDIR_BINDIR = f"{output}/data/bin" diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 033517e0..56aba9da 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -104,6 +104,7 @@ def _ask(self, question: str, options: list[str]) -> str: self.input_event.set() self.choice_event.wait() self.choice_event.clear() + # XXX: This is always a freeform input, perhaps we should have some sort of validation? return self.choice_q.get() def user_input_processor(self, evt=None): diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index a01ac496..dd793cc2 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -2,8 +2,8 @@ import logging import os import tempfile -from datetime import datetime from typing import Optional +from warnings import deprecated from . import constants @@ -117,6 +117,8 @@ check_if_indexing = None +# XXX: remove this +@deprecated def get_config_file_dict(config_file_path): config_dict = {} if config_file_path.endswith('.json'): @@ -158,6 +160,7 @@ def get_config_file_dict(config_file_path): return config_dict +# XXX: remove this def set_config_env(config_file_path): config_dict = get_config_file_dict(config_file_path) if config_dict is None: @@ -171,7 +174,7 @@ def set_config_env(config_file_path): global APPDIR_BINDIR APPDIR_BINDIR = f"{installdir}/data/bin" - +# XXX: remove this def get_env_config(): for var in globals().keys(): val = os.getenv(var) diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index b8084e4f..4a785a1c 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -9,6 +9,7 @@ # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 +CONFIG_FILE_ENV = "CONFIG_PATH" LEGACY_CONFIG_FILES = [ os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index f210a932..d1326c18 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -12,6 +12,8 @@ import time from pathlib import Path +from ou_dedetai.app import App + from . import config from . import constants from . import msg @@ -31,16 +33,16 @@ def delete_log_file_contents(): f.write('') -def backup(app=None): +def backup(app: App): backup_and_restore(mode='backup', app=app) -def restore(app=None): +def restore(app: App): backup_and_restore(mode='restore', app=app) # FIXME: consider moving this into it's own file/module. -def backup_and_restore(mode='backup', app=None): +def backup_and_restore(mode: str, app: App): data_dirs = ['Data', 'Documents', 'Users'] # Ensure BACKUPDIR is defined. if config.BACKUPDIR is None: diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 3ebdfa95..8d34aee8 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -15,7 +15,7 @@ from tkinter.ttk import Style from typing import Optional -from ou_dedetai.app import App +from ou_dedetai.app import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE, App from . import config from . import constants @@ -54,6 +54,18 @@ def spawn_dialog(): if answer is None: self.root_to_destory_on_none.destroy() return None + elif answer == PROMPT_OPTION_DIRECTORY: + answer = fd.askdirectory( + parent=self.root_to_destory_on_none, + title=question, + initialdir=Path().home(), + ) + elif answer == PROMPT_OPTION_FILE: + answer = fd.askopenfilename( + parent=self.root_to_destory_on_none, + title=question, + initialdir=Path().home(), + ) return answer class Root(Tk): diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 7982f0d3..988382ae 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -8,7 +8,7 @@ from queue import Queue from typing import Optional -from ou_dedetai.app import App +from ou_dedetai.app import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE, App from . import config from . import control @@ -361,6 +361,8 @@ def choice_processor(self, stdscr, screen_id, choice): 0: self.main_menu_select, 1: self.custom_appimage_select, 2: self.handle_ask_response, + 3: self.handle_ask_file_response, + 4: self.handle_ask_directory_response, 8: self.waiting, 9: self.config_update_select, 10: self.waiting_releases, @@ -652,7 +654,17 @@ def _ask(self, question: str, options: list[str]) -> Optional[str]: # Now wait for it to complete self.ask_answer_event.wait() - return self.ask_answer_queue.get() + answer = self.ask_answer_queue.get() + + if answer == PROMPT_OPTION_FILE or answer == PROMPT_OPTION_DIRECTORY: + stack_index = 3 if answer == PROMPT_OPTION_FILE else 4 + self.screen_q.put(self.stack_input(stack_index, Queue(), threading.Event(), question, + os.path.expanduser(f"~/"), dialog=config.use_python_dialog)) + # Now wait for it to complete + self.ask_answer_event.wait() + answer = self.ask_answer_queue.get() + + return answer def handle_ask_response(self, choice: Optional[str]): if choice is not None: @@ -660,6 +672,16 @@ def handle_ask_response(self, choice: Optional[str]): self.ask_answer_event.set() self.switch_screen(config.use_python_dialog) + def handle_ask_file_response(self, choice: Optional[str]): + # XXX: can there be some sort of feedback if this file path isn't valid? + if choice is not None and Path(choice).exists() and Path(choice).is_file(): + self.handle_ask_response(choice) + + def handle_ask_directory_response(self, choice: Optional[str]): + # XXX: can there be some sort of feedback if this directory path isn't valid? + if choice is not None and Path(choice).exists() and Path(choice).is_dir(): + self.handle_ask_response(choice) + def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 0324021a..c9ec1973 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -58,7 +58,6 @@ def set_default_config(): system.get_package_manager() if config.CONFIG_FILE is None: config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH - config.PRESENT_WORKING_DIRECTORY = os.getcwd() config.MYDOWNLOADS = get_user_downloads_dir() os.makedirs(os.path.dirname(config.LOGOS_LOG), exist_ok=True) From a2001e86d6d0370a8b4cdfbc804ece785203b195 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:53:56 -0800 Subject: [PATCH 007/137] feat(wip): more progress --- ou_dedetai/app.py | 283 +++++++++++++++++++++++++++++++++------- ou_dedetai/cli.py | 23 ++-- ou_dedetai/config.py | 29 ++-- ou_dedetai/control.py | 137 ++++++------------- ou_dedetai/gui.py | 45 ++++--- ou_dedetai/gui_app.py | 156 ++++++++-------------- ou_dedetai/installer.py | 205 +++++++++++------------------ ou_dedetai/logos.py | 47 ++++--- ou_dedetai/main.py | 6 +- ou_dedetai/network.py | 30 +++-- ou_dedetai/system.py | 16 ++- ou_dedetai/tui_app.py | 181 ++++++++----------------- ou_dedetai/utils.py | 193 +++++++-------------------- ou_dedetai/wine.py | 124 +++++++----------- 14 files changed, 656 insertions(+), 819 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index fad8b332..39864cc6 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,19 +1,21 @@ import abc from dataclasses import dataclass from datetime import datetime -import enum import json import logging import os from pathlib import Path from typing import Optional -from ou_dedetai import config, network, utils, constants +from ou_dedetai import msg, network, utils, constants # Strings for choosing a follow up file or directory PROMPT_OPTION_DIRECTORY = "Choose Directory" PROMPT_OPTION_FILE = "Choose File" +# String for when a binary is meant to be downloaded later +DOWNLOAD = "Download" + class App(abc.ABC): def __init__(self, **kwargs) -> None: self.conf = Config(self) @@ -25,7 +27,10 @@ def ask(self, question: str, options: list[str]) -> str: If the internal ask function returns None, the process will exit with an error code 1 """ - if options is not None and self._exit_option is not None: + if len(options) == 1 and (PROMPT_OPTION_DIRECTORY in options or PROMPT_OPTION_FILE in options): + # Set the only option to be the follow up prompt + options = options[0] + elif options is not None and self._exit_option is not None: options += [self._exit_option] answer = self._ask(question, options) if answer == self._exit_option: @@ -39,19 +44,22 @@ def ask(self, question: str, options: list[str]) -> str: _exit_option: Optional[str] = "Exit" @abc.abstractmethod - def _ask(self, question: str, options: list[str] = None) -> Optional[str]: + def _ask(self, question: str, options: list[str] | str) -> Optional[str]: """Implementation for asking a question pre-front end Options may include ability to prompt for an additional value. Implementations MUST handle the follow up prompt before returning + Options may be a single value, + Implementations MUST handle this single option being a follow up prompt + If you would otherwise return None, consider shutting down cleanly, the calling function will exit the process with an error code of one if this function returns None """ raise NotImplementedError() - def _hook(self): + def _config_updated(self): """A hook for any changes the individual apps want to do when the config changes""" pass @@ -61,7 +69,7 @@ def update_progress(self, message: str, percent: Optional[int] = None): """Updates the progress of the current operation""" pass - +# XXX: move these configs into config.py once it's cleared out @dataclass class LegacyConfiguration: """Configuration and it's keys from before the user configuration class existed. @@ -88,7 +96,7 @@ class LegacyConfiguration: @classmethod def config_file_path() -> str: return os.getenv(constants.CONFIG_FILE_ENV) or constants.DEFAULT_CONFIG_PATH - + @classmethod def from_file_and_env() -> "LegacyConfiguration": config_file_path = LegacyConfiguration.config_file_path() @@ -147,8 +155,8 @@ class UserConfiguration: faithlife_product_version: Optional[str] = None faithlife_product_release: Optional[str] = None install_dir: Optional[Path] = None - winetricks_binary: Optional[Path] = None - wine_binary: Optional[Path] = None + winetricks_binary: Optional[str] = None + wine_binary: Optional[str] = None # This is where to search for wine wine_binary_code: Optional[str] = None backup_directory: Optional[Path] = None @@ -199,8 +207,25 @@ def from_legacy(legacy: LegacyConfiguration) -> "UserConfiguration": def write_config(self): config_file_path = LegacyConfiguration.config_file_path() - with open(config_file_path, 'w') as config_file: - json.dump(self.__dict__, config_file) + output = self.__dict__ + + logging.info(f"Writing config to {config_file_path}") + os.makedirs(os.path.dirname(config_file_path), exist_ok=True) + + # Ensure all paths stored are relative to install_dir + for k, v in output.items(): + # XXX: test this + if isinstance(v, Path) or (isinstance(v, str) and v.startswith(self.install_dir)): + output[k] = utils.get_relative_path(v, self.install_dir) + + try: + with open(config_file_path, 'w') as config_file: + json.dump(output, config_file, indent=4, sort_keys=True) + config_file.write('\n') + except IOError as e: + msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + # Continue, the installer can still operate even if it fails to write. + # XXX: what to do with these? # Used to be called current_logos_version, but actually could be used in Verbium too. @@ -221,80 +246,250 @@ class Config: If the user hasn't selected a particular value yet, they will be prompted in their UI.""" # Storage for the keys - user: UserConfiguration + _raw: UserConfiguration + + _curses_colors_valid_values = ["Light", "Dark", "Logos"] + + # Singleton logic, this enforces that only one config object exists at a time. + def __new__(cls) -> "Config": + if not hasattr(cls, '_instance'): + cls._instance = super(Config, cls).__new__(cls) + return cls._instance def __init__(self, app: App) -> None: self.app = app - self.user = UserConfiguration.read_from_file_and_env() + self._raw = UserConfiguration.read_from_file_and_env() logging.debug("Current persistent config:") - for k, v in self.user.__dict__.items(): + for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") - def _ask_if_not_found(self, config_key: str, question: str, options: list[str], dependent_config_keys: Optional[list[str]] = None) -> str: + def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: # XXX: should this also update the feedback? - if not getattr(config, config_key): - if dependent_config_keys is not None: - for dependent_config_key in dependent_config_keys: - setattr(config, dependent_config_key, None) - setattr(config, config_key, self.app.ask(question, options)) - self.app._hook() - # And update the file so it's always up to date. - self.user.write_config() - return getattr(config, config_key) + if not getattr(self._raw, parameter): + if dependent_parameters is not None: + for dependent_config_key in dependent_parameters: + setattr(self._raw, dependent_config_key, None) + answer = self.app.ask(question, options) + # Use the setter on this class if found, otherwise set in self._user + if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: + getattr(Config, parameter).fset(self, answer) + else: + setattr(self._raw, parameter, answer) + self._write() + return getattr(self._raw, parameter) + + def _write(self): + """Writes configuration to file and lets the app know something changed""" + self._raw.write_config() + self.app._config_updated() + + @property + def config_file_path(self) -> str: + return LegacyConfiguration.config_file_path() @property def faithlife_product(self) -> str: question = "Choose which FaithLife product the script should install: " # noqa: E501 options = ["Logos", "Verbum"] - return self._ask_if_not_found("FLPRODUCT", question, options, ["TARGETVERSION", "TARGET_RELEASE_VERSION"]) - + return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) + + @faithlife_product.setter + def faithlife_product(self, value: Optional[str]): + if self._raw.faithlife_product != value: + self._raw.faithlife_product = value + # Reset dependent variables + self.faithlife_product_release = None + + self._write() + @property def faithlife_product_version(self) -> str: question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 options = ["10", "9"] - return self._ask_if_not_found("FLPRODUCT", question, options, ["TARGET_RELEASE_VERSION"]) + return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) + + @faithlife_product_version.setter + def faithlife_product_version(self, value: Optional[str]): + if self._raw.faithlife_product_version != value: + self._raw.faithlife_product_release = None + # Install Dir has the name of the product and it's version. Reset it too + self._raw.install_dir = None + # Wine is dependent on the product/version selected + self._raw.wine_binary = None + self._raw.wine_binary_code = None + self._raw.winetricks_binary = None + + self._write() @property def faithlife_product_release(self) -> str: question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 options = network.get_logos_releases(None) - return self._ask_if_not_found("TARGET_RELEASE_VERSION", question, options) + return self._ask_if_not_found("faithlife_product_release", question, options) + + @faithlife_product_release.setter + def faithlife_product_release(self, value: str): + if self._raw.faithlife_product_release != value: + self._raw.faithlife_product_release = value + self._write() + + @property + def faithlife_product_release_channel(self) -> str: + return self._raw.faithlife_product_release_channel + + @property + def installer_release_channel(self) -> str: + return self._raw.installer_release_channel - # FIXME: should this just ensure that winetricks is installed and in the right location? That isn't really a matter for a config... @property def winetricks_binary(self) -> str: + """This may be a path to the winetricks binary or it may be "Download" + """ question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux.", # noqa: E501 options = utils.get_winetricks_options() - return self._ask_if_not_found("WINETRICKSBIN", question, options) + return self._ask_if_not_found("winetricks_binary", question, options) + @winetricks_binary.setter + def winetricks_binary(self, value: Optional[str | Path]): + if value is not None and value != "Download": + value = Path(value) + if not value.exists(): + raise ValueError("Winetricks binary must exist") + if self._raw.winetricks_binary != value: + self._raw.winetricks_binary = value + self._write() + @property def install_dir(self) -> str: default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 options = [default, PROMPT_OPTION_DIRECTORY] - output = self._ask_if_not_found("INSTALLDIR", question, options) - # XXX: Why is this stored separately if it's always at this relative path? Shouldn't this relative string be a constant? - config.APPDIR_BINDIR = f"{output}/data/bin" + output = self._ask_if_not_found("install_dir", question, options) return output + @property + # XXX: used to be called APPDIR_BINDIR + def installer_binary_directory(self) -> str: + return f"{self.install_dir}/data/bin" + + @property + # XXX: used to be called WINEPREFIX + def wine_prefix(self) -> str: + return f"{self.install_dir}/data/wine64_bottle" + @property def wine_binary(self) -> str: - if not config.WINE_EXE: + """Returns absolute path to the wine binary""" + if not self._raw.wine_binary: question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 network.set_recommended_appimage_config() options = utils.get_wine_options( - utils.find_appimage_files(config.TARGET_RELEASE_VERSION), - utils.find_wine_binary_files(config.TARGET_RELEASE_VERSION) + utils.find_appimage_files(self.faithlife_product_release), + utils.find_wine_binary_files(self.faithlife_product_release) ) choice = self.app.ask(question, options) - # Make the possibly relative path absolute before storing - config.WINE_EXE = utils.get_relative_path( - utils.get_config_var(choice), - self.install_dir - ) - self.app._hook() - return config.WINE_EXE + self.wine_binary = choice + # Return the full path so we the callee doesn't need to think about it + if not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): + return str(Path(self.install_dir) / self._raw.wine_binary) + return self._raw.wine_binary + + @wine_binary.setter + def wine_binary(self, value: str): + if (Path(self.install_dir) / value).exists(): + value = (Path(self.install_dir) / value).absolute() + if not Path(value).is_file(): + raise ValueError("Wine Binary path must be a valid file") + + if self._raw.wine_binary != value: + if value is not None: + value = Path(value).absolute() + self._raw.wine_binary = value + # Reset dependents + self._raw.wine_binary_code = None + self._write() + + @property + def wine64_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wine64') + + @property + # XXX: used to be called WINESERVER_EXE + def wineserver_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wineserver') + + + def toggle_faithlife_product_release_channel(self): + if self._raw.faithlife_product_release_channel == "stable": + new_channel = "beta" + else: + new_channel = "stable" + self._raw.faithlife_product_release_channel = new_channel + self._write() + + def toggle_installer_release_channel(self): + if self._raw.installer_release_channel == "stable": + new_channel = "dev" + else: + new_channel = "stable" + self._raw.installer_release_channel = new_channel + self._write() + + @property + def backup_directory(self) -> Path: + question = "New or existing folder to store backups in: " + options = [PROMPT_OPTION_DIRECTORY] + output = Path(self._ask_if_not_found("backup_directory", question, options)) + output.mkdir(parents=True) + return output + + @property + def curses_colors(self) -> str: + """Color for the curses dialog + + returns one of: Logos, Light or Dark""" + return self._raw.curses_colors + + @curses_colors.setter + def curses_colors(self, value: str): + if value not in self._curses_colors_valid_values: + raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") + self._raw.curses_colors = value + self._write() - \ No newline at end of file + def cycle_curses_color_scheme(self): + new_index = self._curses_colors_valid_values.index(self.curses_colors) + 1 + if new_index == len(self._curses_colors_valid_values): + new_index = 0 + self.curses_colors = self._curses_colors_valid_values[new_index] + + @property + def logos_exe(self) -> Optional[str]: + # XXX: consider caching this value? This is a directory walk, and it's called by a wine user and logos_*_exe + return utils.find_installed_product(self.faithlife_product, self.wine_prefix) + + @property + def wine_user(self) -> Optional[str]: + path: Optional[str] = self.logos_exe + if path is None: + return None + normalized_path: str = os.path.normpath(path) + path_parts = normalized_path.split(os.sep) + return path_parts[path_parts.index('users') + 1] + + @property + def logos_cef_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + + @property + def logos_indexer_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + + @property + def logos_login_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 56aba9da..2cc7e365 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -1,6 +1,5 @@ import queue import threading -from typing import Optional from ou_dedetai.app import App @@ -25,13 +24,13 @@ def backup(self): control.backup(app=self) def create_shortcuts(self): - installer.create_launcher_shortcuts() + installer.create_launcher_shortcuts(self) def edit_config(self): - control.edit_config() + control.edit_file(self.conf.config_file_path) def get_winetricks(self): - control.set_winetricks() + control.set_winetricks(self) def install_app(self): self.thread = utils.start_thread( @@ -41,13 +40,13 @@ def install_app(self): self.user_input_processor() def install_d3d_compiler(self): - wine.install_d3d_compiler() + wine.install_d3d_compiler(self) def install_dependencies(self): utils.install_dependencies(app=self) def install_fonts(self): - wine.install_fonts() + wine.install_fonts(self) def install_icu(self): wine.enforce_icu_data_files() @@ -56,7 +55,7 @@ def remove_index_files(self): control.remove_all_index_files() def remove_install_dir(self): - control.remove_install_dir() + control.remove_install_dir(self) def remove_library_catalog(self): control.remove_library_catalog() @@ -71,7 +70,7 @@ def run_installed_app(self): self.logos.start() def run_winetricks(self): - wine.run_winetricks() + wine.run_winetricks(self) def set_appimage(self): utils.set_appimage_symlink(app=self) @@ -83,23 +82,25 @@ def toggle_app_logging(self): self.logos.switch_logging() def update_latest_appimage(self): - utils.update_to_latest_recommended_appimage() + utils.update_to_latest_recommended_appimage(self) def update_self(self): utils.update_to_latest_lli_release() def winetricks(self): import config - wine.run_winetricks_cmd(*config.winetricks_args) + wine.run_winetricks_cmd(self, *config.winetricks_args) _exit_option: str = "Exit" - def _ask(self, question: str, options: list[str]) -> str: + def _ask(self, question: str, options: list[str] | str) -> str: """Passes the user input to the user_input_processor thread The user_input_processor is running on the thread that the user's stdin/stdout is attached to This function is being called from another thread so we need to pass the information between threads using a queue/event """ + if isinstance(options, str): + options = [options] self.input_q.put((question, options)) self.input_event.set() self.choice_event.wait() diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index dd793cc2..c0c4af59 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -7,18 +7,18 @@ from . import constants - # Define and set variables that are required in the config file. -core_config_keys = [ - "FLPRODUCT", "TARGETVERSION", "TARGET_RELEASE_VERSION", - "current_logos_version", "curses_colors", - "INSTALLDIR", "WINETRICKSBIN", "WINEBIN_CODE", "WINE_EXE", - "WINECMD_ENCODING", "LOGS", "BACKUPDIR", "LAST_UPDATED", - "RECOMMENDED_WINE64_APPIMAGE_URL", "LLI_LATEST_VERSION", - "logos_release_channel", "lli_release_channel", -] -for k in core_config_keys: - globals()[k] = os.getenv(k) +# XXX: slowly kill these +current_logos_version = None +INSTALLDIR = None +WINEBIN_CODE = None +WINE_EXE = None +WINECMD_ENCODING = None +LOGS = None +LAST_UPDATED = None +RECOMMENDED_WINE64_APPIMAGE_URL = None +LLI_LATEST_VERSION = None +lli_release_channel = None # Define and set additional variables that can be set in the env. extended_config = { @@ -59,11 +59,9 @@ ACTION: str = 'app' APPIMAGE_FILE_PATH: Optional[str] = None BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. -FLPRODUCTi: Optional[str] = None INSTALL_STEP: int = 0 INSTALL_STEPS_COUNT: int = 0 L9PACKAGES = None -LLI_LATEST_VERSION: Optional[str] = None LOG_LEVEL = logging.WARNING LOGOS_DIR = os.path.dirname(LOGOS_EXE) if LOGOS_EXE else None # noqa: F821 LOGOS_FORCE_ROOT: bool = False @@ -89,7 +87,6 @@ RECOMMENDED_WINE64_APPIMAGE_VERSION: Optional[str] = None RECOMMENDED_WINE64_APPIMAGE_BRANCH: Optional[str] = None SUPERUSER_COMMAND: Optional[str] = None -VERBUM_PATH: Optional[str] = None wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") install_finished = False @@ -103,10 +100,6 @@ resizing = False processes = {} threads = [] -logos_login_cmd = None -logos_cef_cmd = None -logos_indexer_cmd = None -logos_indexer_exe = None logos_linux_installer_status = None logos_linux_installer_status_info = { 0: "yes", diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index d1326c18..09a21d07 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -12,7 +12,7 @@ import time from pathlib import Path -from ou_dedetai.app import App +from ou_dedetai.app import DOWNLOAD, App from . import config from . import constants @@ -23,8 +23,8 @@ from . import utils -def edit_config(): - subprocess.Popen(['xdg-open', config.CONFIG_FILE]) +def edit_file(config_file: str): + subprocess.Popen(['xdg-open', config_file]) def delete_log_file_contents(): @@ -44,42 +44,24 @@ def restore(app: App): # FIXME: consider moving this into it's own file/module. def backup_and_restore(mode: str, app: App): data_dirs = ['Data', 'Documents', 'Users'] - # Ensure BACKUPDIR is defined. - if config.BACKUPDIR is None: - if config.DIALOG == 'tk': - pass # config.BACKUPDIR is already set in GUI - elif config.DIALOG == 'curses': - app.todo_e.wait() # Wait for TUI to resolve config.BACKUPDIR - app.todo_e.clear() - else: - try: - config.BACKUPDIR = input("New or existing folder to store backups in: ") # noqa: E501 - except KeyboardInterrupt: - print() - msg.logos_error("Cancelled with Ctrl+C") - config.BACKUPDIR = Path(config.BACKUPDIR).expanduser().resolve() - utils.update_config_file( - config.CONFIG_FILE, - 'BACKUPDIR', - str(config.BACKUPDIR) - ) + backup_dir = Path(app.conf.backup_directory).expanduser().resolve() - # Confirm BACKUPDIR. + # FIXME: Why is this different per UI? Should this always accept? if config.DIALOG == 'tk' or config.DIALOG == 'curses': pass # user confirms in GUI or TUI else: verb = 'Use' if mode == 'backup' else 'Restore backup from' - if not msg.cli_question(f"{verb} existing backups folder \"{config.BACKUPDIR}\"?", ""): # noqa: E501 + if not msg.cli_question(f"{verb} existing backups folder \"{app.conf.backup_directory}\"?", ""): # noqa: E501 answer = None while answer is None or (mode == 'restore' and not answer.is_dir()): # noqa: E501 answer = msg.cli_ask_filepath("Please provide a backups folder path:") answer = Path(answer).expanduser().resolve() if not answer.is_dir(): msg.status(f"Not a valid folder path: {answer}", app=app) - config.BACKUPDIR = answer + config.app.conf.backup_directory = answer # Set source folders. - backup_dir = Path(config.BACKUPDIR) + backup_dir = Path(app.conf.backup_directory) try: backup_dir.mkdir(exist_ok=True, parents=True) except PermissionError: @@ -90,28 +72,28 @@ def backup_and_restore(mode: str, app: App): return if mode == 'restore': - config.RESTOREDIR = utils.get_latest_folder(config.BACKUPDIR) - config.RESTOREDIR = Path(config.RESTOREDIR).expanduser().resolve() + restore_dir = utils.get_latest_folder(app.conf.backup_directory) + restore_dir = Path(restore_dir).expanduser().resolve() if config.DIALOG == 'tk': pass elif config.DIALOG == 'curses': app.screen_q.put(app.stack_confirm(24, app.todo_q, app.todo_e, - f"Restore most-recent backup?: {config.RESTOREDIR}", "", "", + f"Restore most-recent backup?: {restore_dir}", "", "", dialog=config.use_python_dialog)) - app.todo_e.wait() # Wait for TUI to confirm RESTOREDIR + app.todo_e.wait() # Wait for TUI to confirm restore_dir app.todo_e.clear() if app.tmp == "No": question = "Please choose a different restore folder path:" - app.screen_q.put(app.stack_input(25, app.todo_q, app.todo_e, question, f"{config.RESTOREDIR}", + app.screen_q.put(app.stack_input(25, app.todo_q, app.todo_e, question, f"{restore_dir}", dialog=config.use_python_dialog)) app.todo_e.wait() app.todo_e.clear() - config.RESTOREDIR = Path(app.tmp).expanduser().resolve() + restore_dir = Path(app.tmp).expanduser().resolve() else: # Offer to restore the most recent backup. - if not msg.cli_question(f"Restore most-recent backup?: {config.RESTOREDIR}", ""): # noqa: E501 - config.RESTOREDIR = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 - source_dir_base = config.RESTOREDIR + if not msg.cli_question(f"Restore most-recent backup?: {restore_dir}", ""): # noqa: E501 + restore_dir = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 + source_dir_base = restore_dir else: source_dir_base = Path(config.LOGOS_EXE).parent src_dirs = [source_dir_base / d for d in data_dirs if Path(source_dir_base / d).is_dir()] # noqa: E501 @@ -120,6 +102,7 @@ def backup_and_restore(mode: str, app: App): msg.logos_warning(f"No files to {mode}", app=app) return + # FIXME: UI specific code if config.DIALOG == 'curses': if mode == 'backup': app.screen_q.put(app.stack_text(8, app.todo_q, app.todo_e, "Backing up data…", wait=True)) @@ -157,7 +140,7 @@ def backup_and_restore(mode: str, app: App): shutil.rmtree(dst) else: # backup mode timestamp = utils.get_timestamp().replace('-', '') - current_backup_name = f"{config.FLPRODUCT}{config.TARGETVERSION}-{timestamp}" # noqa: E501 + current_backup_name = f"{app.conf.faithlife_product}{app.conf.faithlife_product_version}-{timestamp}" # noqa: E501 dst_dir = backup_dir / current_backup_name logging.debug(f"Backup directory path: \"{dst_dir}\".") @@ -213,8 +196,8 @@ def copy_data(src_dirs, dst_dir): shutil.copytree(src, Path(dst_dir) / src.name) -def remove_install_dir(): - folder = Path(config.INSTALLDIR) +def remove_install_dir(app: App): + folder = Path(app.conf.install_dir) if ( folder.is_dir() and msg.cli_question(f"Delete \"{folder}\" and all its contents?") @@ -261,62 +244,28 @@ def remove_library_catalog(): logging.error(f"Error removing {file_to_remove}: {e}") -def set_winetricks(): +def set_winetricks(app: App): msg.status("Preparing winetricks…") - if not config.APPDIR_BINDIR: - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - # Check if local winetricks version available; else, download it - if config.WINETRICKSBIN is None or not os.access(config.WINETRICKSBIN, os.X_OK): # noqa: E501 - local_winetricks_path = shutil.which('winetricks') - if local_winetricks_path is not None: - # Check if local winetricks version is up-to-date; if so, offer to - # use it or to download; else, download it. - local_winetricks_version = subprocess.check_output(["winetricks", "--version"]).split()[0] # noqa: E501 - if str(local_winetricks_version) != constants.WINETRICKS_VERSION: # noqa: E501 - if config.DIALOG == 'tk': #FIXME: CLI client not considered - logging.info("Setting winetricks to the local binary…") - config.WINETRICKSBIN = local_winetricks_path - else: - title = "Choose Winetricks" - question_text = "Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that FLPRODUCT requires on Linux." # noqa: E501 - - options = [ - "1: Use local winetricks.", - "2: Download winetricks from the Internet" - ] - winetricks_choice = tui_curses.menu(options, title, question_text) # noqa: E501 - - logging.debug(f"winetricks_choice: {winetricks_choice}") - if winetricks_choice.startswith("1"): - logging.info("Setting winetricks to the local binary…") - config.WINETRICKSBIN = local_winetricks_path - return 0 - elif winetricks_choice.startswith("2"): - system.install_winetricks(config.APPDIR_BINDIR) - config.WINETRICKSBIN = os.path.join( - config.APPDIR_BINDIR, - "winetricks" - ) - return 0 - else: - # FIXME: Should this call a function on the app object? - msg.status("Installation canceled!") - sys.exit(0) - else: - msg.status("The system's winetricks is too old. Downloading an up-to-date winetricks from the Internet…") # noqa: E501 - system.install_winetricks(config.APPDIR_BINDIR) - config.WINETRICKSBIN = os.path.join( - config.APPDIR_BINDIR, - "winetricks" - ) - return 0 - else: - msg.status("Local winetricks not found. Downloading winetricks from the Internet…") # noqa: E501 - system.install_winetricks(config.APPDIR_BINDIR) - config.WINETRICKSBIN = os.path.join( - config.APPDIR_BINDIR, - "winetricks" - ) + if app.conf.winetricks_binary != DOWNLOAD: + valid = True + # Double check it's a valid winetricks + if not Path(app.conf.winetricks_binary).exists(): + logging.warning("Winetricks path does not exist, downloading instead...") + valid = False + if not os.access(app.conf.winetricks_binary, os.X_OK): + logging.warning("Winetricks path given is not executable, downloading instead...") + valid = False + if not utils.check_winetricks_version(app.conf.winetricks_binary): + logging.warning("Winetricks version mismatch, downloading instead...") + valid = False + if valid: + logging.info(f"Found valid winetricks: {app.conf.winetricks_binary}") return 0 - return 0 + # Continue executing the download if it wasn't valid + system.install_winetricks(app.conf.installer_binary_directory, app) + app.conf.wine_binary = os.path.join( + app.conf.installer_binary_directory, + "winetricks" + ) + return 0 diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index b0501297..bc1aa3d1 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -14,6 +14,8 @@ from tkinter.ttk import Radiobutton from tkinter.ttk import Separator +from ou_dedetai.app import App + from . import config from . import utils from . import constants @@ -51,26 +53,24 @@ def __init__(self, root, question: str, options: list[str], **kwargs): class InstallerGui(Frame): - def __init__(self, root, **kwargs): + def __init__(self, root, app: App, **kwargs): super(InstallerGui, self).__init__(root, **kwargs) self.italic = font.Font(slant='italic') self.config(padding=5) self.grid(row=0, column=0, sticky='nwes') + self.app = app + + # XXX: remove these # Initialize vars from ENV. - self.flproduct = config.FLPRODUCT - self.targetversion = config.TARGETVERSION - self.logos_release_version = config.TARGET_RELEASE_VERSION - self.default_config_path = constants.DEFAULT_CONFIG_PATH - self.wine_exe = utils.get_wine_exe_path() - self.winetricksbin = config.WINETRICKSBIN + self.wine_exe = app.conf.wine_binary self.skip_fonts = config.SKIP_FONTS if self.skip_fonts is None: self.skip_fonts = 0 self.skip_dependencies = config.SKIP_DEPENDENCIES - if self.skip_fonts is None: - self.skip_fonts = 0 + if self.skip_dependencies is None: + self.skip_dependencies = 0 # Product/Version row. self.product_label = Label(self, text="Product & Version: ") @@ -79,8 +79,8 @@ def __init__(self, root, **kwargs): self.product_dropdown = Combobox(self, textvariable=self.productvar) self.product_dropdown.state(['readonly']) self.product_dropdown['values'] = ('Logos', 'Verbum') - if self.flproduct in self.product_dropdown['values']: - self.product_dropdown.set(self.flproduct) + if app.conf.faithlife_product in self.product_dropdown['values']: + self.product_dropdown.set(app.conf.faithlife_product) # version drop-down menu self.versionvar = StringVar() self.version_dropdown = Combobox( @@ -91,8 +91,8 @@ def __init__(self, root, **kwargs): self.version_dropdown.state(['readonly']) self.version_dropdown['values'] = ('9', '10') self.versionvar.set(self.version_dropdown['values'][1]) - if self.targetversion in self.version_dropdown['values']: - self.version_dropdown.set(self.targetversion) + if app.conf.faithlife_product_version in self.version_dropdown['values']: + self.version_dropdown.set(app.conf.faithlife_product_version) # Release row. self.release_label = Label(self, text="Release: ") @@ -101,9 +101,9 @@ def __init__(self, root, **kwargs): self.release_dropdown = Combobox(self, textvariable=self.releasevar) self.release_dropdown.state(['readonly']) self.release_dropdown['values'] = [] - if self.logos_release_version: - self.release_dropdown['values'] = [self.logos_release_version] - self.releasevar.set(self.logos_release_version) + if app.conf._raw.faithlife_product_release: + self.release_dropdown['values'] = [app.conf._raw.faithlife_product_release] + self.releasevar.set(app.conf._raw.faithlife_product_release) # release check button self.release_check_button = Button(self, text="Get Release List") @@ -127,8 +127,8 @@ def __init__(self, root, **kwargs): self.tricks_dropdown = Combobox(self, textvariable=self.tricksvar) self.tricks_dropdown.state(['readonly']) values = ['Download'] - if self.winetricksbin: - values.insert(0, self.winetricksbin) + if app.conf._raw.winetricks_binary: + values.insert(0, app.conf._raw.winetricks_binary) self.tricks_dropdown['values'] = values self.tricksvar.set(self.tricks_dropdown['values'][0]) @@ -188,16 +188,15 @@ def __init__(self, root, **kwargs): class ControlGui(Frame): - def __init__(self, root, *args, **kwargs): + def __init__(self, root, app: App, *args, **kwargs): super(ControlGui, self).__init__(root, **kwargs) self.config(padding=5) self.grid(row=0, column=0, sticky='nwes') + self.app = app + + # XXX: remove these # Initialize vars from ENV. - self.installdir = config.INSTALLDIR - self.flproduct = config.FLPRODUCT - self.targetversion = config.TARGETVERSION - self.logos_release_version = config.TARGET_RELEASE_VERSION self.logs = config.LOGS self.config_file = config.CONFIG_FILE diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 8d34aee8..bac75303 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -37,7 +37,7 @@ def __init__(self, root: "Root", **kwargs): super().__init__() self.root_to_destory_on_none = root - def _ask(self, question: str, options: list[str] = None) -> Optional[str]: + def _ask(self, question: str, options: list[str] | str) -> Optional[str]: answer_q = Queue() answer_event = Event() def spawn_dialog(): @@ -46,15 +46,18 @@ def spawn_dialog(): # Run the mainloop in this thread pop_up.mainloop() - - utils.start_thread(spawn_dialog) - - answer_event.wait() - answer = answer_q.get() - if answer is None: - self.root_to_destory_on_none.destroy() - return None - elif answer == PROMPT_OPTION_DIRECTORY: + if isinstance(options, list): + utils.start_thread(spawn_dialog) + + answer_event.wait() + answer = answer_q.get() + if answer is None: + self.root_to_destory_on_none.destroy() + return None + elif isinstance(options, str): + answer = options + + if answer == PROMPT_OPTION_DIRECTORY: answer = fd.askdirectory( parent=self.root_to_destory_on_none, title=question, @@ -163,24 +166,18 @@ def on_cancel_released(self, evt=None): class InstallerWindow(GuiApp): - def __init__(self, new_win, root: Root, **kwargs): + def __init__(self, new_win, root: Root, app: App, **kwargs): super().__init__(root) # Set root parameters. self.win = new_win self.root = root self.win.title(f"{constants.APP_NAME} Installer") self.win.resizable(False, False) - self.gui = gui.InstallerGui(self.win) + self.gui = gui.InstallerGui(self.win, app) # Initialize variables. - self.flproduct = None # config.FLPRODUCT self.config_thread = None - self.wine_exe = None - self.winetricksbin = None self.appimages = None - # self.appimage_verified = None - # self.logos_verified = None - # self.tricks_verified = None # Set widget callbacks and event bindings. self.gui.product_dropdown.bind( @@ -249,26 +246,22 @@ def __init__(self, new_win, root: Root, **kwargs): "<>", self.todo ) - self.product_q = Queue() - self.version_q = Queue() self.releases_q = Queue() - self.release_q = Queue() self.wine_q = Queue() - self.tricksbin_q = Queue() # Run commands. self.get_winetricks_options() - def _hook(self): + def _config_updated(self): """Update the GUI to reflect changes in the configuration if they were prompted separately""" - # The configuration enforces dependencies, if FLPRODUCT is unset, so will it's dependents (TARGETVERSION and TARGET_RELEASE_VERSION) + # The configuration enforces dependencies, if product is unset, so will it's dependents (version and release) # XXX: test this hook. Interesting thing is, this may never be called in production, as it's only called (presently) when the separate prompt returns # Returns either from config or the dropdown - self.gui.productvar.set(config.FLPRODUCT or self.gui.product_dropdown['values'][0]) - self.gui.versionvar.set(config.TARGETVERSION or self.gui.version_dropdown['values'][-1]) - self.gui.releasevar.set(config.TARGET_RELEASE_VERSION or self.gui.release_dropdown['values'][0]) - # Returns either WINE_EXE if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' - self.gui.winevar.set(config.WINE_EXE or next(iter(self.gui.wine_dropdown['values']), '')) + self.gui.productvar.set(self.conf._raw.faithlife_product or self.gui.product_dropdown['values'][0]) + self.gui.versionvar.set(self.conf._raw.faithlife_product_version or self.gui.version_dropdown['values'][-1]) + self.gui.releasevar.set(self.conf._raw.faithlife_product_release or self.gui.release_dropdown['values'][0]) + # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' + self.gui.winevar.set(self.conf._raw.wine_binary or next(iter(self.gui.wine_dropdown['values']), '')) def start_ensure_config(self): # Ensure progress counter is reset. @@ -280,8 +273,8 @@ def start_ensure_config(self): ) def get_winetricks_options(self): - config.WINETRICKSBIN = None # override config file b/c "Download" accounts for that # noqa: E501 - self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + self.conf.winetricks_binary = None # override config file b/c "Download" accounts for that # noqa: E501 + self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + ['Return to Main Menu'] self.gui.tricksvar.set(self.gui.tricks_dropdown['values'][0]) def set_input_widgets_state(self, state, widgets='all'): @@ -320,60 +313,30 @@ def todo(self, evt=None, task=None): self.set_input_widgets_state('disabled') elif task == 'DONE': self.update_install_progress() - elif task == 'CONFIG': - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) def set_product(self, evt=None): if self.gui.productvar.get().startswith('C'): # ignore default text return - self.gui.flproduct = self.gui.productvar.get() + self.conf.faithlife_product = self.gui.productvar.get() self.gui.product_dropdown.selection_clear() if evt: # manual override; reset dependent variables - logging.debug(f"User changed FLPRODUCT to '{self.gui.flproduct}'") - config.FLPRODUCT = self.gui.flproduct - config.FLPRODUCTi = None - config.VERBUM_PATH = None - - config.TARGETVERSION = None + logging.debug(f"User changed faithlife_product to '{self.conf.faithlife_product}'") self.gui.versionvar.set('') - - config.TARGET_RELEASE_VERSION = None self.gui.releasevar.set('') - - config.INSTALLDIR = None - config.APPDIR_BINDIR = None - - config.WINE_EXE = None self.gui.winevar.set('') - config.SELECTED_APPIMAGE_FILENAME = None - config.WINEBIN_CODE = None self.start_ensure_config() - else: - self.product_q.put(self.gui.flproduct) def set_version(self, evt=None): - self.gui.targetversion = self.gui.versionvar.get() + self.conf.faithlife_product_version = self.gui.versionvar.get() self.gui.version_dropdown.selection_clear() if evt: # manual override; reset dependent variables - logging.debug(f"User changed TARGETVERSION to '{self.gui.targetversion}'") # noqa: E501 - config.TARGETVERSION = None - self.gui.releasevar.set('') - config.TARGET_RELEASE_VERSION = None + logging.debug(f"User changed Target Version to '{self.conf.faithlife_product_version}'") # noqa: E501 self.gui.releasevar.set('') - config.INSTALLDIR = None - config.APPDIR_BINDIR = None - - config.WINE_EXE = None self.gui.winevar.set('') - config.SELECTED_APPIMAGE_FILENAME = None - config.WINEBIN_CODE = None self.start_ensure_config() - else: - self.version_q.put(self.gui.targetversion) def start_releases_check(self): # Disable button; clear list. @@ -396,23 +359,14 @@ def start_releases_check(self): def set_release(self, evt=None): if self.gui.releasevar.get()[0] == 'C': # ignore default text return - self.gui.logos_release_version = self.gui.releasevar.get() + self.conf.faithlife_product_release = self.gui.releasevar.get() self.gui.release_dropdown.selection_clear() if evt: # manual override - config.TARGET_RELEASE_VERSION = self.gui.logos_release_version - logging.debug(f"User changed TARGET_RELEASE_VERSION to '{self.gui.logos_release_version}'") # noqa: E501 - - config.INSTALLDIR = None - config.APPDIR_BINDIR = None + logging.debug(f"User changed release version to '{self.conf.faithlife_product_release}'") # noqa: E501 - config.WINE_EXE = None self.gui.winevar.set('') - config.SELECTED_APPIMAGE_FILENAME = None - config.WINEBIN_CODE = None self.start_ensure_config() - else: - self.release_q.put(self.gui.logos_release_version) def start_find_appimage_files(self, release_version): # Setup queue, signal, thread. @@ -458,11 +412,10 @@ def start_wine_versions_check(self, release_version): ) def set_wine(self, evt=None): - self.gui.wine_exe = self.gui.winevar.get() + self.conf.wine_binary = self.gui.winevar.get() self.gui.wine_dropdown.selection_clear() if evt: # manual override - logging.debug(f"User changed WINE_EXE to '{self.gui.wine_exe}'") - config.WINE_EXE = None + logging.debug(f"User changed wine binary to '{self.conf.wine_binary}'") config.SELECTED_APPIMAGE_FILENAME = None config.WINEBIN_CODE = None @@ -471,25 +424,23 @@ def set_wine(self, evt=None): self.wine_q.put( utils.get_relative_path( utils.get_config_var(self.gui.wine_exe), - config.INSTALLDIR + self.conf.install_dir ) ) def set_winetricks(self, evt=None): - self.gui.winetricksbin = self.gui.tricksvar.get() + self.conf.winetricks_binary = self.gui.tricksvar.get() self.gui.tricks_dropdown.selection_clear() if evt: # manual override - config.WINETRICKSBIN = None + self.conf.winetricks_binary = None self.start_ensure_config() - else: - self.tricksbin_q.put(self.gui.winetricksbin) def on_release_check_released(self, evt=None): self.start_releases_check() def on_wine_check_released(self, evt=None): self.gui.wine_check_button.state(['disabled']) - self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) + self.start_wine_versions_check(self.conf.faithlife_product_release) def set_skip_fonts(self, evt=None): self.gui.skip_fonts = 1 - self.gui.fontsvar.get() # invert True/False @@ -541,7 +492,7 @@ def update_find_appimage_progress(self, evt=None): self.stop_indeterminate_progress() if not self.appimage_q.empty(): self.appimages = self.appimage_q.get() - self.start_wine_versions_check(config.TARGET_RELEASE_VERSION) + self.start_wine_versions_check(self.conf.faithlife_product_release) def update_wine_check_progress(self, evt=None): if evt and self.wines_q.empty(): @@ -604,7 +555,7 @@ def __init__(self, root, *args, **kwargs): self.root = root self.root.title(f"{constants.APP_NAME} Control Panel") self.root.resizable(False, False) - self.gui = gui.ControlGui(self.root) + self.gui = gui.ControlGui(self.root, app=self) self.actioncmd = None self.logos = logos.LogosManager(app=self) @@ -635,7 +586,7 @@ def __init__(self, root, *args, **kwargs): ) self.gui.logging_button.state(['disabled']) - self.gui.config_button.config(command=control.edit_config) + self.gui.config_button.config(command=self.edit_config) self.gui.deps_button.config(command=self.install_deps) self.gui.backup_button.config(command=self.run_backup) self.gui.restore_button.config(command=self.run_restore) @@ -700,10 +651,13 @@ def __init__(self, root, *args, **kwargs): self.start_indeterminate_progress() utils.start_thread(self.logos.get_app_logging_state) + def edit_config(self): + control.edit_file(self.conf.config_file_path) + def configure_app_button(self, evt=None): if utils.app_is_installed(): # wine.set_logos_paths() - self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") + self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.gui.app_button.config(command=self.run_logos) self.gui.get_winetricks_button.state(['!disabled']) else: @@ -712,7 +666,7 @@ def configure_app_button(self, evt=None): def run_installer(self, evt=None): classname = constants.BINARY_NAME self.installer_win = Toplevel() - InstallerWindow(self.installer_win, self.root, class_=classname) + InstallerWindow(self.installer_win, self.root, app=self, class_=classname) self.root.icon = config.LOGOS_ICON_URL def run_logos(self, evt=None): @@ -749,16 +703,6 @@ def install_icu(self, evt=None): utils.start_thread(wine.enforce_icu_data_files, app=self) def run_backup(self, evt=None): - # Get backup folder. - if config.BACKUPDIR is None: - config.BACKUPDIR = fd.askdirectory( - parent=self.root, - title=f"Choose folder for {config.FLPRODUCT} backups", - initialdir=Path().home(), - ) - if not config.BACKUPDIR: # user cancelled - return - # Prepare progress bar. self.gui.progress.state(['!disabled']) self.gui.progress.config(mode='determinate') @@ -818,11 +762,14 @@ def get_winetricks(self, evt=None): def launch_winetricks(self, evt=None): self.gui.statusvar.set("Launching Winetricks…") # Start winetricks in thread. - utils.start_thread(wine.run_winetricks) + utils.start_thread(self.run_winetricks) # Start thread to clear status after delay. args = [12000, self.root.event_generate, '<>'] utils.start_thread(self.root.after, *args) + def run_winetricks(self): + wine.run_winetricks(self) + def switch_logging(self, evt=None): desired_state = self.gui.loggingstatevar.get() self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") @@ -855,7 +802,8 @@ def update_logging_button(self, evt=None): def update_app_button(self, evt=None): self.gui.app_button.state(['!disabled']) - self.gui.app_buttonvar.set(f"Run {config.FLPRODUCT}") + # XXX: we may need another hook here to update the product version should it change + self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.configure_app_button() self.update_run_winetricks_button() self.gui.logging_button.state(['!disabled']) @@ -880,7 +828,7 @@ def update_latest_lli_release_button(self, evt=None): self.gui.update_lli_button.state([state]) def update_latest_appimage_button(self, evt=None): - status, reason = utils.compare_recommended_appimage_version() + status, reason = utils.compare_recommended_appimage_version(self) msg = None if status == 0: state = '!disabled' @@ -897,7 +845,7 @@ def update_latest_appimage_button(self, evt=None): self.gui.latest_appimage_button.state([state]) def update_run_winetricks_button(self, evt=None): - if utils.file_exists(config.WINETRICKSBIN): + if utils.file_exists(self.conf.winetricks_binary): state = '!disabled' else: state = 'disabled' diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 70f93fdd..5568b323 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -from ou_dedetai.app import App +from ou_dedetai.app import DOWNLOAD, App from . import config from . import constants @@ -20,21 +20,8 @@ def ensure_product_choice(app: App): config.INSTALL_STEPS_COUNT += 1 update_install_feedback("Choose product…", app=app) logging.debug('- config.FLPRODUCT') - logging.debug('- config.FLPRODUCTi') - logging.debug('- config.VERBUM_PATH') - # accessing app.conf.faithlife_product ensures the product is selected - # Eventually we'd migrate all of these kind of variables in config to this pattern - # That require a user selection if they are found to be None - config.FLPRODUCTi = get_flproducti_name(app.conf.faithlife_product) - if app.conf.faithlife_product == 'Logos': - config.VERBUM_PATH = "/" - elif app.conf.faithlife_product == 'Verbum': - config.VERBUM_PATH = "/Verbum/" - - logging.debug(f"> {app.conf.faithlife_product=}") - logging.debug(f"> {config.FLPRODUCTi=}") - logging.debug(f"> {config.VERBUM_PATH=}") + logging.debug(f"> config.FLPRODUCT={app.conf.faithlife_product}") # XXX: we don't need this install step anymore @@ -46,7 +33,7 @@ def ensure_version_choice(app: App): logging.debug('- config.TARGETVERSION') # Accessing this ensures it's set app.conf.faithlife_product_version - logging.debug(f"> {config.TARGETVERSION=}") + logging.debug(f"> config.TARGETVERSION={app.conf.faithlife_product_version=}") # XXX: no longer needed @@ -58,7 +45,7 @@ def ensure_release_choice(app: App): logging.debug('- config.TARGET_RELEASE_VERSION') # accessing this sets the config app.conf.faithlife_product_release - logging.debug(f"> {config.TARGET_RELEASE_VERSION=}") + logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") def ensure_install_dir_choice(app: App): @@ -69,8 +56,8 @@ def ensure_install_dir_choice(app: App): logging.debug('- config.INSTALLDIR') # Accessing this sets install_dir and bin_dir app.conf.install_dir - logging.debug(f"> {config.INSTALLDIR=}") - logging.debug(f"> {config.APPDIR_BINDIR=}") + logging.debug(f"> config.INSTALLDIR={app.conf.install_dir=}") + logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_directory}") def ensure_wine_choice(app: App): @@ -85,23 +72,20 @@ def ensure_wine_choice(app: App): logging.debug('- config.WINE_EXE') logging.debug('- config.WINEBIN_CODE') - # This sets config.WINE_EXE - app.conf.wine_binary - # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. - m = f"Preparing to process WINE_EXE. Currently set to: {utils.get_wine_exe_path()}." # noqa: E501 + m = f"Preparing to process WINE_EXE. Currently set to: {app.conf.wine_binary}." # noqa: E501 logging.debug(m) - if str(utils.get_wine_exe_path()).lower().endswith('.appimage'): - config.SELECTED_APPIMAGE_FILENAME = str(utils.get_wine_exe_path()) + if str(app.conf.wine_binary).lower().endswith('.appimage'): + config.SELECTED_APPIMAGE_FILENAME = str(app.conf.wine_binary) if not config.WINEBIN_CODE: - config.WINEBIN_CODE = utils.get_winebin_code_and_desc(utils.get_wine_exe_path())[0] # noqa: E501 + config.WINEBIN_CODE = utils.get_winebin_code_and_desc(app.conf.wine_binary)[0] # noqa: E501 logging.debug(f"> {config.SELECTED_APPIMAGE_FILENAME=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_URL=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FILENAME=}") logging.debug(f"> {config.WINEBIN_CODE=}") - logging.debug(f"> {utils.get_wine_exe_path()=}") + logging.debug(f"> {app.conf.wine_binary=}") # XXX: this isn't needed anymore @@ -113,7 +97,7 @@ def ensure_winetricks_choice(app: App): logging.debug('- config.WINETRICKSBIN') # Accessing the winetricks_binary variable will do this. app.conf.winetricks_binary - logging.debug(f"> {config.WINETRICKSBIN=}") + logging.debug(f"> config.WINETRICKSBIN={app.conf.winetricks_binary}") # XXX: huh? What does this do? @@ -141,7 +125,7 @@ def ensure_check_sys_deps_choice(app=None): logging.debug(f"> {config.SKIP_DEPENDENCIES=}") -def ensure_installation_config(app=None): +def ensure_installation_config(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_check_sys_deps_choice(app=app) config.INSTALL_STEP += 1 @@ -157,13 +141,15 @@ def ensure_installation_config(app=None): # Set icon variables. app_dir = Path(__file__).parent - flproducti = get_flproducti_name(config.FLPRODUCT) + flproducti = get_flproducti_name(app.conf.faithlife_product) logos_icon_url = app_dir / 'img' / f"{flproducti}-128-icon.png" + # XXX: stop stting all these config keys config.LOGOS_ICON_URL = str(logos_icon_url) config.LOGOS_ICON_FILENAME = logos_icon_url.name - config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{config.TARGETVERSION}{config.VERBUM_PATH}Installer/{config.TARGET_RELEASE_VERSION}/{config.FLPRODUCT}-x64.msi" # noqa: E501 + after_version_url_part = "/Verbum/" if app.conf.faithlife_product == "Verbum" else "/" + config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{app.conf.faithlife_product_version}{after_version_url_part}Installer/{app.conf.faithlife_product_release}/{app.conf.faithlife_product}-x64.msi" # noqa: E501 - config.LOGOS_VERSION = config.TARGET_RELEASE_VERSION + config.LOGOS_VERSION = app.conf.faithlife_product_version config.LOGOS64_MSI = Path(config.LOGOS64_URL).name logging.debug(f"> {config.LOGOS_ICON_URL=}") @@ -179,7 +165,7 @@ def ensure_installation_config(app=None): msg.logos_msg("Install is running…") -def ensure_install_dirs(app=None): +def ensure_install_dirs(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_installation_config(app=app) config.INSTALL_STEP += 1 @@ -190,23 +176,18 @@ def ensure_install_dirs(app=None): logging.debug('- data/wine64_bottle') wine_dir = Path("") - if config.INSTALLDIR is None: - config.INSTALLDIR = f"{os.getenv('HOME')}/{config.FLPRODUCT}Bible{config.TARGETVERSION}" # noqa: E501 - - config.APPDIR_BINDIR = f"{config.INSTALLDIR}/data/bin" - bin_dir = Path(config.APPDIR_BINDIR) + bin_dir = Path(app.conf.installer_binary_directory) bin_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {bin_dir} exists?: {bin_dir.is_dir()}") - logging.debug(f"> {config.INSTALLDIR=}") + logging.debug(f"> config.INSTALLDIR={app.conf.installer_binary_directory}") logging.debug(f"> {config.APPDIR_BINDIR=}") - config.WINEPREFIX = f"{config.INSTALLDIR}/data/wine64_bottle" - wine_dir = Path(f"{config.WINEPREFIX}") + wine_dir = Path(f"{app.conf.wine_prefix}") wine_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") - logging.debug(f"> {config.WINEPREFIX=}") + logging.debug(f"> config.WINEPREFIX={app.conf.wine_prefix}") # XXX: what does this task do? Shouldn't that logic be here? if config.DIALOG in ['curses', 'dialog', 'tk']: @@ -228,11 +209,11 @@ def ensure_sys_deps(app=None): logging.debug("> Skipped.") -def ensure_appimage_download(app=None): +def ensure_appimage_download(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_sys_deps(app=app) config.INSTALL_STEP += 1 - if config.TARGETVERSION != '9' and not str(utils.get_wine_exe_path()).lower().endswith('appimage'): # noqa: E501 + if app.conf.faithlife_product_version != '9' and not str(app.conf.wine_binary).lower().endswith('appimage'): # noqa: E501 return update_install_feedback( "Ensuring wine AppImage is downloaded…", @@ -254,7 +235,7 @@ def ensure_appimage_download(app=None): logging.debug(f"> File exists?: {downloaded_file}: {Path(downloaded_file).is_file()}") # noqa: E501 -def ensure_wine_executables(app=None): +def ensure_wine_executables(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_appimage_download(app=app) config.INSTALL_STEP += 1 @@ -268,7 +249,7 @@ def ensure_wine_executables(app=None): logging.debug('- wineserver') # Add APPDIR_BINDIR to PATH. - if not os.access(utils.get_wine_exe_path(), os.X_OK): + if not os.access(app.conf.wine_binary, os.X_OK): msg.status("Creating wine appimage symlinks…", app=app) create_wine_appimage_symlinks(app=app) @@ -285,7 +266,7 @@ def ensure_wine_executables(app=None): logging.debug(f"> winetricks path: {config.APPDIR_BINDIR}/winetricks") -def ensure_winetricks_executable(app=None): +def ensure_winetricks_executable(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_wine_executables(app=app) config.INSTALL_STEP += 1 @@ -294,23 +275,21 @@ def ensure_winetricks_executable(app=None): app=app ) - if config.WINETRICKSBIN is None or config.WINETRICKSBIN.startswith('Download'): # noqa: E501 - config.WINETRICKSBIN = f"{config.APPDIR_BINDIR}/winetricks" # default - if not os.access(config.WINETRICKSBIN, os.X_OK): + if app.conf.winetricks_binary == DOWNLOAD or not os.access(app.conf.winetricks_binary, os.X_OK): # Either previous system winetricks is no longer accessible, or the # or the user has chosen to download it. msg.status("Downloading winetricks from the Internet…", app=app) - system.install_winetricks(config.APPDIR_BINDIR, app=app) + system.install_winetricks(app.conf.installer_binary_directory, app=app) - logging.debug(f"> {config.WINETRICKSBIN} is executable?: {os.access(config.WINETRICKSBIN, os.X_OK)}") # noqa: E501 + logging.debug(f"> {app.conf.winetricks_binary} is executable?: {os.access(app.conf.winetricks_binary, os.X_OK)}") # noqa: E501 return 0 -def ensure_premade_winebottle_download(app=None): +def ensure_premade_winebottle_download(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_winetricks_executable(app=app) config.INSTALL_STEP += 1 - if config.TARGETVERSION != '9': + if app.conf.faithlife_product_version != '9': return update_install_feedback( f"Ensuring {constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME} bottle is downloaded…", # noqa: E501 @@ -327,26 +306,26 @@ def ensure_premade_winebottle_download(app=None): app=app, ) # Install bottle. - bottle = Path(f"{config.INSTALLDIR}/data/wine64_bottle") + bottle = Path(app.conf.wine_prefix) if not bottle.is_dir(): utils.install_premade_wine_bottle( config.MYDOWNLOADS, - f"{config.INSTALLDIR}/data" + f"{app.conf.install_dir}/data" ) logging.debug(f"> '{downloaded_file}' exists?: {Path(downloaded_file).is_file()}") # noqa: E501 -def ensure_product_installer_download(app=None): +def ensure_product_installer_download(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_premade_winebottle_download(app=app) config.INSTALL_STEP += 1 update_install_feedback( - f"Ensuring {config.FLPRODUCT} installer is downloaded…", + f"Ensuring {app.conf.faithlife_product} installer is downloaded…", app=app ) - config.LOGOS_EXECUTABLE = f"{config.FLPRODUCT}_v{config.LOGOS_VERSION}-x64.msi" # noqa: E501 + config.LOGOS_EXECUTABLE = f"{app.conf.faithlife_product}_v{config.LOGOS_VERSION}-x64.msi" # noqa: E501 downloaded_file = utils.get_downloaded_file_path(config.LOGOS_EXECUTABLE) if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE @@ -356,32 +335,32 @@ def ensure_product_installer_download(app=None): config.MYDOWNLOADS, app=app, ) - # Copy file into INSTALLDIR. - installer = Path(f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}") + # Copy file into install dir. + installer = Path(f"{app.conf.install_dir}/data/{config.LOGOS_EXECUTABLE}") if not installer.is_file(): shutil.copy(downloaded_file, installer.parent) logging.debug(f"> '{downloaded_file}' exists?: {Path(downloaded_file).is_file()}") # noqa: E501 -def ensure_wineprefix_init(app=None): +def ensure_wineprefix_init(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_product_installer_download(app=app) config.INSTALL_STEP += 1 update_install_feedback("Ensuring wineprefix is initialized…", app=app) - init_file = Path(f"{config.WINEPREFIX}/system.reg") + init_file = Path(f"{app.conf.wine_prefix}/system.reg") logging.debug(f"{init_file=}") if not init_file.is_file(): logging.debug(f"{init_file} does not exist") - if config.TARGETVERSION == '9': + if app.conf.faithlife_product_version == '9': utils.install_premade_wine_bottle( config.MYDOWNLOADS, - f"{config.INSTALLDIR}/data", + f"{app.conf.install_dir}/data", ) else: logging.debug("Initializing wineprefix.") - process = wine.initializeWineBottle() + process = wine.initializeWineBottle(app) wine.wait_pid(process) # wine.light_wineserver_wait() wine.wineserver_wait() @@ -389,7 +368,7 @@ def ensure_wineprefix_init(app=None): logging.debug(f"> {init_file} exists?: {init_file.is_file()}") -def ensure_winetricks_applied(app=None): +def ensure_winetricks_applied(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_wineprefix_init(app=app) config.INSTALL_STEP += 1 @@ -412,38 +391,38 @@ def ensure_winetricks_applied(app=None): if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): msg.status("Disabling winemenubuilder…", app) - wine.disable_winemenubuilder() + wine.disable_winemenubuilder(app.conf.wine64_binary) if not utils.grep(r'"renderer"="gdi"', usr_reg): msg.status("Setting Renderer to GDI…", app) - wine.set_renderer("gdi") + wine.set_renderer(app, "gdi") if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): msg.status("Setting Font Smooting to RGB…", app) - wine.install_font_smoothing() + wine.install_font_smoothing(app) if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 msg.status("Installing fonts…", app) - wine.install_fonts() + wine.install_fonts(app) if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): msg.status("Installing D3D…", app) - wine.install_d3d_compiler() + wine.install_d3d_compiler(app) if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - msg.status(f"Setting {config.FLPRODUCT} to Win10 Mode…", app) - wine.set_win_version("logos", "win10") + msg.status(f"Setting {app.conf.faithlife_product} to Win10 Mode…", app) + wine.set_win_version(app, "logos", "win10") # NOTE: Can't use utils.grep check here because the string # "Version"="win10" might appear elsewhere in the registry. - msg.logos_msg(f"Setting {config.FLPRODUCT} Bible Indexing to Win10 Mode…") # noqa: E501 - wine.set_win_version("indexer", "win10") + msg.logos_msg(f"Setting {app.conf.faithlife_product} Bible Indexing to Win10 Mode…") # noqa: E501 + wine.set_win_version(app, "indexer", "win10") # wine.light_wineserver_wait() wine.wineserver_wait() logging.debug("> Done.") -def ensure_icu_data_files(app=None): +def ensure_icu_data_files(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_winetricks_applied(app=app) config.INSTALL_STEP += 1 @@ -459,22 +438,20 @@ def ensure_icu_data_files(app=None): logging.debug('> ICU data files installed') -def ensure_product_installed(app=None): +def ensure_product_installed(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_icu_data_files(app=app) config.INSTALL_STEP += 1 update_install_feedback( - f"Ensuring {config.FLPRODUCT} is installed…", + f"Ensuring {app.conf.faithlife_product} is installed…", app=app ) - if not utils.find_installed_product(): - process = wine.install_msi() + if not utils.find_installed_product(app.conf.faithlife_product, config.WINEPREFIX): + process = wine.install_msi(app) wine.wait_pid(process) - config.LOGOS_EXE = utils.find_installed_product() - config.current_logos_version = config.TARGET_RELEASE_VERSION - - wine.set_logos_paths() + config.LOGOS_EXE = utils.find_installed_product(app.conf.faithlife_product, config.WINEPREFIX) + config.current_logos_version = app.conf.faithlife_product_release # Clean up temp files, etc. utils.clean_all() @@ -482,7 +459,7 @@ def ensure_product_installed(app=None): logging.debug(f"> Product path: {config.LOGOS_EXE}") -def ensure_config_file(app=None): +def ensure_config_file(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_product_installed(app=app) config.INSTALL_STEP += 1 @@ -490,25 +467,6 @@ def ensure_config_file(app=None): # XXX: Why the platform specific logic? - if not Path(config.CONFIG_FILE).is_file(): - logging.info(f"No config file at {config.CONFIG_FILE}") - create_config_file() - else: - logging.info(f"Config file exists at {config.CONFIG_FILE}.") - if config_has_changed(): - if config.DIALOG == 'cli': - if msg.logos_acknowledge_question( - f"Update config file at {config.CONFIG_FILE}?", - "The existing config file was not overwritten.", - "" - ): - logging.info("Updating config file.") - utils.write_config(config.CONFIG_FILE) - else: - utils.send_task(app, 'CONFIG') - if config.DIALOG == 'curses': - app.config_e.wait() - if config.DIALOG == 'cli': msg.logos_msg("Install has finished.") else: @@ -517,19 +475,19 @@ def ensure_config_file(app=None): logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 -def ensure_launcher_executable(app=None): +def ensure_launcher_executable(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_config_file(app=app) config.INSTALL_STEP += 1 runmode = system.get_runmode() if runmode == 'binary': update_install_feedback( - f"Copying launcher to {config.INSTALLDIR}…", + f"Copying launcher to {app.conf.install_dir}…", app=app ) - # Copy executable to config.INSTALLDIR. - launcher_exe = Path(f"{config.INSTALLDIR}/{constants.BINARY_NAME}") + # Copy executable into install dir. + launcher_exe = Path(f"{app.conf.install_dir}/{constants.BINARY_NAME}") if launcher_exe.is_file(): logging.debug("Removing existing launcher binary.") launcher_exe.unlink() @@ -543,7 +501,7 @@ def ensure_launcher_executable(app=None): ) -def ensure_launcher_shortcuts(app=None): +def ensure_launcher_shortcuts(app: App): config.INSTALL_STEPS_COUNT += 1 ensure_launcher_executable(app=app) config.INSTALL_STEP += 1 @@ -551,7 +509,7 @@ def ensure_launcher_shortcuts(app=None): runmode = system.get_runmode() if runmode == 'binary': update_install_feedback("Creating launcher shortcuts…", app=app) - create_launcher_shortcuts() + create_launcher_shortcuts(app) else: update_install_feedback( "Running from source. Skipping launcher creation.", @@ -578,9 +536,9 @@ def get_progress_pct(current, total): return round(current * 100 / total) -def create_wine_appimage_symlinks(app=None): - appdir_bindir = Path(config.APPDIR_BINDIR) - os.environ['PATH'] = f"{config.APPDIR_BINDIR}:{os.getenv('PATH')}" +def create_wine_appimage_symlinks(app: App): + appdir_bindir = Path(app.conf.installer_binary_directory) + os.environ['PATH'] = f"{app.conf.installer_binary_directory}:{os.getenv('PATH')}" # Ensure AppImage symlink. appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) @@ -632,19 +590,6 @@ def create_config_file(): msg.logos_warn(f"{config_dir} does not exist. Failed to create config file.") # noqa: E501 -def config_has_changed(): - # Compare existing config file contents with installer config. - logging.info("Comparing its contents with current config.") - current_config_file_dict = config.get_config_file_dict(config.CONFIG_FILE) - changed = False - - for key in config.core_config_keys: - if current_config_file_dict.get(key) != config.__dict__.get(key): - changed = True - break - return changed - - def create_desktop_file(name, contents): launcher_path = Path(f"~/.local/share/applications/{name}").expanduser() if launcher_path.is_file(): @@ -657,10 +602,10 @@ def create_desktop_file(name, contents): os.chmod(launcher_path, 0o755) -def create_launcher_shortcuts(): +def create_launcher_shortcuts(app: App): # Set variables for use in launcher files. - flproduct = config.FLPRODUCT - installdir = Path(config.INSTALLDIR) + flproduct = app.conf.faithlife_product + installdir = Path(app.conf.install_dir) m = "Can't create launchers" if flproduct is None: reason = "because the FaithLife product is not defined." diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 4e5189ba..1000003a 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -4,6 +4,8 @@ import psutil import threading +from ou_dedetai.app import App + from . import config from . import main from . import msg @@ -20,23 +22,23 @@ class State(Enum): class LogosManager: - def __init__(self, app=None): + def __init__(self, app: App): self.logos_state = State.STOPPED self.indexing_state = State.STOPPED self.app = app def monitor_indexing(self): - if config.logos_indexer_cmd in config.processes: - indexer = config.processes.get(config.logos_indexer_cmd) + if self.app.conf.logos_indexer_exe in config.processes: + indexer = config.processes.get(self.app.conf.logos_indexer_exe) if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): # noqa: E501 self.indexing_state = State.RUNNING else: self.indexing_state = State.STOPPED def monitor_logos(self): - splash = config.processes.get(config.LOGOS_EXE, []) - login = config.processes.get(config.logos_login_cmd, []) - cef = config.processes.get(config.logos_cef_cmd, []) + splash = config.processes.get(self.app.conf.logos_exe, []) + login = config.processes.get(self.app.conf.logos_login_exe, []) + cef = config.processes.get(self.app.conf.logos_cef_exe, []) splash_running = splash[0].is_running() if splash else False login_running = login[0].is_running() if login else False @@ -62,7 +64,7 @@ def monitor_logos(self): def monitor(self): if utils.app_is_installed(): - system.get_logos_pids() + system.get_logos_pids(self.app) try: self.monitor_indexing() self.monitor_logos() @@ -72,12 +74,12 @@ def monitor(self): def start(self): self.logos_state = State.STARTING - wine_release, _ = wine.get_wine_release(str(utils.get_wine_exe_path())) + wine_release, _ = wine.get_wine_release(self.app.conf.wine_binary) def run_logos(): wine.run_wine_proc( - str(utils.get_wine_exe_path()), - exe=config.LOGOS_EXE + self.app.conf.wine_binary, + exe=self.app.conf.logos_exe ) # Ensure wine version is compatible with Logos release version. @@ -93,7 +95,7 @@ def run_logos(): if config.DIALOG == 'tk': # Don't send "Running" message to GUI b/c it never clears. app = None - msg.status(f"Running {config.FLPRODUCT}…", app=app) + msg.status(f"Running {app.conf.faithlife_product}…", app=app) utils.start_thread(run_logos, daemon_bool=False) # NOTE: The following code would keep the CLI open while running # Logos, but since wine logging is sent directly to wine.log, @@ -115,7 +117,11 @@ def stop(self): self.logos_state = State.STOPPING if self.app: pids = [] - for process_name in [config.LOGOS_EXE, config.logos_login_cmd, config.logos_cef_cmd]: # noqa: E501 + for process_name in [ + self.app.conf.logos_exe, + self.app.conf.logos_login_exe, + self.app.conf.logos_cef_exe + ]: process_list = config.processes.get(process_name) if process_list: pids.extend([str(process.pid) for process in process_list]) @@ -140,8 +146,8 @@ def index(self): def run_indexing(): wine.run_wine_proc( - str(utils.get_wine_exe_path()), - exe=config.logos_indexer_exe + self.app.conf.wine_binary, + exe=self.app.conf.logos_indexer_exe ) def check_if_indexing(process): @@ -173,10 +179,10 @@ def wait_on_indexing(): self.indexing_state = State.RUNNING # If we don't wait the process won't yet be launched when we try to # pull it from config.processes. - while config.processes.get(config.logos_indexer_exe) is None: + while config.processes.get(self.app.conf.logos_indexer_exe) is None: time.sleep(0.1) logging.debug(f"{config.processes=}") - process = config.processes[config.logos_indexer_exe] + process = config.processes[self.app.conf.logos_indexer_exe] check_thread = utils.start_thread( check_if_indexing, process, @@ -184,7 +190,7 @@ def wait_on_indexing(): ) wait_thread = utils.start_thread(wait_on_indexing, daemon_bool=False) main.threads.extend([index_thread, check_thread, wait_thread]) - config.processes[config.logos_indexer_exe] = index_thread + config.processes[self.app.conf.logos_indexer_exe] = index_thread config.processes[config.check_if_indexing] = check_thread config.processes[wait_on_indexing] = wait_thread @@ -192,7 +198,7 @@ def stop_indexing(self): self.indexing_state = State.STOPPING if self.app: pids = [] - for process_name in [config.logos_indexer_exe]: + for process_name in [self.app.conf.logos_indexer_exe]: process_list = config.processes.get(process_name) if process_list: pids.extend([str(process.pid) for process in process_list]) @@ -215,7 +221,8 @@ def get_app_logging_state(self, init=False): state = 'DISABLED' current_value = wine.get_registry_value( 'HKCU\\Software\\Logos4\\Logging', - 'Enabled' + 'Enabled', + self.app ) if current_value == '0x1': state = 'ENABLED' @@ -254,7 +261,7 @@ def switch_logging(self, action=None): '/t', 'REG_DWORD', '/d', value, '/f' ] process = wine.run_wine_proc( - str(utils.get_wine_exe_path()), + self.app.conf.wine_binary, exe='reg', exe_args=exe_args ) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index e093c136..6661ece3 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -350,7 +350,6 @@ def set_config(): # Update config based on environment variables. config.get_env_config() - utils.set_runtime_config() # Update terminal log level if set in environment and changed from current # level. if config.VERBOSE: @@ -432,7 +431,7 @@ def run(): elif utils.app_is_installed(): # install_required; checking for app # wine.set_logos_paths() # Run the desired Logos action. - logging.info(f"Running function for {config.FLPRODUCT}: {config.ACTION.__name__}") # noqa: E501 + logging.info(f"Running function: {config.ACTION.__name__}") # noqa: E501 config.ACTION() else: # install_required, but app not installed msg.logos_error("App not installed…") @@ -442,9 +441,6 @@ def main(): set_config() set_dialog() - # Log persistent config. - utils.log_current_persistent_config() - # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that # can be run in conjunction with other actions, so it gets special # treatment here once config is set. diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index eaa11aa5..2b2da11a 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -15,6 +15,7 @@ from xml.etree import ElementTree as ET from ou_dedetai import wine +from ou_dedetai.app import App from . import config from . import constants @@ -159,10 +160,10 @@ def logos_reuse_download( sourceurl, file, targetdir, - app=None, + app: App, ): dirs = [ - config.INSTALLDIR, + app.conf.install_dir, os.getcwd(), config.MYDOWNLOADS, ] @@ -443,6 +444,7 @@ def set_recommended_appimage_config(): config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" +# XXX: this may be a bit of an issue, this is before the app initializes. I supposed we could load a proto-config that doesn't have any of the ensuring the user is prompted def check_for_updates(): # We limit the number of times set_recommended_appimage_config is run in # order to avoid GitHub API limits. This sets the check to once every 12 @@ -479,7 +481,7 @@ def check_for_updates(): logging.debug("Skipping self-update.") -def get_recommended_appimage(): +def get_recommended_appimage(app: App): wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 dest_path = Path(config.APPDIR_BINDIR) / wine64_appimage_full_filename if dest_path.is_file(): @@ -488,29 +490,31 @@ def get_recommended_appimage(): logos_reuse_download( config.RECOMMENDED_WINE64_APPIMAGE_URL, config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, - config.APPDIR_BINDIR) + config.APPDIR_BINDIR, + app=app + ) -def get_logos_releases(app=None) -> list[str]: +def get_logos_releases(app: App) -> list[str]: # Use already-downloaded list if requested again. downloaded_releases = None - if config.TARGETVERSION == '9' and config.LOGOS9_RELEASES: + if app.conf.faithlife_product_version == '9' and config.LOGOS9_RELEASES: downloaded_releases = config.LOGOS9_RELEASES - elif config.TARGETVERSION == '10' and config.LOGOS10_RELEASES: + elif app.conf.faithlife_product_version == '10' and config.LOGOS10_RELEASES: downloaded_releases = config.LOGOS10_RELEASES if downloaded_releases: - logging.debug(f"Using already-downloaded list of v{config.TARGETVERSION} releases") # noqa: E501 + logging.debug(f"Using already-downloaded list of v{app.conf.faithlife_product_version} releases") # noqa: E501 if app: app.releases_q.put(downloaded_releases) app.root.event_generate(app.release_evt) return downloaded_releases - msg.status(f"Downloading release list for {config.FLPRODUCT} {config.TARGETVERSION}…") # noqa: E501 + msg.status(f"Downloading release list for {app.conf.faithlife_product} {app.conf.faithlife_product_version}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. - if config.logos_release_channel is None or config.logos_release_channel == "stable": # noqa: E501 - url = f"https://clientservices.logos.com/update/v1/feed/logos{config.TARGETVERSION}/stable.xml" # noqa: E501 - elif config.logos_release_channel == "beta": + if app.conf.faithlife_product_release_channel == "beta": url = "https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 - + else: + url = f"https://clientservices.logos.com/update/v1/feed/logos{app.conf.faithlife_product_version}/stable.xml" # noqa: E501 + response_xml_bytes = net_get(url) # if response_xml is None and None not in [q, app]: if response_xml_bytes is None: diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index e4b5bfa3..2671719d 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -10,6 +10,8 @@ import zipfile from pathlib import Path +from ou_dedetai.app import App + from . import config from . import constants @@ -225,11 +227,11 @@ def get_pids(query): return results -def get_logos_pids(): - config.processes[config.LOGOS_EXE] = get_pids(config.LOGOS_EXE) - config.processes[config.logos_login_cmd] = get_pids(config.logos_login_cmd) - config.processes[config.logos_cef_cmd] = get_pids(config.logos_cef_cmd) - config.processes[config.logos_indexer_exe] = get_pids(config.logos_indexer_exe) # noqa: E501 +def get_logos_pids(app: App): + config.processes[app.conf.logos_exe] = get_pids(app.conf.logos_exe) + config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) + config.processes[app.conf.logos_cef_exe] = get_pids(app.conf.logos_cef_exe) + config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) # noqa: E501 # def get_pids_using_file(file_path, mode=None): @@ -815,7 +817,7 @@ def check_libs(libraries, app=None): def install_winetricks( installdir, - app=None, + app: App, version=constants.WINETRICKS_VERSION, ): msg.status(f"Installing winetricks v{version}…") @@ -838,5 +840,5 @@ def install_winetricks( z.extract(zi, path=installdir) break os.chmod(f"{installdir}/winetricks", 0o755) - config.WINETRICKSBIN = f"{installdir}/winetricks" + app.conf.winetricks_binary = f"{installdir}/winetricks" logging.debug("Winetricks installed.") diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 988382ae..665c4142 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -32,8 +32,8 @@ def __init__(self, stdscr): super().__init__() self.stdscr = stdscr # if config.current_logos_version is not None: - self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 - self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {config.current_logos_version} ({self.conf.faithlife_product_release_channel})" # noqa: E501 # else: # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" @@ -129,32 +129,19 @@ def set_curses_style(): curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) - def set_curses_colors_logos(self): - self.stdscr.bkgd(' ', curses.color_pair(3)) - self.main_window.bkgd(' ', curses.color_pair(3)) - self.menu_window.bkgd(' ', curses.color_pair(3)) - - def set_curses_colors_light(self): - self.stdscr.bkgd(' ', curses.color_pair(6)) - self.main_window.bkgd(' ', curses.color_pair(6)) - self.menu_window.bkgd(' ', curses.color_pair(6)) - - def set_curses_colors_dark(self): - self.stdscr.bkgd(' ', curses.color_pair(7)) - self.main_window.bkgd(' ', curses.color_pair(7)) - self.menu_window.bkgd(' ', curses.color_pair(7)) - - def change_color_scheme(self): - if config.curses_colors == "Logos": - config.curses_colors = "Light" - self.set_curses_colors_light() - elif config.curses_colors == "Light": - config.curses_colors = "Dark" - self.set_curses_colors_dark() - else: - config.curses_colors = "Logos" - config.curses_colors = "Logos" - self.set_curses_colors_logos() + def set_curses_colors(self): + if self.conf.curses_colors == "Logos": + self.stdscr.bkgd(' ', curses.color_pair(3)) + self.main_window.bkgd(' ', curses.color_pair(3)) + self.menu_window.bkgd(' ', curses.color_pair(3)) + elif self.conf.curses_colors == "Light": + self.stdscr.bkgd(' ', curses.color_pair(6)) + self.main_window.bkgd(' ', curses.color_pair(6)) + self.menu_window.bkgd(' ', curses.color_pair(6)) + elif self.conf.curses_colors == "Dark": + self.stdscr.bkgd(' ', curses.color_pair(7)) + self.main_window.bkgd(' ', curses.color_pair(7)) + self.menu_window.bkgd(' ', curses.color_pair(7)) def update_windows(self): if isinstance(self.active_screen, tui_screen.CursesScreen): @@ -178,18 +165,8 @@ def refresh(self): def init_curses(self): try: if curses.has_colors(): - if config.curses_colors is None or config.curses_colors == "Logos": - config.curses_colors = "Logos" - self.set_curses_style() - self.set_curses_colors_logos() - elif config.curses_colors == "Light": - config.curses_colors = "Light" - self.set_curses_style() - self.set_curses_colors_light() - elif config.curses_colors == "Dark": - config.curses_colors = "Dark" - self.set_curses_style() - self.set_curses_colors_dark() + self.set_curses_style() + self.set_curses_colors() curses.curs_set(0) curses.noecho() @@ -209,6 +186,9 @@ def init_curses(self): logging.error(f"An error occurred in init_curses(): {e}") raise + def _config_updated(self): + self.set_curses_colors() + def end_curses(self): try: self.stdscr.keypad(False) @@ -228,8 +208,8 @@ def end(self, signal, frame): def update_main_window_contents(self): self.clear() - self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({config.lli_release_channel})" # noqa: E501 - self.subtitle = f"Logos Version: {config.current_logos_version} ({config.logos_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {config.current_logos_version} ({self.conf.faithlife_product_release_channel})" # noqa: E501 self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) # self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) @@ -349,8 +329,6 @@ def task_processor(self, evt=None, task=None): utils.start_thread(self.get_waiting, config.use_python_dialog) elif task == 'INSTALLING_PW': utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) - elif task == 'CONFIG': - utils.start_thread(self.get_config, config.use_python_dialog) elif task == 'DONE': self.update_main_window_contents() elif task == 'PID': @@ -364,7 +342,6 @@ def choice_processor(self, stdscr, screen_id, choice): 3: self.handle_ask_file_response, 4: self.handle_ask_directory_response, 8: self.waiting, - 9: self.config_update_select, 10: self.waiting_releases, 11: self.winetricks_menu_select, 12: self.logos.start, @@ -377,8 +354,6 @@ def choice_processor(self, stdscr, screen_id, choice): 19: self.renderer_select, 20: self.win_ver_logos_select, 21: self.win_ver_index_select, - 22: self.verify_backup_path, - 23: self.use_backup_path, 24: self.confirm_restore_dir, 25: self.choose_restore_dir } @@ -421,12 +396,12 @@ def main_menu_select(self, choice): ) elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release() - elif choice == f"Run {config.FLPRODUCT}": + elif choice == f"Run {self.conf.faithlife_product}": self.reset_screen() self.logos.start() self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) self.switch_q.put(1) - elif choice == f"Stop {config.FLPRODUCT}": + elif choice == f"Stop {self.conf.faithlife_product}": self.reset_screen() self.logos.stop() self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) @@ -450,7 +425,7 @@ def main_menu_select(self, choice): self.set_utilities_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Change Color Scheme": - self.change_color_scheme() + self.conf.cycle_curses_color_scheme() msg.status("Changing color scheme", self) self.reset_screen() utils.write_config(config.CONFIG_FILE) @@ -458,18 +433,18 @@ def main_menu_select(self, choice): def winetricks_menu_select(self, choice): if choice == "Download or Update Winetricks": self.reset_screen() - control.set_winetricks() + control.set_winetricks(self) self.go_to_main_menu() elif choice == "Run Winetricks": self.reset_screen() - wine.run_winetricks() + wine.run_winetricks(self) self.go_to_main_menu() elif choice == "Install d3dcompiler": self.reset_screen() - wine.install_d3d_compiler() + wine.install_d3d_compiler(self) elif choice == "Install Fonts": self.reset_screen() - wine.install_fonts() + wine.install_fonts(self) self.go_to_main_menu() elif choice == "Set Renderer": self.reset_screen() @@ -504,16 +479,16 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Edit Config": self.reset_screen() - control.edit_config() + control.edit_file(self.conf.config_file_path) self.go_to_main_menu() elif choice == "Change Logos Release Channel": self.reset_screen() - utils.change_logos_release_channel() + self.conf.toggle_faithlife_product_release_channel() self.update_main_window_contents() self.go_to_main_menu() elif choice == f"Change {constants.APP_NAME} Release Channel": self.reset_screen() - utils.change_lli_release_channel() + self.conf.toggle_installer_release_channel() network.set_logoslinuxinstaller_latest_release_config() self.update_main_window_contents() self.go_to_main_menu() @@ -525,19 +500,17 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Back Up Data": self.reset_screen() - self.get_backup_path(mode="backup") utils.start_thread(self.do_backup) elif choice == "Restore Data": self.reset_screen() - self.get_backup_path(mode="restore") utils.start_thread(self.do_backup) elif choice == "Update to Latest AppImage": self.reset_screen() - utils.update_to_latest_recommended_appimage() + utils.update_to_latest_recommended_appimage(self) self.go_to_main_menu() elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files(utils.which_release()) + appimages = utils.find_appimage_files() appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) @@ -560,7 +533,7 @@ def custom_appimage_select(self, choice): else: appimage_filename = choice config.SELECTED_APPIMAGE_FILENAME = appimage_filename - utils.set_appimage_symlink() + utils.set_appimage_symlink(self) self.menu_screen.choice = "Processing" self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) self.appimage_e.set() @@ -568,18 +541,6 @@ def custom_appimage_select(self, choice): def waiting(self, choice): pass - def config_update_select(self, choice): - if choice: - if choice == "Yes": - msg.status("Updating config file.", self) - utils.write_config(config.CONFIG_FILE) - else: - msg.status("Config file left unchanged.", self) - self.menu_screen.choice = "Processing" - self.config_q.put(True) - self.config_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Finishing install…", dialog=config.use_python_dialog)) - def waiting_releases(self, choice): pass @@ -609,21 +570,21 @@ def install_dependencies_confirm(self, choice): def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: self.reset_screen() - wine.set_renderer(choice) + wine.set_renderer(self, choice) msg.status(f"Changed renderer to {choice}.", self) self.go_to_main_menu() def win_ver_logos_select(self, choice): if choice in ["vista", "win7", "win8", "win10", "win11"]: self.reset_screen() - wine.set_win_version("logos", choice) + wine.set_win_version(self, "logos", choice) msg.status(f"Changed Windows version for Logos to {choice}.", self) self.go_to_main_menu() def win_ver_index_select(self, choice): if choice in ["vista", "win7", "win8", "win10", "win11"]: self.reset_screen() - wine.set_win_version("indexer", choice) + wine.set_win_version(self, "indexer", choice) msg.status(f"Changed Windows version for Indexer to {choice}.", self) self.go_to_main_menu() @@ -647,16 +608,20 @@ def switch_screen(self, dialog): _exit_option = "Return to Main Menu" - def _ask(self, question: str, options: list[str]) -> Optional[str]: - options = self.which_dialog_options(options, config.use_python_dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, options, dialog=config.use_python_dialog)) + def _ask(self, question: str, options: list[str] | str) -> Optional[str]: + if isinstance(options, str): + answer = options + + if isinstance(options, list): + options = self.which_dialog_options(options, config.use_python_dialog) + self.menu_options = options + self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, options, dialog=config.use_python_dialog)) - # Now wait for it to complete - self.ask_answer_event.wait() - answer = self.ask_answer_queue.get() + # Now wait for it to complete + self.ask_answer_event.wait() + answer = self.ask_answer_queue.get() - if answer == PROMPT_OPTION_FILE or answer == PROMPT_OPTION_DIRECTORY: + if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: stack_index = 3 if answer == PROMPT_OPTION_FILE else 4 self.screen_q.put(self.stack_input(stack_index, Queue(), threading.Event(), question, os.path.expanduser(f"~/"), dialog=config.use_python_dialog)) @@ -689,53 +654,11 @@ def get_waiting(self, dialog, screen_id=8): self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, dialog=dialog)) - def get_config(self, dialog): - question = f"Update config file at {config.CONFIG_FILE}?" - labels = ["Yes", "No"] - options = self.which_dialog_options(labels, dialog) - self.menu_options = options - #TODO: Switch to msg.logos_continue_message - self.screen_q.put(self.stack_menu(9, self.config_q, self.config_e, question, options, dialog=dialog)) - # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " # f"Please provide your password to provide escalation privileges.") # self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) - def get_backup_path(self, mode): - self.tmp = mode - if config.BACKUPDIR is None or not Path(config.BACKUPDIR).is_dir(): - if config.BACKUPDIR is None: - question = "Please provide a backups folder path:" - else: - question = f"Current backups folder path \"{config.BACKUPDIR}\" is invalid. Please provide a new one:" - self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, - os.path.expanduser("~/Backups"), dialog=config.use_python_dialog)) - else: - verb = 'Use' if mode == 'backup' else 'Restore backup from' - question = f"{verb} backup from existing backups folder \"{config.BACKUPDIR}\"?" - self.screen_q.put(self.stack_confirm(23, self.todo_q, self.todo_e, question, "", - "", dialog=config.use_python_dialog)) - - def verify_backup_path(self, choice): - if choice: - if not Path(choice).is_dir(): - msg.status(f"Not a valid folder path: {choice}. Try again.", app=self) - question = "Please provide a different backups folder path:" - self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, - os.path.expanduser("~/Backups"), dialog=config.use_python_dialog)) - else: - config.BACKUPDIR = choice - self.todo_e.set() - - def use_backup_path(self, choice): - if choice == "No": - question = "Please provide a new backups folder path:" - self.screen_q.put(self.stack_input(22, self.todo_q, self.todo_e, question, - os.path.expanduser(f"{config.BACKUPDIR}"), dialog=config.use_python_dialog)) - else: - self.todo_e.set() - def confirm_restore_dir(self, choice): if choice: if choice == "Yes": @@ -791,9 +714,9 @@ def set_tui_menu_options(self, dialog=False): if utils.app_is_installed(): if self.logos.logos_state in [logos.State.STARTING, logos.State.RUNNING]: # noqa: E501 - run = f"Stop {config.FLPRODUCT}" + run = f"Stop {self.conf.faithlife_product}" elif self.logos.logos_state in [logos.State.STOPPING, logos.State.STOPPED]: # noqa: E501 - run = f"Run {config.FLPRODUCT}" + run = f"Run {self.conf.faithlife_product}" if self.logos.indexing_state == logos.State.RUNNING: indexing = "Stop Indexing" diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index c9ec1973..3bf94bdd 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -16,6 +16,7 @@ import threading import time import tkinter as tk +from ou_dedetai.app import App from packaging import version from pathlib import Path from typing import List, Optional, Union @@ -52,6 +53,7 @@ def append_unique(list, item): # Set "global" variables. +# XXX: fold this into config def set_default_config(): system.get_os() system.get_superuser_command() @@ -62,51 +64,12 @@ def set_default_config(): os.makedirs(os.path.dirname(config.LOGOS_LOG), exist_ok=True) -def set_runtime_config(): - # Set runtime variables that are dependent on ones from config file. - if config.INSTALLDIR and not config.WINEPREFIX: - config.WINEPREFIX = f"{config.INSTALLDIR}/data/wine64_bottle" - if get_wine_exe_path() and not config.WINESERVER_EXE: - bin_dir = Path(get_wine_exe_path()).parent - config.WINESERVER_EXE = str(bin_dir / 'wineserver') - if config.FLPRODUCT and config.WINEPREFIX and not config.LOGOS_EXE: - config.LOGOS_EXE = find_installed_product() - if app_is_installed(): - wine.set_logos_paths() - - -def log_current_persistent_config(): - logging.debug("Current persistent config:") - for k in config.core_config_keys: - logging.debug(f"{k}: {config.__dict__.get(k)}") - - +# XXX: remove, no need. def write_config(config_file_path): - logging.info(f"Writing config to {config_file_path}") - os.makedirs(os.path.dirname(config_file_path), exist_ok=True) - - config_data = {key: config.__dict__.get(key) for key in config.core_config_keys} # noqa: E501 - - try: - for key, value in config_data.items(): - if key == "WINE_EXE": - # We store the value of WINE_EXE as relative path if it is in - # the install directory. - if value is not None: - value = get_relative_path( - get_config_var(value), - config.INSTALLDIR - ) - if isinstance(value, Path): - config_data[key] = str(value) - with open(config_file_path, 'w') as config_file: - json.dump(config_data, config_file, indent=4, sort_keys=True) - config_file.write('\n') - - except IOError as e: - msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + pass +# XXX: refactor def update_config_file(config_file_path, key, value): config_file_path = Path(config_file_path) with config_file_path.open(mode='r') as f: @@ -226,9 +189,9 @@ def delete_symlink(symlink_path): logging.error(f"Error removing symlink: {e}") -def install_dependencies(app=None): - if config.TARGETVERSION: - targetversion = int(config.TARGETVERSION) +def install_dependencies(app: App): + if app.conf.faithlife_product_version: + targetversion = int(app.conf.faithlife_product_version) else: targetversion = 10 msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) @@ -243,7 +206,7 @@ def install_dependencies(app=None): app=app ) else: - logging.error(f"TARGETVERSION not found: {config.TARGETVERSION}.") + logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") if config.DIALOG == "tk": # FIXME: This should get moved to gui_app. @@ -258,40 +221,6 @@ def file_exists(file_path): return False -def change_logos_release_channel(): - if config.logos_release_channel == "stable": - config.logos_release_channel = "beta" - update_config_file( - config.CONFIG_FILE, - 'logos_release_channel', - "beta" - ) - else: - config.logos_release_channel = "stable" - update_config_file( - config.CONFIG_FILE, - 'logos_release_channel', - "stable" - ) - - -def change_lli_release_channel(): - if config.lli_release_channel == "stable": - config.logos_release_channel = "dev" - update_config_file( - config.CONFIG_FILE, - 'lli_release_channel', - "dev" - ) - else: - config.lli_release_channel = "stable" - update_config_file( - config.CONFIG_FILE, - 'lli_release_channel', - "stable" - ) - - def get_current_logos_version(): path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) @@ -334,14 +263,6 @@ def convert_logos_release(logos_release): ] return logos_release_arr - -def which_release(): - if config.current_logos_release: - return config.current_logos_release - else: - return config.TARGET_RELEASE_VERSION - - def check_logos_release_version(version, threshold, check_version_part): if version is not None: version_parts = list(map(int, version.split('.'))) @@ -438,12 +359,9 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li def get_winetricks_options(): local_winetricks_path = shutil.which('winetricks') - winetricks_options = ['Download', 'Return to Main Menu'] + winetricks_options = ['Download'] if local_winetricks_path is not None: - # Check if local winetricks version is up-to-date. - cmd = ["winetricks", "--version"] - local_winetricks_version = subprocess.check_output(cmd).split()[0] - if str(local_winetricks_version) != constants.WINETRICKS_VERSION: #noqa: E501 + if check_winetricks_version(local_winetricks_path): winetricks_options.insert(0, local_winetricks_path) else: logging.info("Local winetricks is too old.") @@ -451,6 +369,12 @@ def get_winetricks_options(): logging.info("Local winetricks not found.") return winetricks_options +def check_winetricks_version(winetricks_path: str) -> bool: + # Check if local winetricks version matches expected + cmd = [winetricks_path, "--version"] + local_winetricks_version = subprocess.check_output(cmd).split()[0] + return str(local_winetricks_version) == constants.WINETRICKS_VERSION #noqa: E501 + def get_procs_using_file(file_path, mode=None): procs = set() @@ -510,10 +434,10 @@ def app_is_installed(): return config.LOGOS_EXE is not None and os.access(config.LOGOS_EXE, os.X_OK) # noqa: E501 -def find_installed_product() -> Optional[str]: - if config.FLPRODUCT and config.WINEPREFIX: - drive_c = Path(f"{config.WINEPREFIX}/drive_c/") - name = config.FLPRODUCT +def find_installed_product(faithlife_product: str, wine_prefix: str) -> Optional[str]: + if faithlife_product and wine_prefix: + drive_c = Path(f"{wine_prefix}/drive_c/") + name = faithlife_product exe = None for root, _, files in drive_c.walk(follow_symlinks=False): if root.name == name and f"{name}.exe" in files: @@ -609,40 +533,36 @@ def compare_logos_linux_installer_version( return status, message -def compare_recommended_appimage_version(): +def compare_recommended_appimage_version(app: App): status = None message = None wine_release = [] - wine_exe_path = get_wine_exe_path() - if wine_exe_path is not None: - wine_release, error_message = wine.get_wine_release(wine_exe_path) - if wine_release is not None and wine_release is not False: - current_version = '.'.join([str(n) for n in wine_release[:2]]) - logging.debug(f"Current wine release: {current_version}") - - if config.RECOMMENDED_WINE64_APPIMAGE_VERSION: - logging.debug(f"Recommended wine release: {config.RECOMMENDED_WINE64_APPIMAGE_VERSION}") # noqa: E501 - if current_version < config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Current release is older than recommended. - status = 0 - message = "yes" - elif current_version == config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Current release is latest. - status = 1 - message = "uptodate" - elif current_version > config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Installed version is custom - status = 2 - message = "no" - else: - status = False - message = f"Error: {error_message}" + wine_exe_path = app.conf.wine_binary + wine_release, error_message = wine.get_wine_release(wine_exe_path) + if wine_release is not None and wine_release is not False: + current_version = '.'.join([str(n) for n in wine_release[:2]]) + logging.debug(f"Current wine release: {current_version}") + + if config.RECOMMENDED_WINE64_APPIMAGE_VERSION: + logging.debug(f"Recommended wine release: {config.RECOMMENDED_WINE64_APPIMAGE_VERSION}") # noqa: E501 + if current_version < config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 + # Current release is older than recommended. + status = 0 + message = "yes" + elif current_version == config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 + # Current release is latest. + status = 1 + message = "uptodate" + elif current_version > config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 + # Installed version is custom + status = 2 + message = "no" else: status = False message = f"Error: {error_message}" else: status = False - message = "config.WINE_EXE is not set." + message = f"Error: {error_message}" logging.debug(f"{status=}; {message=}") return status, message @@ -712,7 +632,8 @@ def check_appimage(filestr): return False -def find_appimage_files(release_version, app=None): +def find_appimage_files(app: App): + release_version = config.current_logos_version or app.conf.faithlife_product_version appimages = [] directories = [ os.path.expanduser("~") + "/bin", @@ -785,7 +706,7 @@ def find_wine_binary_files(release_version): return binaries -def set_appimage_symlink(app=None): +def set_appimage_symlink(app: App): # This function assumes make_skel() has been run once. # if config.APPIMAGE_FILE_PATH is None: # config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 @@ -797,7 +718,7 @@ def set_appimage_symlink(app=None): appimage_symlink_path = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME if appimage_file_path.name == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 # Default case. - network.get_recommended_appimage() + network.get_recommended_appimage(app) selected_appimage_file_path = Path(config.APPDIR_BINDIR) / appimage_file_path.name # noqa: E501 bindir_appimage = selected_appimage_file_path / config.APPDIR_BINDIR if not bindir_appimage.exists(): @@ -858,11 +779,11 @@ def update_to_latest_lli_release(app=None): logging.debug(f"{constants.APP_NAME} is at a newer version than the latest.") # noqa: 501 -def update_to_latest_recommended_appimage(): +def update_to_latest_recommended_appimage(app: App): config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 - status, _ = compare_recommended_appimage_version() + status, _ = compare_recommended_appimage_version(app) if status == 0: - set_appimage_symlink() + set_appimage_symlink(app) elif status == 1: logging.debug("The AppImage is already set to the latest recommended.") elif status == 2: @@ -980,20 +901,6 @@ def get_config_var(var): else: return None - -def get_wine_exe_path(): - if config.WINE_EXE is not None: - path = get_relative_path( - get_config_var(config.WINE_EXE), - config.INSTALLDIR - ) - wine_exe_path = Path(create_dynamic_path(path, config.INSTALLDIR)) - logging.debug(f"{wine_exe_path=}") - return wine_exe_path - else: - return None - - def stopwatch(start_time=None, interval=10.0): if start_time is None: start_time = time.time() diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index ca4535b2..8242d1f9 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -5,7 +5,8 @@ import signal import subprocess from pathlib import Path -from typing import Optional + +from ou_dedetai.app import App from . import config from . import constants @@ -16,29 +17,6 @@ from .config import processes - -def get_wine_user(): - path: Optional[str] = config.LOGOS_EXE - normalized_path: str = os.path.normpath(path) - path_parts = normalized_path.split(os.sep) - config.wine_user = path_parts[path_parts.index('users') + 1] - - -def set_logos_paths(): - if config.wine_user is None: - get_wine_user() - logos_cmds = [ - config.logos_cef_cmd, - config.logos_indexer_cmd, - config.logos_login_cmd, - ] - if None in logos_cmds: - config.logos_cef_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 - config.logos_indexer_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - config.logos_login_cmd = f'C:\\users\\{config.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 - config.logos_indexer_exe = str(Path(utils.find_installed_product()).parent / 'System' / 'LogosIndexer.exe') # noqa: E501 - - def check_wineserver(): try: process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) @@ -123,19 +101,19 @@ def get_wine_release(binary): return False, f"Error: {e}" -def check_wine_rules(wine_release, release_version): +def check_wine_rules(wine_release, release_version, faithlife_product_version: str): # Does not check for Staging. Will not implement: expecting merging of # commits in time. logging.debug(f"Checking {wine_release} for {release_version}.") - if config.TARGETVERSION == "10": + if faithlife_product_version == "10": if utils.check_logos_release_version(release_version, 30, 1): required_wine_minimum = [7, 18] else: required_wine_minimum = [9, 10] - elif config.TARGETVERSION == "9": + elif faithlife_product_version == "9": required_wine_minimum = [7, 0] else: - raise ValueError(f"Invalid TARGETVERSION: {config.TARGETVERSION} ({type(config.TARGETVERSION)})") # noqa: E501 + raise ValueError(f"Invalid target version, expecting 9 or 10 but got: {faithlife_product_version} ({type(faithlife_product_version)})") # noqa: E501 rules = [ { @@ -234,9 +212,9 @@ def check_wine_version_and_branch(release_version, test_binary): return True, "None" -def initializeWineBottle(app=None): +def initializeWineBottle(app: App): msg.status("Initializing wine bottle…") - wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') + wine_exe = app.conf.wine64_binary logging.debug(f"{wine_exe=}") # Avoid wine-mono window orig_overrides = config.WINEDLLOVERRIDES @@ -252,11 +230,11 @@ def initializeWineBottle(app=None): return process -def wine_reg_install(reg_file): +def wine_reg_install(reg_file, wine64_binary): reg_file = str(reg_file) msg.status(f"Installing registry file: {reg_file}") process = run_wine_proc( - str(utils.get_wine_exe_path().parent / 'wine64'), + wine64_binary, exe="regedit.exe", exe_args=[reg_file] ) @@ -273,21 +251,21 @@ def wine_reg_install(reg_file): wineserver_wait() -def disable_winemenubuilder(): +def disable_winemenubuilder(wine64_binary: str): reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' reg_file.write_text(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') - wine_reg_install(reg_file) + wine_reg_install(reg_file, wine64_binary) -def install_msi(app=None): +def install_msi(app: App): msg.status(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.", app) # Execute the .MSI - wine_exe = str(utils.get_wine_exe_path().parent / 'wine64') - exe_args = ["/i", f"{config.INSTALLDIR}/data/{config.LOGOS_EXECUTABLE}"] + wine_exe = app.conf.wine64_binary + exe_args = ["/i", f"{app.conf.install_dir}/data/{config.LOGOS_EXECUTABLE}"] if config.PASSIVE is True: exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") @@ -299,7 +277,7 @@ def wait_pid(process): os.waitpid(-process.pid, 0) -def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): +def run_wine_proc(winecmd, app: App, exe=None, exe_args=list(), init=False): logging.debug("Getting wine environment.") env = get_wine_env() if not init and config.WINECMD_ENCODING is None: @@ -307,7 +285,8 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): logging.debug("Getting wine system's cmd.exe encoding.") registry_value = get_registry_value( 'HKCU\\Software\\Wine\\Fonts', - 'Codepages' + 'Codepages', + app ) if registry_value is not None: codepages = registry_value.split(',') # noqa: E501 @@ -364,36 +343,36 @@ def run_wine_proc(winecmd, exe=None, exe_args=list(), init=False): return process -def run_winetricks(cmd=None): - process = run_wine_proc(config.WINETRICKSBIN, exe=cmd) +def run_winetricks(app: App, cmd=None): + process = run_wine_proc(app.conf.winetricks_binary, exe=cmd) wait_pid(process) wineserver_wait() - -def run_winetricks_cmd(*args): +# XXX: this function looks similar to the one above. duplicate? +def run_winetricks_cmd(app: App, *args): cmd = [*args] msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") - process = run_wine_proc(config.WINETRICKSBIN, exe_args=cmd) + process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") # heavy_wineserver_wait() wineserver_wait() - logging.debug(f"procs using {config.WINEPREFIX}:") - for proc in utils.get_procs_using_file(config.WINEPREFIX): + logging.debug(f"procs using {app.conf.wine_prefix}:") + for proc in utils.get_procs_using_file(app.conf.wine_prefix): logging.debug(f"{proc=}") else: logging.debug('') -def install_d3d_compiler(): +def install_d3d_compiler(app: App): cmd = ['d3dcompiler_47'] if config.WINETRICKS_UNATTENDED is None: cmd.insert(0, '-q') - run_winetricks_cmd(*cmd) + run_winetricks_cmd(app, *cmd) -def install_fonts(): +def install_fonts(app: App): msg.status("Configuring fonts…") fonts = ['corefonts', 'tahoma'] if not config.SKIP_FONTS: @@ -401,26 +380,26 @@ def install_fonts(): args = [f] if config.WINETRICKS_UNATTENDED: args.insert(0, '-q') - run_winetricks_cmd(*args) + run_winetricks_cmd(app, *args) -def install_font_smoothing(): +def install_font_smoothing(app: App): msg.status("Setting font smoothing…") args = ['settings', 'fontsmooth=rgb'] if config.WINETRICKS_UNATTENDED: args.insert(0, '-q') - run_winetricks_cmd(*args) + run_winetricks_cmd(app, *args) -def set_renderer(renderer): - run_winetricks_cmd("-q", "settings", f"renderer={renderer}") +def set_renderer(app: App, renderer: str): + run_winetricks_cmd(app, "-q", "settings", f"renderer={renderer}") -def set_win_version(exe, windows_version): +def set_win_version(app: App, exe: str, windows_version: str): if exe == "logos": - run_winetricks_cmd('-q', 'settings', f'{windows_version}') + run_winetricks_cmd(app, '-q', 'settings', f'{windows_version}') elif exe == "indexer": - reg = f"HKCU\\Software\\Wine\\AppDefaults\\{config.FLPRODUCT}Indexer.exe" # noqa: E501 + reg = f"HKCU\\Software\\Wine\\AppDefaults\\{app.conf.faithlife_product}Indexer.exe" # noqa: E501 exe_args = [ 'add', reg, @@ -429,14 +408,14 @@ def set_win_version(exe, windows_version): "/d", f"{windows_version}", "/f", ] process = run_wine_proc( - str(utils.get_wine_exe_path()), + app.conf.wine_binary, exe='reg', exe_args=exe_args ) wait_pid(process) -def enforce_icu_data_files(app=None): +def enforce_icu_data_files(app: App): repo = "FaithLife-Community/icu" json_data = network.get_latest_release_data(repo) icu_url = network.get_first_asset_url(json_data) @@ -454,7 +433,7 @@ def enforce_icu_data_files(app=None): config.MYDOWNLOADS, app=app ) - drive_c = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c" + drive_c = f"{app.conf.wine_prefix}/drive_c" utils.untar_file(f"{config.MYDOWNLOADS}/{icu_filename}", drive_c) # Ensure the target directory exists @@ -472,7 +451,7 @@ def enforce_icu_data_files(app=None): app.install_icu_e.set() -def get_registry_value(reg_path, name): +def get_registry_value(reg_path, name, app: App): logging.debug(f"Get value for: {reg_path=}; {name=}") # NOTE: Can't use run_wine_proc here because of infinite recursion while # trying to determine WINECMD_ENCODING. @@ -480,7 +459,7 @@ def get_registry_value(reg_path, name): env = get_wine_env() cmd = [ - str(utils.get_wine_exe_path().parent / 'wine64'), + app.conf.wine64_binary, 'reg', 'query', reg_path, '/v', name, ] err_msg = f"Failed to get registry value: {reg_path}\\{name}" @@ -556,20 +535,20 @@ def get_wine_branch(binary): return get_mscoree_winebranch(mscoree64) -def get_wine_env(): +def get_wine_env(app: App): wine_env = os.environ.copy() - winepath = utils.get_wine_exe_path() + winepath = Path(app.conf.wine_binary) if winepath.name != 'wine64': # AppImage # Winetricks commands can fail if 'wine64' is not explicitly defined. # https://github.com/Winetricks/winetricks/issues/2084#issuecomment-1639259359 - winepath = winepath.parent / 'wine64' + winepath = app.conf.wine64_binary wine_env_defaults = { 'WINE': str(winepath), 'WINEDEBUG': config.WINEDEBUG, 'WINEDLLOVERRIDES': config.WINEDLLOVERRIDES, 'WINELOADER': str(winepath), - 'WINEPREFIX': config.WINEPREFIX, - 'WINESERVER': config.WINESERVER_EXE, + 'WINEPREFIX': app.conf.wine_prefix, + 'WINESERVER': app.conf.wineserver_binary, # The following seems to cause some winetricks commands to fail; e.g. # 'winetricks settings win10' exits with ec = 1 b/c it fails to find # %ProgramFiles%, %AppData%, etc. @@ -577,17 +556,6 @@ def get_wine_env(): } for k, v in wine_env_defaults.items(): wine_env[k] = v - # if config.LOG_LEVEL > logging.INFO: - # wine_env['WINETRICKS_SUPER_QUIET'] = "1" - - # Config file takes precedence over the above variables. - cfg = config.get_config_file_dict(config.CONFIG_FILE) - if cfg is not None: - for key, value in cfg.items(): - if value is None: - continue # or value = ''? - if key in wine_env_defaults.keys(): - wine_env[key] = value updated_env = {k: wine_env.get(k) for k in wine_env_defaults.keys()} logging.debug(f"Wine env: {updated_env}") From 630bbcf108831a1451efd29dd7f94a599ba6bcb4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:11:40 -0800 Subject: [PATCH 008/137] feat: load legacy envs into new framework and migrate APPDIR_BINDIR and WINESERVER_EXE --- ou_dedetai/app.py | 85 +++++++++++++++++++++++++++++++++++++++-- ou_dedetai/config.py | 5 --- ou_dedetai/gui_app.py | 4 +- ou_dedetai/installer.py | 23 +++++------ ou_dedetai/logos.py | 12 +++--- ou_dedetai/network.py | 4 +- ou_dedetai/utils.py | 32 ++++++++-------- ou_dedetai/wine.py | 43 ++++++++------------- 8 files changed, 134 insertions(+), 74 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 39864cc6..af3c1f4a 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -69,6 +69,57 @@ def update_progress(self, message: str, percent: Optional[int] = None): """Updates the progress of the current operation""" pass +# XXX: What about legacy envs? From the extended config? +# Like APPDIR_BINDIR? This no longer can be modified directly, unless we store an override. + +@dataclass +class LegacyEnvOverrides: + """Previous versions of the installer allowed some values to be overridden by environment. + This keeps that compatibility.""" + APPIMAGE_LINK_SELECTION_NAME: Optional[str] + APPDIR_BINDIR: Optional[str] + CHECK_UPDATES: Optional[bool] + CONFIG_FILE: Optional[str] + CUSTOMBINPATH: Optional[str] + DEBUG: Optional[bool] + DELETE_LOG: Optional[str] + DIALOG: Optional[str] + # XXX: default used to be `os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log")` + LOGOS_LOG: Optional[str] + # XXX: default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` + wine_log: Optional[str] + LOGOS_EXE: Optional[str] + LOGOS_EXECUTABLE: Optional[str] + LOGOS_VERSION: Optional[str] + # XXX: Default value used to be "Logos-x64.msi", we may have to handle this + LOGOS64_MSI: Optional[str] + LOGOS64_URL: Optional[str] + REINSTALL_DEPENDENCIES: Optional[bool] + SELECTED_APPIMAGE_FILENAME: Optional[str] + SKIP_DEPENDENCIES: Optional[bool] + SKIP_FONTS: Optional[bool] + SKIP_WINETRICKS: Optional[bool] + use_python_dialog: Optional[str] + VERBOSE: Optional[bool] + WINEBIN_CODE: Optional[str] + # XXX: move this out of this struct + WINEDEBUG: Optional[str] = "fixme-all,err-all", + WINEDLLOVERRIDES: Optional[str] + WINEPREFIX: Optional[str] + WINE_EXE: Optional[str] + WINESERVER_EXE: Optional[str] + WINETRICKS_UNATTENDED: Optional[str] + + @classmethod + def from_env() -> "LegacyEnvOverrides": + legacy_envs = LegacyEnvOverrides() + # Now update from ENV + for var in LegacyEnvOverrides().__dict__.keys(): + if os.getenv(var) is not None: + legacy_envs[var] = os.getenv(var) + return legacy_envs + + # XXX: move these configs into config.py once it's cleared out @dataclass class LegacyConfiguration: @@ -144,6 +195,28 @@ def from_file_and_env() -> "LegacyConfiguration": return config_dict +@dataclass +class EnvironmentOverrides: + """Allows some values to be overridden from environment. + + The actually name of the environment variables remains unchanged from before, + this translates the environment variable names to the new variable names""" + + installer_binary_directory: Optional[str] + wineserver_binary: Optional[str] + + @classmethod + def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": + EnvironmentOverrides( + installer_binary_directory=legacy.APPDIR_BINDIR, + wineserver_binary=legacy.WINESERVER_EXE + ) + + @classmethod + def from_env() -> "EnvironmentOverrides": + return EnvironmentOverrides.from_legacy(LegacyEnvOverrides.from_env()) + + @dataclass class UserConfiguration: """This is the class that actually stores the values. @@ -171,7 +244,7 @@ class UserConfiguration: @classmethod def read_from_file_and_env() -> "UserConfiguration": # First read in the legacy configuration - new_config = UserConfiguration.from_legacy(LegacyConfiguration.from_file_and_env()) + new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.from_file_and_env()) # Then read the file again this time looking for the new keys config_file_path = LegacyConfiguration.config_file_path() @@ -247,6 +320,9 @@ class Config: # Storage for the keys _raw: UserConfiguration + + # Overriding programmatically generated values from ENV + _overrides: EnvironmentOverrides _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -368,8 +444,10 @@ def install_dir(self) -> str: return output @property - # XXX: used to be called APPDIR_BINDIR + # This used to be called APPDIR_BINDIR def installer_binary_directory(self) -> str: + if self._overrides.installer_binary_directory is not None: + return self._overrides.installer_binary_directory return f"{self.install_dir}/data/bin" @property @@ -384,6 +462,7 @@ def wine_binary(self) -> str: question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 network.set_recommended_appimage_config() options = utils.get_wine_options( + self, utils.find_appimage_files(self.faithlife_product_release), utils.find_wine_binary_files(self.faithlife_product_release) ) @@ -416,7 +495,7 @@ def wine64_binary(self) -> str: return str(Path(self.wine_binary).parent / 'wine64') @property - # XXX: used to be called WINESERVER_EXE + # This used to be called WINESERVER_EXE def wineserver_binary(self) -> str: return str(Path(self.wine_binary).parent / 'wineserver') diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index c0c4af59..1a445940 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -23,7 +23,6 @@ # Define and set additional variables that can be set in the env. extended_config = { 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', - 'APPDIR_BINDIR': None, 'CHECK_UPDATES': False, 'CONFIG_FILE': None, 'CUSTOMBINPATH': None, @@ -49,7 +48,6 @@ 'WINEDLLOVERRIDES': '', 'WINEPREFIX': None, 'WINE_EXE': None, - 'WINESERVER_EXE': None, 'WINETRICKS_UNATTENDED': None, } for key, default in extended_config.items(): @@ -163,9 +161,6 @@ def set_config_env(config_file_path): for key, value in config_dict.items(): globals()[key] = value installdir = config_dict.get('INSTALLDIR') - if installdir: - global APPDIR_BINDIR - APPDIR_BINDIR = f"{installdir}/data/bin" # XXX: remove this def get_env_config(): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index bac75303..e5cdb2ef 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -406,9 +406,9 @@ def start_wine_versions_check(self, release_version): # Start thread. utils.start_thread( utils.get_wine_options, + self, self.appimages, utils.find_wine_binary_files(release_version), - app=self, ) def set_wine(self, evt=None): @@ -754,7 +754,7 @@ def get_winetricks(self, evt=None): self.gui.statusvar.set("Installing Winetricks…") utils.start_thread( system.install_winetricks, - config.APPDIR_BINDIR, + self.conf.installer_binary_directory, app=self ) self.update_run_winetricks_button() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index eb51784a..ca9f678c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -78,7 +78,7 @@ def ensure_wine_choice(app: App): if str(app.conf.wine_binary).lower().endswith('.appimage'): config.SELECTED_APPIMAGE_FILENAME = str(app.conf.wine_binary) if not config.WINEBIN_CODE: - config.WINEBIN_CODE = utils.get_winebin_code_and_desc(app.conf.wine_binary)[0] # noqa: E501 + config.WINEBIN_CODE = utils.get_winebin_code_and_desc(app, app.conf.wine_binary)[0] # noqa: E501 logging.debug(f"> {config.SELECTED_APPIMAGE_FILENAME=}") logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_URL=}") @@ -181,7 +181,7 @@ def ensure_install_dirs(app: App): logging.debug(f"> {bin_dir} exists?: {bin_dir.is_dir()}") logging.debug(f"> config.INSTALLDIR={app.conf.installer_binary_directory}") - logging.debug(f"> {config.APPDIR_BINDIR=}") + logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_directory}") wine_dir = Path(f"{app.conf.wine_prefix}") wine_dir.mkdir(parents=True, exist_ok=True) @@ -248,22 +248,17 @@ def ensure_wine_executables(app: App): logging.debug('- wine64') logging.debug('- wineserver') - # Add APPDIR_BINDIR to PATH. if not os.access(app.conf.wine_binary, os.X_OK): msg.status("Creating wine appimage symlinks…", app=app) create_wine_appimage_symlinks(app=app) - # Set WINESERVER_EXE. - config.WINESERVER_EXE = f"{config.APPDIR_BINDIR}/wineserver" - # PATH is modified if wine appimage isn't found, but it's not modified # during a restarted installation, so shutil.which doesn't find the # executables in that case. - logging.debug(f"> {config.WINESERVER_EXE=}") - logging.debug(f"> wine path: {config.APPDIR_BINDIR}/wine") - logging.debug(f"> wine64 path: {config.APPDIR_BINDIR}/wine64") - logging.debug(f"> wineserver path: {config.APPDIR_BINDIR}/wineserver") - logging.debug(f"> winetricks path: {config.APPDIR_BINDIR}/winetricks") + logging.debug(f"> wine path: {app.conf.wine_binary}") + logging.debug(f"> wine64 path: {app.conf.wine64_binary}") + logging.debug(f"> wineserver path: {app.conf.wineserver_binary}") + logging.debug(f"> winetricks path: {app.conf.winetricks_binary}") def ensure_winetricks_executable(app: App): @@ -363,7 +358,7 @@ def ensure_wineprefix_init(app: App): process = wine.initializeWineBottle(app) wine.wait_pid(process) # wine.light_wineserver_wait() - wine.wineserver_wait() + wine.wineserver_wait(app) logging.debug("Wine init complete.") logging.debug(f"> {init_file} exists?: {init_file.is_file()}") @@ -391,7 +386,7 @@ def ensure_winetricks_applied(app: App): if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): msg.status("Disabling winemenubuilder…", app) - wine.disable_winemenubuilder(app.conf.wine64_binary) + wine.disable_winemenubuilder(app, app.conf.wine64_binary) if not utils.grep(r'"renderer"="gdi"', usr_reg): msg.status("Setting Renderer to GDI…", app) @@ -418,7 +413,7 @@ def ensure_winetricks_applied(app: App): msg.logos_msg(f"Setting {app.conf.faithlife_product} Bible Indexing to Win10 Mode…") # noqa: E501 wine.set_win_version(app, "indexer", "win10") # wine.light_wineserver_wait() - wine.wineserver_wait() + wine.wineserver_wait(app) logging.debug("> Done.") diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 1000003a..512c028b 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -90,7 +90,7 @@ def run_logos(): if not good_wine: msg.logos_error(reason, app=self) else: - wine.wineserver_kill() + wine.wineserver_kill(app=self.app) app = self.app if config.DIALOG == 'tk': # Don't send "Running" message to GUI b/c it never clears. @@ -138,7 +138,7 @@ def stop(self): else: logging.debug("No Logos processes to stop.") self.logos_state = State.STOPPED - wine.wineserver_wait() + wine.wineserver_wait(app) def index(self): self.indexing_state = State.STARTING @@ -171,9 +171,9 @@ def wait_on_indexing(): index_finished.wait() self.indexing_state = State.STOPPED msg.status("Indexing has finished.", self.app) - wine.wineserver_wait() + wine.wineserver_wait(app=self.app) - wine.wineserver_kill() + wine.wineserver_kill(app=self.app) msg.status("Indexing has begun…", self.app) index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING @@ -215,7 +215,7 @@ def stop_indexing(self): else: logging.debug("No LogosIndexer processes to stop.") self.indexing_state = State.STOPPED - wine.wineserver_wait() + wine.wineserver_wait(app=self.app) def get_app_logging_state(self, init=False): state = 'DISABLED' @@ -266,7 +266,7 @@ def switch_logging(self, action=None): exe_args=exe_args ) wine.wait_pid(process) - wine.wineserver_wait() + wine.wineserver_wait(app=self.app) config.LOGS = state if config.DIALOG in ['curses', 'dialog', 'tk']: self.app.logging_q.put(state) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 2b2da11a..1b53ea19 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -483,14 +483,14 @@ def check_for_updates(): def get_recommended_appimage(app: App): wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 - dest_path = Path(config.APPDIR_BINDIR) / wine64_appimage_full_filename + dest_path = Path(app.conf.installer_binary_directory) / wine64_appimage_full_filename if dest_path.is_file(): return else: logos_reuse_download( config.RECOMMENDED_WINE64_APPIMAGE_URL, config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, - config.APPDIR_BINDIR, + app.conf.installer_binary_directory, app=app ) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 3bf94bdd..7a115fc9 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -275,7 +275,8 @@ def filter_versions(versions, threshold, check_version_part): return [version for version in versions if check_logos_release_version(version, threshold, check_version_part)] # noqa: E501 -def get_winebin_code_and_desc(binary): +# XXX: figure this out and fold into config +def get_winebin_code_and_desc(app: App, binary): # Set binary code, description, and path based on path codes = { "Recommended": "Use the recommended AppImage", @@ -296,7 +297,7 @@ def get_winebin_code_and_desc(binary): # Does it work? if isinstance(binary, Path): binary = str(binary) - if binary == f"{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 + if binary == f"{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 code = "Recommended" elif binary.lower().endswith('.appimage'): code = "AppImage" @@ -313,20 +314,20 @@ def get_winebin_code_and_desc(binary): return code, desc -def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], List[str]]: # noqa: E501 +def get_wine_options(app: App, appimages, binaries) -> Union[List[List[str]], List[str]]: # noqa: E501 logging.debug(f"{appimages=}") logging.debug(f"{binaries=}") wine_binary_options = [] # Add AppImages to list # if config.DIALOG == 'tk': - wine_binary_options.append(f"{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 + wine_binary_options.append(f"{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 wine_binary_options.extend(appimages) # else: # appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 # wine_binary_options.append([ # "Recommended", # Code - # f'{config.APPDIR_BINDIR}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}', # noqa: E501 + # f'{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}', # noqa: E501 # f"AppImage of Wine64 {config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION}" # noqa: E501 # ]) # wine_binary_options.extend(appimage_entries) @@ -335,7 +336,7 @@ def get_wine_options(appimages, binaries, app=None) -> Union[List[List[str]], Li logging.debug(f"{sorted_binaries=}") for WINEBIN_PATH in sorted_binaries: - WINEBIN_CODE, WINEBIN_DESCRIPTION = get_winebin_code_and_desc(WINEBIN_PATH) # noqa: E501 + WINEBIN_CODE, WINEBIN_DESCRIPTION = get_winebin_code_and_desc(app, WINEBIN_PATH) # noqa: E501 # Create wine binary option array # if config.DIALOG == 'tk': @@ -637,7 +638,7 @@ def find_appimage_files(app: App): appimages = [] directories = [ os.path.expanduser("~") + "/bin", - config.APPDIR_BINDIR, + app.conf.installer_binary_directory, config.MYDOWNLOADS ] if config.CUSTOMBINPATH is not None: @@ -714,16 +715,16 @@ def set_appimage_symlink(app: App): logging.debug(f"{config.APPIMAGE_FILE_PATH=}") logging.debug(f"{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") appimage_file_path = Path(config.APPIMAGE_FILE_PATH) - appdir_bindir = Path(config.APPDIR_BINDIR) + appdir_bindir = Path(app.conf.installer_binary_directory) appimage_symlink_path = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME if appimage_file_path.name == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 # Default case. network.get_recommended_appimage(app) - selected_appimage_file_path = Path(config.APPDIR_BINDIR) / appimage_file_path.name # noqa: E501 - bindir_appimage = selected_appimage_file_path / config.APPDIR_BINDIR + selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 + bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_directory # noqa: E501 if not bindir_appimage.exists(): - logging.info(f"Copying {selected_appimage_file_path} to {config.APPDIR_BINDIR}.") # noqa: E501 - shutil.copy(selected_appimage_file_path, f"{config.APPDIR_BINDIR}") + logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_directory}.") # noqa: E501 + shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_directory}") else: selected_appimage_file_path = appimage_file_path # Verify user-selected AppImage. @@ -733,8 +734,9 @@ def set_appimage_symlink(app: App): # Determine if user wants their AppImage in the app bin dir. copy_message = ( f"Should the program copy {selected_appimage_file_path} to the" - f" {config.APPDIR_BINDIR} directory?" + f" {app.conf.installer_binary_directory} directory?" ) + # XXX: move this to .ask # FIXME: What if user cancels the confirmation dialog? if config.DIALOG == "tk": # TODO: With the GUI this runs in a thread. It's not clear if the @@ -751,10 +753,10 @@ def set_appimage_symlink(app: App): # Copy AppImage if confirmed. if confirm is True or confirm == 'yes': - logging.info(f"Copying {selected_appimage_file_path} to {config.APPDIR_BINDIR}.") # noqa: E501 + logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_directory}.") # noqa: E501 dest = appdir_bindir / selected_appimage_file_path.name if not dest.exists(): - shutil.copy(selected_appimage_file_path, f"{config.APPDIR_BINDIR}") # noqa: E501 + shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_directory}") # noqa: E501 selected_appimage_file_path = dest delete_symlink(appimage_symlink_path) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 8242d1f9..e71bf50d 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -17,38 +17,29 @@ from .config import processes -def check_wineserver(): +def check_wineserver(app: App): try: - process = run_wine_proc(config.WINESERVER, exe_args=["-p"]) + # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of WINESERVER_EXE + # changed it to use wineserver_binary, this change may alter the behavior, to match what the code intended + process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-p"]) wait_pid(process) return process.returncode == 0 except Exception: return False -def wineserver_kill(): - if check_wineserver(): - process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-k"]) +def wineserver_kill(app: App): + if check_wineserver(app): + process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-k"]) wait_pid(process) -def wineserver_wait(): - if check_wineserver(): - process = run_wine_proc(config.WINESERVER_EXE, exe_args=["-w"]) +def wineserver_wait(app: App): + if check_wineserver(app): + process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-w"]) wait_pid(process) -# def light_wineserver_wait(): -# command = [f"{config.WINESERVER_EXE}", "-w"] -# system.wait_on(command) - - -# def heavy_wineserver_wait(): -# utils.wait_process_using_dir(config.WINEPREFIX) -# # system.wait_on([f"{config.WINESERVER_EXE}", "-w"]) -# wineserver_wait() - - def end_wine_processes(): for process_name, process in processes.items(): if isinstance(process, subprocess.Popen): @@ -230,7 +221,7 @@ def initializeWineBottle(app: App): return process -def wine_reg_install(reg_file, wine64_binary): +def wine_reg_install(app: App, reg_file, wine64_binary): reg_file = str(reg_file) msg.status(f"Installing registry file: {reg_file}") process = run_wine_proc( @@ -247,18 +238,17 @@ def wine_reg_install(reg_file, wine64_binary): msg.logos_error(f"{failed}: {reg_file}") elif process.returncode == 0: logging.info(f"{reg_file} installed.") - # light_wineserver_wait() - wineserver_wait() + wineserver_wait(app) -def disable_winemenubuilder(wine64_binary: str): +def disable_winemenubuilder(app: App, wine64_binary: str): reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' reg_file.write_text(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') - wine_reg_install(reg_file, wine64_binary) + wine_reg_install(app, reg_file, wine64_binary) def install_msi(app: App): @@ -346,7 +336,7 @@ def run_wine_proc(winecmd, app: App, exe=None, exe_args=list(), init=False): def run_winetricks(app: App, cmd=None): process = run_wine_proc(app.conf.winetricks_binary, exe=cmd) wait_pid(process) - wineserver_wait() + wineserver_wait(app) # XXX: this function looks similar to the one above. duplicate? def run_winetricks_cmd(app: App, *args): @@ -356,8 +346,7 @@ def run_winetricks_cmd(app: App, *args): process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") - # heavy_wineserver_wait() - wineserver_wait() + wineserver_wait(app) logging.debug(f"procs using {app.conf.wine_prefix}:") for proc in utils.get_procs_using_file(app.conf.wine_prefix): logging.debug(f"{proc=}") From c68e29f008f2b90c7846cc97e814fb6fc9ffb119 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:24:50 -0800 Subject: [PATCH 009/137] fix: migrate CUSTOMBINPATH --- ou_dedetai/app.py | 8 ++++++-- ou_dedetai/config.py | 1 - ou_dedetai/gui_app.py | 2 +- ou_dedetai/main.py | 3 ++- ou_dedetai/utils.py | 11 ++++++----- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index af3c1f4a..1fbc71bf 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -204,12 +204,16 @@ class EnvironmentOverrides: installer_binary_directory: Optional[str] wineserver_binary: Optional[str] + # Additional path to look for when searching for binaries. + # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) + custom_binary_path: Optional[str] @classmethod def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": EnvironmentOverrides( installer_binary_directory=legacy.APPDIR_BINDIR, - wineserver_binary=legacy.WINESERVER_EXE + wineserver_binary=legacy.WINESERVER_EXE, + custom_binary_path=legacy.CUSTOMBINPATH ) @classmethod @@ -464,7 +468,7 @@ def wine_binary(self) -> str: options = utils.get_wine_options( self, utils.find_appimage_files(self.faithlife_product_release), - utils.find_wine_binary_files(self.faithlife_product_release) + utils.find_wine_binary_files(self.app, self.faithlife_product_release) ) choice = self.app.ask(question, options) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 1a445940..e1d93ae7 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -25,7 +25,6 @@ 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', 'CHECK_UPDATES': False, 'CONFIG_FILE': None, - 'CUSTOMBINPATH': None, 'DEBUG': False, 'DELETE_LOG': None, 'DIALOG': None, diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index e5cdb2ef..7cae5c25 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -408,7 +408,7 @@ def start_wine_versions_check(self, release_version): utils.get_wine_options, self, self.appimages, - utils.find_wine_binary_files(release_version), + utils.find_wine_binary_files(self, release_version), ) def set_wine(self, evt=None): diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 6661ece3..60ec7d5b 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -235,7 +235,8 @@ def parse_args(args, parser): if args.custom_binary_path: if os.path.isdir(args.custom_binary_path): - config.CUSTOMBINPATH = args.custom_binary_path + # Set legacy environment variable for config to pick up + os.environ["CUSTOMBINPATH"] = args.custom_binary_path else: message = f"Custom binary path does not exist: \"{args.custom_binary_path}\"\n" # noqa: E501 parser.exit(status=1, message=message) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 7a115fc9..426eed19 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -641,8 +641,9 @@ def find_appimage_files(app: App): app.conf.installer_binary_directory, config.MYDOWNLOADS ] - if config.CUSTOMBINPATH is not None: - directories.append(config.CUSTOMBINPATH) + # FIXME: consider what we should do with this, promote to top level config? + if app.conf._overrides.custom_binary_path is not None: + directories.append(app.conf._overrides.custom_binary_path) if sys.version_info < (3, 12): raise RuntimeError("Python 3.12 or higher is required for .rglob() flag `case-sensitive` ") # noqa: E501 @@ -667,7 +668,7 @@ def find_appimage_files(app: App): return appimages -def find_wine_binary_files(release_version): +def find_wine_binary_files(app: App, release_version): wine_binary_path_list = [ "/usr/local/bin", os.path.expanduser("~") + "/bin", @@ -675,8 +676,8 @@ def find_wine_binary_files(release_version): os.path.expanduser("~") + "/.steam/steam/steamapps/common/Proton*/files/bin", # noqa: E501 ] - if config.CUSTOMBINPATH is not None: - wine_binary_path_list.append(config.CUSTOMBINPATH) + if app.conf._overrides.custom_binary_path is not None: + wine_binary_path_list.append(app.conf._overrides.custom_binary_path) # Temporarily modify PATH for additional WINE64 binaries. for p in wine_binary_path_list: From 43d7644b1ede879207d5c752f66b6dd8bee0e515 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:33:43 -0800 Subject: [PATCH 010/137] fix: migrate LOGOS_VERSION --- ou_dedetai/app.py | 10 +++++++++- ou_dedetai/config.py | 4 ++-- ou_dedetai/installer.py | 5 ++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 1fbc71bf..52ea04ab 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -204,6 +204,8 @@ class EnvironmentOverrides: installer_binary_directory: Optional[str] wineserver_binary: Optional[str] + faithlife_product_version: Optional[str] + # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] @@ -213,7 +215,8 @@ def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": EnvironmentOverrides( installer_binary_directory=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, - custom_binary_path=legacy.CUSTOMBINPATH + custom_binary_path=legacy.CUSTOMBINPATH, + faithlife_product_version=legacy.LOGOS_VERSION ) @classmethod @@ -228,6 +231,9 @@ class UserConfiguration: Normally shouldn't be used directly, as it's types may be None Easy reading to/from JSON and supports legacy keys""" + + # XXX: store a version in this config? Just in case we need to do conditional logic reading old version's configurations + faithlife_product: Optional[str] = None faithlife_product_version: Optional[str] = None faithlife_product_release: Optional[str] = None @@ -384,6 +390,8 @@ def faithlife_product(self, value: Optional[str]): @property def faithlife_product_version(self) -> str: + if self._overrides.faithlife_product_version is not None: + return self._overrides.faithlife_product_version question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 options = ["10", "9"] return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e1d93ae7..5e49f0c8 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -30,9 +30,10 @@ 'DIALOG': None, 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log"), # noqa: E501 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 + # This is the installed Logos.exe 'LOGOS_EXE': None, + # This is the logos installer executable (which is also called logos confusingly) 'LOGOS_EXECUTABLE': None, - 'LOGOS_VERSION': None, 'LOGOS64_MSI': "Logos-x64.msi", 'LOGOS64_URL': None, 'REINSTALL_DEPENDENCIES': False, @@ -60,7 +61,6 @@ INSTALL_STEPS_COUNT: int = 0 L9PACKAGES = None LOG_LEVEL = logging.WARNING -LOGOS_DIR = os.path.dirname(LOGOS_EXE) if LOGOS_EXE else None # noqa: F821 LOGOS_FORCE_ROOT: bool = False LOGOS_ICON_FILENAME: Optional[str] = None LOGOS_ICON_URL: Optional[str] = None diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index ca9f678c..4dd84252 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -149,12 +149,11 @@ def ensure_installation_config(app: App): after_version_url_part = "/Verbum/" if app.conf.faithlife_product == "Verbum" else "/" config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{app.conf.faithlife_product_version}{after_version_url_part}Installer/{app.conf.faithlife_product_release}/{app.conf.faithlife_product}-x64.msi" # noqa: E501 - config.LOGOS_VERSION = app.conf.faithlife_product_version config.LOGOS64_MSI = Path(config.LOGOS64_URL).name logging.debug(f"> {config.LOGOS_ICON_URL=}") logging.debug(f"> {config.LOGOS_ICON_FILENAME=}") - logging.debug(f"> {config.LOGOS_VERSION=}") + logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") logging.debug(f"> {config.LOGOS64_MSI=}") logging.debug(f"> {config.LOGOS64_URL=}") @@ -320,7 +319,7 @@ def ensure_product_installer_download(app: App): app=app ) - config.LOGOS_EXECUTABLE = f"{app.conf.faithlife_product}_v{config.LOGOS_VERSION}-x64.msi" # noqa: E501 + config.LOGOS_EXECUTABLE = f"{app.conf.faithlife_product}_v{app.conf.faithlife_product_version}-x64.msi" # noqa: E501 downloaded_file = utils.get_downloaded_file_path(config.LOGOS_EXECUTABLE) if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE From 49a3c8c8e18e21534fd0f0921320b7f5d27a32a8 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:37:12 -0800 Subject: [PATCH 011/137] fix: migrate LOGOS_EXECUTABLE --- ou_dedetai/app.py | 11 ++++++++++- ou_dedetai/config.py | 2 -- ou_dedetai/installer.py | 11 +++++------ ou_dedetai/wine.py | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 52ea04ab..a820ca1f 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -89,6 +89,7 @@ class LegacyEnvOverrides: # XXX: default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` wine_log: Optional[str] LOGOS_EXE: Optional[str] + # This is the logos installer executable name (NOT path) LOGOS_EXECUTABLE: Optional[str] LOGOS_VERSION: Optional[str] # XXX: Default value used to be "Logos-x64.msi", we may have to handle this @@ -205,6 +206,7 @@ class EnvironmentOverrides: installer_binary_directory: Optional[str] wineserver_binary: Optional[str] faithlife_product_version: Optional[str] + faithlife_installer_name: Optional[str] # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) @@ -216,7 +218,8 @@ def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": installer_binary_directory=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, custom_binary_path=legacy.CUSTOMBINPATH, - faithlife_product_version=legacy.LOGOS_VERSION + faithlife_product_version=legacy.LOGOS_VERSION, + faithlife_installer_name=legacy.LOGOS_EXECUTABLE ) @classmethod @@ -421,6 +424,12 @@ def faithlife_product_release(self, value: str): self._raw.faithlife_product_release = value self._write() + @property + def faithlife_installer_name(self) -> str: + if self._overrides.faithlife_installer_name is not None: + return self._overrides.faithlife_installer_name + return f"{self.faithlife_product}_v{self.faithlife_product_version}-x64.msi" + @property def faithlife_product_release_channel(self) -> str: return self._raw.faithlife_product_release_channel diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 5e49f0c8..839f294f 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -32,8 +32,6 @@ 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 # This is the installed Logos.exe 'LOGOS_EXE': None, - # This is the logos installer executable (which is also called logos confusingly) - 'LOGOS_EXECUTABLE': None, 'LOGOS64_MSI': "Logos-x64.msi", 'LOGOS64_URL': None, 'REINSTALL_DEPENDENCIES': False, diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 4dd84252..09aac512 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -292,7 +292,7 @@ def ensure_premade_winebottle_download(app: App): downloaded_file = utils.get_downloaded_file_path(constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: - downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE + downloaded_file = Path(config.MYDOWNLOADS) / app.conf.faithlife_installer_name network.logos_reuse_download( constants.LOGOS9_WINE64_BOTTLE_TARGZ_URL, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, @@ -319,18 +319,17 @@ def ensure_product_installer_download(app: App): app=app ) - config.LOGOS_EXECUTABLE = f"{app.conf.faithlife_product}_v{app.conf.faithlife_product_version}-x64.msi" # noqa: E501 - downloaded_file = utils.get_downloaded_file_path(config.LOGOS_EXECUTABLE) + downloaded_file = utils.get_downloaded_file_path(app.conf.faithlife_installer_name) if not downloaded_file: - downloaded_file = Path(config.MYDOWNLOADS) / config.LOGOS_EXECUTABLE + downloaded_file = Path(config.MYDOWNLOADS) / app.conf.faithlife_installer_name network.logos_reuse_download( config.LOGOS64_URL, - config.LOGOS_EXECUTABLE, + app.conf.faithlife_installer_name, config.MYDOWNLOADS, app=app, ) # Copy file into install dir. - installer = Path(f"{app.conf.install_dir}/data/{config.LOGOS_EXECUTABLE}") + installer = Path(f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}") if not installer.is_file(): shutil.copy(downloaded_file, installer.parent) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index e71bf50d..d74eeb3b 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -252,10 +252,10 @@ def disable_winemenubuilder(app: App, wine64_binary: str): def install_msi(app: App): - msg.status(f"Running MSI installer: {config.LOGOS_EXECUTABLE}.", app) + msg.status(f"Running MSI installer: {app.conf.faithlife_installer_name}.", app) # Execute the .MSI wine_exe = app.conf.wine64_binary - exe_args = ["/i", f"{app.conf.install_dir}/data/{config.LOGOS_EXECUTABLE}"] + exe_args = ["/i", f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}"] if config.PASSIVE is True: exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") From 3246bc55189c29f7a29e3b39f2fd9c1050812653 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:41:59 -0800 Subject: [PATCH 012/137] fix: remove unused LOGOS64_MSI --- ou_dedetai/app.py | 5 +++-- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index a820ca1f..e0e7891d 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -92,8 +92,9 @@ class LegacyEnvOverrides: # This is the logos installer executable name (NOT path) LOGOS_EXECUTABLE: Optional[str] LOGOS_VERSION: Optional[str] - # XXX: Default value used to be "Logos-x64.msi", we may have to handle this - LOGOS64_MSI: Optional[str] + # This wasn't overridable in the bash version of this installer (at 554c9a6), + # nor was it used in the python version (at 8926435) + # LOGOS64_MSI: Optional[str] LOGOS64_URL: Optional[str] REINSTALL_DEPENDENCIES: Optional[bool] SELECTED_APPIMAGE_FILENAME: Optional[str] diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 839f294f..8ab18bf9 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -32,7 +32,6 @@ 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 # This is the installed Logos.exe 'LOGOS_EXE': None, - 'LOGOS64_MSI': "Logos-x64.msi", 'LOGOS64_URL': None, 'REINSTALL_DEPENDENCIES': False, 'SELECTED_APPIMAGE_FILENAME': None, diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 09aac512..1dc106e6 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -133,7 +133,6 @@ def ensure_installation_config(app: App): logging.debug('- config.LOGOS_ICON_URL') logging.debug('- config.LOGOS_ICON_FILENAME') logging.debug('- config.LOGOS_VERSION') - logging.debug('- config.LOGOS64_MSI') logging.debug('- config.LOGOS64_URL') # XXX: This doesn't prompt the user for anything, all values are derived from other user-supplied values @@ -149,12 +148,9 @@ def ensure_installation_config(app: App): after_version_url_part = "/Verbum/" if app.conf.faithlife_product == "Verbum" else "/" config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{app.conf.faithlife_product_version}{after_version_url_part}Installer/{app.conf.faithlife_product_release}/{app.conf.faithlife_product}-x64.msi" # noqa: E501 - config.LOGOS64_MSI = Path(config.LOGOS64_URL).name - logging.debug(f"> {config.LOGOS_ICON_URL=}") logging.debug(f"> {config.LOGOS_ICON_FILENAME=}") logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") - logging.debug(f"> {config.LOGOS64_MSI=}") logging.debug(f"> {config.LOGOS64_URL=}") # XXX: What does the install task do? Shouldn't that logic be here? From d930d47940191260d7614f1b601b51844691f176 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:45:32 -0800 Subject: [PATCH 013/137] fix: migrate LOGOS64_URL --- ou_dedetai/app.py | 11 ++++++++++- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 6 ++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index e0e7891d..dd0b5be2 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -208,6 +208,7 @@ class EnvironmentOverrides: wineserver_binary: Optional[str] faithlife_product_version: Optional[str] faithlife_installer_name: Optional[str] + faithlife_installer_download_url: Optional[str] # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) @@ -220,7 +221,8 @@ def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": wineserver_binary=legacy.WINESERVER_EXE, custom_binary_path=legacy.CUSTOMBINPATH, faithlife_product_version=legacy.LOGOS_VERSION, - faithlife_installer_name=legacy.LOGOS_EXECUTABLE + faithlife_installer_name=legacy.LOGOS_EXECUTABLE, + faithlife_installer_download_url=legacy.LOGOS64_URL ) @classmethod @@ -431,6 +433,13 @@ def faithlife_installer_name(self) -> str: return self._overrides.faithlife_installer_name return f"{self.faithlife_product}_v{self.faithlife_product_version}-x64.msi" + @property + def faithlife_installer_download_url(self) -> str: + if self._overrides.faithlife_installer_download_url is not None: + return self._overrides.faithlife_installer_download_url + after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" + return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 + @property def faithlife_product_release_channel(self) -> str: return self._raw.faithlife_product_release_channel diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 8ab18bf9..1a18e979 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -32,7 +32,6 @@ 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 # This is the installed Logos.exe 'LOGOS_EXE': None, - 'LOGOS64_URL': None, 'REINSTALL_DEPENDENCIES': False, 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 1dc106e6..4879d60b 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -145,13 +145,11 @@ def ensure_installation_config(app: App): # XXX: stop stting all these config keys config.LOGOS_ICON_URL = str(logos_icon_url) config.LOGOS_ICON_FILENAME = logos_icon_url.name - after_version_url_part = "/Verbum/" if app.conf.faithlife_product == "Verbum" else "/" - config.LOGOS64_URL = f"https://downloads.logoscdn.com/LBS{app.conf.faithlife_product_version}{after_version_url_part}Installer/{app.conf.faithlife_product_release}/{app.conf.faithlife_product}-x64.msi" # noqa: E501 logging.debug(f"> {config.LOGOS_ICON_URL=}") logging.debug(f"> {config.LOGOS_ICON_FILENAME=}") logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") - logging.debug(f"> {config.LOGOS64_URL=}") + logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") # XXX: What does the install task do? Shouldn't that logic be here? if config.DIALOG in ['curses', 'dialog', 'tk']: @@ -319,7 +317,7 @@ def ensure_product_installer_download(app: App): if not downloaded_file: downloaded_file = Path(config.MYDOWNLOADS) / app.conf.faithlife_installer_name network.logos_reuse_download( - config.LOGOS64_URL, + app.conf.faithlife_installer_download_url, app.conf.faithlife_installer_name, config.MYDOWNLOADS, app=app, From 197609c50f11d935859f5aa1a857d75e3191656c Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:04:54 -0800 Subject: [PATCH 014/137] fix: migrate WINEDLLOVERRIDES --- ou_dedetai/app.py | 10 +++++++++- ou_dedetai/config.py | 2 +- ou_dedetai/wine.py | 28 +++++++++++++++++++--------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index dd0b5be2..21a71eb2 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -210,6 +210,8 @@ class EnvironmentOverrides: faithlife_installer_name: Optional[str] faithlife_installer_download_url: Optional[str] + wine_dll_overrides: Optional[str] + # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] @@ -529,7 +531,13 @@ def wine64_binary(self) -> str: # This used to be called WINESERVER_EXE def wineserver_binary(self) -> str: return str(Path(self.wine_binary).parent / 'wineserver') - + + @property + def wine_dll_overrides(self) -> str: + if self._overrides.wine_dll_overrides is not None: + return self._overrides.wine_dll_overrides + # Default is no overrides + return '' def toggle_faithlife_product_release_channel(self): if self._raw.faithlife_product_release_channel == "stable": diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 1a18e979..accac4f8 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -37,11 +37,11 @@ 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, 'SKIP_WINETRICKS': False, + # Dependent on DIALOG with env override 'use_python_dialog': None, 'VERBOSE': False, 'WINEBIN_CODE': None, 'WINEDEBUG': "fixme-all,err-all", - 'WINEDLLOVERRIDES': '', 'WINEPREFIX': None, 'WINE_EXE': None, 'WINETRICKS_UNATTENDED': None, diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index d74eeb3b..ac14fb8b 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -5,6 +5,7 @@ import signal import subprocess from pathlib import Path +from typing import Optional from ou_dedetai.app import App @@ -208,16 +209,15 @@ def initializeWineBottle(app: App): wine_exe = app.conf.wine64_binary logging.debug(f"{wine_exe=}") # Avoid wine-mono window - orig_overrides = config.WINEDLLOVERRIDES - config.WINEDLLOVERRIDES = f"{config.WINEDLLOVERRIDES};mscoree=" + wine_dll_override="mscoree=" logging.debug(f"Running: {wine_exe} wineboot --init") process = run_wine_proc( wine_exe, exe='wineboot', exe_args=['--init'], - init=True + init=True, + additional_wine_dll_overrides=wine_dll_override ) - config.WINEDLLOVERRIDES = orig_overrides return process @@ -267,9 +267,16 @@ def wait_pid(process): os.waitpid(-process.pid, 0) -def run_wine_proc(winecmd, app: App, exe=None, exe_args=list(), init=False): +def run_wine_proc( + winecmd, + app: App, + exe=None, + exe_args=list(), + init=False, + additional_wine_dll_overrides: Optional[str] = None +): logging.debug("Getting wine environment.") - env = get_wine_env() + env = get_wine_env(app, additional_wine_dll_overrides) if not init and config.WINECMD_ENCODING is None: # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. logging.debug("Getting wine system's cmd.exe encoding.") @@ -445,7 +452,7 @@ def get_registry_value(reg_path, name, app: App): # NOTE: Can't use run_wine_proc here because of infinite recursion while # trying to determine WINECMD_ENCODING. value = None - env = get_wine_env() + env = get_wine_env(app) cmd = [ app.conf.wine64_binary, @@ -524,7 +531,7 @@ def get_wine_branch(binary): return get_mscoree_winebranch(mscoree64) -def get_wine_env(app: App): +def get_wine_env(app: App, additional_wine_dll_overrides: Optional[str]=None): wine_env = os.environ.copy() winepath = Path(app.conf.wine_binary) if winepath.name != 'wine64': # AppImage @@ -534,7 +541,7 @@ def get_wine_env(app: App): wine_env_defaults = { 'WINE': str(winepath), 'WINEDEBUG': config.WINEDEBUG, - 'WINEDLLOVERRIDES': config.WINEDLLOVERRIDES, + 'WINEDLLOVERRIDES': app.conf.wine_dll_overrides, 'WINELOADER': str(winepath), 'WINEPREFIX': app.conf.wine_prefix, 'WINESERVER': app.conf.wineserver_binary, @@ -546,6 +553,9 @@ def get_wine_env(app: App): for k, v in wine_env_defaults.items(): wine_env[k] = v + if additional_wine_dll_overrides is not None: + wine_env["WINEDLLOVERRIDES"] += ";" + additional_wine_dll_overrides # noqa: E501 + updated_env = {k: wine_env.get(k) for k in wine_env_defaults.keys()} logging.debug(f"Wine env: {updated_env}") return wine_env From 4b76466666fd5215a9e00f4f74bb6cb4c6137d2b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:14:43 -0800 Subject: [PATCH 015/137] fix: migrate SKIP_WINETRICKS --- ou_dedetai/app.py | 13 +++++++++++-- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 2 +- ou_dedetai/main.py | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 21a71eb2..591cd43c 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -115,9 +115,9 @@ class LegacyEnvOverrides: @classmethod def from_env() -> "LegacyEnvOverrides": legacy_envs = LegacyEnvOverrides() - # Now update from ENV for var in LegacyEnvOverrides().__dict__.keys(): if os.getenv(var) is not None: + # XXX: this doesn't load bools properly. Use get_type_hints to fid this. legacy_envs[var] = os.getenv(var) return legacy_envs @@ -210,6 +210,8 @@ class EnvironmentOverrides: faithlife_installer_name: Optional[str] faithlife_installer_download_url: Optional[str] + winetricks_skip: Optional[bool] + wine_dll_overrides: Optional[str] # Additional path to look for when searching for binaries. @@ -224,7 +226,8 @@ def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": custom_binary_path=legacy.CUSTOMBINPATH, faithlife_product_version=legacy.LOGOS_VERSION, faithlife_installer_name=legacy.LOGOS_EXECUTABLE, - faithlife_installer_download_url=legacy.LOGOS64_URL + faithlife_installer_download_url=legacy.LOGOS64_URL, + winetricks_skip=legacy.SKIP_WINETRICKS ) @classmethod @@ -611,3 +614,9 @@ def logos_indexer_exe(self) -> Optional[str]: def logos_login_exe(self) -> Optional[str]: if self.wine_user is not None: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + + @property + def skip_winetricks(self) -> bool: + if self._overrides.winetricks_skip is not None: + return self._overrides.winetricks_skip + return False \ No newline at end of file diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index accac4f8..e6ebf231 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -36,7 +36,6 @@ 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, - 'SKIP_WINETRICKS': False, # Dependent on DIALOG with env override 'use_python_dialog': None, 'VERBOSE': False, diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 4879d60b..0ca1558e 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -368,7 +368,7 @@ def ensure_winetricks_applied(app: App): logging.debug('- settings fontsmooth=rgb') logging.debug('- d3dcompiler_47') - if not config.SKIP_WINETRICKS: + if not app.conf.skip_winetricks: usr_reg = None sys_reg = None workdir = Path(f"{config.WORKDIR}") diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 60ec7d5b..8892a5f2 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -219,7 +219,7 @@ def parse_args(args, parser): config.SKIP_FONTS = True if args.skip_winetricks: - config.SKIP_WINETRICKS = True + os.environ["SKIP_WINETRICKS"] = "True" if network.check_for_updates: config.CHECK_UPDATES = True From 65e53699189d4b1d41eb82076152e5874829b908 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:39:53 -0800 Subject: [PATCH 016/137] fix: remove unused REINSTALL_DEPENDENCIES --- ou_dedetai/app.py | 2 +- ou_dedetai/config.py | 1 - ou_dedetai/logos.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 591cd43c..0abffb85 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -96,7 +96,6 @@ class LegacyEnvOverrides: # nor was it used in the python version (at 8926435) # LOGOS64_MSI: Optional[str] LOGOS64_URL: Optional[str] - REINSTALL_DEPENDENCIES: Optional[bool] SELECTED_APPIMAGE_FILENAME: Optional[str] SKIP_DEPENDENCIES: Optional[bool] SKIP_FONTS: Optional[bool] @@ -616,6 +615,7 @@ def logos_login_exe(self) -> Optional[str]: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 @property + # XXX: don't like this pattern. def skip_winetricks(self) -> bool: if self._overrides.winetricks_skip is not None: return self._overrides.winetricks_skip diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e6ebf231..870d13e3 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -32,7 +32,6 @@ 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 # This is the installed Logos.exe 'LOGOS_EXE': None, - 'REINSTALL_DEPENDENCIES': False, 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 512c028b..3cb7fd3c 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -138,7 +138,7 @@ def stop(self): else: logging.debug("No Logos processes to stop.") self.logos_state = State.STOPPED - wine.wineserver_wait(app) + wine.wineserver_wait(self.app) def index(self): self.indexing_state = State.STARTING From ac5308bfb5607b6c839745eabd426e429f5e7d76 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:27:38 -0800 Subject: [PATCH 017/137] fix: migrate WINEDEBUG, VERBOSE, DEBUG and LOG_LEVEL --- ou_dedetai/app.py | 147 +++++++++++++++++++++++++--------------- ou_dedetai/config.py | 7 +- ou_dedetai/constants.py | 3 +- ou_dedetai/main.py | 34 ++++------ ou_dedetai/utils.py | 10 --- ou_dedetai/wine.py | 2 +- 6 files changed, 108 insertions(+), 95 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 0abffb85..3a6c1fa1 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -72,61 +72,13 @@ def update_progress(self, message: str, percent: Optional[int] = None): # XXX: What about legacy envs? From the extended config? # Like APPDIR_BINDIR? This no longer can be modified directly, unless we store an override. -@dataclass -class LegacyEnvOverrides: - """Previous versions of the installer allowed some values to be overridden by environment. - This keeps that compatibility.""" - APPIMAGE_LINK_SELECTION_NAME: Optional[str] - APPDIR_BINDIR: Optional[str] - CHECK_UPDATES: Optional[bool] - CONFIG_FILE: Optional[str] - CUSTOMBINPATH: Optional[str] - DEBUG: Optional[bool] - DELETE_LOG: Optional[str] - DIALOG: Optional[str] - # XXX: default used to be `os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log")` - LOGOS_LOG: Optional[str] - # XXX: default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` - wine_log: Optional[str] - LOGOS_EXE: Optional[str] - # This is the logos installer executable name (NOT path) - LOGOS_EXECUTABLE: Optional[str] - LOGOS_VERSION: Optional[str] - # This wasn't overridable in the bash version of this installer (at 554c9a6), - # nor was it used in the python version (at 8926435) - # LOGOS64_MSI: Optional[str] - LOGOS64_URL: Optional[str] - SELECTED_APPIMAGE_FILENAME: Optional[str] - SKIP_DEPENDENCIES: Optional[bool] - SKIP_FONTS: Optional[bool] - SKIP_WINETRICKS: Optional[bool] - use_python_dialog: Optional[str] - VERBOSE: Optional[bool] - WINEBIN_CODE: Optional[str] - # XXX: move this out of this struct - WINEDEBUG: Optional[str] = "fixme-all,err-all", - WINEDLLOVERRIDES: Optional[str] - WINEPREFIX: Optional[str] - WINE_EXE: Optional[str] - WINESERVER_EXE: Optional[str] - WINETRICKS_UNATTENDED: Optional[str] - - @classmethod - def from_env() -> "LegacyEnvOverrides": - legacy_envs = LegacyEnvOverrides() - for var in LegacyEnvOverrides().__dict__.keys(): - if os.getenv(var) is not None: - # XXX: this doesn't load bools properly. Use get_type_hints to fid this. - legacy_envs[var] = os.getenv(var) - return legacy_envs - - # XXX: move these configs into config.py once it's cleared out @dataclass class LegacyConfiguration: """Configuration and it's keys from before the user configuration class existed. Useful for one directional compatibility""" + # Legacy Core Configuration FLPRODUCT: Optional[str] = None TARGETVERSION: Optional[str] = None TARGET_RELEASE_VERSION: Optional[str] = None @@ -145,12 +97,63 @@ class LegacyConfiguration: logos_release_channel: Optional[str] = None lli_release_channel: Optional[str] = None + # Legacy Extended Configuration + APPIMAGE_LINK_SELECTION_NAME: Optional[str] = None + APPDIR_BINDIR: Optional[str] = None + CHECK_UPDATES: Optional[bool] = None + CONFIG_FILE: Optional[str] = None + CUSTOMBINPATH: Optional[str] = None + DEBUG: Optional[bool] = None + DELETE_LOG: Optional[str] = None + DIALOG: Optional[str] = None + # XXX: default used to be `os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log")` + LOGOS_LOG: Optional[str] = None + # XXX: default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` + wine_log: Optional[str] = None + LOGOS_EXE: Optional[str] = None + # This is the logos installer executable name (NOT path) + LOGOS_EXECUTABLE: Optional[str] = None + LOGOS_VERSION: Optional[str] = None + # This wasn't overridable in the bash version of this installer (at 554c9a6), + # nor was it used in the python version (at 8926435) + # LOGOS64_MSI: Optional[str] + LOGOS64_URL: Optional[str] = None + SELECTED_APPIMAGE_FILENAME: Optional[str] = None + SKIP_DEPENDENCIES: Optional[bool] = None + SKIP_FONTS: Optional[bool] = None + SKIP_WINETRICKS: Optional[bool] = None + use_python_dialog: Optional[str] = None + VERBOSE: Optional[bool] = None + WINEBIN_CODE: Optional[str] = None + # Default was "fixme-all,err-all" + WINEDEBUG: Optional[str] = None, + WINEDLLOVERRIDES: Optional[str] = None + WINEPREFIX: Optional[str] = None + WINE_EXE: Optional[str] = None + WINESERVER_EXE: Optional[str] = None + WINETRICKS_UNATTENDED: Optional[str] = None + + @classmethod def config_file_path() -> str: - return os.getenv(constants.CONFIG_FILE_ENV) or constants.DEFAULT_CONFIG_PATH + # XXX: consider legacy config files + return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH @classmethod - def from_file_and_env() -> "LegacyConfiguration": + def load() -> "LegacyConfiguration": + """Find the relevant config file and load it""" + # Update config from CONFIG_FILE. + if not utils.file_exists(LegacyConfiguration.config_file_path): # noqa: E501 + for legacy_config in constants.LEGACY_CONFIG_FILES: + if utils.file_exists(legacy_config): + return LegacyConfiguration._load(legacy_config) + else: + return LegacyConfiguration._load(legacy_config) + logging.debug("Couldn't find config file, loading defaults...") + return LegacyConfiguration() + + @classmethod + def _load(path: str) -> "LegacyConfiguration": config_file_path = LegacyConfiguration.config_file_path() config_dict = LegacyConfiguration() if config_file_path.endswith('.json'): @@ -196,6 +199,9 @@ def from_file_and_env() -> "LegacyConfiguration": return config_dict +# XXX: rename, this is a set of overrides set by the user (via env) for values that are normally programatic. +# These DO NOT represent normal user choices, however normally fallback to defaults +# We can recover from all of these being optional (assuming the user choices are filled out), while in the case of UserConfigration we'd have to call out to the app. @dataclass class EnvironmentOverrides: """Allows some values to be overridden from environment. @@ -208,17 +214,30 @@ class EnvironmentOverrides: faithlife_product_version: Optional[str] faithlife_installer_name: Optional[str] faithlife_installer_download_url: Optional[str] + log_level: Optional[str | int] winetricks_skip: Optional[bool] + # Corresponds to wine's WINEDLLOVERRIDES wine_dll_overrides: Optional[str] + # Corresponds to wine's WINEDEBUG + wine_debug: Optional[str] # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] @classmethod - def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": + def from_legacy(legacy: LegacyConfiguration) -> "EnvironmentOverrides": + log_level = None + wine_debug = legacy.WINEDEBUG + if legacy.DEBUG: + log_level = logging.DEBUG + # FIXME: shouldn't this be `fixme-all,err-all`? + wine_debug = "" + elif legacy.VERBOSE: + log_level = logging.INFO + wine_debug = "" EnvironmentOverrides( installer_binary_directory=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, @@ -226,12 +245,14 @@ def from_legacy(legacy: LegacyEnvOverrides) -> "EnvironmentOverrides": faithlife_product_version=legacy.LOGOS_VERSION, faithlife_installer_name=legacy.LOGOS_EXECUTABLE, faithlife_installer_download_url=legacy.LOGOS64_URL, - winetricks_skip=legacy.SKIP_WINETRICKS + winetricks_skip=legacy.SKIP_WINETRICKS, + log_level=log_level, + wine_debug=wine_debug ) @classmethod - def from_env() -> "EnvironmentOverrides": - return EnvironmentOverrides.from_legacy(LegacyEnvOverrides.from_env()) + def load() -> "EnvironmentOverrides": + return EnvironmentOverrides.from_legacy(LegacyConfiguration.load()) @dataclass @@ -264,7 +285,7 @@ class UserConfiguration: @classmethod def read_from_file_and_env() -> "UserConfiguration": # First read in the legacy configuration - new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.from_file_and_env()) + new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.load()) # Then read the file again this time looking for the new keys config_file_path = LegacyConfiguration.config_file_path() @@ -536,11 +557,19 @@ def wineserver_binary(self) -> str: @property def wine_dll_overrides(self) -> str: + """Used to set WINEDLLOVERRIDES""" if self._overrides.wine_dll_overrides is not None: return self._overrides.wine_dll_overrides # Default is no overrides return '' + @property + def wine_debug(self) -> str: + """Used to set WINEDEBUG""" + if self._overrides.wine_debug is not None: + return self._overrides.wine_debug + return "fixme-all,err-all" + def toggle_faithlife_product_release_channel(self): if self._raw.faithlife_product_release_channel == "stable": new_channel = "beta" @@ -614,6 +643,12 @@ def logos_login_exe(self) -> Optional[str]: if self.wine_user is not None: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + @property + def log_level(self) -> str | int: + if self._overrides.log_level is not None: + return self._overrides.log_level + return constants.DEFAULT_LOG_LEVEL + @property # XXX: don't like this pattern. def skip_winetricks(self) -> bool: diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 870d13e3..98a11c4b 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -25,7 +25,6 @@ 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', 'CHECK_UPDATES': False, 'CONFIG_FILE': None, - 'DEBUG': False, 'DELETE_LOG': None, 'DIALOG': None, 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log"), # noqa: E501 @@ -37,9 +36,7 @@ 'SKIP_FONTS': False, # Dependent on DIALOG with env override 'use_python_dialog': None, - 'VERBOSE': False, 'WINEBIN_CODE': None, - 'WINEDEBUG': "fixme-all,err-all", 'WINEPREFIX': None, 'WINE_EXE': None, 'WINETRICKS_UNATTENDED': None, @@ -54,7 +51,6 @@ INSTALL_STEP: int = 0 INSTALL_STEPS_COUNT: int = 0 L9PACKAGES = None -LOG_LEVEL = logging.WARNING LOGOS_FORCE_ROOT: bool = False LOGOS_ICON_FILENAME: Optional[str] = None LOGOS_ICON_URL: Optional[str] = None @@ -151,6 +147,9 @@ def set_config_env(config_file_path): return # msg.logos_error(f"Error: Unable to get config at {config_file_path}") logging.info(f"Setting {len(config_dict)} variables from config file.") + # XXX: this could literally set any of the global values, but they're normally read from config. + # Does that still work? What's going on here? + # Guess I could read all legacy keys and legacy env from the file... YIKES. for key, value in config_dict.items(): globals()[key] = value installdir = config_dict.get('INSTALLDIR') diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 4a785a1c..f4a798d0 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -9,14 +9,13 @@ # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 -CONFIG_FILE_ENV = "CONFIG_PATH" LEGACY_CONFIG_FILES = [ os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 ] LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" LLI_CURRENT_VERSION = "4.0.0-beta.4" -LOG_LEVEL = logging.WARNING +DEFAULT_LOG_LEVEL = logging.WARNING LOGOS_BLUE = '#0082FF' LOGOS_GRAY = '#E7E7E7' LOGOS_WHITE = '#FCFCFC' diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 8892a5f2..77cfdaa7 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import argparse import curses + +from ou_dedetai.app import EnvironmentOverrides try: import dialog # noqa: F401 except ImportError: @@ -204,10 +206,10 @@ def parse_args(args, parser): config.set_config_env(config.CONFIG_FILE) if args.verbose: - utils.set_verbose() + msg.update_log_level(logging.INFO) if args.debug: - utils.set_debug() + msg.update_log_level(logging.DEBUG) if args.delete_log: config.DELETE_LOG = True @@ -230,9 +232,6 @@ def parse_args(args, parser): if args.force_root: config.LOGOS_FORCE_ROOT = True - if args.debug: - utils.set_debug() - if args.custom_binary_path: if os.path.isdir(args.custom_binary_path): # Set legacy environment variable for config to pick up @@ -319,18 +318,24 @@ def run_control_panel(): raise e +# XXX: fold this into new config def set_config(): parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately + # Get config based on env and configuration file + log_level = EnvironmentOverrides.load().log_level | constants.DEFAULT_LOG_LEVEL + # Set runtime config. # Initialize logging. - msg.initialize_logging(config.LOG_LEVEL) - current_log_level = config.LOG_LEVEL + msg.initialize_logging(log_level) # Set default config; incl. defining CONFIG_FILE. utils.set_default_config() + # XXX: do this in the new scheme (read then write the config). + # We also want to remove the old file, that may be tricky. + # Update config from CONFIG_FILE. if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 for legacy_config in constants.LEGACY_CONFIG_FILES: @@ -344,21 +349,6 @@ def set_config(): # Parse CLI args and update affected config vars. parse_args(cli_args, parser) - # Update terminal log level if set in CLI and changed from current level. - if config.LOG_LEVEL != current_log_level: - msg.update_log_level(config.LOG_LEVEL) - current_log_level = config.LOG_LEVEL - - # Update config based on environment variables. - config.get_env_config() - # Update terminal log level if set in environment and changed from current - # level. - if config.VERBOSE: - config.LOG_LEVEL = logging.VERBOSE - if config.DEBUG: - config.LOG_LEVEL = logging.DEBUG - if config.LOG_LEVEL != current_log_level: - msg.update_log_level(config.LOG_LEVEL) def set_dialog(): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 426eed19..1fc13186 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -137,16 +137,6 @@ def restart_lli(): sys.exit() -def set_verbose(): - config.LOG_LEVEL = logging.INFO - config.WINEDEBUG = '' - - -def set_debug(): - config.LOG_LEVEL = logging.DEBUG - config.WINEDEBUG = "" - - def clean_all(): logging.info("Cleaning all temp files…") os.system("rm -fr /tmp/LBS.*") diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index ac14fb8b..31753cc1 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -540,7 +540,7 @@ def get_wine_env(app: App, additional_wine_dll_overrides: Optional[str]=None): winepath = app.conf.wine64_binary wine_env_defaults = { 'WINE': str(winepath), - 'WINEDEBUG': config.WINEDEBUG, + 'WINEDEBUG': app.conf.wine_debug, 'WINEDLLOVERRIDES': app.conf.wine_dll_overrides, 'WINELOADER': str(winepath), 'WINEPREFIX': app.conf.wine_prefix, From 7c162e0907ecf8f2f8b734d651abf7fa2b778afd Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:30:46 -0800 Subject: [PATCH 018/137] fix: migrate WINEPREFIX --- ou_dedetai/app.py | 9 +++++++-- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 3a6c1fa1..565c2073 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -222,6 +222,8 @@ class EnvironmentOverrides: wine_dll_overrides: Optional[str] # Corresponds to wine's WINEDEBUG wine_debug: Optional[str] + # Corresponds to wine's WINEPREFIX + wine_prefix: Optional[str] # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) @@ -247,7 +249,8 @@ def from_legacy(legacy: LegacyConfiguration) -> "EnvironmentOverrides": faithlife_installer_download_url=legacy.LOGOS64_URL, winetricks_skip=legacy.SKIP_WINETRICKS, log_level=log_level, - wine_debug=wine_debug + wine_debug=wine_debug, + wine_prefix=legacy.WINEPREFIX ) @classmethod @@ -507,8 +510,10 @@ def installer_binary_directory(self) -> str: return f"{self.install_dir}/data/bin" @property - # XXX: used to be called WINEPREFIX + # This used to be called WINEPREFIX def wine_prefix(self) -> str: + if self._overrides.wine_prefix is not None: + return self._overrides.wine_prefix return f"{self.install_dir}/data/wine64_bottle" @property diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 98a11c4b..decd2d8f 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -37,7 +37,6 @@ # Dependent on DIALOG with env override 'use_python_dialog': None, 'WINEBIN_CODE': None, - 'WINEPREFIX': None, 'WINE_EXE': None, 'WINETRICKS_UNATTENDED': None, } diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 0ca1558e..c70424c0 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -373,8 +373,8 @@ def ensure_winetricks_applied(app: App): sys_reg = None workdir = Path(f"{config.WORKDIR}") workdir.mkdir(parents=True, exist_ok=True) - usr_reg = Path(f"{config.WINEPREFIX}/user.reg") - sys_reg = Path(f"{config.WINEPREFIX}/system.reg") + usr_reg = Path(f"{app.conf.wine_prefix}/user.reg") + sys_reg = Path(f"{app.conf.wine_prefix}/system.reg") if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): msg.status("Disabling winemenubuilder…", app) @@ -434,10 +434,10 @@ def ensure_product_installed(app: App): app=app ) - if not utils.find_installed_product(app.conf.faithlife_product, config.WINEPREFIX): + if not utils.find_installed_product(app.conf.faithlife_product, app.conf.wine_prefix): process = wine.install_msi(app) wine.wait_pid(process) - config.LOGOS_EXE = utils.find_installed_product(app.conf.faithlife_product, config.WINEPREFIX) + config.LOGOS_EXE = utils.find_installed_product(app.conf.faithlife_product, app.conf.wine_prefix) config.current_logos_version = app.conf.faithlife_product_release # Clean up temp files, etc. From 45067bfb97d8db7de1cacc4846e1985d5e0ee8bf Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:39:05 -0800 Subject: [PATCH 019/137] fix: migrate wine_log and standardize config naming --- ou_dedetai/app.py | 44 ++++++++++++++++++++++++++++++----------- ou_dedetai/config.py | 1 - ou_dedetai/constants.py | 1 + ou_dedetai/control.py | 12 +++++------ ou_dedetai/gui_app.py | 2 +- ou_dedetai/installer.py | 14 ++++++------- ou_dedetai/network.py | 4 ++-- ou_dedetai/utils.py | 20 +++++++++---------- ou_dedetai/wine.py | 5 ++--- 9 files changed, 61 insertions(+), 42 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 565c2073..884d61c6 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -108,7 +108,7 @@ class LegacyConfiguration: DIALOG: Optional[str] = None # XXX: default used to be `os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log")` LOGOS_LOG: Optional[str] = None - # XXX: default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` + # Default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` wine_log: Optional[str] = None LOGOS_EXE: Optional[str] = None # This is the logos installer executable name (NOT path) @@ -209,7 +209,7 @@ class EnvironmentOverrides: The actually name of the environment variables remains unchanged from before, this translates the environment variable names to the new variable names""" - installer_binary_directory: Optional[str] + installer_binary_dir: Optional[str] wineserver_binary: Optional[str] faithlife_product_version: Optional[str] faithlife_installer_name: Optional[str] @@ -225,6 +225,9 @@ class EnvironmentOverrides: # Corresponds to wine's WINEPREFIX wine_prefix: Optional[str] + # Our concept of logging wine's output to a separate file + wine_log_path: Optional[str] + # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] @@ -241,7 +244,7 @@ def from_legacy(legacy: LegacyConfiguration) -> "EnvironmentOverrides": log_level = logging.INFO wine_debug = "" EnvironmentOverrides( - installer_binary_directory=legacy.APPDIR_BINDIR, + installer_binary_dir=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, custom_binary_path=legacy.CUSTOMBINPATH, faithlife_product_version=legacy.LOGOS_VERSION, @@ -250,7 +253,8 @@ def from_legacy(legacy: LegacyConfiguration) -> "EnvironmentOverrides": winetricks_skip=legacy.SKIP_WINETRICKS, log_level=log_level, wine_debug=wine_debug, - wine_prefix=legacy.WINEPREFIX + wine_prefix=legacy.WINEPREFIX, + wine_log_path=legacy.wine_log ) @classmethod @@ -276,7 +280,7 @@ class UserConfiguration: wine_binary: Optional[str] = None # This is where to search for wine wine_binary_code: Optional[str] = None - backup_directory: Optional[Path] = None + backup_dir: Optional[Path] = None # Color to use in curses. Either "Logos", "Light", or "Dark" curses_colors: str = "Logos" @@ -310,7 +314,7 @@ def read_from_file_and_env() -> "UserConfiguration": def from_legacy(legacy: LegacyConfiguration) -> "UserConfiguration": return UserConfiguration( faithlife_product=legacy.FLPRODUCT, - backup_directory=legacy.BACKUPDIR, + backup_dir=legacy.BACKUPDIR, curses_colors=legacy.curses_colors, faithlife_product_release=legacy.TARGET_RELEASE_VERSION, faithlife_product_release_channel=legacy.logos_release_channel, @@ -360,7 +364,16 @@ def write_config(self): class Config: """Set of configuration values. - If the user hasn't selected a particular value yet, they will be prompted in their UI.""" + If the user hasn't selected a particular value yet, they will be prompted in their UI. + """ + + # Naming conventions: + # Use `dir` instead of `directory` + # Use snake_case + # scope with faithlife if it's theirs + # suffix with _binary if it's a linux binary + # suffix with _exe if it's a windows binary + # suffix with _path if it's a file path # Storage for the keys _raw: UserConfiguration @@ -504,9 +517,9 @@ def install_dir(self) -> str: @property # This used to be called APPDIR_BINDIR - def installer_binary_directory(self) -> str: - if self._overrides.installer_binary_directory is not None: - return self._overrides.installer_binary_directory + def installer_binary_dir(self) -> str: + if self._overrides.installer_binary_dir is not None: + return self._overrides.installer_binary_dir return f"{self.install_dir}/data/bin" @property @@ -575,6 +588,13 @@ def wine_debug(self) -> str: return self._overrides.wine_debug return "fixme-all,err-all" + @property + def wine_log_path(self) -> str: + """Our concept of logging wine to a separate file.""" + if self._overrides.wine_log_path is not None: + return self._overrides.wine_log_path + return constants.DEFAULT_WINE_LOG_PATH + def toggle_faithlife_product_release_channel(self): if self._raw.faithlife_product_release_channel == "stable": new_channel = "beta" @@ -592,10 +612,10 @@ def toggle_installer_release_channel(self): self._write() @property - def backup_directory(self) -> Path: + def backup_dir(self) -> Path: question = "New or existing folder to store backups in: " options = [PROMPT_OPTION_DIRECTORY] - output = Path(self._ask_if_not_found("backup_directory", question, options)) + output = Path(self._ask_if_not_found("backup_dir", question, options)) output.mkdir(parents=True) return output diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index decd2d8f..7f65ba13 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -28,7 +28,6 @@ 'DELETE_LOG': None, 'DIALOG': None, 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log"), # noqa: E501 - 'wine_log': os.path.expanduser("~/.local/state/FaithLife-Community/wine.log"), # noqa: #E501 # This is the installed Logos.exe 'LOGOS_EXE': None, 'SELECTED_APPIMAGE_FILENAME': None, diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index f4a798d0..f451fa1e 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -9,6 +9,7 @@ # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 +DEFAULT_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 LEGACY_CONFIG_FILES = [ os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 09a21d07..de15b901 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -44,14 +44,14 @@ def restore(app: App): # FIXME: consider moving this into it's own file/module. def backup_and_restore(mode: str, app: App): data_dirs = ['Data', 'Documents', 'Users'] - backup_dir = Path(app.conf.backup_directory).expanduser().resolve() + backup_dir = Path(app.conf.backup_dir).expanduser().resolve() # FIXME: Why is this different per UI? Should this always accept? if config.DIALOG == 'tk' or config.DIALOG == 'curses': pass # user confirms in GUI or TUI else: verb = 'Use' if mode == 'backup' else 'Restore backup from' - if not msg.cli_question(f"{verb} existing backups folder \"{app.conf.backup_directory}\"?", ""): # noqa: E501 + if not msg.cli_question(f"{verb} existing backups folder \"{app.conf.backup_dir}\"?", ""): # noqa: E501 answer = None while answer is None or (mode == 'restore' and not answer.is_dir()): # noqa: E501 answer = msg.cli_ask_filepath("Please provide a backups folder path:") @@ -61,7 +61,7 @@ def backup_and_restore(mode: str, app: App): config.app.conf.backup_directory = answer # Set source folders. - backup_dir = Path(app.conf.backup_directory) + backup_dir = Path(app.conf.backup_dir) try: backup_dir.mkdir(exist_ok=True, parents=True) except PermissionError: @@ -72,7 +72,7 @@ def backup_and_restore(mode: str, app: App): return if mode == 'restore': - restore_dir = utils.get_latest_folder(app.conf.backup_directory) + restore_dir = utils.get_latest_folder(app.conf.backup_dir) restore_dir = Path(restore_dir).expanduser().resolve() if config.DIALOG == 'tk': pass @@ -263,9 +263,9 @@ def set_winetricks(app: App): return 0 # Continue executing the download if it wasn't valid - system.install_winetricks(app.conf.installer_binary_directory, app) + system.install_winetricks(app.conf.installer_binary_dir, app) app.conf.wine_binary = os.path.join( - app.conf.installer_binary_directory, + app.conf.installer_binary_dir, "winetricks" ) return 0 diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 7cae5c25..6dd8f61b 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -754,7 +754,7 @@ def get_winetricks(self, evt=None): self.gui.statusvar.set("Installing Winetricks…") utils.start_thread( system.install_winetricks, - self.conf.installer_binary_directory, + self.conf.installer_binary_dir, app=self ) self.update_run_winetricks_button() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index c70424c0..ab47957a 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -57,7 +57,7 @@ def ensure_install_dir_choice(app: App): # Accessing this sets install_dir and bin_dir app.conf.install_dir logging.debug(f"> config.INSTALLDIR={app.conf.install_dir=}") - logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_directory}") + logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_dir}") def ensure_wine_choice(app: App): @@ -169,12 +169,12 @@ def ensure_install_dirs(app: App): logging.debug('- data/wine64_bottle') wine_dir = Path("") - bin_dir = Path(app.conf.installer_binary_directory) + bin_dir = Path(app.conf.installer_binary_dir) bin_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {bin_dir} exists?: {bin_dir.is_dir()}") - logging.debug(f"> config.INSTALLDIR={app.conf.installer_binary_directory}") - logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_directory}") + logging.debug(f"> config.INSTALLDIR={app.conf.installer_binary_dir}") + logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_dir}") wine_dir = Path(f"{app.conf.wine_prefix}") wine_dir.mkdir(parents=True, exist_ok=True) @@ -267,7 +267,7 @@ def ensure_winetricks_executable(app: App): # Either previous system winetricks is no longer accessible, or the # or the user has chosen to download it. msg.status("Downloading winetricks from the Internet…", app=app) - system.install_winetricks(app.conf.installer_binary_directory, app=app) + system.install_winetricks(app.conf.installer_binary_dir, app=app) logging.debug(f"> {app.conf.winetricks_binary} is executable?: {os.access(app.conf.winetricks_binary, os.X_OK)}") # noqa: E501 return 0 @@ -524,8 +524,8 @@ def get_progress_pct(current, total): def create_wine_appimage_symlinks(app: App): - appdir_bindir = Path(app.conf.installer_binary_directory) - os.environ['PATH'] = f"{app.conf.installer_binary_directory}:{os.getenv('PATH')}" + appdir_bindir = Path(app.conf.installer_binary_dir) + os.environ['PATH'] = f"{app.conf.installer_binary_dir}:{os.getenv('PATH')}" # Ensure AppImage symlink. appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 1b53ea19..f1c3349b 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -483,14 +483,14 @@ def check_for_updates(): def get_recommended_appimage(app: App): wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 - dest_path = Path(app.conf.installer_binary_directory) / wine64_appimage_full_filename + dest_path = Path(app.conf.installer_binary_dir) / wine64_appimage_full_filename if dest_path.is_file(): return else: logos_reuse_download( config.RECOMMENDED_WINE64_APPIMAGE_URL, config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, - app.conf.installer_binary_directory, + app.conf.installer_binary_dir, app=app ) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 1fc13186..52c02763 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -287,7 +287,7 @@ def get_winebin_code_and_desc(app: App, binary): # Does it work? if isinstance(binary, Path): binary = str(binary) - if binary == f"{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 + if binary == f"{app.conf.installer_binary_dir}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 code = "Recommended" elif binary.lower().endswith('.appimage'): code = "AppImage" @@ -311,7 +311,7 @@ def get_wine_options(app: App, appimages, binaries) -> Union[List[List[str]], Li # Add AppImages to list # if config.DIALOG == 'tk': - wine_binary_options.append(f"{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 + wine_binary_options.append(f"{app.conf.installer_binary_dir}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 wine_binary_options.extend(appimages) # else: # appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 @@ -628,7 +628,7 @@ def find_appimage_files(app: App): appimages = [] directories = [ os.path.expanduser("~") + "/bin", - app.conf.installer_binary_directory, + app.conf.installer_binary_dir, config.MYDOWNLOADS ] # FIXME: consider what we should do with this, promote to top level config? @@ -706,16 +706,16 @@ def set_appimage_symlink(app: App): logging.debug(f"{config.APPIMAGE_FILE_PATH=}") logging.debug(f"{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") appimage_file_path = Path(config.APPIMAGE_FILE_PATH) - appdir_bindir = Path(app.conf.installer_binary_directory) + appdir_bindir = Path(app.conf.installer_binary_dir) appimage_symlink_path = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME if appimage_file_path.name == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 # Default case. network.get_recommended_appimage(app) selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 - bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_directory # noqa: E501 + bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_dir # noqa: E501 if not bindir_appimage.exists(): - logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_directory}.") # noqa: E501 - shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_directory}") + logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 + shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_dir}") else: selected_appimage_file_path = appimage_file_path # Verify user-selected AppImage. @@ -725,7 +725,7 @@ def set_appimage_symlink(app: App): # Determine if user wants their AppImage in the app bin dir. copy_message = ( f"Should the program copy {selected_appimage_file_path} to the" - f" {app.conf.installer_binary_directory} directory?" + f" {app.conf.installer_binary_dir} directory?" ) # XXX: move this to .ask # FIXME: What if user cancels the confirmation dialog? @@ -744,10 +744,10 @@ def set_appimage_symlink(app: App): # Copy AppImage if confirmed. if confirm is True or confirm == 'yes': - logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_directory}.") # noqa: E501 + logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 dest = appdir_bindir / selected_appimage_file_path.name if not dest.exists(): - shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_directory}") # noqa: E501 + shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_dir}") # noqa: E501 selected_appimage_file_path = dest delete_symlink(appimage_symlink_path) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 31753cc1..ed7ebf2e 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -302,11 +302,10 @@ def run_wine_proc( command.extend(exe_args) cmd = f"subprocess cmd: '{' '.join(command)}'" - with open(config.wine_log, 'a') as wine_log: - print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) logging.debug(cmd) try: - with open(config.wine_log, 'a') as wine_log: + with open(app.conf.wine_log_path, 'a') as wine_log: + print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) process = system.popen_command( command, stdout=wine_log, From d53ba49ad3698cc169e68462fc0e1bdb0bbb01f6 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:07:49 -0800 Subject: [PATCH 020/137] feat: migrate LOGOS_LOG fixed circular import issues, and initial startup errors --- ou_dedetai/app.py | 638 +-------------------------------------- ou_dedetai/config.py | 3 - ou_dedetai/constants.py | 11 +- ou_dedetai/control.py | 10 +- ou_dedetai/gui_app.py | 3 +- ou_dedetai/installer.py | 4 +- ou_dedetai/main.py | 14 +- ou_dedetai/msg.py | 31 +- ou_dedetai/network.py | 2 +- ou_dedetai/new_config.py | 631 ++++++++++++++++++++++++++++++++++++++ ou_dedetai/tui_app.py | 3 +- ou_dedetai/utils.py | 1 - ou_dedetai/wine.py | 2 +- 13 files changed, 689 insertions(+), 664 deletions(-) create mode 100644 ou_dedetai/new_config.py diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 884d61c6..61d79c22 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,24 +1,16 @@ + import abc -from dataclasses import dataclass -from datetime import datetime -import json -import logging -import os -from pathlib import Path from typing import Optional -from ou_dedetai import msg, network, utils, constants - -# Strings for choosing a follow up file or directory -PROMPT_OPTION_DIRECTORY = "Choose Directory" -PROMPT_OPTION_FILE = "Choose File" +from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE -# String for when a binary is meant to be downloaded later -DOWNLOAD = "Download" class App(abc.ABC): def __init__(self, **kwargs) -> None: + # This lazy load is required otherwise it would be a circular import + from ou_dedetai.new_config import Config self.conf = Config(self) + pass def ask(self, question: str, options: list[str]) -> str: """Asks the user a question with a list of supplied options @@ -64,619 +56,7 @@ def _config_updated(self): pass # XXX: unused at present - @abc.abstractmethod - def update_progress(self, message: str, percent: Optional[int] = None): - """Updates the progress of the current operation""" - pass - -# XXX: What about legacy envs? From the extended config? -# Like APPDIR_BINDIR? This no longer can be modified directly, unless we store an override. - -# XXX: move these configs into config.py once it's cleared out -@dataclass -class LegacyConfiguration: - """Configuration and it's keys from before the user configuration class existed. - - Useful for one directional compatibility""" - # Legacy Core Configuration - FLPRODUCT: Optional[str] = None - TARGETVERSION: Optional[str] = None - TARGET_RELEASE_VERSION: Optional[str] = None - current_logos_version: Optional[str] = None - curses_colors: Optional[str] = None - INSTALLDIR: Optional[str] = None - WINETRICKSBIN: Optional[str] = None - WINEBIN_CODE: Optional[str] = None - WINE_EXE: Optional[str] = None - WINECMD_ENCODING: Optional[str] = None - LOGS: Optional[str] = None - BACKUPDIR: Optional[str] = None - LAST_UPDATED: Optional[str] = None - RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None - LLI_LATEST_VERSION: Optional[str] = None - logos_release_channel: Optional[str] = None - lli_release_channel: Optional[str] = None - - # Legacy Extended Configuration - APPIMAGE_LINK_SELECTION_NAME: Optional[str] = None - APPDIR_BINDIR: Optional[str] = None - CHECK_UPDATES: Optional[bool] = None - CONFIG_FILE: Optional[str] = None - CUSTOMBINPATH: Optional[str] = None - DEBUG: Optional[bool] = None - DELETE_LOG: Optional[str] = None - DIALOG: Optional[str] = None - # XXX: default used to be `os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log")` - LOGOS_LOG: Optional[str] = None - # Default used to be `os.path.expanduser("~/.local/state/FaithLife-Community/wine.log")` - wine_log: Optional[str] = None - LOGOS_EXE: Optional[str] = None - # This is the logos installer executable name (NOT path) - LOGOS_EXECUTABLE: Optional[str] = None - LOGOS_VERSION: Optional[str] = None - # This wasn't overridable in the bash version of this installer (at 554c9a6), - # nor was it used in the python version (at 8926435) - # LOGOS64_MSI: Optional[str] - LOGOS64_URL: Optional[str] = None - SELECTED_APPIMAGE_FILENAME: Optional[str] = None - SKIP_DEPENDENCIES: Optional[bool] = None - SKIP_FONTS: Optional[bool] = None - SKIP_WINETRICKS: Optional[bool] = None - use_python_dialog: Optional[str] = None - VERBOSE: Optional[bool] = None - WINEBIN_CODE: Optional[str] = None - # Default was "fixme-all,err-all" - WINEDEBUG: Optional[str] = None, - WINEDLLOVERRIDES: Optional[str] = None - WINEPREFIX: Optional[str] = None - WINE_EXE: Optional[str] = None - WINESERVER_EXE: Optional[str] = None - WINETRICKS_UNATTENDED: Optional[str] = None - - - @classmethod - def config_file_path() -> str: - # XXX: consider legacy config files - return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH - - @classmethod - def load() -> "LegacyConfiguration": - """Find the relevant config file and load it""" - # Update config from CONFIG_FILE. - if not utils.file_exists(LegacyConfiguration.config_file_path): # noqa: E501 - for legacy_config in constants.LEGACY_CONFIG_FILES: - if utils.file_exists(legacy_config): - return LegacyConfiguration._load(legacy_config) - else: - return LegacyConfiguration._load(legacy_config) - logging.debug("Couldn't find config file, loading defaults...") - return LegacyConfiguration() - - @classmethod - def _load(path: str) -> "LegacyConfiguration": - config_file_path = LegacyConfiguration.config_file_path() - config_dict = LegacyConfiguration() - if config_file_path.endswith('.json'): - try: - with open(config_file_path, 'r') as config_file: - cfg = json.load(config_file) - - for key, value in cfg.items(): - config_dict[key] = value - except TypeError as e: - logging.error("Error opening Config file.") - logging.error(e) - raise e - except FileNotFoundError: - logging.info(f"No config file not found at {config_file_path}") - except json.JSONDecodeError as e: - logging.error("Config file could not be read.") - logging.error(e) - raise e - elif config_file_path.endswith('.conf'): - # Legacy config from bash script. - logging.info("Reading from legacy config file.") - with open(config_file_path, 'r') as config_file: - for line in config_file: - line = line.strip() - if len(line) == 0: # skip blank lines - continue - if line[0] == '#': # skip commented lines - continue - parts = line.split('=') - if len(parts) == 2: - value = parts[1].strip('"').strip("'") # remove quotes - vparts = value.split('#') # get rid of potential comment - if len(vparts) > 1: - value = vparts[0].strip().strip('"').strip("'") - config_dict[parts[0]] = value - - # Now update from ENV - for var in LegacyConfiguration().__dict__.keys(): - if os.getenv(var) is not None: - config_dict[var] = os.getenv(var) - - return config_dict - - -# XXX: rename, this is a set of overrides set by the user (via env) for values that are normally programatic. -# These DO NOT represent normal user choices, however normally fallback to defaults -# We can recover from all of these being optional (assuming the user choices are filled out), while in the case of UserConfigration we'd have to call out to the app. -@dataclass -class EnvironmentOverrides: - """Allows some values to be overridden from environment. - - The actually name of the environment variables remains unchanged from before, - this translates the environment variable names to the new variable names""" - - installer_binary_dir: Optional[str] - wineserver_binary: Optional[str] - faithlife_product_version: Optional[str] - faithlife_installer_name: Optional[str] - faithlife_installer_download_url: Optional[str] - log_level: Optional[str | int] - - winetricks_skip: Optional[bool] - - # Corresponds to wine's WINEDLLOVERRIDES - wine_dll_overrides: Optional[str] - # Corresponds to wine's WINEDEBUG - wine_debug: Optional[str] - # Corresponds to wine's WINEPREFIX - wine_prefix: Optional[str] - - # Our concept of logging wine's output to a separate file - wine_log_path: Optional[str] - - # Additional path to look for when searching for binaries. - # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) - custom_binary_path: Optional[str] - - @classmethod - def from_legacy(legacy: LegacyConfiguration) -> "EnvironmentOverrides": - log_level = None - wine_debug = legacy.WINEDEBUG - if legacy.DEBUG: - log_level = logging.DEBUG - # FIXME: shouldn't this be `fixme-all,err-all`? - wine_debug = "" - elif legacy.VERBOSE: - log_level = logging.INFO - wine_debug = "" - EnvironmentOverrides( - installer_binary_dir=legacy.APPDIR_BINDIR, - wineserver_binary=legacy.WINESERVER_EXE, - custom_binary_path=legacy.CUSTOMBINPATH, - faithlife_product_version=legacy.LOGOS_VERSION, - faithlife_installer_name=legacy.LOGOS_EXECUTABLE, - faithlife_installer_download_url=legacy.LOGOS64_URL, - winetricks_skip=legacy.SKIP_WINETRICKS, - log_level=log_level, - wine_debug=wine_debug, - wine_prefix=legacy.WINEPREFIX, - wine_log_path=legacy.wine_log - ) - - @classmethod - def load() -> "EnvironmentOverrides": - return EnvironmentOverrides.from_legacy(LegacyConfiguration.load()) - - -@dataclass -class UserConfiguration: - """This is the class that actually stores the values. - - Normally shouldn't be used directly, as it's types may be None - - Easy reading to/from JSON and supports legacy keys""" - - # XXX: store a version in this config? Just in case we need to do conditional logic reading old version's configurations - - faithlife_product: Optional[str] = None - faithlife_product_version: Optional[str] = None - faithlife_product_release: Optional[str] = None - install_dir: Optional[Path] = None - winetricks_binary: Optional[str] = None - wine_binary: Optional[str] = None - # This is where to search for wine - wine_binary_code: Optional[str] = None - backup_dir: Optional[Path] = None - - # Color to use in curses. Either "Logos", "Light", or "Dark" - curses_colors: str = "Logos" - # Faithlife's release channel. Either "stable" or "beta" - faithlife_product_release_channel: str = "stable" - # The Installer's release channel. Either "stable" or "beta" - installer_release_channel: str = "stable" - - @classmethod - def read_from_file_and_env() -> "UserConfiguration": - # First read in the legacy configuration - new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.load()) - # Then read the file again this time looking for the new keys - config_file_path = LegacyConfiguration.config_file_path() - - new_keys = UserConfiguration().__dict__.keys() - - if config_file_path.endswith('.json'): - with open(config_file_path, 'r') as config_file: - cfg = json.load(config_file) - - for key, value in cfg.items(): - if key in new_keys: - new_config[key] = value - else: - logging.info("Not reading new values from non-json config") - - return new_config - - @classmethod - def from_legacy(legacy: LegacyConfiguration) -> "UserConfiguration": - return UserConfiguration( - faithlife_product=legacy.FLPRODUCT, - backup_dir=legacy.BACKUPDIR, - curses_colors=legacy.curses_colors, - faithlife_product_release=legacy.TARGET_RELEASE_VERSION, - faithlife_product_release_channel=legacy.logos_release_channel, - faithlife_product_version=legacy.TARGETVERSION, - install_dir=legacy.INSTALLDIR, - installer_release_channel=legacy.lli_release_channel, - wine_binary=legacy.WINE_EXE, - wine_binary_code=legacy.WINEBIN_CODE, - winetricks_binary=legacy.WINETRICKSBIN - ) - - def write_config(self): - config_file_path = LegacyConfiguration.config_file_path() - output = self.__dict__ - - logging.info(f"Writing config to {config_file_path}") - os.makedirs(os.path.dirname(config_file_path), exist_ok=True) - - # Ensure all paths stored are relative to install_dir - for k, v in output.items(): - # XXX: test this - if isinstance(v, Path) or (isinstance(v, str) and v.startswith(self.install_dir)): - output[k] = utils.get_relative_path(v, self.install_dir) - - try: - with open(config_file_path, 'w') as config_file: - json.dump(output, config_file, indent=4, sort_keys=True) - config_file.write('\n') - except IOError as e: - msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 - # Continue, the installer can still operate even if it fails to write. - - -# XXX: what to do with these? -# Used to be called current_logos_version, but actually could be used in Verbium too. -installed_faithlife_product_release: Optional[str] = None -# Whether or not the installed faithlife product is configured for additional logging. -# Used to be called "LOGS" -installed_faithlife_logging: Optional[bool] = None -# Text encoding of the wine command. This calue can be retrieved from the system -winecmd_encoding: Optional[str] = None -last_updated: Optional[datetime] = None -recommended_wine_url: Optional[str] = None -latest_installer_version: Optional[str] = None - - -class Config: - """Set of configuration values. - - If the user hasn't selected a particular value yet, they will be prompted in their UI. - """ - - # Naming conventions: - # Use `dir` instead of `directory` - # Use snake_case - # scope with faithlife if it's theirs - # suffix with _binary if it's a linux binary - # suffix with _exe if it's a windows binary - # suffix with _path if it's a file path - - # Storage for the keys - _raw: UserConfiguration - - # Overriding programmatically generated values from ENV - _overrides: EnvironmentOverrides - - _curses_colors_valid_values = ["Light", "Dark", "Logos"] - - # Singleton logic, this enforces that only one config object exists at a time. - def __new__(cls) -> "Config": - if not hasattr(cls, '_instance'): - cls._instance = super(Config, cls).__new__(cls) - return cls._instance - - def __init__(self, app: App) -> None: - self.app = app - self._raw = UserConfiguration.read_from_file_and_env() - logging.debug("Current persistent config:") - for k, v in self._raw.__dict__.items(): - logging.debug(f"{k}: {v}") - - def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: - # XXX: should this also update the feedback? - if not getattr(self._raw, parameter): - if dependent_parameters is not None: - for dependent_config_key in dependent_parameters: - setattr(self._raw, dependent_config_key, None) - answer = self.app.ask(question, options) - # Use the setter on this class if found, otherwise set in self._user - if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: - getattr(Config, parameter).fset(self, answer) - else: - setattr(self._raw, parameter, answer) - self._write() - return getattr(self._raw, parameter) - - def _write(self): - """Writes configuration to file and lets the app know something changed""" - self._raw.write_config() - self.app._config_updated() - - @property - def config_file_path(self) -> str: - return LegacyConfiguration.config_file_path() - - @property - def faithlife_product(self) -> str: - question = "Choose which FaithLife product the script should install: " # noqa: E501 - options = ["Logos", "Verbum"] - return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) - - @faithlife_product.setter - def faithlife_product(self, value: Optional[str]): - if self._raw.faithlife_product != value: - self._raw.faithlife_product = value - # Reset dependent variables - self.faithlife_product_release = None - - self._write() - - @property - def faithlife_product_version(self) -> str: - if self._overrides.faithlife_product_version is not None: - return self._overrides.faithlife_product_version - question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 - options = ["10", "9"] - return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) - - @faithlife_product_version.setter - def faithlife_product_version(self, value: Optional[str]): - if self._raw.faithlife_product_version != value: - self._raw.faithlife_product_release = None - # Install Dir has the name of the product and it's version. Reset it too - self._raw.install_dir = None - # Wine is dependent on the product/version selected - self._raw.wine_binary = None - self._raw.wine_binary_code = None - self._raw.winetricks_binary = None - - self._write() - - @property - def faithlife_product_release(self) -> str: - question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 - options = network.get_logos_releases(None) - return self._ask_if_not_found("faithlife_product_release", question, options) - - @faithlife_product_release.setter - def faithlife_product_release(self, value: str): - if self._raw.faithlife_product_release != value: - self._raw.faithlife_product_release = value - self._write() - - @property - def faithlife_installer_name(self) -> str: - if self._overrides.faithlife_installer_name is not None: - return self._overrides.faithlife_installer_name - return f"{self.faithlife_product}_v{self.faithlife_product_version}-x64.msi" - - @property - def faithlife_installer_download_url(self) -> str: - if self._overrides.faithlife_installer_download_url is not None: - return self._overrides.faithlife_installer_download_url - after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" - return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 - - @property - def faithlife_product_release_channel(self) -> str: - return self._raw.faithlife_product_release_channel - - @property - def installer_release_channel(self) -> str: - return self._raw.installer_release_channel - - @property - def winetricks_binary(self) -> str: - """This may be a path to the winetricks binary or it may be "Download" - """ - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux.", # noqa: E501 - options = utils.get_winetricks_options() - return self._ask_if_not_found("winetricks_binary", question, options) - - @winetricks_binary.setter - def winetricks_binary(self, value: Optional[str | Path]): - if value is not None and value != "Download": - value = Path(value) - if not value.exists(): - raise ValueError("Winetricks binary must exist") - if self._raw.winetricks_binary != value: - self._raw.winetricks_binary = value - self._write() - - @property - def install_dir(self) -> str: - default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 - question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 - options = [default, PROMPT_OPTION_DIRECTORY] - output = self._ask_if_not_found("install_dir", question, options) - return output - - @property - # This used to be called APPDIR_BINDIR - def installer_binary_dir(self) -> str: - if self._overrides.installer_binary_dir is not None: - return self._overrides.installer_binary_dir - return f"{self.install_dir}/data/bin" - - @property - # This used to be called WINEPREFIX - def wine_prefix(self) -> str: - if self._overrides.wine_prefix is not None: - return self._overrides.wine_prefix - return f"{self.install_dir}/data/wine64_bottle" - - @property - def wine_binary(self) -> str: - """Returns absolute path to the wine binary""" - if not self._raw.wine_binary: - question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 - network.set_recommended_appimage_config() - options = utils.get_wine_options( - self, - utils.find_appimage_files(self.faithlife_product_release), - utils.find_wine_binary_files(self.app, self.faithlife_product_release) - ) - - choice = self.app.ask(question, options) - - self.wine_binary = choice - # Return the full path so we the callee doesn't need to think about it - if not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): - return str(Path(self.install_dir) / self._raw.wine_binary) - return self._raw.wine_binary - - @wine_binary.setter - def wine_binary(self, value: str): - if (Path(self.install_dir) / value).exists(): - value = (Path(self.install_dir) / value).absolute() - if not Path(value).is_file(): - raise ValueError("Wine Binary path must be a valid file") - - if self._raw.wine_binary != value: - if value is not None: - value = Path(value).absolute() - self._raw.wine_binary = value - # Reset dependents - self._raw.wine_binary_code = None - self._write() - - @property - def wine64_binary(self) -> str: - return str(Path(self.wine_binary).parent / 'wine64') - - @property - # This used to be called WINESERVER_EXE - def wineserver_binary(self) -> str: - return str(Path(self.wine_binary).parent / 'wineserver') - - @property - def wine_dll_overrides(self) -> str: - """Used to set WINEDLLOVERRIDES""" - if self._overrides.wine_dll_overrides is not None: - return self._overrides.wine_dll_overrides - # Default is no overrides - return '' - - @property - def wine_debug(self) -> str: - """Used to set WINEDEBUG""" - if self._overrides.wine_debug is not None: - return self._overrides.wine_debug - return "fixme-all,err-all" - - @property - def wine_log_path(self) -> str: - """Our concept of logging wine to a separate file.""" - if self._overrides.wine_log_path is not None: - return self._overrides.wine_log_path - return constants.DEFAULT_WINE_LOG_PATH - - def toggle_faithlife_product_release_channel(self): - if self._raw.faithlife_product_release_channel == "stable": - new_channel = "beta" - else: - new_channel = "stable" - self._raw.faithlife_product_release_channel = new_channel - self._write() - - def toggle_installer_release_channel(self): - if self._raw.installer_release_channel == "stable": - new_channel = "dev" - else: - new_channel = "stable" - self._raw.installer_release_channel = new_channel - self._write() - - @property - def backup_dir(self) -> Path: - question = "New or existing folder to store backups in: " - options = [PROMPT_OPTION_DIRECTORY] - output = Path(self._ask_if_not_found("backup_dir", question, options)) - output.mkdir(parents=True) - return output - - @property - def curses_colors(self) -> str: - """Color for the curses dialog - - returns one of: Logos, Light or Dark""" - return self._raw.curses_colors - - @curses_colors.setter - def curses_colors(self, value: str): - if value not in self._curses_colors_valid_values: - raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") - self._raw.curses_colors = value - self._write() - - def cycle_curses_color_scheme(self): - new_index = self._curses_colors_valid_values.index(self.curses_colors) + 1 - if new_index == len(self._curses_colors_valid_values): - new_index = 0 - self.curses_colors = self._curses_colors_valid_values[new_index] - - @property - def logos_exe(self) -> Optional[str]: - # XXX: consider caching this value? This is a directory walk, and it's called by a wine user and logos_*_exe - return utils.find_installed_product(self.faithlife_product, self.wine_prefix) - - @property - def wine_user(self) -> Optional[str]: - path: Optional[str] = self.logos_exe - if path is None: - return None - normalized_path: str = os.path.normpath(path) - path_parts = normalized_path.split(os.sep) - return path_parts[path_parts.index('users') + 1] - - @property - def logos_cef_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 - - @property - def logos_indexer_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - - @property - def logos_login_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 - - @property - def log_level(self) -> str | int: - if self._overrides.log_level is not None: - return self._overrides.log_level - return constants.DEFAULT_LOG_LEVEL - - @property - # XXX: don't like this pattern. - def skip_winetricks(self) -> bool: - if self._overrides.winetricks_skip is not None: - return self._overrides.winetricks_skip - return False \ No newline at end of file + # @abc.abstractmethod + # def update_progress(self, message: str, percent: Optional[int] = None): + # """Updates the progress of the current operation""" + # pass \ No newline at end of file diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 7f65ba13..90267208 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -3,7 +3,6 @@ import os import tempfile from typing import Optional -from warnings import deprecated from . import constants @@ -27,7 +26,6 @@ 'CONFIG_FILE': None, 'DELETE_LOG': None, 'DIALOG': None, - 'LOGOS_LOG': os.path.expanduser(f"~/.local/state/FaithLife-Community/{constants.BINARY_NAME}.log"), # noqa: E501 # This is the installed Logos.exe 'LOGOS_EXE': None, 'SELECTED_APPIMAGE_FILENAME': None, @@ -96,7 +94,6 @@ # XXX: remove this -@deprecated def get_config_file_dict(config_file_path): config_dict = {} if config_file_path.endswith('.json'): diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index f451fa1e..0e6364ad 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -9,7 +9,9 @@ # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 -DEFAULT_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 +DEFAULT_APP_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 +DEFAULT_APP_LOG_PATH= os.path.expanduser(f"~/.local/state/FaithLife-Community/{BINARY_NAME}.log") # noqa: E501 +DEFAULT_WINEDEBUG = "fixme-all,err-all" LEGACY_CONFIG_FILES = [ os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 @@ -24,3 +26,10 @@ LOGOS9_WINE64_BOTTLE_TARGZ_URL = f"https://github.com/ferion11/wine64_bottle_dotnet/releases/download/v5.11b/{LOGOS9_WINE64_BOTTLE_TARGZ_NAME}" # noqa: E501 PID_FILE = f'/tmp/{BINARY_NAME}.pid' WINETRICKS_VERSION = '20220411' + +# Strings for choosing a follow up file or directory +PROMPT_OPTION_DIRECTORY = "Choose Directory" +PROMPT_OPTION_FILE = "Choose File" + +# String for when a binary is meant to be downloaded later +DOWNLOAD = "Download" diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index de15b901..ca2d9f3b 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -12,7 +12,7 @@ import time from pathlib import Path -from ou_dedetai.app import DOWNLOAD, App +from ou_dedetai.app import App from . import config from . import constants @@ -27,12 +27,6 @@ def edit_file(config_file: str): subprocess.Popen(['xdg-open', config_file]) -def delete_log_file_contents(): - # Write empty file. - with open(config.LOGOS_LOG, 'w') as f: - f.write('') - - def backup(app: App): backup_and_restore(mode='backup', app=app) @@ -246,7 +240,7 @@ def remove_library_catalog(): def set_winetricks(app: App): msg.status("Preparing winetricks…") - if app.conf.winetricks_binary != DOWNLOAD: + if app.conf.winetricks_binary != constants.DOWNLOAD: valid = True # Double check it's a valid winetricks if not Path(app.conf.winetricks_binary).exists(): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 6dd8f61b..685da638 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -15,7 +15,8 @@ from tkinter.ttk import Style from typing import Optional -from ou_dedetai.app import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE, App +from ou_dedetai.app import App +from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE from . import config from . import constants diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index ab47957a..a978c2e1 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -from ou_dedetai.app import DOWNLOAD, App +from ou_dedetai.new_config import App from . import config from . import constants @@ -263,7 +263,7 @@ def ensure_winetricks_executable(app: App): app=app ) - if app.conf.winetricks_binary == DOWNLOAD or not os.access(app.conf.winetricks_binary, os.X_OK): + if app.conf.winetricks_binary == constants.DOWNLOAD or not os.access(app.conf.winetricks_binary, os.X_OK): # Either previous system winetricks is no longer accessible, or the # or the user has chosen to download it. msg.status("Downloading winetricks from the Internet…", app=app) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 77cfdaa7..3318e0eb 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -2,7 +2,6 @@ import argparse import curses -from ou_dedetai.app import EnvironmentOverrides try: import dialog # noqa: F401 except ImportError: @@ -212,6 +211,7 @@ def parse_args(args, parser): msg.update_log_level(logging.DEBUG) if args.delete_log: + # XXX: what to do about this? Logging is already initialized, I guess we could clear from underneath? config.DELETE_LOG = True if args.set_appimage: @@ -323,12 +323,9 @@ def set_config(): parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately - # Get config based on env and configuration file - log_level = EnvironmentOverrides.load().log_level | constants.DEFAULT_LOG_LEVEL - # Set runtime config. # Initialize logging. - msg.initialize_logging(log_level) + msg.initialize_logging() # Set default config; incl. defining CONFIG_FILE. utils.set_default_config() @@ -432,12 +429,6 @@ def main(): set_config() set_dialog() - # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that - # can be run in conjunction with other actions, so it gets special - # treatment here once config is set. - if config.DELETE_LOG and os.path.isfile(config.LOGOS_LOG): - control.delete_log_file_contents() - # Run safety checks. # FIXME: Fix utils.die_if_running() for GUI; as it is, it breaks GUI # self-update when updating LLI as it asks for a confirmation in the CLI. @@ -448,7 +439,6 @@ def main(): # Print terminal banner logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 - logging.debug(f"Installer log file: {config.LOGOS_LOG}") check_incompatibilities() diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 042c2cad..4b1bb874 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -7,6 +7,7 @@ import sys from pathlib import Path +from ou_dedetai.new_config import EnvironmentOverrides from . import config from . import constants @@ -67,7 +68,7 @@ def get_log_level_name(level): return name -def initialize_logging(stderr_log_level): +def initialize_logging(): ''' Log levels: Level Value Description @@ -79,14 +80,36 @@ def initialize_logging(stderr_log_level): NOTSET 0 all events are handled ''' + # Get config based on env and configuration file + # This loads from file/env, but won't prompt the user if it can't find something. + # The downside of this is: these values may not be set + config = EnvironmentOverrides.load() + log_level = config.log_level or constants.DEFAULT_LOG_LEVEL + # XXX: somehow this tis tuple + app_log_path = config.app_log_path or constants.DEFAULT_APP_LOG_PATH + del config + + # Ensure the application log's directory exists + os.makedirs(os.path.dirname(app_log_path), exist_ok=True) + + # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that + # can be run in conjunction with other actions, so it gets special + # treatment here once config is set. + # if config.DELETE_LOG and os.path.isfile(app_log_path): + # # Write empty file. + # with open(app_log_path, 'w') as f: + # f.write('') + # Ensure log file parent folders exist. - log_parent = Path(config.LOGOS_LOG).parent + log_parent = Path(app_log_path).parent if not log_parent.is_dir(): log_parent.mkdir(parents=True) + logging.debug(f"Installer log file: {app_log_path}") + # Define logging handlers. file_h = GzippedRotatingFileHandler( - config.LOGOS_LOG, + app_log_path, maxBytes=10*1024*1024, backupCount=5, encoding='UTF8' @@ -98,7 +121,7 @@ def initialize_logging(stderr_log_level): # stdout_h.setLevel(stdout_log_level) stderr_h = logging.StreamHandler(sys.stderr) stderr_h.name = "terminal" - stderr_h.setLevel(stderr_log_level) + stderr_h.setLevel(log_level) stderr_h.addFilter(DeduplicateFilter()) handlers = [ file_h, diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index f1c3349b..e4c4e404 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -473,7 +473,7 @@ def check_for_updates(): set_logoslinuxinstaller_latest_release_config() utils.compare_logos_linux_installer_version() set_recommended_appimage_config() - wine.enforce_icu_data_files() + # wine.enforce_icu_data_files() config.LAST_UPDATED = now.isoformat() utils.write_config(config.CONFIG_FILE) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py new file mode 100644 index 00000000..1a26c3c3 --- /dev/null +++ b/ou_dedetai/new_config.py @@ -0,0 +1,631 @@ +from dataclasses import dataclass +from datetime import datetime +import json +import logging +import os +from pathlib import Path +from typing import Optional + +from ou_dedetai import msg, network, utils, constants + +from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY + +# XXX: move these configs into config.py once it's cleared out +@dataclass +class LegacyConfiguration: + """Configuration and it's keys from before the user configuration class existed. + + Useful for one directional compatibility""" + # Legacy Core Configuration + FLPRODUCT: Optional[str] = None + TARGETVERSION: Optional[str] = None + TARGET_RELEASE_VERSION: Optional[str] = None + current_logos_version: Optional[str] = None + curses_colors: Optional[str] = None + INSTALLDIR: Optional[str] = None + WINETRICKSBIN: Optional[str] = None + WINEBIN_CODE: Optional[str] = None + WINE_EXE: Optional[str] = None + WINECMD_ENCODING: Optional[str] = None + LOGS: Optional[str] = None + BACKUPDIR: Optional[str] = None + LAST_UPDATED: Optional[str] = None + RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None + LLI_LATEST_VERSION: Optional[str] = None + logos_release_channel: Optional[str] = None + lli_release_channel: Optional[str] = None + + # Legacy Extended Configuration + APPIMAGE_LINK_SELECTION_NAME: Optional[str] = None + APPDIR_BINDIR: Optional[str] = None + CHECK_UPDATES: Optional[bool] = None + CONFIG_FILE: Optional[str] = None + CUSTOMBINPATH: Optional[str] = None + DEBUG: Optional[bool] = None + DELETE_LOG: Optional[str] = None + DIALOG: Optional[str] = None + LOGOS_LOG: Optional[str] = None + wine_log: Optional[str] = None + LOGOS_EXE: Optional[str] = None + # This is the logos installer executable name (NOT path) + LOGOS_EXECUTABLE: Optional[str] = None + LOGOS_VERSION: Optional[str] = None + # This wasn't overridable in the bash version of this installer (at 554c9a6), + # nor was it used in the python version (at 8926435) + # LOGOS64_MSI: Optional[str] + LOGOS64_URL: Optional[str] = None + SELECTED_APPIMAGE_FILENAME: Optional[str] = None + SKIP_DEPENDENCIES: Optional[bool] = None + SKIP_FONTS: Optional[bool] = None + SKIP_WINETRICKS: Optional[bool] = None + use_python_dialog: Optional[str] = None + VERBOSE: Optional[bool] = None + WINEBIN_CODE: Optional[str] = None + WINEDEBUG: Optional[str] = None, + WINEDLLOVERRIDES: Optional[str] = None + WINEPREFIX: Optional[str] = None + WINE_EXE: Optional[str] = None + WINESERVER_EXE: Optional[str] = None + WINETRICKS_UNATTENDED: Optional[str] = None + + + @classmethod + def config_file_path(cls) -> str: + # XXX: consider legacy config files + return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH + + @classmethod + def load(cls) -> "LegacyConfiguration": + """Find the relevant config file and load it""" + # Update config from CONFIG_FILE. + config_file_path = LegacyConfiguration.config_file_path() + if not utils.file_exists(config_file_path): # noqa: E501 + for legacy_config in constants.LEGACY_CONFIG_FILES: + if utils.file_exists(legacy_config): + return LegacyConfiguration._load(legacy_config) + else: + return LegacyConfiguration._load(config_file_path) + logging.debug("Couldn't find config file, loading defaults...") + return LegacyConfiguration() + + @classmethod + def _load(cls, path: str) -> "LegacyConfiguration": + config_file_path = LegacyConfiguration.config_file_path() + config_dict = {} + if config_file_path.endswith('.json'): + try: + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) + + for key, value in cfg.items(): + config_dict[key] = value + except TypeError as e: + logging.error("Error opening Config file.") + logging.error(e) + raise e + except FileNotFoundError: + logging.info(f"No config file not found at {config_file_path}") + except json.JSONDecodeError as e: + logging.error("Config file could not be read.") + logging.error(e) + raise e + elif config_file_path.endswith('.conf'): + # Legacy config from bash script. + logging.info("Reading from legacy config file.") + with open(config_file_path, 'r') as config_file: + for line in config_file: + line = line.strip() + if len(line) == 0: # skip blank lines + continue + if line[0] == '#': # skip commented lines + continue + parts = line.split('=') + if len(parts) == 2: + value = parts[1].strip('"').strip("'") # remove quotes + vparts = value.split('#') # get rid of potential comment + if len(vparts) > 1: + value = vparts[0].strip().strip('"').strip("'") + config_dict[parts[0]] = value + + # Now update from ENV + for var in LegacyConfiguration().__dict__.keys(): + if os.getenv(var) is not None: + config_dict[var] = os.getenv(var) + + return LegacyConfiguration(**config_dict) + + +# XXX: rename, this is a set of overrides set by the user (via env) for values that are normally programatic. +# These DO NOT represent normal user choices, however normally fallback to defaults +# We can recover from all of these being optional (assuming the user choices are filled out), while in the case of UserConfigration we'd have to call out to the app. +@dataclass +class EnvironmentOverrides: + """Allows some values to be overridden from environment. + + The actually name of the environment variables remains unchanged from before, + this translates the environment variable names to the new variable names""" + + installer_binary_dir: Optional[str] + wineserver_binary: Optional[str] + faithlife_product_version: Optional[str] + faithlife_installer_name: Optional[str] + faithlife_installer_download_url: Optional[str] + log_level: Optional[str | int] + app_log_path: Optional[str] + # Path to log wine's output to + app_wine_log_path: Optional[str] + + winetricks_skip: Optional[bool] + + # Corresponds to wine's WINEDLLOVERRIDES + wine_dll_overrides: Optional[str] + # Corresponds to wine's WINEDEBUG + wine_debug: Optional[str] + # Corresponds to wine's WINEPREFIX + wine_prefix: Optional[str] + + # Additional path to look for when searching for binaries. + # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) + custom_binary_path: Optional[str] + + @classmethod + def from_legacy(cls, legacy: LegacyConfiguration) -> "EnvironmentOverrides": + log_level = None + wine_debug = legacy.WINEDEBUG + if legacy.DEBUG: + log_level = logging.DEBUG + # FIXME: shouldn't this leave it untouched or fall back to default: `fixme-all,err-all`? + wine_debug = "" + elif legacy.VERBOSE: + log_level = logging.INFO + wine_debug = "" + return EnvironmentOverrides( + installer_binary_dir=legacy.APPDIR_BINDIR, + wineserver_binary=legacy.WINESERVER_EXE, + custom_binary_path=legacy.CUSTOMBINPATH, + faithlife_product_version=legacy.LOGOS_VERSION, + faithlife_installer_name=legacy.LOGOS_EXECUTABLE, + faithlife_installer_download_url=legacy.LOGOS64_URL, + winetricks_skip=legacy.SKIP_WINETRICKS, + log_level=log_level, + wine_debug=wine_debug, + wine_dll_overrides=legacy.WINEDLLOVERRIDES, + wine_prefix=legacy.WINEPREFIX, + app_wine_log_path=legacy.wine_log, + app_log_path=legacy.LOGOS_LOG + ) + + @classmethod + def load(cls) -> "EnvironmentOverrides": + return EnvironmentOverrides.from_legacy(LegacyConfiguration.load()) + + +@dataclass +class UserConfiguration: + """This is the class that actually stores the values. + + Normally shouldn't be used directly, as it's types may be None + + Easy reading to/from JSON and supports legacy keys""" + + # XXX: store a version in this config? Just in case we need to do conditional logic reading old version's configurations + + faithlife_product: Optional[str] = None + faithlife_product_version: Optional[str] = None + faithlife_product_release: Optional[str] = None + install_dir: Optional[Path] = None + winetricks_binary: Optional[str] = None + wine_binary: Optional[str] = None + # This is where to search for wine + wine_binary_code: Optional[str] = None + backup_dir: Optional[Path] = None + + # Color to use in curses. Either "Logos", "Light", or "Dark" + curses_colors: str = "Logos" + # Faithlife's release channel. Either "stable" or "beta" + faithlife_product_release_channel: str = "stable" + # The Installer's release channel. Either "stable" or "beta" + installer_release_channel: str = "stable" + + @classmethod + def read_from_file_and_env(cls) -> "UserConfiguration": + # First read in the legacy configuration + new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.load()) + # Then read the file again this time looking for the new keys + config_file_path = LegacyConfiguration.config_file_path() + + new_keys = new_config.__dict__.keys() + + config_dict = new_config.__dict__ + + if config_file_path.endswith('.json'): + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) + + for key, value in cfg.items(): + if key in new_keys: + config_dict[key] = value + else: + logging.info("Not reading new values from non-json config") + + return UserConfiguration(**config_dict) + + @classmethod + def from_legacy(cls, legacy: LegacyConfiguration) -> "UserConfiguration": + return UserConfiguration( + faithlife_product=legacy.FLPRODUCT, + backup_dir=legacy.BACKUPDIR, + curses_colors=legacy.curses_colors, + faithlife_product_release=legacy.TARGET_RELEASE_VERSION, + faithlife_product_release_channel=legacy.logos_release_channel, + faithlife_product_version=legacy.TARGETVERSION, + install_dir=legacy.INSTALLDIR, + installer_release_channel=legacy.lli_release_channel, + wine_binary=legacy.WINE_EXE, + wine_binary_code=legacy.WINEBIN_CODE, + winetricks_binary=legacy.WINETRICKSBIN + ) + + def write_config(self): + config_file_path = LegacyConfiguration.config_file_path() + output = self.__dict__ + + logging.info(f"Writing config to {config_file_path}") + os.makedirs(os.path.dirname(config_file_path), exist_ok=True) + + # Ensure all paths stored are relative to install_dir + for k, v in output.items(): + # XXX: test this + if isinstance(v, Path) or (isinstance(v, str) and v.startswith(self.install_dir)): + output[k] = utils.get_relative_path(v, self.install_dir) + + try: + with open(config_file_path, 'w') as config_file: + json.dump(output, config_file, indent=4, sort_keys=True) + config_file.write('\n') + except IOError as e: + msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + # Continue, the installer can still operate even if it fails to write. + + +# XXX: what to do with these? +# Used to be called current_logos_version, but actually could be used in Verbium too. +installed_faithlife_product_release: Optional[str] = None +# Whether or not the installed faithlife product is configured for additional logging. +# Used to be called "LOGS" +installed_faithlife_logging: Optional[bool] = None +# Text encoding of the wine command. This calue can be retrieved from the system +winecmd_encoding: Optional[str] = None +last_updated: Optional[datetime] = None +recommended_wine_url: Optional[str] = None +latest_installer_version: Optional[str] = None + + +class Config: + """Set of configuration values. + + If the user hasn't selected a particular value yet, they will be prompted in their UI. + """ + + # Naming conventions: + # Use `dir` instead of `directory` + # Use snake_case + # prefix with faithlife_ if it's theirs + # 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 _path if it's a file path + + # Storage for the keys + _raw: UserConfiguration + + # Overriding programmatically generated values from ENV + _overrides: EnvironmentOverrides + + _curses_colors_valid_values = ["Light", "Dark", "Logos"] + + # Singleton logic, this enforces that only one config object exists at a time. + def __new__(cls, self) -> "Config": + if not hasattr(cls, '_instance'): + cls._instance = super(Config, cls).__new__(cls) + return cls._instance + + def __init__(self, app) -> None: + # This lazy load is required otherwise it would be a circular import + from ou_dedetai.app import App + self.app: App = app + self._raw = UserConfiguration.read_from_file_and_env() + logging.debug("Current persistent config:") + for k, v in self._raw.__dict__.items(): + logging.debug(f"{k}: {v}") + + def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: + # XXX: should this also update the feedback? + if not getattr(self._raw, parameter): + if dependent_parameters is not None: + for dependent_config_key in dependent_parameters: + setattr(self._raw, dependent_config_key, None) + answer = self.app.ask(question, options) + # Use the setter on this class if found, otherwise set in self._user + if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: + getattr(Config, parameter).fset(self, answer) + else: + setattr(self._raw, parameter, answer) + self._write() + return getattr(self._raw, parameter) + + def _write(self): + """Writes configuration to file and lets the app know something changed""" + self._raw.write_config() + self.app._config_updated() + + @property + def config_file_path(self) -> str: + return LegacyConfiguration.config_file_path() + + @property + def faithlife_product(self) -> str: + question = "Choose which FaithLife product the script should install: " # noqa: E501 + options = ["Logos", "Verbum"] + return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) + + @faithlife_product.setter + def faithlife_product(self, value: Optional[str]): + if self._raw.faithlife_product != value: + self._raw.faithlife_product = value + # Reset dependent variables + self.faithlife_product_release = None + + self._write() + + @property + def faithlife_product_version(self) -> str: + if self._overrides.faithlife_product_version is not None: + return self._overrides.faithlife_product_version + question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 + options = ["10", "9"] + return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) + + @faithlife_product_version.setter + def faithlife_product_version(self, value: Optional[str]): + if self._raw.faithlife_product_version != value: + self._raw.faithlife_product_release = None + # Install Dir has the name of the product and it's version. Reset it too + self._raw.install_dir = None + # Wine is dependent on the product/version selected + self._raw.wine_binary = None + self._raw.wine_binary_code = None + self._raw.winetricks_binary = None + + self._write() + + @property + def faithlife_product_release(self) -> str: + question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 + options = network.get_logos_releases(None) + return self._ask_if_not_found("faithlife_product_release", question, options) + + @faithlife_product_release.setter + def faithlife_product_release(self, value: str): + if self._raw.faithlife_product_release != value: + self._raw.faithlife_product_release = value + self._write() + + @property + def faithlife_installer_name(self) -> str: + if self._overrides.faithlife_installer_name is not None: + return self._overrides.faithlife_installer_name + return f"{self.faithlife_product}_v{self.faithlife_product_version}-x64.msi" + + @property + def faithlife_installer_download_url(self) -> str: + if self._overrides.faithlife_installer_download_url is not None: + return self._overrides.faithlife_installer_download_url + after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" + return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 + + @property + def faithlife_product_release_channel(self) -> str: + return self._raw.faithlife_product_release_channel + + @property + def installer_release_channel(self) -> str: + return self._raw.installer_release_channel + + @property + def winetricks_binary(self) -> str: + """This may be a path to the winetricks binary or it may be "Download" + """ + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux.", # noqa: E501 + options = utils.get_winetricks_options() + return self._ask_if_not_found("winetricks_binary", question, options) + + @winetricks_binary.setter + def winetricks_binary(self, value: Optional[str | Path]): + if value is not None and value != "Download": + value = Path(value) + if not value.exists(): + raise ValueError("Winetricks binary must exist") + if self._raw.winetricks_binary != value: + self._raw.winetricks_binary = value + self._write() + + @property + def install_dir(self) -> str: + default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 + question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 + options = [default, PROMPT_OPTION_DIRECTORY] + output = self._ask_if_not_found("install_dir", question, options) + return output + + @property + # This used to be called APPDIR_BINDIR + def installer_binary_dir(self) -> str: + if self._overrides.installer_binary_dir is not None: + return self._overrides.installer_binary_dir + return f"{self.install_dir}/data/bin" + + @property + # This used to be called WINEPREFIX + def wine_prefix(self) -> str: + if self._overrides.wine_prefix is not None: + return self._overrides.wine_prefix + return f"{self.install_dir}/data/wine64_bottle" + + @property + def wine_binary(self) -> str: + """Returns absolute path to the wine binary""" + if not self._raw.wine_binary: + question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 + network.set_recommended_appimage_config() + options = utils.get_wine_options( + self, + utils.find_appimage_files(self.faithlife_product_release), + utils.find_wine_binary_files(self.app, self.faithlife_product_release) + ) + + choice = self.app.ask(question, options) + + self.wine_binary = choice + # Return the full path so we the callee doesn't need to think about it + if not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): + return str(Path(self.install_dir) / self._raw.wine_binary) + return self._raw.wine_binary + + @wine_binary.setter + def wine_binary(self, value: str): + if (Path(self.install_dir) / value).exists(): + value = (Path(self.install_dir) / value).absolute() + if not Path(value).is_file(): + raise ValueError("Wine Binary path must be a valid file") + + if self._raw.wine_binary != value: + if value is not None: + value = Path(value).absolute() + self._raw.wine_binary = value + # Reset dependents + self._raw.wine_binary_code = None + self._write() + + @property + def wine64_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wine64') + + @property + # This used to be called WINESERVER_EXE + def wineserver_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wineserver') + + @property + def wine_dll_overrides(self) -> str: + """Used to set WINEDLLOVERRIDES""" + if self._overrides.wine_dll_overrides is not None: + return self._overrides.wine_dll_overrides + # Default is no overrides + return '' + + @property + def wine_debug(self) -> str: + """Used to set WINEDEBUG""" + if self._overrides.wine_debug is not None: + return self._overrides.wine_debug + return constants.DEFAULT_WINEDEBUG + + @property + def app_wine_log_path(self) -> str: + if self._overrides.app_wine_log_path is not None: + return self._overrides.app_wine_log_path + return constants.DEFAULT_APP_WINE_LOG_PATH + + @property + def app_log_path(self) -> str: + if self._overrides.app_log_path is not None: + return self._overrides.app_log_path + return constants.DEFAULT_APP_LOG_PATH + + def toggle_faithlife_product_release_channel(self): + if self._raw.faithlife_product_release_channel == "stable": + new_channel = "beta" + else: + new_channel = "stable" + self._raw.faithlife_product_release_channel = new_channel + self._write() + + def toggle_installer_release_channel(self): + if self._raw.installer_release_channel == "stable": + new_channel = "dev" + else: + new_channel = "stable" + self._raw.installer_release_channel = new_channel + self._write() + + @property + def backup_dir(self) -> Path: + question = "New or existing folder to store backups in: " + options = [PROMPT_OPTION_DIRECTORY] + output = Path(self._ask_if_not_found("backup_dir", question, options)) + output.mkdir(parents=True) + return output + + @property + def curses_colors(self) -> str: + """Color for the curses dialog + + returns one of: Logos, Light or Dark""" + return self._raw.curses_colors + + @curses_colors.setter + def curses_colors(self, value: str): + if value not in self._curses_colors_valid_values: + raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") + self._raw.curses_colors = value + self._write() + + def cycle_curses_color_scheme(self): + new_index = self._curses_colors_valid_values.index(self.curses_colors) + 1 + if new_index == len(self._curses_colors_valid_values): + new_index = 0 + self.curses_colors = self._curses_colors_valid_values[new_index] + + @property + def logos_exe(self) -> Optional[str]: + # XXX: consider caching this value? This is a directory walk, and it's called by a wine user and logos_*_exe + return utils.find_installed_product(self.faithlife_product, self.wine_prefix) + + @property + def wine_user(self) -> Optional[str]: + path: Optional[str] = self.logos_exe + if path is None: + return None + normalized_path: str = os.path.normpath(path) + path_parts = normalized_path.split(os.sep) + return path_parts[path_parts.index('users') + 1] + + @property + def logos_cef_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + + @property + def logos_indexer_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + + @property + def logos_login_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + + @property + def log_level(self) -> str | int: + if self._overrides.log_level is not None: + return self._overrides.log_level + return constants.DEFAULT_LOG_LEVEL + + @property + # XXX: don't like this pattern. + def skip_winetricks(self) -> bool: + if self._overrides.winetricks_skip is not None: + return self._overrides.winetricks_skip + return False \ No newline at end of file diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 665c4142..05d1bc19 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -8,7 +8,8 @@ from queue import Queue from typing import Optional -from ou_dedetai.app import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE, App +from ou_dedetai.app import App +from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE from . import config from . import control diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 52c02763..23e9aa39 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -61,7 +61,6 @@ def set_default_config(): if config.CONFIG_FILE is None: config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH config.MYDOWNLOADS = get_user_downloads_dir() - os.makedirs(os.path.dirname(config.LOGOS_LOG), exist_ok=True) # XXX: remove, no need. diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index ed7ebf2e..37033921 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -304,7 +304,7 @@ def run_wine_proc( cmd = f"subprocess cmd: '{' '.join(command)}'" logging.debug(cmd) try: - with open(app.conf.wine_log_path, 'a') as wine_log: + with open(app.conf.app_wine_log_path, 'a') as wine_log: print(f"{utils.get_timestamp()}: {cmd}", file=wine_log) process = system.popen_command( command, From b138e522ad8fa1f13dab72017cb056048ec201f7 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:13:10 -0800 Subject: [PATCH 021/137] fix: migrate INSTALLDIR --- ou_dedetai/config.py | 5 ----- ou_dedetai/msg.py | 1 - ou_dedetai/network.py | 13 +++++++++++-- ou_dedetai/utils.py | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 90267208..e39bf9fd 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -9,7 +9,6 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these current_logos_version = None -INSTALLDIR = None WINEBIN_CODE = None WINE_EXE = None WINECMD_ENCODING = None @@ -142,12 +141,8 @@ def set_config_env(config_file_path): return # msg.logos_error(f"Error: Unable to get config at {config_file_path}") logging.info(f"Setting {len(config_dict)} variables from config file.") - # XXX: this could literally set any of the global values, but they're normally read from config. - # Does that still work? What's going on here? - # Guess I could read all legacy keys and legacy env from the file... YIKES. for key, value in config_dict.items(): globals()[key] = value - installdir = config_dict.get('INSTALLDIR') # XXX: remove this def get_env_config(): diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 4b1bb874..b2a79c19 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -85,7 +85,6 @@ def initialize_logging(): # The downside of this is: these values may not be set config = EnvironmentOverrides.load() log_level = config.log_level or constants.DEFAULT_LOG_LEVEL - # XXX: somehow this tis tuple app_log_path = config.app_log_path or constants.DEFAULT_APP_LOG_PATH del config diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index e4c4e404..0b1a9e21 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -16,6 +16,7 @@ from ou_dedetai import wine from ou_dedetai.app import App +from ou_dedetai.new_config import EnvironmentOverrides from . import config from . import constants @@ -444,13 +445,21 @@ def set_recommended_appimage_config(): config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" -# XXX: this may be a bit of an issue, this is before the app initializes. I supposed we could load a proto-config that doesn't have any of the ensuring the user is prompted def check_for_updates(): # We limit the number of times set_recommended_appimage_config is run in # order to avoid GitHub API limits. This sets the check to once every 12 # hours. - config.current_logos_version = utils.get_current_logos_version() + # Get config based on env and configuration file + # This loads from file/env, but won't prompt the user if it can't find something. + # The downside of this is: these values may not be set + # XXX: rename + conf = EnvironmentOverrides.load() + install_dir = config.installer_binary_dir + del conf + + if install_dir is not None: + config.current_logos_version = utils.get_current_logos_version(install_dir) utils.write_config(config.CONFIG_FILE) # TODO: Check for New Logos Versions. See #116. diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 23e9aa39..62c729d3 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -210,8 +210,8 @@ def file_exists(file_path): return False -def get_current_logos_version(): - path_regex = f"{config.INSTALLDIR}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 +def get_current_logos_version(install_dir: str): + path_regex = f"{install_dir}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) logos_version_number = None if file_paths: From 07df426c553f983d0cf3543d3badd7e921ab8eed Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:13:52 -0800 Subject: [PATCH 022/137] fix: migrate WINE_EXE --- ou_dedetai/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e39bf9fd..64b49658 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -10,7 +10,6 @@ # XXX: slowly kill these current_logos_version = None WINEBIN_CODE = None -WINE_EXE = None WINECMD_ENCODING = None LOGS = None LAST_UPDATED = None @@ -33,7 +32,6 @@ # Dependent on DIALOG with env override 'use_python_dialog': None, 'WINEBIN_CODE': None, - 'WINE_EXE': None, 'WINETRICKS_UNATTENDED': None, } for key, default in extended_config.items(): From 29af5ada1d173ff07ec9b2ed4ce370cd58369419 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:29:49 -0800 Subject: [PATCH 023/137] refactor: passed config explicitly trying to avoid monster object being passed around a ton --- ou_dedetai/installer.py | 4 ++-- ou_dedetai/logos.py | 4 ++-- ou_dedetai/system.py | 13 +++++++------ ou_dedetai/utils.py | 2 ++ ou_dedetai/wine.py | 25 ++++++++++++------------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index a978c2e1..1c8f13e7 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -from ou_dedetai.new_config import App +from ou_dedetai.app import App from . import config from . import constants @@ -347,7 +347,7 @@ def ensure_wineprefix_init(app: App): ) else: logging.debug("Initializing wineprefix.") - process = wine.initializeWineBottle(app) + process = wine.initializeWineBottle(app.conf.wine64_binary) wine.wait_pid(process) # wine.light_wineserver_wait() wine.wineserver_wait(app) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 3cb7fd3c..41f598de 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -90,7 +90,7 @@ def run_logos(): if not good_wine: msg.logos_error(reason, app=self) else: - wine.wineserver_kill(app=self.app) + wine.wineserver_kill(self.app.conf.wineserver_binary) app = self.app if config.DIALOG == 'tk': # Don't send "Running" message to GUI b/c it never clears. @@ -173,7 +173,7 @@ def wait_on_indexing(): msg.status("Indexing has finished.", self.app) wine.wineserver_wait(app=self.app) - wine.wineserver_kill(app=self.app) + wine.wineserver_kill(self.app.conf.wineserver_binary) msg.status("Indexing has begun…", self.app) index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 33a3cac7..c1d3770f 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -198,6 +198,7 @@ def get_pids(query): return results +# XXX: should this be in config? def get_logos_pids(app: App): config.processes[app.conf.logos_exe] = get_pids(app.conf.logos_exe) config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) @@ -391,7 +392,7 @@ def get_runmode(): return 'script' -def query_packages(packages, mode="install", app=None): +def query_packages(packages, mode="install"): result = "" missing_packages = [] conflicting_packages = [] @@ -507,6 +508,7 @@ def parse_date(version): def remove_appimagelauncher(app=None): pkg = "appimagelauncher" cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, pkg] # noqa: E501 + # FIXME: should this status be higher? (the caller of this function) msg.status("Removing AppImageLauncher…", app) try: logging.debug(f"Running command: {cmd}") @@ -547,7 +549,7 @@ def postinstall_dependencies_steamos(): return command -def preinstall_dependencies(app=None): +def preinstall_dependencies(): command = [] logging.debug("Performing pre-install dependencies…") if config.OS_NAME == "Steam": @@ -557,7 +559,7 @@ def preinstall_dependencies(app=None): return command -def postinstall_dependencies(app=None): +def postinstall_dependencies(): command = [] logging.debug("Performing post-install dependencies…") if config.OS_NAME == "Steam": @@ -567,6 +569,7 @@ def postinstall_dependencies(app=None): return command +# XXX: move this to control, prompts additional values from app def install_dependencies(packages, bad_packages, logos9_packages=None, app=None): # noqa: E501 if config.SKIP_DEPENDENCIES: return @@ -599,12 +602,10 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) logging.debug("Querying packages…") missing_packages = query_packages( package_list, - app=app ) conflicting_packages = query_packages( bad_package_list, mode="remove", - app=app ) if config.PACKAGE_MANAGER_COMMAND_INSTALL: @@ -658,7 +659,7 @@ def install_dependencies(packages, bad_packages, logos9_packages=None, app=None) else: logging.debug("No conflicting packages detected.") - postinstall_command = postinstall_dependencies(app) + postinstall_command = postinstall_dependencies() if preinstall_command: command.extend(preinstall_command) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 62c729d3..5ca6ab60 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -178,6 +178,7 @@ def delete_symlink(symlink_path): logging.error(f"Error removing symlink: {e}") +# XXX: seems like it should be in control def install_dependencies(app: App): if app.conf.faithlife_product_version: targetversion = int(app.conf.faithlife_product_version) @@ -771,6 +772,7 @@ def update_to_latest_lli_release(app=None): logging.debug(f"{constants.APP_NAME} is at a newer version than the latest.") # noqa: 501 +# XXX: seems like this should be in control def update_to_latest_recommended_appimage(app: App): config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 status, _ = compare_recommended_appimage_version(app) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 37033921..8ebc50f4 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -18,26 +18,26 @@ from .config import processes -def check_wineserver(app: App): +def check_wineserver(wineserver_binary: str): try: # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of WINESERVER_EXE # changed it to use wineserver_binary, this change may alter the behavior, to match what the code intended - process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-p"]) + process = run_wine_proc(wineserver_binary, exe_args=["-p"]) wait_pid(process) return process.returncode == 0 except Exception: return False -def wineserver_kill(app: App): - if check_wineserver(app): - process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-k"]) +def wineserver_kill(wineserver_binary: str): + if check_wineserver(wineserver_binary): + process = run_wine_proc(wineserver_binary, exe_args=["-k"]) wait_pid(process) -def wineserver_wait(app: App): - if check_wineserver(app): - process = run_wine_proc(app.conf.wineserver_binary, exe_args=["-w"]) +def wineserver_wait(wineserver_binary: str): + if check_wineserver(wineserver_binary): + process = run_wine_proc(wineserver_binary, exe_args=["-w"]) wait_pid(process) @@ -204,15 +204,14 @@ def check_wine_version_and_branch(release_version, test_binary): return True, "None" -def initializeWineBottle(app: App): +def initializeWineBottle(wine64_binary: str): msg.status("Initializing wine bottle…") - wine_exe = app.conf.wine64_binary - logging.debug(f"{wine_exe=}") + logging.debug(f"{wine64_binary=}") # Avoid wine-mono window wine_dll_override="mscoree=" - logging.debug(f"Running: {wine_exe} wineboot --init") + logging.debug(f"Running: {wine64_binary} wineboot --init") process = run_wine_proc( - wine_exe, + wine64_binary, exe='wineboot', exe_args=['--init'], init=True, From ac353bf4e90bb9a28e44d5e87a6d109a51992d43 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:39:26 -0800 Subject: [PATCH 024/137] fix: migrate INSTALL_STEP --- ou_dedetai/app.py | 8 +++- ou_dedetai/config.py | 2 - ou_dedetai/gui_app.py | 4 +- ou_dedetai/installer.py | 97 ++++++++++++++++++++-------------------- ou_dedetai/tui_app.py | 7 +-- ou_dedetai/tui_screen.py | 12 ++--- 6 files changed, 68 insertions(+), 62 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 61d79c22..e75c0905 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,11 +1,17 @@ import abc +from dataclasses import dataclass from typing import Optional from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE class App(abc.ABC): + installer_step_count: int = 0 + """Total steps in the installer, only set the installation process has started.""" + installer_step: int = 1 + """Step the installer is on. Starts at 0""" + def __init__(self, **kwargs) -> None: # This lazy load is required otherwise it would be a circular import from ou_dedetai.new_config import Config @@ -59,4 +65,4 @@ def _config_updated(self): # @abc.abstractmethod # def update_progress(self, message: str, percent: Optional[int] = None): # """Updates the progress of the current operation""" - # pass \ No newline at end of file + # pass diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 64b49658..e5edd801 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -41,8 +41,6 @@ ACTION: str = 'app' APPIMAGE_FILE_PATH: Optional[str] = None BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. -INSTALL_STEP: int = 0 -INSTALL_STEPS_COUNT: int = 0 L9PACKAGES = None LOGOS_FORCE_ROOT: bool = False LOGOS_ICON_FILENAME: Optional[str] = None diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 685da638..7ddaf673 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -266,8 +266,8 @@ def _config_updated(self): def start_ensure_config(self): # Ensure progress counter is reset. - config.INSTALL_STEP = 1 - config.INSTALL_STEPS_COUNT = 0 + self.installer_step = 0 + self.installer_step_count = 0 self.config_thread = utils.start_thread( installer.ensure_installation_config, app=self, diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 1c8f13e7..d5a2fe3d 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -17,7 +17,7 @@ # XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses def ensure_product_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 update_install_feedback("Choose product…", app=app) logging.debug('- config.FLPRODUCT') @@ -26,21 +26,20 @@ def ensure_product_choice(app: App): # XXX: we don't need this install step anymore def ensure_version_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_product_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step += 1 update_install_feedback("Choose version…", app=app) logging.debug('- config.TARGETVERSION') # Accessing this ensures it's set - app.conf.faithlife_product_version logging.debug(f"> config.TARGETVERSION={app.conf.faithlife_product_version=}") # XXX: no longer needed def ensure_release_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_version_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Choose product release…", app=app) logging.debug('- config.TARGET_RELEASE_VERSION') # accessing this sets the config @@ -49,9 +48,9 @@ def ensure_release_choice(app: App): def ensure_install_dir_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_release_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Choose installation folder…", app=app) logging.debug('- config.INSTALLDIR') # Accessing this sets install_dir and bin_dir @@ -61,9 +60,9 @@ def ensure_install_dir_choice(app: App): def ensure_wine_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_install_dir_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Choose wine binary…", app=app) logging.debug('- config.SELECTED_APPIMAGE_FILENAME') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_URL') @@ -90,9 +89,9 @@ def ensure_wine_choice(app: App): # XXX: this isn't needed anymore def ensure_winetricks_choice(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_wine_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') # Accessing the winetricks_binary variable will do this. @@ -102,9 +101,9 @@ def ensure_winetricks_choice(app: App): # XXX: huh? What does this do? def ensure_install_fonts_choice(app=None): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_winetricks_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring install fonts choice…", app=app) logging.debug('- config.SKIP_FONTS') @@ -113,9 +112,9 @@ def ensure_install_fonts_choice(app=None): # XXX: huh? What does this do? def ensure_check_sys_deps_choice(app=None): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_install_fonts_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback( "Ensuring check system dependencies choice…", app=app @@ -126,9 +125,9 @@ def ensure_check_sys_deps_choice(app=None): def ensure_installation_config(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_check_sys_deps_choice(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring installation config is set…", app=app) logging.debug('- config.LOGOS_ICON_URL') logging.debug('- config.LOGOS_ICON_FILENAME') @@ -159,9 +158,9 @@ def ensure_installation_config(app: App): def ensure_install_dirs(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_installation_config(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring installation directories…", app=app) logging.debug('- config.INSTALLDIR') logging.debug('- config.WINEPREFIX') @@ -188,9 +187,9 @@ def ensure_install_dirs(app: App): def ensure_sys_deps(app=None): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_install_dirs(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring system dependencies are met…", app=app) if not config.SKIP_DEPENDENCIES: @@ -203,9 +202,9 @@ def ensure_sys_deps(app=None): def ensure_appimage_download(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_sys_deps(app=app) - config.INSTALL_STEP += 1 + app.installer_step if app.conf.faithlife_product_version != '9' and not str(app.conf.wine_binary).lower().endswith('appimage'): # noqa: E501 return update_install_feedback( @@ -229,9 +228,9 @@ def ensure_appimage_download(app: App): def ensure_wine_executables(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_appimage_download(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback( "Ensuring wine executables are available…", app=app @@ -255,9 +254,9 @@ def ensure_wine_executables(app: App): def ensure_winetricks_executable(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_wine_executables(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback( "Ensuring winetricks executable is available…", app=app @@ -274,9 +273,9 @@ def ensure_winetricks_executable(app: App): def ensure_premade_winebottle_download(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_winetricks_executable(app=app) - config.INSTALL_STEP += 1 + app.installer_step if app.conf.faithlife_product_version != '9': return update_install_feedback( @@ -305,9 +304,9 @@ def ensure_premade_winebottle_download(app: App): def ensure_product_installer_download(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_premade_winebottle_download(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback( f"Ensuring {app.conf.faithlife_product} installer is downloaded…", app=app @@ -331,9 +330,9 @@ def ensure_product_installer_download(app: App): def ensure_wineprefix_init(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_product_installer_download(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring wineprefix is initialized…", app=app) init_file = Path(f"{app.conf.wine_prefix}/system.reg") @@ -356,9 +355,9 @@ def ensure_wineprefix_init(app: App): def ensure_winetricks_applied(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_wineprefix_init(app=app) - config.INSTALL_STEP += 1 + app.installer_step status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) logging.debug('- disable winemenubuilder') @@ -410,9 +409,9 @@ def ensure_winetricks_applied(app: App): def ensure_icu_data_files(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_winetricks_applied(app=app) - config.INSTALL_STEP += 1 + app.installer_step status = "Ensuring ICU data files are installed…" update_install_feedback(status, app=app) logging.debug('- ICU data files') @@ -426,9 +425,9 @@ def ensure_icu_data_files(app: App): def ensure_product_installed(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_icu_data_files(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback( f"Ensuring {app.conf.faithlife_product} is installed…", app=app @@ -447,9 +446,9 @@ def ensure_product_installed(app: App): def ensure_config_file(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_product_installed(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Ensuring config file is up-to-date…", app=app) # XXX: Why the platform specific logic? @@ -463,9 +462,9 @@ def ensure_config_file(app: App): def ensure_launcher_executable(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_config_file(app=app) - config.INSTALL_STEP += 1 + app.installer_step runmode = system.get_runmode() if runmode == 'binary': update_install_feedback( @@ -489,9 +488,9 @@ def ensure_launcher_executable(app: App): def ensure_launcher_shortcuts(app: App): - config.INSTALL_STEPS_COUNT += 1 + app.installer_step_count += 1 ensure_launcher_executable(app=app) - config.INSTALL_STEP += 1 + app.installer_step update_install_feedback("Creating launcher shortcuts…", app=app) runmode = system.get_runmode() if runmode == 'binary': @@ -512,9 +511,9 @@ def ensure_launcher_shortcuts(app: App): app.stop() -def update_install_feedback(text, app=None): - percent = get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) - logging.debug(f"Install step {config.INSTALL_STEP} of {config.INSTALL_STEPS_COUNT}") # noqa: E501 +def update_install_feedback(text, app: App): + percent = get_progress_pct(app.installer_step, app.installer_step_count) + logging.debug(f"Install step {app.installer_step} of {app.installer_step_count}") # noqa: E501 msg.progress(percent, app=app) msg.status(text, app=app) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 05d1bc19..1635b294 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -388,8 +388,8 @@ def main_menu_select(self, choice): self.llirunning = False elif choice.startswith("Install"): self.reset_screen() - config.INSTALL_STEPS_COUNT = 0 - config.INSTALL_STEP = 0 + self.installer_step = 0 + self.installer_step_count = 0 utils.start_thread( installer.ensure_launcher_shortcuts, daemon_bool=True, @@ -651,7 +651,8 @@ def handle_ask_directory_response(self, choice: Optional[str]): def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) - percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + + percent = installer.get_progress_pct(self.installer_step, self.installer_step_count) self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, dialog=dialog)) diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index b9b3b1a7..764c8322 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -3,6 +3,8 @@ import time from pathlib import Path +from ou_dedetai.app import App + from . import config from . import installer from . import system @@ -13,7 +15,7 @@ class Screen: - def __init__(self, app, screen_id, queue, event): + def __init__(self, app: App, screen_id, queue, event): self.app = app self.stdscr = "" self.screen_id = screen_id @@ -353,8 +355,8 @@ def __str__(self): def display(self): if self.running == 0: if self.wait: - if config.INSTALL_STEPS_COUNT > 0: - self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + if self.app.installer_step_count > 0: + self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) else: self.percent = 0 @@ -365,8 +367,8 @@ def display(self): self.running = 1 elif self.running == 1: if self.wait: - if config.INSTALL_STEPS_COUNT > 0: - self.percent = installer.get_progress_pct(config.INSTALL_STEP, config.INSTALL_STEPS_COUNT) + if self.app.installer_step_count > 0: + self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) else: self.percent = 0 From 6045412cabcdf21ab5c7d500b2e17b7205a4eb12 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:48:32 -0800 Subject: [PATCH 025/137] fix: migrate WINETRICKS_UNATTENDED --- ou_dedetai/config.py | 3 --- ou_dedetai/network.py | 14 +------------- ou_dedetai/new_config.py | 16 ++++++++++++++-- ou_dedetai/wine.py | 9 +++------ 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e5edd801..ba2376ce 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -32,7 +32,6 @@ # Dependent on DIALOG with env override 'use_python_dialog': None, 'WINEBIN_CODE': None, - 'WINETRICKS_UNATTENDED': None, } for key, default in extended_config.items(): globals()[key] = os.getenv(key, default) @@ -47,8 +46,6 @@ LOGOS_ICON_URL: Optional[str] = None LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None -LOGOS9_RELEASES = None # used to save downloaded releases list # FIXME: not set #noqa: E501 -LOGOS10_RELEASES = None # used to save downloaded releases list # FIXME: not set #noqa: E501 MYDOWNLOADS: Optional[str] = None # FIXME: Should this use ~/.cache? OS_NAME: Optional[str] = None OS_RELEASE: Optional[str] = None diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 0b1a9e21..5090487a 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -504,19 +504,7 @@ def get_recommended_appimage(app: App): ) def get_logos_releases(app: App) -> list[str]: - # Use already-downloaded list if requested again. - downloaded_releases = None - if app.conf.faithlife_product_version == '9' and config.LOGOS9_RELEASES: - downloaded_releases = config.LOGOS9_RELEASES - elif app.conf.faithlife_product_version == '10' and config.LOGOS10_RELEASES: - downloaded_releases = config.LOGOS10_RELEASES - if downloaded_releases: - logging.debug(f"Using already-downloaded list of v{app.conf.faithlife_product_version} releases") # noqa: E501 - if app: - app.releases_q.put(downloaded_releases) - app.root.event_generate(app.release_evt) - return downloaded_releases - + # TODO: Use already-downloaded list if requested again. msg.status(f"Downloading release list for {app.conf.faithlife_product} {app.conf.faithlife_product_version}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. if app.conf.faithlife_product_release_channel == "beta": diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 1a26c3c3..be452909 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -152,8 +152,12 @@ class EnvironmentOverrides: faithlife_installer_download_url: Optional[str] log_level: Optional[str | int] app_log_path: Optional[str] - # Path to log wine's output to app_wine_log_path: Optional[str] + """Path to log wine's output to""" + app_winetricks_unattended: Optional[bool] + """Whether or not to send -q to winetricks for all winetricks commands. + + Some commands always send -q""" winetricks_skip: Optional[bool] @@ -192,7 +196,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EnvironmentOverrides": wine_dll_overrides=legacy.WINEDLLOVERRIDES, wine_prefix=legacy.WINEPREFIX, app_wine_log_path=legacy.wine_log, - app_log_path=legacy.LOGOS_LOG + app_log_path=legacy.LOGOS_LOG, + app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED ) @classmethod @@ -544,6 +549,13 @@ def app_log_path(self) -> str: return self._overrides.app_log_path return constants.DEFAULT_APP_LOG_PATH + @property + def app_winetricks_unattended(self) -> bool: + """If true, pass -q to winetricks""" + if self._overrides.app_winetricks_unattended is not None: + return self._overrides.app_winetricks_unattended + return False + def toggle_faithlife_product_release_channel(self): if self._raw.faithlife_product_release_channel == "stable": new_channel = "beta" diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 8ebc50f4..b5d05a94 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -346,6 +346,9 @@ def run_winetricks(app: App, cmd=None): # XXX: this function looks similar to the one above. duplicate? def run_winetricks_cmd(app: App, *args): cmd = [*args] + # FIXME: test this to ensure it behaves as expected + if "-q" not in args and app.conf.winetricks_binary: + cmd.insert(0, "-q") msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) @@ -361,8 +364,6 @@ def run_winetricks_cmd(app: App, *args): def install_d3d_compiler(app: App): cmd = ['d3dcompiler_47'] - if config.WINETRICKS_UNATTENDED is None: - cmd.insert(0, '-q') run_winetricks_cmd(app, *cmd) @@ -372,16 +373,12 @@ def install_fonts(app: App): if not config.SKIP_FONTS: for f in fonts: args = [f] - if config.WINETRICKS_UNATTENDED: - args.insert(0, '-q') run_winetricks_cmd(app, *args) def install_font_smoothing(app: App): msg.status("Setting font smoothing…") args = ['settings', 'fontsmooth=rgb'] - if config.WINETRICKS_UNATTENDED: - args.insert(0, '-q') run_winetricks_cmd(app, *args) From f87448b6ec5e661c6aa16d9f74e1f179a4efacbd Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:25:37 -0800 Subject: [PATCH 026/137] fix: plum new configuration through to main --- ou_dedetai/app.py | 5 +-- ou_dedetai/cli.py | 91 +++++++++++++++++++++------------------- ou_dedetai/config.py | 1 + ou_dedetai/gui_app.py | 13 +++--- ou_dedetai/main.py | 40 ++++++++++-------- ou_dedetai/msg.py | 4 +- ou_dedetai/network.py | 4 +- ou_dedetai/new_config.py | 85 +++++++++++++++++++++---------------- ou_dedetai/tui_app.py | 9 ++-- 9 files changed, 138 insertions(+), 114 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index e75c0905..70573e57 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,6 +1,5 @@ import abc -from dataclasses import dataclass from typing import Optional from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -12,10 +11,10 @@ class App(abc.ABC): installer_step: int = 1 """Step the installer is on. Starts at 0""" - def __init__(self, **kwargs) -> None: + def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise it would be a circular import from ou_dedetai.new_config import Config - self.conf = Config(self) + self.conf = Config(config, self) pass def ask(self, question: str, options: list[str]) -> str: diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 2cc7e365..f2665449 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -2,6 +2,7 @@ import threading from ou_dedetai.app import App +from ou_dedetai.new_config import EphemeralConfiguration from . import control from . import installer @@ -11,8 +12,8 @@ class CLI(App): - def __init__(self): - super().__init__() + def __init__(self, ephemeral_config: EphemeralConfiguration): + super().__init__(ephemeral_config) self.running: bool = True self.choice_q = queue.Queue() self.input_q = queue.Queue() @@ -141,85 +142,87 @@ def user_input_processor(self, evt=None): # NOTE: These subcommands are outside the CLI class so that the class can be # instantiated at the moment the subcommand is run. This lets any CLI-specific # code get executed along with the subcommand. -def backup(): - CLI().backup() +# NOTE: we should be able to achieve the same effect without re-declaring these functions +def backup(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).backup() -def create_shortcuts(): - CLI().create_shortcuts() +def create_shortcuts(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).create_shortcuts() -def edit_config(): - CLI().edit_config() +def edit_config(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).edit_config() -def get_winetricks(): - CLI().get_winetricks() +def get_winetricks(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).get_winetricks() -def install_app(): - CLI().install_app() +def install_app(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).install_app() -def install_d3d_compiler(): - CLI().install_d3d_compiler() +def install_d3d_compiler(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).install_d3d_compiler() -def install_dependencies(): - CLI().install_dependencies() +def install_dependencies(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).install_dependencies() -def install_fonts(): - CLI().install_fonts() +def install_fonts(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).install_fonts() -def install_icu(): - CLI().install_icu() +def install_icu(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).install_icu() -def remove_index_files(): - CLI().remove_index_files() +def remove_index_files(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).remove_index_files() -def remove_install_dir(): - CLI().remove_install_dir() +def remove_install_dir(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).remove_install_dir() -def remove_library_catalog(): - CLI().remove_library_catalog() +def remove_library_catalog(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).remove_library_catalog() -def restore(): - CLI().restore() +def restore(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).restore() -def run_indexing(): - CLI().run_indexing() +def run_indexing(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).run_indexing() -def run_installed_app(): - CLI().run_installed_app() +def run_installed_app(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).run_installed_app() -def run_winetricks(): - CLI().run_winetricks() +def run_winetricks(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).run_winetricks() -def set_appimage(): - CLI().set_appimage() +def set_appimage(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).set_appimage() -def toggle_app_logging(): - CLI().toggle_app_logging() +def toggle_app_logging(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).toggle_app_logging() -def update_latest_appimage(): - CLI().update_latest_appimage() +def update_latest_appimage(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).update_latest_appimage() -def update_self(): - CLI().update_self() +def update_self(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).update_self() -def winetricks(): - CLI().winetricks() + +def winetricks(ephemeral_config: EphemeralConfiguration): + CLI(ephemeral_config).winetricks() diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index ba2376ce..03f7f48d 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -50,6 +50,7 @@ OS_NAME: Optional[str] = None OS_RELEASE: Optional[str] = None PACKAGE_MANAGER_COMMAND_INSTALL: Optional[list[str]] = None +PACKAGE_MANAGER_COMMAND_DOWNLOAD: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_REMOVE: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_QUERY: Optional[list[str]] = None PACKAGES: Optional[str] = None diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 7ddaf673..9499fc8f 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -17,6 +17,7 @@ from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE +from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import constants @@ -34,8 +35,8 @@ class GuiApp(App): _exit_option: Optional[str] = None - def __init__(self, root: "Root", **kwargs): - super().__init__() + def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwargs): + super().__init__(ephemeral_config) self.root_to_destory_on_none = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: @@ -550,8 +551,8 @@ def update_install_progress(self, evt=None): class ControlWindow(GuiApp): - def __init__(self, root, *args, **kwargs): - super().__init__(root) + def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwargs): + super().__init__(root, ephemeral_config) # Set root parameters. self.root = root self.root.title(f"{constants.APP_NAME} Control Panel") @@ -902,9 +903,9 @@ def stop_indeterminate_progress(self, evt=None): self.gui.progressvar.set(0) -def control_panel_app(): +def control_panel_app(ephemeral_config: EphemeralConfiguration): utils.set_debug() classname = constants.BINARY_NAME root = Root(className=classname) - ControlWindow(root, class_=classname) + ControlWindow(root, ephemeral_config, class_=classname) root.mainloop() diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 3318e0eb..38084ab4 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -2,6 +2,8 @@ import argparse import curses +from ou_dedetai.new_config import EphemeralConfiguration + try: import dialog # noqa: F401 except ImportError: @@ -199,11 +201,13 @@ def get_parser(): return parser -def parse_args(args, parser): +def parse_args(args, parser) -> EphemeralConfiguration: if args.config: - config.CONFIG_FILE = args.config - config.set_config_env(config.CONFIG_FILE) + ephemeral_config = EphemeralConfiguration.load_from_path(args.config) + else: + ephemeral_config = EphemeralConfiguration.load() + # XXX: move the following options into the ephemeral_config if args.verbose: msg.update_log_level(logging.INFO) @@ -221,7 +225,7 @@ def parse_args(args, parser): config.SKIP_FONTS = True if args.skip_winetricks: - os.environ["SKIP_WINETRICKS"] = "True" + ephemeral_config.winetricks_skip = True if network.check_for_updates: config.CHECK_UPDATES = True @@ -292,15 +296,16 @@ def parse_args(args, parser): if config.ACTION is None: config.ACTION = run_control_panel logging.debug(f"{config.ACTION=}") + return ephemeral_config -def run_control_panel(): +def run_control_panel(ephemeral_config: EphemeralConfiguration): logging.info(f"Using DIALOG: {config.DIALOG}") if config.DIALOG is None or config.DIALOG == 'tk': - gui_app.control_panel_app() + gui_app.control_panel_app(ephemeral_config) else: try: - curses.wrapper(tui_app.control_panel_app) + curses.wrapper(tui_app.control_panel_app, ephemeral_config) except KeyboardInterrupt: raise except SystemExit: @@ -318,8 +323,7 @@ def run_control_panel(): raise e -# XXX: fold this into new config -def set_config(): +def setup_config() -> EphemeralConfiguration: parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately @@ -331,7 +335,7 @@ def set_config(): utils.set_default_config() # XXX: do this in the new scheme (read then write the config). - # We also want to remove the old file, that may be tricky. + # We also want to remove the old file, (stored in CONFIG_FILE?) # Update config from CONFIG_FILE. if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 @@ -345,7 +349,7 @@ def set_config(): config.set_config_env(config.CONFIG_FILE) # Parse CLI args and update affected config vars. - parse_args(cli_args, parser) + return parse_args(cli_args, parser) def set_dialog(): @@ -384,14 +388,14 @@ def check_incompatibilities(): system.remove_appimagelauncher() -def run(): +def run(ephemeral_config: EphemeralConfiguration): # Run desired action (requested function, defaults to control_panel) if config.ACTION == "disabled": msg.logos_error("That option is disabled.", "info") if config.ACTION.__name__ == 'run_control_panel': # if utils.app_is_installed(): # wine.set_logos_paths() - config.ACTION() # run control_panel right away + config.ACTION(ephemeral_config) # run control_panel right away return # Only control_panel ACTION uses TUI/GUI interface; all others are CLI. @@ -415,20 +419,22 @@ def run(): ] if config.ACTION.__name__ not in install_required: logging.info(f"Running function: {config.ACTION.__name__}") - config.ACTION() + config.ACTION(ephemeral_config) elif utils.app_is_installed(): # install_required; checking for app # wine.set_logos_paths() # Run the desired Logos action. logging.info(f"Running function: {config.ACTION.__name__}") # noqa: E501 - config.ACTION() + config.ACTION(ephemeral_config) else: # install_required, but app not installed msg.logos_error("App not installed…") def main(): - set_config() + ephemeral_config = setup_config() set_dialog() + # XXX: consider configuration migration from legacy to new + # Run safety checks. # FIXME: Fix utils.die_if_running() for GUI; as it is, it breaks GUI # self-update when updating LLI as it asks for a confirmation in the CLI. @@ -444,7 +450,7 @@ def main(): network.check_for_updates() - run() + run(ephemeral_config) def close(): diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index b2a79c19..0c04ec62 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -7,7 +7,7 @@ import sys from pathlib import Path -from ou_dedetai.new_config import EnvironmentOverrides +from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import constants @@ -83,7 +83,7 @@ def initialize_logging(): # Get config based on env and configuration file # This loads from file/env, but won't prompt the user if it can't find something. # The downside of this is: these values may not be set - config = EnvironmentOverrides.load() + config = EphemeralConfiguration.load() log_level = config.log_level or constants.DEFAULT_LOG_LEVEL app_log_path = config.app_log_path or constants.DEFAULT_APP_LOG_PATH del config diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 5090487a..d75062a5 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -16,7 +16,7 @@ from ou_dedetai import wine from ou_dedetai.app import App -from ou_dedetai.new_config import EnvironmentOverrides +from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import constants @@ -454,7 +454,7 @@ def check_for_updates(): # This loads from file/env, but won't prompt the user if it can't find something. # The downside of this is: these values may not be set # XXX: rename - conf = EnvironmentOverrides.load() + conf = EphemeralConfiguration.load() install_dir = config.installer_binary_dir del conf diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index be452909..c0202287 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -68,7 +68,6 @@ class LegacyConfiguration: WINESERVER_EXE: Optional[str] = None WINETRICKS_UNATTENDED: Optional[str] = None - @classmethod def config_file_path(cls) -> str: # XXX: consider legacy config files @@ -82,15 +81,14 @@ def load(cls) -> "LegacyConfiguration": if not utils.file_exists(config_file_path): # noqa: E501 for legacy_config in constants.LEGACY_CONFIG_FILES: if utils.file_exists(legacy_config): - return LegacyConfiguration._load(legacy_config) + return LegacyConfiguration.load_from_path(legacy_config) else: - return LegacyConfiguration._load(config_file_path) + return LegacyConfiguration.load_from_path(config_file_path) logging.debug("Couldn't find config file, loading defaults...") return LegacyConfiguration() @classmethod - def _load(cls, path: str) -> "LegacyConfiguration": - config_file_path = LegacyConfiguration.config_file_path() + def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": config_dict = {} if config_file_path.endswith('.json'): try: @@ -132,18 +130,20 @@ def _load(cls, path: str) -> "LegacyConfiguration": if os.getenv(var) is not None: config_dict[var] = os.getenv(var) + # Populate the path this config was loaded from + config_dict["CONFIG_FILE"] = config_file_path + return LegacyConfiguration(**config_dict) -# XXX: rename, this is a set of overrides set by the user (via env) for values that are normally programatic. -# These DO NOT represent normal user choices, however normally fallback to defaults -# We can recover from all of these being optional (assuming the user choices are filled out), while in the case of UserConfigration we'd have to call out to the app. @dataclass -class EnvironmentOverrides: - """Allows some values to be overridden from environment. - - The actually name of the environment variables remains unchanged from before, - this translates the environment variable names to the new variable names""" +class EphemeralConfiguration: + """A set of overrides that don't need to be stored. + + Populated from environment/command arguments/etc + + Changes to this are not saved to disk, but remain while the program runs + """ installer_binary_dir: Optional[str] wineserver_binary: Optional[str] @@ -172,8 +172,11 @@ class EnvironmentOverrides: # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] + config_path: str + """Path this config was loaded from""" + @classmethod - def from_legacy(cls, legacy: LegacyConfiguration) -> "EnvironmentOverrides": + def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": log_level = None wine_debug = legacy.WINEDEBUG if legacy.DEBUG: @@ -183,7 +186,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EnvironmentOverrides": elif legacy.VERBOSE: log_level = logging.INFO wine_debug = "" - return EnvironmentOverrides( + return EphemeralConfiguration( installer_binary_dir=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, custom_binary_path=legacy.CUSTOMBINPATH, @@ -197,21 +200,32 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EnvironmentOverrides": wine_prefix=legacy.WINEPREFIX, app_wine_log_path=legacy.wine_log, app_log_path=legacy.LOGOS_LOG, - app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED + app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED, + config_path=legacy.CONFIG_FILE ) @classmethod - def load(cls) -> "EnvironmentOverrides": - return EnvironmentOverrides.from_legacy(LegacyConfiguration.load()) + def load(cls) -> "EphemeralConfiguration": + return EphemeralConfiguration.from_legacy(LegacyConfiguration.load()) + + @classmethod + def load_from_path(cls, path: str) -> "EphemeralConfiguration": + return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) @dataclass -class UserConfiguration: - """This is the class that actually stores the values. +class PersistentConfiguration: + """This class stores the options the user chose - Normally shouldn't be used directly, as it's types may be None + Normally shouldn't be used directly, as it's types may be None, + doesn't handle updates. Use through the `App`'s `Config` instead. + + Easy reading to/from JSON and supports legacy keys + + These values should be stored across invocations - Easy reading to/from JSON and supports legacy keys""" + MUST be saved explicitly + """ # XXX: store a version in this config? Just in case we need to do conditional logic reading old version's configurations @@ -233,11 +247,11 @@ class UserConfiguration: installer_release_channel: str = "stable" @classmethod - def read_from_file_and_env(cls) -> "UserConfiguration": + def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": + # XXX: handle legacy migration + # First read in the legacy configuration - new_config: UserConfiguration = UserConfiguration.from_legacy(LegacyConfiguration.load()) - # Then read the file again this time looking for the new keys - config_file_path = LegacyConfiguration.config_file_path() + new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) new_keys = new_config.__dict__.keys() @@ -253,11 +267,11 @@ def read_from_file_and_env(cls) -> "UserConfiguration": else: logging.info("Not reading new values from non-json config") - return UserConfiguration(**config_dict) + return PersistentConfiguration(**config_dict) @classmethod - def from_legacy(cls, legacy: LegacyConfiguration) -> "UserConfiguration": - return UserConfiguration( + def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": + return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, backup_dir=legacy.BACKUPDIR, curses_colors=legacy.curses_colors, @@ -323,10 +337,10 @@ class Config: # suffix with _path if it's a file path # Storage for the keys - _raw: UserConfiguration + _raw: PersistentConfiguration # Overriding programmatically generated values from ENV - _overrides: EnvironmentOverrides + _overrides: EphemeralConfiguration _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -336,11 +350,10 @@ def __new__(cls, self) -> "Config": cls._instance = super(Config, cls).__new__(cls) return cls._instance - def __init__(self, app) -> None: - # This lazy load is required otherwise it would be a circular import - from ou_dedetai.app import App - self.app: App = app - self._raw = UserConfiguration.read_from_file_and_env() + def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: + self.app = app + self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) + self._overrides = ephemeral_config logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 1635b294..b95c9387 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -10,6 +10,7 @@ from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE +from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import control @@ -29,8 +30,8 @@ # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. class TUI(App): - def __init__(self, stdscr): - super().__init__() + def __init__(self, stdscr, ephemeral_config: EphemeralConfiguration): + super().__init__(ephemeral_config) self.stdscr = stdscr # if config.current_logos_version is not None: self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 @@ -917,6 +918,6 @@ def get_menu_window(self): return self.menu_window -def control_panel_app(stdscr): +def control_panel_app(stdscr, ephemeral_config: EphemeralConfiguration): os.environ.setdefault('ESCDELAY', '100') - TUI(stdscr).run() + TUI(stdscr, ephemeral_config).run() From 443c6a85b35500461f879805ae9fd47660b7d064 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:54:03 -0800 Subject: [PATCH 027/137] fix: misc --- ou_dedetai/installer.py | 42 ++++++++++++++++++++-------------------- ou_dedetai/main.py | 13 +++++++++++-- ou_dedetai/msg.py | 11 +---------- ou_dedetai/network.py | 11 +---------- ou_dedetai/new_config.py | 15 ++++++++++---- ou_dedetai/wine.py | 1 + 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index d5a2fe3d..011e6c74 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -39,7 +39,7 @@ def ensure_version_choice(app: App): def ensure_release_choice(app: App): app.installer_step_count += 1 ensure_version_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Choose product release…", app=app) logging.debug('- config.TARGET_RELEASE_VERSION') # accessing this sets the config @@ -50,7 +50,7 @@ def ensure_release_choice(app: App): def ensure_install_dir_choice(app: App): app.installer_step_count += 1 ensure_release_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Choose installation folder…", app=app) logging.debug('- config.INSTALLDIR') # Accessing this sets install_dir and bin_dir @@ -62,7 +62,7 @@ def ensure_install_dir_choice(app: App): def ensure_wine_choice(app: App): app.installer_step_count += 1 ensure_install_dir_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Choose wine binary…", app=app) logging.debug('- config.SELECTED_APPIMAGE_FILENAME') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_URL') @@ -91,7 +91,7 @@ def ensure_wine_choice(app: App): def ensure_winetricks_choice(app: App): app.installer_step_count += 1 ensure_wine_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') # Accessing the winetricks_binary variable will do this. @@ -103,7 +103,7 @@ def ensure_winetricks_choice(app: App): def ensure_install_fonts_choice(app=None): app.installer_step_count += 1 ensure_winetricks_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring install fonts choice…", app=app) logging.debug('- config.SKIP_FONTS') @@ -114,7 +114,7 @@ def ensure_install_fonts_choice(app=None): def ensure_check_sys_deps_choice(app=None): app.installer_step_count += 1 ensure_install_fonts_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback( "Ensuring check system dependencies choice…", app=app @@ -127,7 +127,7 @@ def ensure_check_sys_deps_choice(app=None): def ensure_installation_config(app: App): app.installer_step_count += 1 ensure_check_sys_deps_choice(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring installation config is set…", app=app) logging.debug('- config.LOGOS_ICON_URL') logging.debug('- config.LOGOS_ICON_FILENAME') @@ -160,7 +160,7 @@ def ensure_installation_config(app: App): def ensure_install_dirs(app: App): app.installer_step_count += 1 ensure_installation_config(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring installation directories…", app=app) logging.debug('- config.INSTALLDIR') logging.debug('- config.WINEPREFIX') @@ -189,7 +189,7 @@ def ensure_install_dirs(app: App): def ensure_sys_deps(app=None): app.installer_step_count += 1 ensure_install_dirs(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring system dependencies are met…", app=app) if not config.SKIP_DEPENDENCIES: @@ -204,7 +204,7 @@ def ensure_sys_deps(app=None): def ensure_appimage_download(app: App): app.installer_step_count += 1 ensure_sys_deps(app=app) - app.installer_step + app.installer_step += 1 if app.conf.faithlife_product_version != '9' and not str(app.conf.wine_binary).lower().endswith('appimage'): # noqa: E501 return update_install_feedback( @@ -230,7 +230,7 @@ def ensure_appimage_download(app: App): def ensure_wine_executables(app: App): app.installer_step_count += 1 ensure_appimage_download(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback( "Ensuring wine executables are available…", app=app @@ -256,7 +256,7 @@ def ensure_wine_executables(app: App): def ensure_winetricks_executable(app: App): app.installer_step_count += 1 ensure_wine_executables(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback( "Ensuring winetricks executable is available…", app=app @@ -275,7 +275,7 @@ def ensure_winetricks_executable(app: App): def ensure_premade_winebottle_download(app: App): app.installer_step_count += 1 ensure_winetricks_executable(app=app) - app.installer_step + app.installer_step += 1 if app.conf.faithlife_product_version != '9': return update_install_feedback( @@ -306,7 +306,7 @@ def ensure_premade_winebottle_download(app: App): def ensure_product_installer_download(app: App): app.installer_step_count += 1 ensure_premade_winebottle_download(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback( f"Ensuring {app.conf.faithlife_product} installer is downloaded…", app=app @@ -332,7 +332,7 @@ def ensure_product_installer_download(app: App): def ensure_wineprefix_init(app: App): app.installer_step_count += 1 ensure_product_installer_download(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring wineprefix is initialized…", app=app) init_file = Path(f"{app.conf.wine_prefix}/system.reg") @@ -357,7 +357,7 @@ def ensure_wineprefix_init(app: App): def ensure_winetricks_applied(app: App): app.installer_step_count += 1 ensure_wineprefix_init(app=app) - app.installer_step + app.installer_step += 1 status = "Ensuring winetricks & other settings are applied…" update_install_feedback(status, app=app) logging.debug('- disable winemenubuilder') @@ -411,7 +411,7 @@ def ensure_winetricks_applied(app: App): def ensure_icu_data_files(app: App): app.installer_step_count += 1 ensure_winetricks_applied(app=app) - app.installer_step + app.installer_step += 1 status = "Ensuring ICU data files are installed…" update_install_feedback(status, app=app) logging.debug('- ICU data files') @@ -427,7 +427,7 @@ def ensure_icu_data_files(app: App): def ensure_product_installed(app: App): app.installer_step_count += 1 ensure_icu_data_files(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback( f"Ensuring {app.conf.faithlife_product} is installed…", app=app @@ -448,7 +448,7 @@ def ensure_product_installed(app: App): def ensure_config_file(app: App): app.installer_step_count += 1 ensure_product_installed(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Ensuring config file is up-to-date…", app=app) # XXX: Why the platform specific logic? @@ -464,7 +464,7 @@ def ensure_config_file(app: App): def ensure_launcher_executable(app: App): app.installer_step_count += 1 ensure_config_file(app=app) - app.installer_step + app.installer_step += 1 runmode = system.get_runmode() if runmode == 'binary': update_install_feedback( @@ -490,7 +490,7 @@ def ensure_launcher_executable(app: App): def ensure_launcher_shortcuts(app: App): app.installer_step_count += 1 ensure_launcher_executable(app=app) - app.installer_step + app.installer_step += 1 update_install_feedback("Creating launcher shortcuts…", app=app) runmode = system.get_runmode() if runmode == 'binary': diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 38084ab4..fd85044a 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -327,9 +327,16 @@ def setup_config() -> EphemeralConfiguration: parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately + # Get config based on env and configuration file temporarily just to load a couple values out + # We'll load this fully later. + temp = EphemeralConfiguration.load() + log_level = temp.log_level or constants.DEFAULT_LOG_LEVEL + app_log_path = temp.app_log_path or constants.DEFAULT_APP_LOG_PATH + del temp + # Set runtime config. # Initialize logging. - msg.initialize_logging() + msg.initialize_logging(log_level, app_log_path) # Set default config; incl. defining CONFIG_FILE. utils.set_default_config() @@ -448,7 +455,9 @@ def main(): check_incompatibilities() - network.check_for_updates() + # XXX: Consider how to get the install dir from here, we'd have to read the config...which isn't done yet. + # I suppose we could read the persistent config at this point + network.check_for_updates(None) run(ephemeral_config) diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 0c04ec62..43c41bb7 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -7,7 +7,6 @@ import sys from pathlib import Path -from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import constants @@ -68,7 +67,7 @@ def get_log_level_name(level): return name -def initialize_logging(): +def initialize_logging(log_level: str| int, app_log_path: str): ''' Log levels: Level Value Description @@ -80,14 +79,6 @@ def initialize_logging(): NOTSET 0 all events are handled ''' - # Get config based on env and configuration file - # This loads from file/env, but won't prompt the user if it can't find something. - # The downside of this is: these values may not be set - config = EphemeralConfiguration.load() - log_level = config.log_level or constants.DEFAULT_LOG_LEVEL - app_log_path = config.app_log_path or constants.DEFAULT_APP_LOG_PATH - del config - # Ensure the application log's directory exists os.makedirs(os.path.dirname(app_log_path), exist_ok=True) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index d75062a5..5b693b24 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -16,7 +16,6 @@ from ou_dedetai import wine from ou_dedetai.app import App -from ou_dedetai.new_config import EphemeralConfiguration from . import config from . import constants @@ -445,19 +444,11 @@ def set_recommended_appimage_config(): config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" -def check_for_updates(): +def check_for_updates(install_dir: Optional[str]): # We limit the number of times set_recommended_appimage_config is run in # order to avoid GitHub API limits. This sets the check to once every 12 # hours. - # Get config based on env and configuration file - # This loads from file/env, but won't prompt the user if it can't find something. - # The downside of this is: these values may not be set - # XXX: rename - conf = EphemeralConfiguration.load() - install_dir = config.installer_binary_dir - del conf - if install_dir is not None: config.current_logos_version = utils.get_current_logos_version(install_dir) utils.write_config(config.CONFIG_FILE) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index c0202287..f1469c22 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -61,7 +61,7 @@ class LegacyConfiguration: use_python_dialog: Optional[str] = None VERBOSE: Optional[bool] = None WINEBIN_CODE: Optional[str] = None - WINEDEBUG: Optional[str] = None, + WINEDEBUG: Optional[str] = None WINEDLLOVERRIDES: Optional[str] = None WINEPREFIX: Optional[str] = None WINE_EXE: Optional[str] = None @@ -144,7 +144,7 @@ class EphemeralConfiguration: Changes to this are not saved to disk, but remain while the program runs """ - + # Start user overridable via env or cli arg installer_binary_dir: Optional[str] wineserver_binary: Optional[str] faithlife_product_version: Optional[str] @@ -172,9 +172,14 @@ class EphemeralConfiguration: # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] + # Start internal values config_path: str """Path this config was loaded from""" + # XXX: does this belong here, or should we have a cache file? + # Start cache + _faithlife_product_releases: Optional[list[str]] = None + @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": log_level = None @@ -345,7 +350,7 @@ class Config: _curses_colors_valid_values = ["Light", "Dark", "Logos"] # Singleton logic, this enforces that only one config object exists at a time. - def __new__(cls, self) -> "Config": + def __new__(cls, *args, **kwargs) -> "Config": if not hasattr(cls, '_instance'): cls._instance = super(Config, cls).__new__(cls) return cls._instance @@ -421,7 +426,9 @@ def faithlife_product_version(self, value: Optional[str]): @property def faithlife_product_release(self) -> str: question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 - options = network.get_logos_releases(None) + if self._overrides._faithlife_product_releases is None: + self._overrides._faithlife_product_releases = network.get_logos_releases(self.app) + options = self._overrides._faithlife_product_releases return self._ask_if_not_found("faithlife_product_release", question, options) @faithlife_product_release.setter diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index b5d05a94..fe30558e 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -400,6 +400,7 @@ def set_win_version(app: App, exe: str, windows_version: str): ] process = run_wine_proc( app.conf.wine_binary, + app, exe='reg', exe_args=exe_args ) From cdc8e0968e7cc9ed1ddc6c9bda4d7ffac4e87f96 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:28:59 -0800 Subject: [PATCH 028/137] fix: misc --- ou_dedetai/app.py | 8 ++++++++ ou_dedetai/gui_app.py | 6 +++--- ou_dedetai/logos.py | 6 ++++-- ou_dedetai/main.py | 13 +++++++++++-- ou_dedetai/new_config.py | 15 +++++++++++---- ou_dedetai/tui_app.py | 2 +- ou_dedetai/utils.py | 6 ++---- ou_dedetai/wine.py | 4 ++-- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 70573e57..fd8633b4 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,5 +1,6 @@ import abc +import os from typing import Optional from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -60,6 +61,13 @@ def _config_updated(self): """A hook for any changes the individual apps want to do when the config changes""" pass + def is_installed(self) -> bool: + """Returns whether the install was successful by + checking if the installed exe exists and is executable""" + if self.conf.logos_exe is not None: + return os.access(self.conf.logos_exe, os.X_OK) + return False + # XXX: unused at present # @abc.abstractmethod # def update_progress(self, message: str, percent: Optional[int] = None): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 9499fc8f..b5eca5a4 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -648,7 +648,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.root.bind(self.check_evt, self.update_file_check_progress) # Start function to determine app logging state. - if utils.app_is_installed(): + if self.is_installed(): self.gui.statusvar.set('Getting current app logging status…') self.start_indeterminate_progress() utils.start_thread(self.logos.get_app_logging_state) @@ -657,7 +657,7 @@ def edit_config(self): control.edit_file(self.conf.config_file_path) def configure_app_button(self, evt=None): - if utils.app_is_installed(): + if self.is_installed(): # wine.set_logos_paths() self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.gui.app_button.config(command=self.run_logos) @@ -679,7 +679,7 @@ def run_action_cmd(self, evt=None): def on_action_radio_clicked(self, evt=None): logging.debug("gui_app.ControlPanel.on_action_radio_clicked START") - if utils.app_is_installed(): + if self.is_installed(): self.gui.actions_button.state(['!disabled']) if self.gui.actionsvar.get() == 'run-indexing': self.actioncmd = self.run_indexing diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 41f598de..f45c45a3 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -63,7 +63,7 @@ def monitor_logos(self): self.logos_state = State.RUNNING def monitor(self): - if utils.app_is_installed(): + if self.app.is_installed(): system.get_logos_pids(self.app) try: self.monitor_indexing() @@ -79,13 +79,15 @@ def start(self): def run_logos(): wine.run_wine_proc( self.app.conf.wine_binary, + self.app, exe=self.app.conf.logos_exe ) # Ensure wine version is compatible with Logos release version. good_wine, reason = wine.check_wine_rules( wine_release, - config.current_logos_version + config.current_logos_version, + self.app.conf.faithlife_product_version ) if not good_wine: msg.logos_error(reason, app=self) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index fd85044a..f687676b 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -2,7 +2,7 @@ import argparse import curses -from ou_dedetai.new_config import EphemeralConfiguration +from ou_dedetai.new_config import EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path try: import dialog # noqa: F401 @@ -395,6 +395,15 @@ def check_incompatibilities(): system.remove_appimagelauncher() +def is_app_installed(ephemeral_config: EphemeralConfiguration): + persistent_config = PersistentConfiguration.load_from_path(ephemeral_config.config_path) + if persistent_config.faithlife_product is None or persistent_config.install_dir is None: + # Not enough information stored to find the product + return False + wine_prefix = ephemeral_config.wine_prefix or get_wine_prefix_path(persistent_config.install_dir) + return utils.find_installed_product(persistent_config.faithlife_product, wine_prefix) + + def run(ephemeral_config: EphemeralConfiguration): # Run desired action (requested function, defaults to control_panel) if config.ACTION == "disabled": @@ -427,7 +436,7 @@ def run(ephemeral_config: EphemeralConfiguration): if config.ACTION.__name__ not in install_required: logging.info(f"Running function: {config.ACTION.__name__}") config.ACTION(ephemeral_config) - elif utils.app_is_installed(): # install_required; checking for app + elif is_app_installed(ephemeral_config): # install_required; checking for app # wine.set_logos_paths() # Run the desired Logos action. logging.info(f"Running function: {config.ACTION.__name__}") # noqa: E501 diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index f1469c22..9bd517cc 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -179,6 +179,7 @@ class EphemeralConfiguration: # XXX: does this belong here, or should we have a cache file? # Start cache _faithlife_product_releases: Optional[list[str]] = None + _logos_exe: Optional[str] = None @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": @@ -325,6 +326,10 @@ def write_config(self): latest_installer_version: Optional[str] = None +# Needed this logic outside this class too for before when when the app is initialized +def get_wine_prefix_path(install_dir: str) -> str: + return f"{install_dir}/data/wine64_bottle" + class Config: """Set of configuration values. @@ -496,7 +501,7 @@ def installer_binary_dir(self) -> str: def wine_prefix(self) -> str: if self._overrides.wine_prefix is not None: return self._overrides.wine_prefix - return f"{self.install_dir}/data/wine64_bottle" + return get_wine_prefix_path(self.install_dir) @property def wine_binary(self) -> str: @@ -622,8 +627,10 @@ def cycle_curses_color_scheme(self): @property def logos_exe(self) -> Optional[str]: - # XXX: consider caching this value? This is a directory walk, and it's called by a wine user and logos_*_exe - return utils.find_installed_product(self.faithlife_product, self.wine_prefix) + # Cache a successful result + if self._overrides._logos_exe is None: + self._overrides._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) + return self._overrides._logos_exe @property def wine_user(self) -> Optional[str]: @@ -660,4 +667,4 @@ def log_level(self) -> str | int: def skip_winetricks(self) -> bool: if self._overrides.winetricks_skip is not None: return self._overrides.winetricks_skip - return False \ No newline at end of file + return False diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index b95c9387..6400fe8f 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -715,7 +715,7 @@ def set_tui_menu_options(self, dialog=False): else: logging.error(f"{error_message}") - if utils.app_is_installed(): + if self.is_installed(): if self.logos.logos_state in [logos.State.STARTING, logos.State.RUNNING]: # noqa: E501 run = f"Stop {self.conf.faithlife_product}" elif self.logos.logos_state in [logos.State.STOPPING, logos.State.STOPPED]: # noqa: E501 diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 5ca6ab60..8b504106 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -421,10 +421,6 @@ def write_progress_bar(percent, screen_width=80): print(f" [{y * l_y}{n * l_n}] {percent:>3}%", end='\r') -def app_is_installed(): - return config.LOGOS_EXE is not None and os.access(config.LOGOS_EXE, os.X_OK) # noqa: E501 - - def find_installed_product(faithlife_product: str, wine_prefix: str) -> Optional[str]: if faithlife_product and wine_prefix: drive_c = Path(f"{wine_prefix}/drive_c/") @@ -645,6 +641,7 @@ def find_appimage_files(app: App): output1, output2 = wine.check_wine_version_and_branch( release_version, p, + app.conf.faithlife_product_version ) if output1 is not None and output1: appimages.append(str(p)) @@ -688,6 +685,7 @@ def find_wine_binary_files(app: App, release_version): output1, output2 = wine.check_wine_version_and_branch( release_version, binary, + app.conf.faithlife_product_version ) if output1 is not None and output1: continue diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index fe30558e..23d1cb33 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -180,7 +180,7 @@ def check_wine_rules(wine_release, release_version, faithlife_product_version: s return result -def check_wine_version_and_branch(release_version, test_binary): +def check_wine_version_and_branch(release_version, test_binary, faithlife_product_version): if not os.path.exists(test_binary): reason = "Binary does not exist." return False, reason @@ -194,7 +194,7 @@ def check_wine_version_and_branch(release_version, test_binary): if wine_release is False and error_message is not None: return False, error_message - result, message = check_wine_rules(wine_release, release_version) + result, message = check_wine_rules(wine_release, release_version, faithlife_product_version) if not result: return result, message From 2c5a4fa1c6af2e0b8cd317039df3f1804b1ce084 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:33:02 -0800 Subject: [PATCH 029/137] fix: migrate LOGOS_EXE --- ou_dedetai/cli.py | 4 ++-- ou_dedetai/config.py | 2 -- ou_dedetai/control.py | 12 ++++++------ ou_dedetai/gui_app.py | 2 +- ou_dedetai/installer.py | 5 ++--- ou_dedetai/logos.py | 2 +- ou_dedetai/tui_app.py | 16 ++++++++-------- 7 files changed, 20 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index f2665449..0ec3a437 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -53,13 +53,13 @@ def install_icu(self): wine.enforce_icu_data_files() def remove_index_files(self): - control.remove_all_index_files() + control.remove_all_index_files(self) def remove_install_dir(self): control.remove_install_dir(self) def remove_library_catalog(self): - control.remove_library_catalog() + control.remove_library_catalog(self) def restore(self): control.restore(app=self) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 03f7f48d..61a12f04 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -24,8 +24,6 @@ 'CONFIG_FILE': None, 'DELETE_LOG': None, 'DIALOG': None, - # This is the installed Logos.exe - 'LOGOS_EXE': None, 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index ca2d9f3b..b738bdaa 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -89,7 +89,7 @@ def backup_and_restore(mode: str, app: App): restore_dir = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 source_dir_base = restore_dir else: - source_dir_base = Path(config.LOGOS_EXE).parent + source_dir_base = Path(app.conf.logos_exe).parent src_dirs = [source_dir_base / d for d in data_dirs if Path(source_dir_base / d).is_dir()] # noqa: E501 logging.debug(f"{src_dirs=}") if not src_dirs: @@ -126,7 +126,7 @@ def backup_and_restore(mode: str, app: App): # Set destination folder. if mode == 'restore': - dst_dir = Path(config.LOGOS_EXE).parent + dst_dir = Path(app.conf.logos_exe).parent # Remove existing data. for d in data_dirs: dst = Path(dst_dir) / d @@ -202,8 +202,8 @@ def remove_install_dir(app: App): logging.info(f"Folder doesn't exist: {folder}") -def remove_all_index_files(app=None): - logos_dir = os.path.dirname(config.LOGOS_EXE) +def remove_all_index_files(app: App): + logos_dir = os.path.dirname(app.conf.logos_exe) index_paths = [ os.path.join(logos_dir, "Data", "*", "BibleIndex"), os.path.join(logos_dir, "Data", "*", "LibraryIndex"), @@ -227,8 +227,8 @@ def remove_all_index_files(app=None): sys.exit(0) -def remove_library_catalog(): - logos_dir = os.path.dirname(config.LOGOS_EXE) +def remove_library_catalog(app: App): + logos_dir = os.path.dirname(app.conf.logos_exe) files_to_remove = glob.glob(f"{logos_dir}/Data/*/LibraryCatalog/*") for file_to_remove in files_to_remove: try: diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index b5eca5a4..c2bddc8f 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -694,7 +694,7 @@ def run_indexing(self, evt=None): utils.start_thread(self.logos.index) def remove_library_catalog(self, evt=None): - control.remove_library_catalog() + control.remove_library_catalog(self) def remove_indexes(self, evt=None): self.gui.statusvar.set("Removing indexes…") diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 011e6c74..0d0e1e2b 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -433,16 +433,15 @@ def ensure_product_installed(app: App): app=app ) - if not utils.find_installed_product(app.conf.faithlife_product, app.conf.wine_prefix): + if not app.is_installed(): process = wine.install_msi(app) wine.wait_pid(process) - config.LOGOS_EXE = utils.find_installed_product(app.conf.faithlife_product, app.conf.wine_prefix) config.current_logos_version = app.conf.faithlife_product_release # Clean up temp files, etc. utils.clean_all() - logging.debug(f"> Product path: {config.LOGOS_EXE}") + logging.debug(f"> Product path: config.LOGOS_EXE={app.conf.logos_exe}") def ensure_config_file(app: App): diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index f45c45a3..061eb538 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -106,7 +106,7 @@ def run_logos(): # if config.DIALOG == 'cli': # run_logos() # self.monitor() - # while config.processes.get(config.LOGOS_EXE) is None: + # while config.processes.get(app.conf.logos_exe) is None: # time.sleep(0.1) # while self.logos_state != State.STOPPED: # time.sleep(0.1) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 6400fe8f..81c99273 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -415,7 +415,7 @@ def main_menu_select(self, choice): elif choice == "Remove Library Catalog": self.active_screen.running = 0 self.active_screen.choice = "Processing" - control.remove_library_catalog() + control.remove_library_catalog(self) elif choice.startswith("Winetricks"): self.reset_screen() self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", @@ -473,11 +473,11 @@ def winetricks_menu_select(self, choice): def utilities_menu_select(self, choice): if choice == "Remove Library Catalog": self.reset_screen() - control.remove_library_catalog() + control.remove_library_catalog(self) self.go_to_main_menu() elif choice == "Remove All Index Files": self.reset_screen() - control.remove_all_index_files() + control.remove_all_index_files(self) self.go_to_main_menu() elif choice == "Edit Config": self.reset_screen() @@ -803,11 +803,11 @@ def set_win_ver_menu_options(self, dialog=False): def set_utilities_menu_options(self, dialog=False): labels = [] - if utils.file_exists(config.LOGOS_EXE): + if self.is_installed(): labels_catalog = [ - "Remove Library Catalog", - "Remove All Index Files", - "Install ICU" + "Remove Library Catalog", + "Remove All Index Files", + "Install ICU" ] labels.extend(labels_catalog) @@ -817,7 +817,7 @@ def set_utilities_menu_options(self, dialog=False): ] labels.extend(labels_utilities) - if utils.file_exists(config.LOGOS_EXE): + if self.is_installed(): labels_utils_installed = [ "Change Logos Release Channel", f"Change {constants.APP_NAME} Release Channel", From ec6abe34aff2ac2b9e40054f7157d7205143ac56 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:36:48 -0800 Subject: [PATCH 030/137] fix: migrate DELETE_LOG --- ou_dedetai/config.py | 1 - ou_dedetai/main.py | 12 ++++++++++-- ou_dedetai/msg.py | 10 +--------- ou_dedetai/new_config.py | 11 +++++++---- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 61a12f04..b1b86d15 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -22,7 +22,6 @@ 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', 'CHECK_UPDATES': False, 'CONFIG_FILE': None, - 'DELETE_LOG': None, 'DIALOG': None, 'SELECTED_APPIMAGE_FILENAME': None, 'SKIP_DEPENDENCIES': False, diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index f687676b..e1892aca 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -215,8 +215,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: msg.update_log_level(logging.DEBUG) if args.delete_log: - # XXX: what to do about this? Logging is already initialized, I guess we could clear from underneath? - config.DELETE_LOG = True + ephemeral_config.delete_log = True if args.set_appimage: config.APPIMAGE_FILE_PATH = args.set_appimage[0] @@ -451,6 +450,15 @@ def main(): # XXX: consider configuration migration from legacy to new + # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that + # can be run in conjunction with other actions, so it gets special + # treatment here once config is set. + app_log_path = ephemeral_config.app_log_path | constants.DEFAULT_APP_LOG_PATH + if ephemeral_config.delete_log and os.path.isfile(app_log_path): + # Write empty file. + with open(app_log_path, 'w') as f: + f.write('') + # Run safety checks. # FIXME: Fix utils.die_if_running() for GUI; as it is, it breaks GUI # self-update when updating LLI as it asks for a confirmation in the CLI. diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 43c41bb7..fb893b7b 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -67,7 +67,7 @@ def get_log_level_name(level): return name -def initialize_logging(log_level: str| int, app_log_path: str): +def initialize_logging(log_level: str | int, app_log_path: str): ''' Log levels: Level Value Description @@ -82,14 +82,6 @@ def initialize_logging(log_level: str| int, app_log_path: str): # Ensure the application log's directory exists os.makedirs(os.path.dirname(app_log_path), exist_ok=True) - # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that - # can be run in conjunction with other actions, so it gets special - # treatment here once config is set. - # if config.DELETE_LOG and os.path.isfile(app_log_path): - # # Write empty file. - # with open(app_log_path, 'w') as f: - # f.write('') - # Ensure log file parent folders exist. log_parent = Path(app_log_path).parent if not log_parent.is_dir(): diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 9bd517cc..76ee9974 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -161,16 +161,19 @@ class EphemeralConfiguration: winetricks_skip: Optional[bool] - # Corresponds to wine's WINEDLLOVERRIDES wine_dll_overrides: Optional[str] - # Corresponds to wine's WINEDEBUG + """Corresponds to wine's WINEDLLOVERRIDES""" wine_debug: Optional[str] - # Corresponds to wine's WINEPREFIX + """Corresponds to wine's WINEDEBUG""" wine_prefix: Optional[str] + """Corresponds to wine's WINEPREFIX""" - # Additional path to look for when searching for binaries. # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) custom_binary_path: Optional[str] + """Additional path to look for when searching for binaries.""" + + delete_log: Optional[bool] + """Whether to clear the log on startup""" # Start internal values config_path: str From b06c38cdfc3bba6b371d18aa5b2096aa7a83db69 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:43:30 -0800 Subject: [PATCH 031/137] fix: migrate CHECK_UPDATES --- ou_dedetai/config.py | 1 - ou_dedetai/main.py | 6 ++++-- ou_dedetai/network.py | 4 ++-- ou_dedetai/new_config.py | 7 ++++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index b1b86d15..213a2072 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -20,7 +20,6 @@ # Define and set additional variables that can be set in the env. extended_config = { 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', - 'CHECK_UPDATES': False, 'CONFIG_FILE': None, 'DIALOG': None, 'SELECTED_APPIMAGE_FILENAME': None, diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index e1892aca..96763061 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -226,8 +226,10 @@ def parse_args(args, parser) -> EphemeralConfiguration: if args.skip_winetricks: ephemeral_config.winetricks_skip = True + # FIXME: Should this have been args.check_for_updates? + # Should this even be an option? if network.check_for_updates: - config.CHECK_UPDATES = True + ephemeral_config.check_updates_now = True if args.skip_dependencies: config.SKIP_DEPENDENCIES = True @@ -474,7 +476,7 @@ def main(): # XXX: Consider how to get the install dir from here, we'd have to read the config...which isn't done yet. # I suppose we could read the persistent config at this point - network.check_for_updates(None) + network.check_for_updates(None, bool(ephemeral_config.check_updates_now)) run(ephemeral_config) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 5b693b24..c386bcc9 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -444,7 +444,7 @@ def set_recommended_appimage_config(): config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" -def check_for_updates(install_dir: Optional[str]): +def check_for_updates(install_dir: Optional[str], force: bool = False): # We limit the number of times set_recommended_appimage_config is run in # order to avoid GitHub API limits. This sets the check to once every 12 # hours. @@ -456,7 +456,7 @@ def check_for_updates(install_dir: Optional[str]): # TODO: Check for New Logos Versions. See #116. now = datetime.now().replace(microsecond=0) - if config.CHECK_UPDATES: + if force: check_again = now elif config.LAST_UPDATED is not None: check_again = datetime.strptime( diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 76ee9974..d9fea821 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -175,6 +175,9 @@ class EphemeralConfiguration: delete_log: Optional[bool] """Whether to clear the log on startup""" + check_updates_now: Optional[bool] + """Whether or not to check updates regardless of if one's due""" + # Start internal values config_path: str """Path this config was loaded from""" @@ -210,7 +213,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": app_wine_log_path=legacy.wine_log, app_log_path=legacy.LOGOS_LOG, app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED, - config_path=legacy.CONFIG_FILE + config_path=legacy.CONFIG_FILE, + check_updates_now=legacy.CHECK_UPDATES, + delete_log=legacy.DELETE_LOG ) @classmethod From f3221eb5db5ae42e032503fec8f7d39b8a11db07 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:52:10 -0800 Subject: [PATCH 032/137] fix: migrate SKIP_DEPENDENCIES --- ou_dedetai/config.py | 1 - ou_dedetai/gui.py | 5 +---- ou_dedetai/gui_app.py | 5 ++--- ou_dedetai/installer.py | 8 ++++---- ou_dedetai/main.py | 2 +- ou_dedetai/new_config.py | 18 +++++++++++++----- ou_dedetai/system.py | 4 ++-- ou_dedetai/utils.py | 6 +++--- 8 files changed, 26 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 213a2072..cad72897 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -23,7 +23,6 @@ 'CONFIG_FILE': None, 'DIALOG': None, 'SELECTED_APPIMAGE_FILENAME': None, - 'SKIP_DEPENDENCIES': False, 'SKIP_FONTS': False, # Dependent on DIALOG with env override 'use_python_dialog': None, diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index bc1aa3d1..358411d1 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -68,9 +68,6 @@ def __init__(self, root, app: App, **kwargs): self.skip_fonts = config.SKIP_FONTS if self.skip_fonts is None: self.skip_fonts = 0 - self.skip_dependencies = config.SKIP_DEPENDENCIES - if self.skip_dependencies is None: - self.skip_dependencies = 0 # Product/Version row. self.product_label = Label(self, text="Product & Version: ") @@ -139,7 +136,7 @@ def __init__(self, root, app: App, **kwargs): # Skip Dependencies row. self.skipdeps_label = Label(self, text="Install Dependencies: ") - self.skipdepsvar = BooleanVar(value=1-self.skip_dependencies) + self.skipdepsvar = BooleanVar(value=1-self.app.conf.skip_install_system_dependencies) self.skipdeps_checkbox = Checkbutton(self, variable=self.skipdepsvar) # Cancel/Okay buttons row. diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index c2bddc8f..90591187 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -450,9 +450,8 @@ def set_skip_fonts(self, evt=None): logging.debug(f"> {config.SKIP_FONTS=}") def set_skip_dependencies(self, evt=None): - self.gui.skip_dependencies = 1 - self.gui.skipdepsvar.get() # invert True/False # noqa: E501 - config.SKIP_DEPENDENCIES = self.gui.skip_dependencies - logging.debug(f"> {config.SKIP_DEPENDENCIES=}") + self.conf.skip_install_system_dependencies = 1 - self.gui.skipdepsvar.get() # invert True/False # noqa: E501 + logging.debug(f"> config.SKIP_DEPENDENCIES={self.conf.skip_install_system_dependencies}") def on_okay_released(self, evt=None): # Update desktop panel icon. diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 0d0e1e2b..2fd09c52 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -111,7 +111,7 @@ def ensure_install_fonts_choice(app=None): # XXX: huh? What does this do? -def ensure_check_sys_deps_choice(app=None): +def ensure_check_sys_deps_choice(app: App): app.installer_step_count += 1 ensure_install_fonts_choice(app=app) app.installer_step += 1 @@ -121,7 +121,7 @@ def ensure_check_sys_deps_choice(app=None): ) logging.debug('- config.SKIP_DEPENDENCIES') - logging.debug(f"> {config.SKIP_DEPENDENCIES=}") + logging.debug(f"> config.SKIP_DEPENDENCIES={app.conf._overrides.winetricks_skip}") def ensure_installation_config(app: App): @@ -186,13 +186,13 @@ def ensure_install_dirs(app: App): utils.send_task(app, 'INSTALLING') -def ensure_sys_deps(app=None): +def ensure_sys_deps(app: App): app.installer_step_count += 1 ensure_install_dirs(app=app) app.installer_step += 1 update_install_feedback("Ensuring system dependencies are met…", app=app) - if not config.SKIP_DEPENDENCIES: + if not app.conf.skip_install_system_dependencies: utils.install_dependencies(app) if config.DIALOG == "curses": app.installdeps_e.wait() diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 96763061..7b2035ea 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -232,7 +232,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: ephemeral_config.check_updates_now = True if args.skip_dependencies: - config.SKIP_DEPENDENCIES = True + ephemeral_config.install_dependencies_skip = True if args.force_root: config.LOGOS_FORCE_ROOT = True diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index d9fea821..0a54a59a 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -160,6 +160,8 @@ class EphemeralConfiguration: Some commands always send -q""" winetricks_skip: Optional[bool] + install_dependencies_skip: Optional[bool] + """Whether to skip installing system package dependencies""" wine_dll_overrides: Optional[str] """Corresponds to wine's WINEDLLOVERRIDES""" @@ -215,7 +217,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED, config_path=legacy.CONFIG_FILE, check_updates_now=legacy.CHECK_UPDATES, - delete_log=legacy.DELETE_LOG + delete_log=legacy.DELETE_LOG, + install_dependencies_skip=legacy.SKIP_DEPENDENCIES ) @classmethod @@ -671,8 +674,13 @@ def log_level(self) -> str | int: return constants.DEFAULT_LOG_LEVEL @property - # XXX: don't like this pattern. def skip_winetricks(self) -> bool: - if self._overrides.winetricks_skip is not None: - return self._overrides.winetricks_skip - return False + return bool(self._overrides.winetricks_skip) + + @property + def skip_install_system_dependencies(self) -> bool: + return bool(self._overrides.install_dependencies_skip) + + @skip_install_system_dependencies.setter + def skip_install_system_dependencies(self, val: bool): + self._overrides.install_dependencies_skip = val \ No newline at end of file diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index c1d3770f..61ac99ec 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -570,8 +570,8 @@ def postinstall_dependencies(): # XXX: move this to control, prompts additional values from app -def install_dependencies(packages, bad_packages, logos9_packages=None, app=None): # noqa: E501 - if config.SKIP_DEPENDENCIES: +def install_dependencies(app: App, packages, bad_packages, logos9_packages=None): # noqa: E501 + if app.conf.skip_install_system_dependencies: return install_deps_failed = False diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 8b504106..2f0e36bc 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -187,13 +187,13 @@ def install_dependencies(app: App): msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) if targetversion == 10: - system.install_dependencies(config.PACKAGES, config.BADPACKAGES, app=app) # noqa: E501 + system.install_dependencies(app, config.PACKAGES, config.BADPACKAGES) # noqa: E501 elif targetversion == 9: system.install_dependencies( + app, config.PACKAGES, config.BADPACKAGES, - config.L9PACKAGES, - app=app + config.L9PACKAGES ) else: logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") From 4a73f8edbe7dc91a9e0773f4239e82ab3e3a9439 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:58:35 -0800 Subject: [PATCH 033/137] fix: migrate SKIP_FONTS --- ou_dedetai/config.py | 1 - ou_dedetai/gui.py | 16 +++++----------- ou_dedetai/gui_app.py | 7 +++---- ou_dedetai/installer.py | 6 +++--- ou_dedetai/main.py | 2 +- ou_dedetai/new_config.py | 17 ++++++++++++++--- ou_dedetai/wine.py | 2 +- 7 files changed, 27 insertions(+), 24 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index cad72897..dd4cbb46 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -23,7 +23,6 @@ 'CONFIG_FILE': None, 'DIALOG': None, 'SELECTED_APPIMAGE_FILENAME': None, - 'SKIP_FONTS': False, # Dependent on DIALOG with env override 'use_python_dialog': None, 'WINEBIN_CODE': None, diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index 358411d1..bb5d432a 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -62,13 +62,6 @@ def __init__(self, root, app: App, **kwargs): self.app = app - # XXX: remove these - # Initialize vars from ENV. - self.wine_exe = app.conf.wine_binary - self.skip_fonts = config.SKIP_FONTS - if self.skip_fonts is None: - self.skip_fonts = 0 - # Product/Version row. self.product_label = Label(self, text="Product & Version: ") # product drop-down menu @@ -112,9 +105,10 @@ def __init__(self, root, app: App, **kwargs): self.wine_dropdown = Combobox(self, textvariable=self.winevar) self.wine_dropdown.state(['readonly']) self.wine_dropdown['values'] = [] - if self.wine_exe: - self.wine_dropdown['values'] = [self.wine_exe] - self.winevar.set(self.wine_exe) + # Conditional only if wine_binary is actually set, don't prompt if it's not + if self.app.conf._raw.wine_binary: + self.wine_dropdown['values'] = [self.app.conf.wine_binary] + self.winevar.set(self.app.conf.wine_binary) self.wine_check_button = Button(self, text="Get EXE List") self.wine_check_button.state(['disabled']) @@ -131,7 +125,7 @@ def __init__(self, root, app: App, **kwargs): # Fonts row. self.fonts_label = Label(self, text="Install Fonts: ") - self.fontsvar = BooleanVar(value=1-self.skip_fonts) + self.fontsvar = BooleanVar(value=1-self.app.conf.skip_install_fonts) self.fonts_checkbox = Checkbutton(self, variable=self.fontsvar) # Skip Dependencies row. diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 90591187..9c4048c7 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -425,7 +425,7 @@ def set_wine(self, evt=None): else: self.wine_q.put( utils.get_relative_path( - utils.get_config_var(self.gui.wine_exe), + utils.get_config_var(self.conf.wine_binary), self.conf.install_dir ) ) @@ -445,9 +445,8 @@ def on_wine_check_released(self, evt=None): self.start_wine_versions_check(self.conf.faithlife_product_release) def set_skip_fonts(self, evt=None): - self.gui.skip_fonts = 1 - self.gui.fontsvar.get() # invert True/False - config.SKIP_FONTS = self.gui.skip_fonts - logging.debug(f"> {config.SKIP_FONTS=}") + self.conf.skip_install_fonts = 1 - self.gui.fontsvar.get() # invert True/False + logging.debug(f"> config.SKIP_FONTS={self.conf.skip_install_fonts}") def set_skip_dependencies(self, evt=None): self.conf.skip_install_system_dependencies = 1 - self.gui.skipdepsvar.get() # invert True/False # noqa: E501 diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 2fd09c52..ad32befe 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -100,14 +100,14 @@ def ensure_winetricks_choice(app: App): # XXX: huh? What does this do? -def ensure_install_fonts_choice(app=None): +def ensure_install_fonts_choice(app: App): app.installer_step_count += 1 ensure_winetricks_choice(app=app) app.installer_step += 1 update_install_feedback("Ensuring install fonts choice…", app=app) logging.debug('- config.SKIP_FONTS') - logging.debug(f"> {config.SKIP_FONTS=}") + logging.debug(f"> config.SKIP_FONTS={app.conf.skip_install_fonts}") # XXX: huh? What does this do? @@ -387,7 +387,7 @@ def ensure_winetricks_applied(app: App): msg.status("Setting Font Smooting to RGB…", app) wine.install_font_smoothing(app) - if not config.SKIP_FONTS and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 + if not app.conf.skip_install_fonts and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 msg.status("Installing fonts…", app) wine.install_fonts(app) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 7b2035ea..ae73408d 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -221,7 +221,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: config.APPIMAGE_FILE_PATH = args.set_appimage[0] if args.skip_fonts: - config.SKIP_FONTS = True + ephemeral_config.install_fonts_skip = True if args.skip_winetricks: ephemeral_config.winetricks_skip = True diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 0a54a59a..903320d4 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -162,6 +162,8 @@ class EphemeralConfiguration: winetricks_skip: Optional[bool] install_dependencies_skip: Optional[bool] """Whether to skip installing system package dependencies""" + install_fonts_skip: Optional[bool] + """Whether to skip installing fonts in the wineprefix""" wine_dll_overrides: Optional[str] """Corresponds to wine's WINEDLLOVERRIDES""" @@ -218,7 +220,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": config_path=legacy.CONFIG_FILE, check_updates_now=legacy.CHECK_UPDATES, delete_log=legacy.DELETE_LOG, - install_dependencies_skip=legacy.SKIP_DEPENDENCIES + install_dependencies_skip=legacy.SKIP_DEPENDENCIES, + install_fonts_skip=legacy.SKIP_FONTS ) @classmethod @@ -676,11 +679,19 @@ def log_level(self) -> str | int: @property def skip_winetricks(self) -> bool: return bool(self._overrides.winetricks_skip) - + @property def skip_install_system_dependencies(self) -> bool: return bool(self._overrides.install_dependencies_skip) @skip_install_system_dependencies.setter def skip_install_system_dependencies(self, val: bool): - self._overrides.install_dependencies_skip = val \ No newline at end of file + self._overrides.install_dependencies_skip = val + + @property + def skip_install_fonts(self) -> bool: + return bool(self._overrides.install_fonts_skip) + + @skip_install_fonts.setter + def skip_install_fonts(self, val: bool): + self._overrides.install_fonts_skip = val diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 23d1cb33..45a62034 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -370,7 +370,7 @@ def install_d3d_compiler(app: App): def install_fonts(app: App): msg.status("Configuring fonts…") fonts = ['corefonts', 'tahoma'] - if not config.SKIP_FONTS: + if not app.conf.skip_fonts: for f in fonts: args = [f] run_winetricks_cmd(app, *args) From db38ed25451c271d88c9794cd19dc097adf5f3b1 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:00:19 -0800 Subject: [PATCH 034/137] fix: remove unused LOGOS_ICON_FILENAME --- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index dd4cbb46..257d2aa9 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -36,7 +36,6 @@ BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. L9PACKAGES = None LOGOS_FORCE_ROOT: bool = False -LOGOS_ICON_FILENAME: Optional[str] = None LOGOS_ICON_URL: Optional[str] = None LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index ad32befe..1a96b515 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -130,7 +130,6 @@ def ensure_installation_config(app: App): app.installer_step += 1 update_install_feedback("Ensuring installation config is set…", app=app) logging.debug('- config.LOGOS_ICON_URL') - logging.debug('- config.LOGOS_ICON_FILENAME') logging.debug('- config.LOGOS_VERSION') logging.debug('- config.LOGOS64_URL') @@ -143,10 +142,8 @@ def ensure_installation_config(app: App): logos_icon_url = app_dir / 'img' / f"{flproducti}-128-icon.png" # XXX: stop stting all these config keys config.LOGOS_ICON_URL = str(logos_icon_url) - config.LOGOS_ICON_FILENAME = logos_icon_url.name logging.debug(f"> {config.LOGOS_ICON_URL=}") - logging.debug(f"> {config.LOGOS_ICON_FILENAME=}") logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") From 753af6433dd6a422b1d4a289468fa0365c8b98e2 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:02:50 -0800 Subject: [PATCH 035/137] refactor: remove need for separate icon name --- .../{logos4-128-icon.png => Logos-128-icon.png} | Bin .../{verbum-128-icon.png => Verbum-128-icon.png} | Bin ou_dedetai/installer.py | 14 ++------------ 3 files changed, 2 insertions(+), 12 deletions(-) rename ou_dedetai/img/{logos4-128-icon.png => Logos-128-icon.png} (100%) rename ou_dedetai/img/{verbum-128-icon.png => Verbum-128-icon.png} (100%) diff --git a/ou_dedetai/img/logos4-128-icon.png b/ou_dedetai/img/Logos-128-icon.png similarity index 100% rename from ou_dedetai/img/logos4-128-icon.png rename to ou_dedetai/img/Logos-128-icon.png diff --git a/ou_dedetai/img/verbum-128-icon.png b/ou_dedetai/img/Verbum-128-icon.png similarity index 100% rename from ou_dedetai/img/verbum-128-icon.png rename to ou_dedetai/img/Verbum-128-icon.png diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 1a96b515..e18ba521 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -138,8 +138,7 @@ def ensure_installation_config(app: App): # Set icon variables. app_dir = Path(__file__).parent - flproducti = get_flproducti_name(app.conf.faithlife_product) - logos_icon_url = app_dir / 'img' / f"{flproducti}-128-icon.png" + logos_icon_url = app_dir / 'img' / f"{app.conf.faithlife_product}-128-icon.png" # XXX: stop stting all these config keys config.LOGOS_ICON_URL = str(logos_icon_url) @@ -554,14 +553,6 @@ def create_wine_appimage_symlinks(app: App): p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") -def get_flproducti_name(product_name) -> str: - lname = product_name.lower() - if lname == 'logos': - return 'logos4' - elif lname == 'verbum': - return lname - - def create_config_file(): config_dir = Path(constants.DEFAULT_CONFIG_PATH).parent config_dir.mkdir(exist_ok=True, parents=True) @@ -595,9 +586,8 @@ def create_launcher_shortcuts(app: App): reason = "because the FaithLife product is not defined." msg.logos_warning(f"{m} {reason}") # noqa: E501 return - flproducti = get_flproducti_name(flproduct) src_dir = Path(__file__).parent - logos_icon_src = src_dir / 'img' / f"{flproducti}-128-icon.png" + logos_icon_src = src_dir / 'img' / f"{flproduct}-128-icon.png" app_icon_src = src_dir / 'img' / 'icon.png' if installdir is None: From fe6e0610b35ba36576e870c6a258d22097887e6e Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:08:11 -0800 Subject: [PATCH 036/137] fix: migrate LOGOS_ICON_URL --- ou_dedetai/config.py | 1 - ou_dedetai/constants.py | 4 ++++ ou_dedetai/gui_app.py | 7 +++---- ou_dedetai/installer.py | 13 +++---------- ou_dedetai/new_config.py | 4 ++++ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 257d2aa9..d997edc6 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -36,7 +36,6 @@ BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. L9PACKAGES = None LOGOS_FORCE_ROOT: bool = False -LOGOS_ICON_URL: Optional[str] = None LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None MYDOWNLOADS: Optional[str] = None # FIXME: Should this use ~/.cache? diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 0e6364ad..df016a15 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -1,5 +1,6 @@ import logging import os +from pathlib import Path # Define app name variables. APP_NAME = 'Ou Dedetai' @@ -7,6 +8,9 @@ PACKAGE_NAME = 'ou_dedetai' REPOSITORY_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller" +# This is relative to this file itself +APP_IMAGE_DIR = Path(__file__).parent / "img" + # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 DEFAULT_APP_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 9c4048c7..bd2b2b57 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -125,8 +125,7 @@ def __init__(self, *args, **kwargs): self.rowconfigure(0, weight=1) # Set panel icon. - app_dir = Path(__file__).parent - self.icon = app_dir / 'img' / 'icon.png' + self.icon = constants.APP_IMAGE_DIR / 'icon.png' self.pi = PhotoImage(file=f'{self.icon}') self.iconphoto(False, self.pi) @@ -454,7 +453,7 @@ def set_skip_dependencies(self, evt=None): def on_okay_released(self, evt=None): # Update desktop panel icon. - self.root.icon = config.LOGOS_ICON_URL + self.root.icon = self.conf.faithlife_product_icon_path self.start_install_thread() def on_cancel_released(self, evt=None): @@ -667,7 +666,7 @@ def run_installer(self, evt=None): classname = constants.BINARY_NAME self.installer_win = Toplevel() InstallerWindow(self.installer_win, self.root, app=self, class_=classname) - self.root.icon = config.LOGOS_ICON_URL + self.root.icon = self.conf.faithlife_product_icon_path def run_logos(self, evt=None): utils.start_thread(self.logos.start) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index e18ba521..ef20fa6e 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -136,13 +136,7 @@ def ensure_installation_config(app: App): # XXX: This doesn't prompt the user for anything, all values are derived from other user-supplied values # these "config" values probably don't need to be stored independently of the values they're derived from - # Set icon variables. - app_dir = Path(__file__).parent - logos_icon_url = app_dir / 'img' / f"{app.conf.faithlife_product}-128-icon.png" - # XXX: stop stting all these config keys - config.LOGOS_ICON_URL = str(logos_icon_url) - - logging.debug(f"> {config.LOGOS_ICON_URL=}") + logging.debug(f"> config.LOGOS_ICON_URL={app.conf.faithlife_product_icon_path}") logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") @@ -586,9 +580,8 @@ def create_launcher_shortcuts(app: App): reason = "because the FaithLife product is not defined." msg.logos_warning(f"{m} {reason}") # noqa: E501 return - src_dir = Path(__file__).parent - logos_icon_src = src_dir / 'img' / f"{flproduct}-128-icon.png" - app_icon_src = src_dir / 'img' / 'icon.png' + logos_icon_src = constants.APP_IMAGE_DIR / f"{flproduct}-128-icon.png" + app_icon_src = constants.APP_IMAGE_DIR / 'icon.png' if installdir is None: reason = "because the installation folder is not defined." diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 903320d4..5b6d0ab4 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -456,6 +456,10 @@ def faithlife_product_release(self, value: str): self._raw.faithlife_product_release = value self._write() + @property + def faithlife_product_icon_path(self) -> str: + return str(constants.APP_IMAGE_DIR / f"{self.faithlife_product}-128-icon.png") + @property def faithlife_installer_name(self) -> str: if self._overrides.faithlife_installer_name is not None: From 2cb83234ed2b6965871d9aba2a34aa36d8ddd6f9 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:41:21 -0800 Subject: [PATCH 037/137] fix: remove OS_NAME and OS_RELEASE --- ou_dedetai/config.py | 2 -- ou_dedetai/system.py | 55 ++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index d997edc6..ddfa7668 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -39,8 +39,6 @@ LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None MYDOWNLOADS: Optional[str] = None # FIXME: Should this use ~/.cache? -OS_NAME: Optional[str] = None -OS_RELEASE: Optional[str] = None PACKAGE_MANAGER_COMMAND_INSTALL: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_DOWNLOAD: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_REMOVE: Optional[list[str]] = None diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 61ac99ec..76211ccc 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple import distro import logging import os @@ -236,14 +236,20 @@ def get_dialog(): config.DIALOG = 'tk' -def get_os(): +def get_os() -> Tuple[str, str]: + """Gets OS information + + Returns: + OS name + OS release + """ # FIXME: Not working? Returns "Linux" on some systems? On Ubuntu 24.04 it # correctly returns "ubuntu". - config.OS_NAME = distro.id() - logging.info(f"OS name: {config.OS_NAME}") - config.OS_RELEASE = distro.version() - logging.info(f"OS release: {config.OS_RELEASE}") - return config.OS_NAME, config.OS_RELEASE + os_name = distro.id() + logging.info(f"OS name: {os_name}") + os_release = distro.version() + logging.info(f"OS release: {os_release}") + return os_name, os_release def get_superuser_command(): @@ -266,7 +272,8 @@ def get_superuser_command(): def get_package_manager(): major_ver = distro.major_version() - logging.debug(f"{config.OS_NAME=}; {major_ver=}") + os_name = distro.id() + logging.debug(f"{os_name=}; {major_ver=}") # Check for package manager and associated packages. # NOTE: cabextract and sed are included in the appimage, so they are not # included as system dependencies. @@ -291,10 +298,10 @@ def get_package_manager(): # - https://en.wikipedia.org/wiki/Elementary_OS # - https://github.com/which-distro/os-release/tree/main if ( - (config.OS_NAME == 'debian' and major_ver >= '13') - or (config.OS_NAME == 'ubuntu' and major_ver >= '24') - or (config.OS_NAME == 'linuxmint' and major_ver >= '22') - or (config.OS_NAME == 'elementary' and major_ver >= '8') + (os_name == 'debian' and major_ver >= '13') + or (os_name == 'ubuntu' and major_ver >= '24') + or (os_name == 'linuxmint' and major_ver >= '22') + or (os_name == 'elementary' and major_ver >= '8') ): config.PACKAGES = ( "libfuse3-3 " # appimages @@ -358,7 +365,7 @@ def get_package_manager(): config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] # noqa: E501 config.PACKAGE_MANAGER_COMMAND_QUERY = ["pacman", "-Q"] config.QUERY_PREFIX = '' - if config.OS_NAME == "steamos": # steamOS + if os_name == "steamos": # steamOS config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: #E501 else: # arch # config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: E501 @@ -552,7 +559,8 @@ def postinstall_dependencies_steamos(): def preinstall_dependencies(): command = [] logging.debug("Performing pre-install dependencies…") - if config.OS_NAME == "Steam": + os_name, _ = get_os() + if os_name == "Steam": command = preinstall_dependencies_steamos() else: logging.debug("No pre-install dependencies required.") @@ -562,7 +570,8 @@ def preinstall_dependencies(): def postinstall_dependencies(): command = [] logging.debug("Performing post-install dependencies…") - if config.OS_NAME == "Steam": + os_name, _ = get_os() + if os_name == "Steam": command = postinstall_dependencies_steamos() else: logging.debug("No post-install dependencies required.") @@ -608,21 +617,22 @@ def install_dependencies(app: App, packages, bad_packages, logos9_packages=None) mode="remove", ) + os_name, _ = get_os() if config.PACKAGE_MANAGER_COMMAND_INSTALL: - if config.OS_NAME in ['fedora', 'arch']: + if os_name in ['fedora', 'arch']: message = False no_message = False secondary = False elif missing_packages and conflicting_packages: - message = f"Your {config.OS_NAME} computer requires installing and removing some software.\nProceed?" # noqa: E501 + message = f"Your {os_name} computer requires installing and removing some software.\nProceed?" # noqa: E501 no_message = "User refused to install and remove software via the application" # noqa: E501 secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nand will remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 elif missing_packages: - message = f"Your {config.OS_NAME} computer requires installing some software.\nProceed?" # noqa: E501 + message = f"Your {os_name} computer requires installing some software.\nProceed?" # noqa: E501 no_message = "User refused to install software via the application." # noqa: E501 secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}" # noqa: E501 elif conflicting_packages: - message = f"Your {config.OS_NAME} computer requires removing some software.\nProceed?" # noqa: E501 + message = f"Your {os_name} computer requires removing some software.\nProceed?" # noqa: E501 no_message = "User refused to remove software via the application." # noqa: E501 secondary = f"To continue, the program will attempt to remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 else: @@ -690,7 +700,7 @@ def install_dependencies(app: App, packages, bad_packages, logos9_packages=None) ] command_str = ' '.join(final_command) # TODO: Fix fedora/arch handling. - if config.OS_NAME in ['fedora', 'arch']: + if os_name in ['fedora', 'arch']: manual_install_required = True sudo_command = command_str.replace("pkexec", "sudo") message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 @@ -741,9 +751,10 @@ def install_dependencies(app: App, packages, bad_packages, logos9_packages=None) install_deps_failed = True else: msg.logos_error( - f"The script could not determine your {config.OS_NAME} install's package manager or it is unsupported. " # noqa: E501 + f"The script could not determine your {os_name} install's package manager or it is unsupported. " # noqa: E501 f"Your computer is missing the command(s) {missing_packages}. " - f"Please install your distro's package(s) associated with {missing_packages} for {config.OS_NAME}.") # noqa: E501 + f"Please install your distro's package(s) associated with {missing_packages} for {os_name}." # noqa: E501 + ) if config.REBOOT_REQUIRED: question = "Should the program reboot the host now?" # noqa: E501 From 89e8718d57be8f8af04b3456f4dc1bc8846c2d0d Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:37:01 -0800 Subject: [PATCH 038/137] refactor: separate network cache --- ou_dedetai/new_config.py | 49 +++++++++++++++++++++++++++++----------- ou_dedetai/utils.py | 1 - 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 5b6d0ab4..cc76c5ce 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -186,11 +186,6 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" - # XXX: does this belong here, or should we have a cache file? - # Start cache - _faithlife_product_releases: Optional[list[str]] = None - _logos_exe: Optional[str] = None - @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": log_level = None @@ -233,6 +228,21 @@ def load_from_path(cls, path: str) -> "EphemeralConfiguration": return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) +@dataclass +class NetworkCache: + """Separate class to store values that while they can be retrieved programmatically + it would take additional time or network connectivity. + + This class handles freshness, does whatever conditional logic it needs to determine if it's values are still up to date""" + + # XXX: consider storing this, but if we do figure out some way to determine freshness + + # Start cache + _faithlife_product_releases: Optional[list[str]] = None + _downloads_dir: Optional[str] = None + + # XXX: add @property defs to automatically retrieve if not found + @dataclass class PersistentConfiguration: """This class stores the options the user chose @@ -327,7 +337,7 @@ def write_config(self): # Continue, the installer can still operate even if it fails to write. -# XXX: what to do with these? +# XXX: Move these into the cache & store # Used to be called current_logos_version, but actually could be used in Verbium too. installed_faithlife_product_release: Optional[str] = None # Whether or not the installed faithlife product is configured for additional logging. @@ -365,7 +375,14 @@ class Config: # Overriding programmatically generated values from ENV _overrides: EphemeralConfiguration - + + # Cache, may or may not be stale, freshness logic is stored within + _cache: NetworkCache + + # Start Cache, values unlikely to change during operation + _logos_exe: Optional[str] = None + + # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] # Singleton logic, this enforces that only one config object exists at a time. @@ -400,6 +417,8 @@ def _ask_if_not_found(self, parameter: str, question: str, options: list[str], d def _write(self): """Writes configuration to file and lets the app know something changed""" self._raw.write_config() + from ou_dedetai.app import App + app: "App" = self.app self.app._config_updated() @property @@ -445,9 +464,9 @@ def faithlife_product_version(self, value: Optional[str]): @property def faithlife_product_release(self) -> str: question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 - if self._overrides._faithlife_product_releases is None: - self._overrides._faithlife_product_releases = network.get_logos_releases(self.app) - options = self._overrides._faithlife_product_releases + if self._cache._faithlife_product_releases is None: + self._cache._faithlife_product_releases = network.get_logos_releases(self.app) + options = self._cache._faithlife_product_releases return self._ask_if_not_found("faithlife_product_release", question, options) @faithlife_product_release.setter @@ -646,9 +665,9 @@ def cycle_curses_color_scheme(self): @property def logos_exe(self) -> Optional[str]: # Cache a successful result - if self._overrides._logos_exe is None: - self._overrides._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) - return self._overrides._logos_exe + if self._logos_exe is None: + self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) + return self._logos_exe @property def wine_user(self) -> Optional[str]: @@ -699,3 +718,7 @@ def skip_install_fonts(self) -> bool: @skip_install_fonts.setter def skip_install_fonts(self, val: bool): self._overrides.install_fonts_skip = val + + @property + def download_dir(self) -> str: + return utils.get_user_downloads_dir() \ No newline at end of file diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 2f0e36bc..a0e84254 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -55,7 +55,6 @@ def append_unique(list, item): # Set "global" variables. # XXX: fold this into config def set_default_config(): - system.get_os() system.get_superuser_command() system.get_package_manager() if config.CONFIG_FILE is None: From d18544a4824c30d4a5a9064c70c0b80c2a20c762 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:00:00 -0800 Subject: [PATCH 039/137] fix: typing/stype thanks to mypy/ruff --- ou_dedetai/app.py | 21 ++++--- ou_dedetai/msg.py | 2 +- ou_dedetai/network.py | 2 +- ou_dedetai/new_config.py | 125 +++++++++++++++++++++++---------------- ou_dedetai/utils.py | 15 +++-- pyproject.toml | 23 ++++++- 6 files changed, 120 insertions(+), 68 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index fd8633b4..9de27c42 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -23,14 +23,18 @@ def ask(self, question: str, options: list[str]) -> str: Returns the option the user picked. - If the internal ask function returns None, the process will exit with an error code 1 + If the internal ask function returns None, the process will exit with 1 """ - if len(options) == 1 and (PROMPT_OPTION_DIRECTORY in options or PROMPT_OPTION_FILE in options): + passed_options: list[str] | str = options + if len(passed_options) == 1 and ( + PROMPT_OPTION_DIRECTORY in passed_options + or PROMPT_OPTION_FILE in passed_options + ): # Set the only option to be the follow up prompt - options = options[0] - elif options is not None and self._exit_option is not None: - options += [self._exit_option] - answer = self._ask(question, options) + passed_options = options[0] + elif passed_options is not None and self._exit_option is not None: + passed_options = options + [self._exit_option] + answer = self._ask(question, passed_options) if answer == self._exit_option: answer = None @@ -57,8 +61,9 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: """ raise NotImplementedError() - def _config_updated(self): - """A hook for any changes the individual apps want to do when the config changes""" + def _config_updated(self) -> None: + """A hook for any changes the individual apps want to do when the config changes + """ pass def is_installed(self) -> bool: diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index fb893b7b..7f80cf25 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -195,7 +195,7 @@ def ui_message(message, secondary=None, detail=None, app=None, parent=None, fata # TODO: I think detail is doing the same thing as secondary. -def logos_error(message, secondary=None, detail=None, app=None, parent=None): +def logos_error(message: str, secondary=None, detail=None, app=None, parent=None): # if detail is None: # detail = '' # WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index c386bcc9..903e1db7 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -423,7 +423,7 @@ def set_logoslinuxinstaller_latest_release_config(): logging.info(f"{config.LLI_LATEST_VERSION=}") -def set_recommended_appimage_config(): +def set_recommended_appimage_config() -> None: repo = "FaithLife-Community/wine-appimages" if not config.RECOMMENDED_WINE64_APPIMAGE_URL: json_data = get_latest_release_data(repo) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index cc76c5ce..4eddff05 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -60,11 +60,9 @@ class LegacyConfiguration: SKIP_WINETRICKS: Optional[bool] = None use_python_dialog: Optional[str] = None VERBOSE: Optional[bool] = None - WINEBIN_CODE: Optional[str] = None WINEDEBUG: Optional[str] = None WINEDLLOVERRIDES: Optional[str] = None WINEPREFIX: Optional[str] = None - WINE_EXE: Optional[str] = None WINESERVER_EXE: Optional[str] = None WINETRICKS_UNATTENDED: Optional[str] = None @@ -172,7 +170,7 @@ class EphemeralConfiguration: wine_prefix: Optional[str] """Corresponds to wine's WINEPREFIX""" - # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) + # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) # noqa: E501 custom_binary_path: Optional[str] """Additional path to look for when searching for binaries.""" @@ -192,11 +190,20 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": wine_debug = legacy.WINEDEBUG if legacy.DEBUG: log_level = logging.DEBUG - # FIXME: shouldn't this leave it untouched or fall back to default: `fixme-all,err-all`? + # FIXME: shouldn't this leave it untouched or fall back to default: `fixme-all,err-all`? # noqa: E501 wine_debug = "" elif legacy.VERBOSE: log_level = logging.INFO wine_debug = "" + app_winetricks_unattended = None + if legacy.WINETRICKS_UNATTENDED is not None: + app_winetricks_unattended = utils.parse_bool(legacy.WINETRICKS_UNATTENDED) + delete_log = None + if legacy.DELETE_LOG is not None: + delete_log = utils.parse_bool(legacy.DELETE_LOG) + config_file = constants.DEFAULT_CONFIG_PATH + if legacy.CONFIG_FILE is not None: + config_file = legacy.CONFIG_FILE return EphemeralConfiguration( installer_binary_dir=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, @@ -211,10 +218,10 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": wine_prefix=legacy.WINEPREFIX, app_wine_log_path=legacy.wine_log, app_log_path=legacy.LOGOS_LOG, - app_winetricks_unattended=legacy.WINETRICKS_UNATTENDED, - config_path=legacy.CONFIG_FILE, + app_winetricks_unattended=app_winetricks_unattended, + config_path=config_file, check_updates_now=legacy.CHECK_UPDATES, - delete_log=legacy.DELETE_LOG, + delete_log=delete_log, install_dependencies_skip=legacy.SKIP_DEPENDENCIES, install_fonts_skip=legacy.SKIP_FONTS ) @@ -225,7 +232,7 @@ def load(cls) -> "EphemeralConfiguration": @classmethod def load_from_path(cls, path: str) -> "EphemeralConfiguration": - return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) + return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) # noqa: E501 @dataclass @@ -233,9 +240,9 @@ class NetworkCache: """Separate class to store values that while they can be retrieved programmatically it would take additional time or network connectivity. - This class handles freshness, does whatever conditional logic it needs to determine if it's values are still up to date""" + This class handles freshness, does whatever conditional logic it needs to determine if it's values are still up to date""" #noqa: E501 - # XXX: consider storing this, but if we do figure out some way to determine freshness + # XXX: consider storing this and enforce freshness # Start cache _faithlife_product_releases: Optional[list[str]] = None @@ -257,7 +264,8 @@ class PersistentConfiguration: MUST be saved explicitly """ - # XXX: store a version in this config? Just in case we need to do conditional logic reading old version's configurations + # XXX: store a version in this config? + # Just in case we need to do conditional logic reading old version's configurations faithlife_product: Optional[str] = None faithlife_product_version: Optional[str] = None @@ -281,7 +289,7 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": # XXX: handle legacy migration # First read in the legacy configuration - new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) + new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) #noqa: E501 new_keys = new_config.__dict__.keys() @@ -301,32 +309,39 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": + backup_dir = None + if legacy.BACKUPDIR is not None: + backup_dir = Path(legacy.BACKUPDIR) + install_dir = None + if legacy.INSTALLDIR is not None: + install_dir = Path(legacy.INSTALLDIR) return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, - backup_dir=legacy.BACKUPDIR, - curses_colors=legacy.curses_colors, + backup_dir=backup_dir, + curses_colors=legacy.curses_colors or 'Logos', faithlife_product_release=legacy.TARGET_RELEASE_VERSION, - faithlife_product_release_channel=legacy.logos_release_channel, + faithlife_product_release_channel=legacy.logos_release_channel or 'stable', faithlife_product_version=legacy.TARGETVERSION, - install_dir=legacy.INSTALLDIR, - installer_release_channel=legacy.lli_release_channel, + install_dir=install_dir, + installer_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, winetricks_binary=legacy.WINETRICKSBIN ) - def write_config(self): + def write_config(self) -> None: config_file_path = LegacyConfiguration.config_file_path() output = self.__dict__ logging.info(f"Writing config to {config_file_path}") os.makedirs(os.path.dirname(config_file_path), exist_ok=True) - # Ensure all paths stored are relative to install_dir - for k, v in output.items(): - # XXX: test this - if isinstance(v, Path) or (isinstance(v, str) and v.startswith(self.install_dir)): - output[k] = utils.get_relative_path(v, self.install_dir) + if self.install_dir is not None: + # Ensure all paths stored are relative to install_dir + for k, v in output.items(): + # XXX: test this + if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 + output[k] = utils.get_relative_path(v, str(self.install_dir)) try: with open(config_file_path, 'w') as config_file: @@ -357,7 +372,7 @@ def get_wine_prefix_path(install_dir: str) -> str: class Config: """Set of configuration values. - If the user hasn't selected a particular value yet, they will be prompted in their UI. + If the user hasn't selected a particular value yet, they will be prompted in the UI. """ # Naming conventions: @@ -392,14 +407,15 @@ def __new__(cls, *args, **kwargs) -> "Config": return cls._instance def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: - self.app = app + from ou_dedetai.app import App + self.app: "App" = app self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) self._overrides = ephemeral_config logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") - def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: + def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 # XXX: should this also update the feedback? if not getattr(self._raw, parameter): if dependent_parameters is not None: @@ -407,18 +423,17 @@ def _ask_if_not_found(self, parameter: str, question: str, options: list[str], d setattr(self._raw, dependent_config_key, None) answer = self.app.ask(question, options) # Use the setter on this class if found, otherwise set in self._user - if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: + if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: # noqa: E501 getattr(Config, parameter).fset(self, answer) else: setattr(self._raw, parameter, answer) self._write() - return getattr(self._raw, parameter) + # parameter given should be a string + return str(getattr(self._raw, parameter)) - def _write(self): + def _write(self) -> None: """Writes configuration to file and lets the app know something changed""" self._raw.write_config() - from ou_dedetai.app import App - app: "App" = self.app self.app._config_updated() @property @@ -429,14 +444,14 @@ def config_file_path(self) -> str: def faithlife_product(self) -> str: question = "Choose which FaithLife product the script should install: " # noqa: E501 options = ["Logos", "Verbum"] - return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) + return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) # noqa: E501 @faithlife_product.setter def faithlife_product(self, value: Optional[str]): if self._raw.faithlife_product != value: self._raw.faithlife_product = value # Reset dependent variables - self.faithlife_product_release = None + self._raw.faithlife_product_release = None self._write() @@ -444,9 +459,9 @@ def faithlife_product(self, value: Optional[str]): def faithlife_product_version(self) -> str: if self._overrides.faithlife_product_version is not None: return self._overrides.faithlife_product_version - question = f"Which version of {self.faithlife_product} should the script install?: ", # noqa: E501 + question = f"Which version of {self.faithlife_product} should the script install?: " # noqa: E501 options = ["10", "9"] - return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) + return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) # noqa: E501 @faithlife_product_version.setter def faithlife_product_version(self, value: Optional[str]): @@ -463,9 +478,9 @@ def faithlife_product_version(self, value: Optional[str]): @property def faithlife_product_release(self) -> str: - question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: ", # noqa: E501 + question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: " # noqa: E501 if self._cache._faithlife_product_releases is None: - self._cache._faithlife_product_releases = network.get_logos_releases(self.app) + self._cache._faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 options = self._cache._faithlife_product_releases return self._ask_if_not_found("faithlife_product_release", question, options) @@ -489,7 +504,7 @@ def faithlife_installer_name(self) -> str: def faithlife_installer_download_url(self) -> str: if self._overrides.faithlife_installer_download_url is not None: return self._overrides.faithlife_installer_download_url - after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" + after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" # noqa: E501 return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 @property @@ -504,15 +519,16 @@ def installer_release_channel(self) -> str: def winetricks_binary(self) -> str: """This may be a path to the winetricks binary or it may be "Download" """ - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux.", # noqa: E501 + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 options = utils.get_winetricks_options() return self._ask_if_not_found("winetricks_binary", question, options) @winetricks_binary.setter def winetricks_binary(self, value: Optional[str | Path]): + if value is not None: + value = str(value) if value is not None and value != "Download": - value = Path(value) - if not value.exists(): + if not Path(value).exists(): raise ValueError("Winetricks binary must exist") if self._raw.winetricks_binary != value: self._raw.winetricks_binary = value @@ -543,33 +559,37 @@ def wine_prefix(self) -> str: @property def wine_binary(self) -> str: """Returns absolute path to the wine binary""" - if not self._raw.wine_binary: - question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: ", # noqa: E501 + output = self._raw.wine_binary + if output is None: + question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: " # noqa: E501 network.set_recommended_appimage_config() options = utils.get_wine_options( - self, - utils.find_appimage_files(self.faithlife_product_release), + self.app, + utils.find_appimage_files(self.app), utils.find_wine_binary_files(self.app, self.faithlife_product_release) ) choice = self.app.ask(question, options) + output = choice self.wine_binary = choice # Return the full path so we the callee doesn't need to think about it - if not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): + if self._raw.wine_binary is not None and not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): # noqa: E501 return str(Path(self.install_dir) / self._raw.wine_binary) - return self._raw.wine_binary + return output @wine_binary.setter def wine_binary(self, value: str): + """Takes in a path to the wine binary and stores it as relative for storage""" + # XXX: change the logic to make ^ true if (Path(self.install_dir) / value).exists(): - value = (Path(self.install_dir) / value).absolute() + value = str((Path(self.install_dir) / Path(value)).absolute()) if not Path(value).is_file(): raise ValueError("Wine Binary path must be a valid file") if self._raw.wine_binary != value: if value is not None: - value = Path(value).absolute() + value = str(Path(value).absolute()) self._raw.wine_binary = value # Reset dependents self._raw.wine_binary_code = None @@ -652,7 +672,7 @@ def curses_colors(self) -> str: @curses_colors.setter def curses_colors(self, value: str): if value not in self._curses_colors_valid_values: - raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") + raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") # noqa: E501 self._raw.curses_colors = value self._write() @@ -666,7 +686,7 @@ def cycle_curses_color_scheme(self): def logos_exe(self) -> Optional[str]: # Cache a successful result if self._logos_exe is None: - self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) + self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) # noqa: E501 return self._logos_exe @property @@ -682,16 +702,19 @@ def wine_user(self) -> Optional[str]: def logos_cef_exe(self) -> Optional[str]: if self.wine_user is not None: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + return None @property def logos_indexer_exe(self) -> Optional[str]: if self.wine_user is not None: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + return None @property def logos_login_exe(self) -> Optional[str]: if self.wine_user is not None: return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + return None @property def log_level(self) -> str | int: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index a0e84254..df6fabca 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -150,7 +150,7 @@ def mkdir_critical(directory): msg.logos_error(f"Can't create the {directory} directory") -def get_user_downloads_dir(): +def get_user_downloads_dir() -> str: home = Path.home() xdg_config = Path(os.getenv('XDG_CONFIG_HOME', home / '.config')) user_dirs_file = xdg_config / 'user-dirs.dirs' @@ -202,7 +202,7 @@ def install_dependencies(app: App): app.root.event_generate('<>') -def file_exists(file_path): +def file_exists(file_path: Optional[str | bytes | Path]) -> bool: if file_path is not None: expanded_path = os.path.expanduser(file_path) return os.path.isfile(expanded_path) @@ -303,7 +303,7 @@ def get_winebin_code_and_desc(app: App, binary): return code, desc -def get_wine_options(app: App, appimages, binaries) -> Union[List[List[str]], List[str]]: # noqa: E501 +def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 logging.debug(f"{appimages=}") logging.debug(f"{binaries=}") wine_binary_options = [] @@ -347,7 +347,7 @@ def get_wine_options(app: App, appimages, binaries) -> Union[List[List[str]], Li return wine_binary_options -def get_winetricks_options(): +def get_winetricks_options() -> list[str]: local_winetricks_path = shutil.which('winetricks') winetricks_options = ['Download'] if local_winetricks_path is not None: @@ -852,13 +852,13 @@ def untar_file(file_path, output_dir): logging.error(f"Error extracting '{file_path}': {e}") -def is_relative_path(path): +def is_relative_path(path: str | Path) -> bool: if isinstance(path, str): path = Path(path) return not path.is_absolute() -def get_relative_path(path, base_path): +def get_relative_path(path: Path | str, base_path: str) -> str | Path: if is_relative_path(path): return path else: @@ -907,3 +907,6 @@ def stopwatch(start_time=None, interval=10.0): def get_timestamp(): return datetime.today().strftime('%Y-%m-%dT%H%M%S') + +def parse_bool(string: str) -> bool: + return string.lower() in ['true', '1', 'y', 'yes'] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1082744c..2de388c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,4 +34,25 @@ version = {attr = "ou_dedetai.constants.LLI_CURRENT_VERSION"} where = ["."] [tool.setuptools.package-data] -"ou_dedetai.img" = ["*icon.png"] \ No newline at end of file +"ou_dedetai.img" = ["*icon.png"] + +[tool.ruff.lint] +select = ["E", "F"] + +[tool.mypy] +warn_unreachable = true +disallow_untyped_defs = false +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +no_implicit_reexport = true +extra_checks = true + +[[tool.mypy.overrides]] +# XXX: change this to config after refactor is complete +module = "ou_dedetai.new_config" +disallow_untyped_calls = true +check_untyped_defs = true + +disallow_any_generic = false +strict_equality = true From d47eb53f8e2963678fa788c63f77f609a3854654 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:05:12 -0800 Subject: [PATCH 040/137] fix: migrate MYDOWNLOADS --- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 24 ++++++++++++------------ ou_dedetai/network.py | 12 ++++++------ ou_dedetai/new_config.py | 8 ++++++-- ou_dedetai/system.py | 4 ++-- ou_dedetai/utils.py | 7 +++---- ou_dedetai/wine.py | 4 ++-- 7 files changed, 31 insertions(+), 29 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index ddfa7668..7d194841 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -38,7 +38,6 @@ LOGOS_FORCE_ROOT: bool = False LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None -MYDOWNLOADS: Optional[str] = None # FIXME: Should this use ~/.cache? PACKAGE_MANAGER_COMMAND_INSTALL: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_DOWNLOAD: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_REMOVE: Optional[list[str]] = None diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index ef20fa6e..0300bbde 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -204,13 +204,13 @@ def ensure_appimage_download(app: App): downloaded_file = None filename = Path(config.SELECTED_APPIMAGE_FILENAME).name - downloaded_file = utils.get_downloaded_file_path(filename) + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, filename) if not downloaded_file: - downloaded_file = Path(f"{config.MYDOWNLOADS}/{filename}") + downloaded_file = Path(f"{app.conf.download_dir}/{filename}") network.logos_reuse_download( config.RECOMMENDED_WINE64_APPIMAGE_URL, filename, - config.MYDOWNLOADS, + app.conf.download_dir, app=app, ) if downloaded_file: @@ -273,20 +273,20 @@ def ensure_premade_winebottle_download(app: App): app=app ) - downloaded_file = utils.get_downloaded_file_path(constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: - downloaded_file = Path(config.MYDOWNLOADS) / app.conf.faithlife_installer_name + downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name network.logos_reuse_download( constants.LOGOS9_WINE64_BOTTLE_TARGZ_URL, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, - config.MYDOWNLOADS, + app.conf.download_dir, app=app, ) # Install bottle. bottle = Path(app.conf.wine_prefix) if not bottle.is_dir(): utils.install_premade_wine_bottle( - config.MYDOWNLOADS, + app.conf.download_dir, f"{app.conf.install_dir}/data" ) @@ -302,13 +302,13 @@ def ensure_product_installer_download(app: App): app=app ) - downloaded_file = utils.get_downloaded_file_path(app.conf.faithlife_installer_name) + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, app.conf.faithlife_installer_name) if not downloaded_file: - downloaded_file = Path(config.MYDOWNLOADS) / app.conf.faithlife_installer_name + downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name network.logos_reuse_download( app.conf.faithlife_installer_download_url, app.conf.faithlife_installer_name, - config.MYDOWNLOADS, + app.conf.download_dir, app=app, ) # Copy file into install dir. @@ -331,7 +331,7 @@ def ensure_wineprefix_init(app: App): logging.debug(f"{init_file} does not exist") if app.conf.faithlife_product_version == '9': utils.install_premade_wine_bottle( - config.MYDOWNLOADS, + app.conf.download_dir, f"{app.conf.install_dir}/data", ) else: @@ -520,7 +520,7 @@ def create_wine_appimage_symlinks(app: App): appimage_filename = Path(config.SELECTED_APPIMAGE_FILENAME).name if config.WINEBIN_CODE in ['AppImage', 'Recommended']: # Ensure appimage is copied to appdir_bindir. - downloaded_file = utils.get_downloaded_file_path(appimage_filename) + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) if not appimage_file.is_file(): msg.status( f"Copying: {downloaded_file} into: {appdir_bindir}", diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 903e1db7..ac2c54c6 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -165,7 +165,7 @@ def logos_reuse_download( dirs = [ app.conf.install_dir, os.getcwd(), - config.MYDOWNLOADS, + app.conf.download_dir, ] found = 1 for i in dirs: @@ -190,7 +190,7 @@ def logos_reuse_download( else: logging.info(f"Incomplete file: {file_path}.") if found == 1: - file_path = os.path.join(config.MYDOWNLOADS, file) + file_path = os.path.join(app.conf.download_dir, file) if config.DIALOG == 'tk' and app: # Ensure progress bar. app.stop_indeterminate_progress() @@ -209,7 +209,7 @@ def logos_reuse_download( ): msg.status(f"Copying: {file} into: {targetdir}") try: - shutil.copy(os.path.join(config.MYDOWNLOADS, file), targetdir) + shutil.copy(os.path.join(app.conf.download_dir, file), targetdir) except shutil.SameFileError: pass else: @@ -546,8 +546,8 @@ def get_logos_releases(app: App) -> list[str]: def update_lli_binary(app=None): lli_file_path = os.path.realpath(sys.argv[0]) - lli_download_path = Path(config.MYDOWNLOADS) / constants.BINARY_NAME - temp_path = Path(config.MYDOWNLOADS) / f"{constants.BINARY_NAME}.tmp" + lli_download_path = Path(app.conf.download_dir) / constants.BINARY_NAME + temp_path = Path(app.conf.download_dir) / f"{constants.BINARY_NAME}.tmp" logging.debug( f"Updating {constants.APP_NAME} to latest version by overwriting: {lli_file_path}") # noqa: E501 @@ -563,7 +563,7 @@ def update_lli_binary(app=None): logos_reuse_download( config.LOGOS_LATEST_VERSION_URL, constants.BINARY_NAME, - config.MYDOWNLOADS, + app.conf.download_dir, app=app, ) shutil.copy(lli_download_path, temp_path) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 4eddff05..889bc419 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -394,8 +394,10 @@ class Config: # Cache, may or may not be stale, freshness logic is stored within _cache: NetworkCache - # Start Cache, values unlikely to change during operation + # Start Cache of values unlikely to change during operation. + # i.e. filesystem traversals _logos_exe: Optional[str] = None + _download_dir: Optional[str] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -744,4 +746,6 @@ def skip_install_fonts(self, val: bool): @property def download_dir(self) -> str: - return utils.get_user_downloads_dir() \ No newline at end of file + if self._download_dir is None: + self._download_dir = utils.get_user_downloads_dir() + return self._download_dir \ No newline at end of file diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 76211ccc..5db13479 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -787,10 +787,10 @@ def install_winetricks( network.logos_reuse_download( f"{base_url}/{version}", zip_name, - config.MYDOWNLOADS, + app.conf.download_dir, app=app, ) - wtzip = f"{config.MYDOWNLOADS}/{zip_name}" + wtzip = f"{app.conf.download_dir}/{zip_name}" logging.debug(f"Extracting winetricks script into {installdir}…") with zipfile.ZipFile(wtzip) as z: for zi in z.infolist(): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index df6fabca..a2161039 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -59,7 +59,6 @@ def set_default_config(): system.get_package_manager() if config.CONFIG_FILE is None: config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH - config.MYDOWNLOADS = get_user_downloads_dir() # XXX: remove, no need. @@ -624,7 +623,7 @@ def find_appimage_files(app: App): directories = [ os.path.expanduser("~") + "/bin", app.conf.installer_binary_dir, - config.MYDOWNLOADS + app.conf.download_dir ] # FIXME: consider what we should do with this, promote to top level config? if app.conf._overrides.custom_binary_path is not None: @@ -781,9 +780,9 @@ def update_to_latest_recommended_appimage(app: App): logging.debug("The AppImage version is newer than the latest recommended.") # noqa: E501 -def get_downloaded_file_path(filename): +def get_downloaded_file_path(download_dir: str, filename: str): dirs = [ - config.MYDOWNLOADS, + Path(download_dir), Path.home(), Path.cwd(), ] diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 45a62034..6652d7f6 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -422,11 +422,11 @@ def enforce_icu_data_files(app: App): network.logos_reuse_download( icu_url, icu_filename, - config.MYDOWNLOADS, + app.conf.download_dir, app=app ) drive_c = f"{app.conf.wine_prefix}/drive_c" - utils.untar_file(f"{config.MYDOWNLOADS}/{icu_filename}", drive_c) + utils.untar_file(f"{app.conf.download_dir}/{icu_filename}", drive_c) # Ensure the target directory exists icu_win_dir = f"{drive_c}/icu-win/windows" From 48570146b6ea3013fb79f327f46592668747cba8 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:13:00 -0800 Subject: [PATCH 041/137] fix: migrate LOGOS_FORCE_ROOT and PASSIVE --- ou_dedetai/config.py | 2 -- ou_dedetai/main.py | 11 ++++++----- ou_dedetai/new_config.py | 4 ++++ ou_dedetai/utils.py | 5 ----- ou_dedetai/wine.py | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 7d194841..9047090e 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -35,7 +35,6 @@ APPIMAGE_FILE_PATH: Optional[str] = None BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. L9PACKAGES = None -LOGOS_FORCE_ROOT: bool = False LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None PACKAGE_MANAGER_COMMAND_INSTALL: Optional[list[str]] = None @@ -43,7 +42,6 @@ PACKAGE_MANAGER_COMMAND_REMOVE: Optional[list[str]] = None PACKAGE_MANAGER_COMMAND_QUERY: Optional[list[str]] = None PACKAGES: Optional[str] = None -PASSIVE: Optional[bool] = None QUERY_PREFIX: Optional[str] = None REBOOT_REQUIRED: Optional[str] = None RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: Optional[str] = None diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index ae73408d..c2ed04c8 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -75,8 +75,8 @@ def get_parser(): cfg.add_argument( '-f', '--force-root', action='store_true', help=( - "set LOGOS_FORCE_ROOT to true, which permits " - "the root user to use the script" + "Running Wine/winetricks as root is highly discouraged. " + "Set this to do allow it anyways" ), ) cfg.add_argument( @@ -235,7 +235,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: ephemeral_config.install_dependencies_skip = True if args.force_root: - config.LOGOS_FORCE_ROOT = True + ephemeral_config.app_run_as_root_permitted = True if args.custom_binary_path: if os.path.isdir(args.custom_binary_path): @@ -246,7 +246,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: parser.exit(status=1, message=message) if args.passive: - config.PASSIVE = True + ephemeral_config.faithlife_install_passive = True # Set ACTION function. actions = { @@ -467,7 +467,8 @@ def main(): # Disabled until it can be fixed. Avoid running multiple instances of the # program. # utils.die_if_running() - utils.die_if_root() + if os.getuid() == 0 and not ephemeral_config.app_run_as_root_permitted: + msg.logos_error("Running Wine/winetricks as root is highly discouraged. Use -f|--force-root if you must run as root. See https://wiki.winehq.org/FAQ#Should_I_run_Wine_as_root.3F") # noqa: E501 # Print terminal banner logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 889bc419..4cc6bceb 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -184,6 +184,10 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" + # Start of values just set via cli arg + faithlife_install_passive: bool = False + app_run_as_root_permitted: bool = False + @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": log_level = None diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index a2161039..7aad3ef7 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -115,11 +115,6 @@ def remove_pid_file(): f.write(str(os.getpid())) -def die_if_root(): - if os.getuid() == 0 and not config.LOGOS_FORCE_ROOT: - msg.logos_error("Running Wine/winetricks as root is highly discouraged. Use -f|--force-root if you must run as root. See https://wiki.winehq.org/FAQ#Should_I_run_Wine_as_root.3F") # noqa: E501 - - def die(message): logging.critical(message) sys.exit(1) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 6652d7f6..0a54a119 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -255,10 +255,10 @@ def install_msi(app: App): # Execute the .MSI wine_exe = app.conf.wine64_binary exe_args = ["/i", f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}"] - if config.PASSIVE is True: + if app.conf._overrides.faithlife_install_passive is True: exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") - process = run_wine_proc(wine_exe, exe="msiexec", exe_args=exe_args) + process = run_wine_proc(wine_exe, app, exe="msiexec", exe_args=exe_args) return process From 6087e99915ad8b35e5a5d4a40ac94d7d0ef81c25 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:40:56 -0800 Subject: [PATCH 042/137] fix: migrate appimage vars removed RECOMMENDED_WINE64_APPIMAGE_BRANCH as it wasn't used RECOMMENDED_WINE64_APPIMAGE_FILENAME as it was only used as an intermediate to get the version, not use storing in it's own right. combined SELECTED_APPIMAGE_FILENAME (stored) amd APPIMAGE_FILE_PATH (was only in-memory before) --- ou_dedetai/config.py | 11 ----- ou_dedetai/gui_app.py | 9 ++-- ou_dedetai/installer.py | 40 ++++++++---------- ou_dedetai/main.py | 20 ++++----- ou_dedetai/network.py | 33 +++++---------- ou_dedetai/new_config.py | 79 +++++++++++++++++++++++++++++++++-- ou_dedetai/tui_app.py | 4 +- ou_dedetai/utils.py | 89 ++++++++++++++++++++++------------------ ou_dedetai/wine.py | 1 + 9 files changed, 168 insertions(+), 118 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 9047090e..23e5aa4a 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -9,30 +9,24 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these current_logos_version = None -WINEBIN_CODE = None WINECMD_ENCODING = None LOGS = None LAST_UPDATED = None -RECOMMENDED_WINE64_APPIMAGE_URL = None LLI_LATEST_VERSION = None lli_release_channel = None # Define and set additional variables that can be set in the env. extended_config = { - 'APPIMAGE_LINK_SELECTION_NAME': 'selected_wine.AppImage', 'CONFIG_FILE': None, 'DIALOG': None, - 'SELECTED_APPIMAGE_FILENAME': None, # Dependent on DIALOG with env override 'use_python_dialog': None, - 'WINEBIN_CODE': None, } for key, default in extended_config.items(): globals()[key] = os.getenv(key, default) # Set other run-time variables not set in the env. ACTION: str = 'app' -APPIMAGE_FILE_PATH: Optional[str] = None BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. L9PACKAGES = None LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME @@ -44,11 +38,6 @@ PACKAGES: Optional[str] = None QUERY_PREFIX: Optional[str] = None REBOOT_REQUIRED: Optional[str] = None -RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: Optional[str] = None -RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION: Optional[str] = None -RECOMMENDED_WINE64_APPIMAGE_FILENAME: Optional[str] = None -RECOMMENDED_WINE64_APPIMAGE_VERSION: Optional[str] = None -RECOMMENDED_WINE64_APPIMAGE_BRANCH: Optional[str] = None SUPERUSER_COMMAND: Optional[str] = None wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index bd2b2b57..27479883 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -417,8 +417,6 @@ def set_wine(self, evt=None): self.gui.wine_dropdown.selection_clear() if evt: # manual override logging.debug(f"User changed wine binary to '{self.conf.wine_binary}'") - config.SELECTED_APPIMAGE_FILENAME = None - config.WINEBIN_CODE = None self.start_ensure_config() else: @@ -595,7 +593,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.gui.latest_appimage_button.config( command=self.update_to_latest_appimage ) - if config.WINEBIN_CODE != "AppImage" and config.WINEBIN_CODE != "Recommended": # noqa: E501 + if self.conf.wine_binary_code != "AppImage" and self.conf.wine_binary_code != "Recommended": # noqa: E501 self.gui.latest_appimage_button.state(['disabled']) gui.ToolTip( self.gui.latest_appimage_button, @@ -734,7 +732,7 @@ def update_to_latest_lli_release(self, evt=None): utils.start_thread(utils.update_to_latest_lli_release, app=self) def update_to_latest_appimage(self, evt=None): - config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 + self.conf.wine_appimage_path = self.conf.wine_appimage_recommended_file_name # noqa: E501 self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest AppImage…") utils.start_thread(utils.set_appimage_symlink, app=self) @@ -744,8 +742,7 @@ def set_appimage(self, evt=None): appimage_filename = self.open_file_dialog("AppImage", "AppImage") if not appimage_filename: return - # config.SELECTED_APPIMAGE_FILENAME = appimage_filename - config.APPIMAGE_FILE_PATH = appimage_filename + self.conf.wine_appimage_path = appimage_filename utils.start_thread(utils.set_appimage_symlink, app=self) def get_winetricks(self, evt=None): diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 0300bbde..e7f36164 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -67,23 +67,16 @@ def ensure_wine_choice(app: App): logging.debug('- config.SELECTED_APPIMAGE_FILENAME') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_URL') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME') - logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_FILENAME') logging.debug('- config.WINE_EXE') logging.debug('- config.WINEBIN_CODE') - # Set WINEBIN_CODE and SELECTED_APPIMAGE_FILENAME. m = f"Preparing to process WINE_EXE. Currently set to: {app.conf.wine_binary}." # noqa: E501 logging.debug(m) - if str(app.conf.wine_binary).lower().endswith('.appimage'): - config.SELECTED_APPIMAGE_FILENAME = str(app.conf.wine_binary) - if not config.WINEBIN_CODE: - config.WINEBIN_CODE = utils.get_winebin_code_and_desc(app, app.conf.wine_binary)[0] # noqa: E501 - - logging.debug(f"> {config.SELECTED_APPIMAGE_FILENAME=}") - logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_URL=}") - logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") - logging.debug(f"> {config.RECOMMENDED_WINE64_APPIMAGE_FILENAME=}") - logging.debug(f"> {config.WINEBIN_CODE=}") + + logging.debug(f"> config.SELECTED_APPIMAGE_FILENAME={app.conf.wine_appimage_path}") + logging.debug(f"> config.RECOMMENDED_WINE64_APPIMAGE_URL={app.conf.wine_appimage_recommended_url}") #noqa: E501 + logging.debug(f"> config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME={app.conf.wine_appimage_recommended_file_name}") # noqa: E501 + logging.debug(f"> config.WINEBIN_CODE={app.conf.wine_binary_code}") logging.debug(f"> {app.conf.wine_binary=}") @@ -203,12 +196,13 @@ def ensure_appimage_download(app: App): ) downloaded_file = None - filename = Path(config.SELECTED_APPIMAGE_FILENAME).name + appimage_path = app.conf.wine_appimage_path or app.conf.wine_appimage_recommended_file_name + filename = Path(appimage_path).name downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, filename) if not downloaded_file: downloaded_file = Path(f"{app.conf.download_dir}/{filename}") network.logos_reuse_download( - config.RECOMMENDED_WINE64_APPIMAGE_URL, + app.conf.wine_appimage_recommended_url, filename, app.conf.download_dir, app=app, @@ -511,16 +505,18 @@ def get_progress_pct(current, total): return round(current * 100 / total) +# FIXME: Consider moving the condition for whether to run this inside the function +# Right now the condition is outside def create_wine_appimage_symlinks(app: App): appdir_bindir = Path(app.conf.installer_binary_dir) os.environ['PATH'] = f"{app.conf.installer_binary_dir}:{os.getenv('PATH')}" # Ensure AppImage symlink. - appimage_link = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME - appimage_file = Path(config.SELECTED_APPIMAGE_FILENAME) - appimage_filename = Path(config.SELECTED_APPIMAGE_FILENAME).name - if config.WINEBIN_CODE in ['AppImage', 'Recommended']: + appimage_link = appdir_bindir / app.conf.wine_appimage_link_file_name + if app.conf.wine_binary_code in ['AppImage', 'Recommended'] and app.conf.wine_appimage_path is not None: #noqa: E501 + appimage_file = Path(app.conf.wine_appimage_path) + appimage_filename = Path(app.conf.wine_appimage_path).name # Ensure appimage is copied to appdir_bindir. - downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) #noqa: E501 if not appimage_file.is_file(): msg.status( f"Copying: {downloaded_file} into: {appdir_bindir}", @@ -529,11 +525,11 @@ def create_wine_appimage_symlinks(app: App): shutil.copy(downloaded_file, str(appdir_bindir)) os.chmod(appimage_file, 0o755) appimage_filename = appimage_file.name - elif config.WINEBIN_CODE in ["System", "Proton", "PlayOnLinux", "Custom"]: + elif app.conf.wine_binary_code in ["System", "Proton", "PlayOnLinux", "Custom"]: appimage_filename = "none.AppImage" else: msg.logos_error( - f"WINEBIN_CODE error. WINEBIN_CODE is {config.WINEBIN_CODE}. Installation canceled!", # noqa: E501 + f"WINEBIN_CODE error. WINEBIN_CODE is {app.conf.wine_binary_code}. Installation canceled!", # noqa: E501 app=app ) @@ -544,7 +540,7 @@ def create_wine_appimage_symlinks(app: App): for name in ["wine", "wine64", "wineserver", "winetricks"]: p = appdir_bindir / name p.unlink(missing_ok=True) - p.symlink_to(f"./{config.APPIMAGE_LINK_SELECTION_NAME}") + p.symlink_to(f"./{app.conf.wine_appimage_link_file_name}") def create_config_file(): diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index c2ed04c8..f424bbf8 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -218,7 +218,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: ephemeral_config.delete_log = True if args.set_appimage: - config.APPIMAGE_FILE_PATH = args.set_appimage[0] + ephemeral_config.wine_appimage_path = args.set_appimage[0] if args.skip_fonts: ephemeral_config.install_fonts_skip = True @@ -276,19 +276,13 @@ def parse_args(args, parser) -> EphemeralConfiguration: config.ACTION = None for arg, action in actions.items(): if getattr(args, arg): - if arg == "update_latest_appimage" or arg == "set_appimage": - logging.debug("Running an AppImage command.") - if config.WINEBIN_CODE != "AppImage" and config.WINEBIN_CODE != "Recommended": # noqa: E501 - config.ACTION = "disabled" - logging.debug("AppImage commands not added since WINEBIN_CODE != (AppImage|Recommended)") # noqa: E501 - break if arg == "set_appimage": - config.APPIMAGE_FILE_PATH = getattr(args, arg)[0] - if not utils.file_exists(config.APPIMAGE_FILE_PATH): - e = f"Invalid file path: '{config.APPIMAGE_FILE_PATH}'. File does not exist." # noqa: E501 + ephemeral_config.wine_appimage_path = getattr(args, arg)[0] + if not utils.file_exists(ephemeral_config.wine_appimage_path): + e = f"Invalid file path: '{ephemeral_config.wine_appimage_path}'. File does not exist." # noqa: E501 raise argparse.ArgumentTypeError(e) - if not utils.check_appimage(config.APPIMAGE_FILE_PATH): - e = f"{config.APPIMAGE_FILE_PATH} is not an AppImage." + if not utils.check_appimage(ephemeral_config.wine_appimage_path): + e = f"{ephemeral_config.wine_appimage_path} is not an AppImage." raise argparse.ArgumentTypeError(e) if arg == 'winetricks': config.winetricks_args = getattr(args, 'winetricks') @@ -455,7 +449,7 @@ def main(): # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that # can be run in conjunction with other actions, so it gets special # treatment here once config is set. - app_log_path = ephemeral_config.app_log_path | constants.DEFAULT_APP_LOG_PATH + app_log_path = ephemeral_config.app_log_path or constants.DEFAULT_APP_LOG_PATH if ephemeral_config.delete_log and os.path.isfile(app_log_path): # Write empty file. with open(app_log_path, 'w') as f: diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index ac2c54c6..6e048b7e 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -423,25 +423,14 @@ def set_logoslinuxinstaller_latest_release_config(): logging.info(f"{config.LLI_LATEST_VERSION=}") -def set_recommended_appimage_config() -> None: +def get_recommended_appimage_url() -> str: repo = "FaithLife-Community/wine-appimages" - if not config.RECOMMENDED_WINE64_APPIMAGE_URL: - json_data = get_latest_release_data(repo) - appimage_url = get_first_asset_url(json_data) - if appimage_url is None: - logging.critical("Unable to set recommended appimage config without URL.") # noqa: E501 - return - config.RECOMMENDED_WINE64_APPIMAGE_URL = appimage_url - config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME = os.path.basename(config.RECOMMENDED_WINE64_APPIMAGE_URL) # noqa: E501 - config.RECOMMENDED_WINE64_APPIMAGE_FILENAME = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME.split(".AppImage")[0] # noqa: E501 - # Getting version and branch rely on the filename having this format: - # wine-[branch]_[version]-[arch] - parts = config.RECOMMENDED_WINE64_APPIMAGE_FILENAME.split('-') - branch_version = parts[1] - branch, version = branch_version.split('_') - config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION = f"v{version}-{branch}" - config.RECOMMENDED_WINE64_APPIMAGE_VERSION = f"{version}" - config.RECOMMENDED_WINE64_APPIMAGE_BRANCH = f"{branch}" + json_data = get_latest_release_data(repo) + appimage_url = get_first_asset_url(json_data) + if appimage_url is None: + # FIXME: changed this to raise an exception as we can't continue. + raise ValueError("Unable to set recommended appimage config without URL.") # noqa: E501 + return appimage_url def check_for_updates(install_dir: Optional[str], force: bool = False): @@ -468,11 +457,11 @@ def check_for_updates(install_dir: Optional[str], force: bool = False): check_again = now if now >= check_again: + # FIXME: refresh network config cache? logging.debug("Running self-update.") set_logoslinuxinstaller_latest_release_config() utils.compare_logos_linux_installer_version() - set_recommended_appimage_config() # wine.enforce_icu_data_files() config.LAST_UPDATED = now.isoformat() @@ -482,14 +471,14 @@ def check_for_updates(install_dir: Optional[str], force: bool = False): def get_recommended_appimage(app: App): - wine64_appimage_full_filename = Path(config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME) # noqa: E501 + wine64_appimage_full_filename = Path(app.conf.wine_appimage_recommended_file_name) # noqa: E501 dest_path = Path(app.conf.installer_binary_dir) / wine64_appimage_full_filename if dest_path.is_file(): return else: logos_reuse_download( - config.RECOMMENDED_WINE64_APPIMAGE_URL, - config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME, + app.conf.wine_appimage_recommended_url, + app.conf.wine_appimage_recommended_file_name, app.conf.installer_binary_dir, app=app ) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 4cc6bceb..af065019 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -142,6 +142,9 @@ class EphemeralConfiguration: Changes to this are not saved to disk, but remain while the program runs """ + + # See naming conventions in Config + # Start user overridable via env or cli arg installer_binary_dir: Optional[str] wineserver_binary: Optional[str] @@ -170,6 +173,13 @@ class EphemeralConfiguration: wine_prefix: Optional[str] """Corresponds to wine's WINEPREFIX""" + # FIXME: seems like the wine appimage logic can be simplified + wine_appimage_link_file_name: Optional[str] + """Syslink file name to the active wine appimage.""" + + wine_appimage_path: Optional[str] + """Path to the selected appimage""" + # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) # noqa: E501 custom_binary_path: Optional[str] """Additional path to look for when searching for binaries.""" @@ -227,7 +237,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": check_updates_now=legacy.CHECK_UPDATES, delete_log=delete_log, install_dependencies_skip=legacy.SKIP_DEPENDENCIES, - install_fonts_skip=legacy.SKIP_FONTS + install_fonts_skip=legacy.SKIP_FONTS, + wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, + wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME ) @classmethod @@ -250,7 +262,9 @@ class NetworkCache: # Start cache _faithlife_product_releases: Optional[list[str]] = None - _downloads_dir: Optional[str] = None + # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? + # in legacy refresh wasn't handled properly + wine_appimage_url: Optional[str] = None # XXX: add @property defs to automatically retrieve if not found @@ -268,6 +282,8 @@ class PersistentConfiguration: MUST be saved explicitly """ + # See naming conventions in Config + # XXX: store a version in this config? # Just in case we need to do conditional logic reading old version's configurations @@ -335,6 +351,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": def write_config(self) -> None: config_file_path = LegacyConfiguration.config_file_path() + # XXX: we may need to merge this dict with the legacy configuration's extended config (as we don't store that persistently anymore) #noqa: E501 output = self.__dict__ logging.info(f"Writing config to {config_file_path}") @@ -388,6 +405,7 @@ class Config: # suffix with _binary if it's a linux binary # suffix with _exe if it's a windows binary # suffix with _path if it's a file path + # suffix with _file_name if it's a file's name (with extension) # Storage for the keys _raw: PersistentConfiguration @@ -568,7 +586,6 @@ def wine_binary(self) -> str: output = self._raw.wine_binary if output is None: question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: " # noqa: E501 - network.set_recommended_appimage_config() options = utils.get_wine_options( self.app, utils.find_appimage_files(self.app), @@ -599,8 +616,16 @@ def wine_binary(self, value: str): self._raw.wine_binary = value # Reset dependents self._raw.wine_binary_code = None + self._overrides.wine_appimage_path = None self._write() + @property + def wine_binary_code(self) -> str: + """""" + if self._raw.wine_binary_code is None: + self._raw.wine_binary_code = utils.get_winebin_code_and_desc(self.app, self.wine_binary)[0] # noqa: E501 + return self._raw.wine_binary_code + @property def wine64_binary(self) -> str: return str(Path(self.wine_binary).parent / 'wine64') @@ -610,6 +635,54 @@ def wine64_binary(self) -> str: def wineserver_binary(self) -> str: return str(Path(self.wine_binary).parent / 'wineserver') + # FIXME: seems like the logic around wine appimages can be simplified + # Should this be folded into wine_binary? + @property + def wine_appimage_path(self) -> Optional[str]: + """Path to the wine appimage + + Returns: + Path if wine is set to use an appimage, otherwise returns None""" + if self._overrides.wine_appimage_path is not None: + return self._overrides.wine_appimage_path + if self.wine_binary.lower().endswith("appimage"): + return self.wine_binary + return None + + @wine_appimage_path.setter + def wine_appimage_path(self, value: Optional[str]): + if self._overrides.wine_appimage_path != value: + self._overrides.wine_appimage_path = value + # Reset dependents + self._raw.wine_binary_code = None + # XXX: Should we save? There should be something here we should store + + @property + def wine_appimage_link_file_name(self) -> str: + if self._overrides.wine_appimage_link_file_name is not None: + return self._overrides.wine_appimage_link_file_name + return 'selected_wine.AppImage' + + @property + def wine_appimage_recommended_url(self) -> str: + """URL to recommended appimage. + + Talks to the network if required""" + if self._cache.wine_appimage_url is None: + self._cache.wine_appimage_url = network.get_recommended_appimage_url() + return self._cache.wine_appimage_url + + @property + def wine_appimage_recommended_file_name(self) -> str: + """Returns the file name of the recommended appimage with extension""" + return os.path.basename(self.wine_appimage_recommended_url) + + @property + def wine_appimage_recommended_version(self) -> str: + # Getting version and branch rely on the filename having this format: + # wine-[branch]_[version]-[arch] + return self.wine_appimage_recommended_file_name.split('-')[1].split('_')[1] + @property def wine_dll_overrides(self) -> str: """Used to set WINEDLLOVERRIDES""" diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 81c99273..2ebbe662 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -534,10 +534,10 @@ def custom_appimage_select(self, choice): appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") else: appimage_filename = choice - config.SELECTED_APPIMAGE_FILENAME = appimage_filename + self.conf.wine_appimage_path = appimage_filename utils.set_appimage_symlink(self) self.menu_screen.choice = "Processing" - self.appimage_q.put(config.SELECTED_APPIMAGE_FILENAME) + self.appimage_q.put(self.conf.wine_appimage_path) self.appimage_e.set() def waiting(self, choice): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 7aad3ef7..8fd3539b 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -19,7 +19,7 @@ from ou_dedetai.app import App from packaging import version from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Tuple, Union from . import config from . import constants @@ -258,8 +258,14 @@ def filter_versions(versions, threshold, check_version_part): return [version for version in versions if check_logos_release_version(version, threshold, check_version_part)] # noqa: E501 -# XXX: figure this out and fold into config -def get_winebin_code_and_desc(app: App, binary): +# FIXME: consider where we want this +def get_winebin_code_and_desc(app: App, binary) -> Tuple[str, str | None]: + """Gets the type of wine in use and it's description + + Returns: + code: One of: Recommended, AppImage, System, Proton, PlayOnLinux, Custom + description: Description of the above + """ # Set binary code, description, and path based on path codes = { "Recommended": "Use the recommended AppImage", @@ -280,7 +286,7 @@ def get_winebin_code_and_desc(app: App, binary): # Does it work? if isinstance(binary, Path): binary = str(binary) - if binary == f"{app.conf.installer_binary_dir}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}": # noqa: E501 + if binary == f"{app.conf.installer_binary_dir}/{app.conf.wine_appimage_recommended_file_name}": # noqa: E501 code = "Recommended" elif binary.lower().endswith('.appimage'): code = "AppImage" @@ -304,29 +310,29 @@ def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 # Add AppImages to list # if config.DIALOG == 'tk': - wine_binary_options.append(f"{app.conf.installer_binary_dir}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}") # noqa: E501 + wine_binary_options.append(f"{app.conf.installer_binary_dir}/{app.conf.wine_appimage_recommended_file_name}") # noqa: E501 wine_binary_options.extend(appimages) # else: # appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 # wine_binary_options.append([ # "Recommended", # Code - # f'{app.conf.installer_binary_directory}/{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME}', # noqa: E501 - # f"AppImage of Wine64 {config.RECOMMENDED_WINE64_APPIMAGE_FULL_VERSION}" # noqa: E501 + # f'{app.conf.installer_binary_directory}/{app.conf.wine_appimage_recommended_file_name}', # noqa: E501 + # f"AppImage of Wine64 {app.conf.wine_appimage_recommended_version}" # noqa: E501 # ]) # wine_binary_options.extend(appimage_entries) sorted_binaries = sorted(list(set(binaries))) logging.debug(f"{sorted_binaries=}") - for WINEBIN_PATH in sorted_binaries: - WINEBIN_CODE, WINEBIN_DESCRIPTION = get_winebin_code_and_desc(app, WINEBIN_PATH) # noqa: E501 + for wine_binary_path in sorted_binaries: + code, description = get_winebin_code_and_desc(app, wine_binary_path) # noqa: E501 # Create wine binary option array # if config.DIALOG == 'tk': - wine_binary_options.append(WINEBIN_PATH) + wine_binary_options.append(wine_binary_path) # else: # wine_binary_options.append( - # [WINEBIN_CODE, WINEBIN_PATH, WINEBIN_DESCRIPTION] + # [code, wine_binary_path, description] # ) # # if config.DIALOG != 'tk': @@ -523,25 +529,23 @@ def compare_recommended_appimage_version(app: App): current_version = '.'.join([str(n) for n in wine_release[:2]]) logging.debug(f"Current wine release: {current_version}") - if config.RECOMMENDED_WINE64_APPIMAGE_VERSION: - logging.debug(f"Recommended wine release: {config.RECOMMENDED_WINE64_APPIMAGE_VERSION}") # noqa: E501 - if current_version < config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Current release is older than recommended. - status = 0 - message = "yes" - elif current_version == config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Current release is latest. - status = 1 - message = "uptodate" - elif current_version > config.RECOMMENDED_WINE64_APPIMAGE_VERSION: # noqa: E501 - # Installed version is custom - status = 2 - message = "no" - else: - status = False - message = f"Error: {error_message}" + recommended_version = app.conf.wine_appimage_recommended_version + logging.debug(f"Recommended wine release: {recommended_version}") + if current_version < recommended_version: + # Current release is older than recommended. + status = 0 + message = "yes" + elif current_version == recommended_version: + # Current release is latest. + status = 1 + message = "uptodate" + elif current_version > recommended_version: + # Installed version is custom + status = 2 + message = "no" else: - status = False + # FIXME: should this raise an exception? + status = -1 message = f"Error: {error_message}" logging.debug(f"{status=}; {message=}") @@ -691,15 +695,19 @@ def find_wine_binary_files(app: App, release_version): def set_appimage_symlink(app: App): # This function assumes make_skel() has been run once. - # if config.APPIMAGE_FILE_PATH is None: - # config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 - - logging.debug(f"{config.APPIMAGE_FILE_PATH=}") - logging.debug(f"{config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME=}") - appimage_file_path = Path(config.APPIMAGE_FILE_PATH) + if app.conf.wine_binary_code not in ["AppImage", "Recommended"]: + logging.debug("AppImage commands disabled since we're not using an appimage") # noqa: E501 + return + if app.conf.wine_appimage_path is None: + logging.debug("No need to set appimage syslink, as it wasn't set") + return + + logging.debug(f"config.APPIMAGE_FILE_PATH={app.conf.wine_appimage_path}") + logging.debug(f"config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME={app.conf.wine_appimage_recommended_file_name}") + appimage_file_path = Path(app.conf.wine_appimage_path) appdir_bindir = Path(app.conf.installer_binary_dir) - appimage_symlink_path = appdir_bindir / config.APPIMAGE_LINK_SELECTION_NAME - if appimage_file_path.name == config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME: # noqa: E501 + appimage_symlink_path = appdir_bindir / app.conf.wine_appimage_link_file_name + if appimage_file_path.name == app.conf.wine_appimage_recommended_file_name: # noqa: E501 # Default case. network.get_recommended_appimage(app) selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 @@ -743,7 +751,7 @@ def set_appimage_symlink(app: App): delete_symlink(appimage_symlink_path) os.symlink(selected_appimage_file_path, appimage_symlink_path) - config.SELECTED_APPIMAGE_FILENAME = f"{selected_appimage_file_path.name}" # noqa: E501 + app.conf.wine_appimage_path = f"{selected_appimage_file_path.name}" # noqa: E501 write_config(config.CONFIG_FILE) if config.DIALOG == 'tk': @@ -765,7 +773,10 @@ def update_to_latest_lli_release(app=None): # XXX: seems like this should be in control def update_to_latest_recommended_appimage(app: App): - config.APPIMAGE_FILE_PATH = config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME # noqa: E501 + if app.conf.wine_binary_code not in ["AppImage", "Recommended"]: + logging.debug("AppImage commands disabled since we're not using an appimage") # noqa: E501 + return + app.conf.wine_appimage_path = app.conf.wine_appimage_recommended_file_name # noqa: E501 status, _ = compare_recommended_appimage_version(app) if status == 0: set_appimage_symlink(app) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 0a54a119..29cbdb14 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -53,6 +53,7 @@ def end_wine_processes(): wait_pid(process) +# FIXME: consider raising exceptions on error def get_wine_release(binary): cmd = [binary, "--version"] try: From ffe81f97588c7a52a42f2cbc46c77d45f6d64c65 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:57:06 -0800 Subject: [PATCH 043/137] fix: migrate winecmd_encoding --- ou_dedetai/config.py | 1 - ou_dedetai/new_config.py | 20 ++++++++++++++---- ou_dedetai/wine.py | 44 ++++++++++++++++++++++------------------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 23e5aa4a..e4121289 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -9,7 +9,6 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these current_logos_version = None -WINECMD_ENCODING = None LOGS = None LAST_UPDATED = None LLI_LATEST_VERSION = None diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index af065019..38041723 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Optional -from ou_dedetai import msg, network, utils, constants +from ou_dedetai import msg, network, utils, constants, wine from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY @@ -172,6 +172,8 @@ class EphemeralConfiguration: """Corresponds to wine's WINEDEBUG""" wine_prefix: Optional[str] """Corresponds to wine's WINEPREFIX""" + wine_output_encoding: Optional[str] + """Override for what encoding wine's output is using""" # FIXME: seems like the wine appimage logic can be simplified wine_appimage_link_file_name: Optional[str] @@ -239,7 +241,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": install_dependencies_skip=legacy.SKIP_DEPENDENCIES, install_fonts_skip=legacy.SKIP_FONTS, wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, - wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME + wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, + wine_output_encoding=legacy.WINECMD_ENCODING ) @classmethod @@ -379,8 +382,6 @@ def write_config(self) -> None: # Whether or not the installed faithlife product is configured for additional logging. # Used to be called "LOGS" installed_faithlife_logging: Optional[bool] = None -# Text encoding of the wine command. This calue can be retrieved from the system -winecmd_encoding: Optional[str] = None last_updated: Optional[datetime] = None recommended_wine_url: Optional[str] = None latest_installer_version: Optional[str] = None @@ -420,6 +421,7 @@ class Config: # i.e. filesystem traversals _logos_exe: Optional[str] = None _download_dir: Optional[str] = None + _wine_output_encoding: Optional[str] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -698,6 +700,16 @@ def wine_debug(self) -> str: return self._overrides.wine_debug return constants.DEFAULT_WINEDEBUG + @property + def wine_output_encoding(self) -> Optional[str]: + """Attempt to guess the encoding of the wine output""" + if self._overrides.wine_output_encoding is not None: + return self._overrides.wine_output_encoding + if self._wine_output_encoding is None: + return wine.get_winecmd_encoding(self.app) + return None + + @property def app_wine_log_path(self) -> str: if self._overrides.app_wine_log_path is not None: diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 29cbdb14..33a1a553 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -267,6 +267,23 @@ def wait_pid(process): os.waitpid(-process.pid, 0) +def get_winecmd_encoding(app: App) -> Optional[str]: + # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. + logging.debug("Getting wine system's cmd.exe encoding.") + registry_value = get_registry_value( + 'HKCU\\Software\\Wine\\Fonts', + 'Codepages', + app + ) + if registry_value is not None: + codepages: str = registry_value.split(',') + return codepages[-1] + else: + m = "wine.wine_proc: wine.get_registry_value returned None." + logging.error(m) + return None + + def run_wine_proc( winecmd, app: App, @@ -277,20 +294,6 @@ def run_wine_proc( ): logging.debug("Getting wine environment.") env = get_wine_env(app, additional_wine_dll_overrides) - if not init and config.WINECMD_ENCODING is None: - # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. - logging.debug("Getting wine system's cmd.exe encoding.") - registry_value = get_registry_value( - 'HKCU\\Software\\Wine\\Fonts', - 'Codepages', - app - ) - if registry_value is not None: - codepages = registry_value.split(',') # noqa: E501 - config.WINECMD_ENCODING = codepages[-1] - else: - m = "wine.wine_proc: wine.get_registry_value returned None." - logging.error(m) if isinstance(winecmd, Path): winecmd = str(winecmd) logging.debug(f"run_wine_proc: {winecmd}; {exe=}; {exe_args=}") @@ -325,10 +328,10 @@ def run_wine_proc( try: logging.info(line.decode().rstrip()) except UnicodeDecodeError: - if config.WINECMD_ENCODING is not None: - logging.info(line.decode(config.WINECMD_ENCODING).rstrip()) # noqa: E501 + if not init and app.conf.wine_output_encoding is not None: # noqa: E501 + logging.info(line.decode(app.conf.wine_output_encoding).rstrip()) # noqa: E501 else: - logging.error("wine.run_wine_proc: Error while decoding: WINECMD_ENCODING is None.") # noqa: E501 + logging.error("wine.run_wine_proc: Error while decoding: wine output encoding could not be found.") # noqa: E501 return process else: return None @@ -446,8 +449,9 @@ def enforce_icu_data_files(app: App): def get_registry_value(reg_path, name, app: App): logging.debug(f"Get value for: {reg_path=}; {name=}") + # FIXME: consider breaking run_wine_proc into a helper function before decoding is attempted # noqa: E501 # NOTE: Can't use run_wine_proc here because of infinite recursion while - # trying to determine WINECMD_ENCODING. + # trying to determine wine_output_encoding. value = None env = get_wine_env(app) @@ -456,7 +460,7 @@ def get_registry_value(reg_path, name, app: App): 'reg', 'query', reg_path, '/v', name, ] err_msg = f"Failed to get registry value: {reg_path}\\{name}" - encoding = config.WINECMD_ENCODING + encoding = app.conf.wine_output_encoding if encoding is None: encoding = 'UTF-8' try: @@ -534,7 +538,7 @@ def get_wine_env(app: App, additional_wine_dll_overrides: Optional[str]=None): if winepath.name != 'wine64': # AppImage # Winetricks commands can fail if 'wine64' is not explicitly defined. # https://github.com/Winetricks/winetricks/issues/2084#issuecomment-1639259359 - winepath = app.conf.wine64_binary + winepath = Path(app.conf.wine64_binary) wine_env_defaults = { 'WINE': str(winepath), 'WINEDEBUG': app.conf.wine_debug, From 78e8479d8b815e66c24d27e9129398a12121e1ae Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:20:05 -0800 Subject: [PATCH 044/137] fix: migrate package manager to new format --- ou_dedetai/config.py | 9 - ou_dedetai/system.py | 455 +++++++++++++++++++++++-------------------- ou_dedetai/utils.py | 6 +- 3 files changed, 250 insertions(+), 220 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e4121289..0cce2233 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -26,17 +26,8 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' -BADPACKAGES: Optional[str] = None # This isn't presently used, but could be if needed. -L9PACKAGES = None LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME LOGOS_LATEST_VERSION_URL: Optional[str] = None -PACKAGE_MANAGER_COMMAND_INSTALL: Optional[list[str]] = None -PACKAGE_MANAGER_COMMAND_DOWNLOAD: Optional[list[str]] = None -PACKAGE_MANAGER_COMMAND_REMOVE: Optional[list[str]] = None -PACKAGE_MANAGER_COMMAND_QUERY: Optional[list[str]] = None -PACKAGES: Optional[str] = None -QUERY_PREFIX: Optional[str] = None -REBOOT_REQUIRED: Optional[str] = None SUPERUSER_COMMAND: Optional[str] = None wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 5db13479..bdbdcf69 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Optional, Tuple import distro import logging @@ -270,21 +271,49 @@ def get_superuser_command(): logging.debug(f"{config.SUPERUSER_COMMAND=}") -def get_package_manager(): +@dataclass +class PackageManager: + """Dataclass to pass around relevant OS context regarding system packages""" + # Commands + install: list[str] + download: list[str] + remove: list[str] + query: list[str] + + query_prefix: str + + packages: str + logos_9_packages: str + + incompatible_packages: str + + +def get_package_manager() -> PackageManager | None: major_ver = distro.major_version() os_name = distro.id() logging.debug(f"{os_name=}; {major_ver=}") # Check for package manager and associated packages. # NOTE: cabextract and sed are included in the appimage, so they are not # included as system dependencies. + + install_command: list[str] + download_command: list[str] + remove_command: list[str] + query_command: list[str] + query_prefix: str + packages: str + # FIXME: Missing Logos 9 Packages + logos_9_packages: str = "" + incompatible_packages: str + if shutil.which('apt') is not None: # debian, ubuntu, & derivatives - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["apt", "install", "-y"] - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["apt", "install", "--download-only", "-y"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["apt", "remove", "-y"] - config.PACKAGE_MANAGER_COMMAND_QUERY = ["dpkg", "-l"] - config.QUERY_PREFIX = '.i ' + install_command = ["apt", "install", "-y"] + download_command = ["apt", "install", "--download-only", "-y"] # noqa: E501 + remove_command = ["apt", "remove", "-y"] + query_command = ["dpkg", "-l"] + query_prefix = '.i ' # Set default package list. - config.PACKAGES = ( + packages = ( "libfuse2 " # appimages "binutils wget winbind " # wine "p7zip-full " # winetricks @@ -303,73 +332,70 @@ def get_package_manager(): or (os_name == 'linuxmint' and major_ver >= '22') or (os_name == 'elementary' and major_ver >= '8') ): - config.PACKAGES = ( + packages = ( "libfuse3-3 " # appimages "binutils wget winbind " # wine "7zip " # winetricks ) - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "" # appimagelauncher handled separately + logos_9_packages = "" + incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('dnf') is not None: # rhel, fedora - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["dnf", "install", "-y"] - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["dnf", "install", "--downloadonly", "-y"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["dnf", "remove", "-y"] + install_command = ["dnf", "install", "-y"] + download_command = ["dnf", "install", "--downloadonly", "-y"] # noqa: E501 + remove_command = ["dnf", "remove", "-y"] # Fedora < 41 uses dnf4, while Fedora 41 uses dnf5. The dnf list # command is sligtly different between the two. # https://discussion.fedoraproject.org/t/after-f41-upgrade-dnf-says-no-packages-are-installed/135391 # noqa: E501 # Fedora < 41 - # config.PACKAGE_MANAGER_COMMAND_QUERY = ["dnf", "list", "installed"] + # query_command = ["dnf", "list", "installed"] # Fedora 41 - # config.PACKAGE_MANAGER_COMMAND_QUERY = ["dnf", "list", "--installed"] - config.PACKAGE_MANAGER_COMMAND_QUERY = ["rpm", "-qa"] # workaround - config.QUERY_PREFIX = '' - # config.PACKAGES = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 - config.PACKAGES = ( + # query_command = ["dnf", "list", "--installed"] + query_command = ["rpm", "-qa"] # workaround + query_prefix = '' + # logos_10_packages = "patch fuse3 fuse3-libs mod_auth_ntlm_winbind samba-winbind samba-winbind-clients cabextract bc libxml2 curl" # noqa: E501 + packages = ( "fuse fuse-libs " # appimages "mod_auth_ntlm_winbind samba-winbind samba-winbind-clients " # wine # noqa: E501 "p7zip-plugins " # winetricks ) - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "" # appimagelauncher handled separately + incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('zypper') is not None: # manjaro - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["zypper", "--non-interactive", "install"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["zypper", "download"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["zypper", "--non-interactive", "remove"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_QUERY = ["zypper", "se", "-si"] - config.QUERY_PREFIX = 'i | ' - config.PACKAGES = ( + install_command = ["zypper", "--non-interactive", "install"] # noqa: E501 + download_command = ["zypper", "download"] # noqa: E501 + remove_command = ["zypper", "--non-interactive", "remove"] # noqa: E501 + query_command = ["zypper", "se", "-si"] + query_prefix = 'i | ' + packages = ( "fuse2 " # appimages "samba wget " # wine "7zip " # winetricks "curl gawk grep " # other ) - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "" # appimagelauncher handled separately + incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('pamac') is not None: # manjaro - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pamac", "remove", "--no-confirm"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_QUERY = ["pamac", "list", "-i"] - config.QUERY_PREFIX = '' - config.PACKAGES = ( + install_command = ["pamac", "install", "--no-upgrade", "--no-confirm"] # noqa: E501 + download_command = ["pamac", "install", "--no-upgrade", "--download-only", "--no-confirm"] # noqa: E501 + remove_command = ["pamac", "remove", "--no-confirm"] # noqa: E501 + query_command = ["pamac", "list", "-i"] + query_prefix = '' + packages = ( "fuse2 " # appimages "samba wget " # wine "p7zip " # winetricks "curl gawk grep " # other ) - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "" # appimagelauncher handled separately + incompatible_packages = "" # appimagelauncher handled separately elif shutil.which('pacman') is not None: # arch, steamOS - config.PACKAGE_MANAGER_COMMAND_INSTALL = ["pacman", "-Syu", "--overwrite", "\\*", "--noconfirm", "--needed"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_DOWNLOAD = ["pacman", "-Sw", "-y"] - config.PACKAGE_MANAGER_COMMAND_REMOVE = ["pacman", "-R", "--no-confirm"] # noqa: E501 - config.PACKAGE_MANAGER_COMMAND_QUERY = ["pacman", "-Q"] - config.QUERY_PREFIX = '' + install_command = ["pacman", "-Syu", "--overwrite", "\\*", "--noconfirm", "--needed"] # noqa: E501 + download_command = ["pacman", "-Sw", "-y"] + remove_command = ["pacman", "-R", "--no-confirm"] # noqa: E501 + query_command = ["pacman", "-Q"] + query_prefix = '' if os_name == "steamos": # steamOS - config.PACKAGES = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: #E501 + packages = "patch wget sed grep gawk cabextract samba bc libxml2 curl print-manager system-config-printer cups-filters nss-mdns foomatic-db-engine foomatic-db-ppds foomatic-db-nonfree-ppds ghostscript glibc samba extra-rel/apparmor core-rel/libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo qt5-virtualkeyboard wine-staging giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: E501 else: # arch - # config.PACKAGES = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: E501 - config.PACKAGES = ( + # logos_10_packages = "patch wget sed grep cabextract samba glibc samba apparmor libcurl-gnutls winetricks appmenu-gtk-module lib32-libjpeg-turbo wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins 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" # noqa: E501 + packages = ( "fuse2 " # appimages "binutils libwbclient samba wget " # wine "p7zip " # winetricks @@ -379,17 +405,25 @@ def get_package_manager(): "libva mpg123 v4l-utils " # video "libxslt sqlite " # misc ) - config.L9PACKAGES = "" # FIXME: Missing Logos 9 Packages - config.BADPACKAGES = "" # appimagelauncher handled separately + incompatible_packages = "" # appimagelauncher handled separately else: # Add more conditions for other package managers as needed. - msg.logos_error("Your package manager is not yet supported. Please contact the developers.") # noqa: E501 + error = "Your package manager is not yet supported. Please contact the developers." + msg.logos_error(error) # noqa: E501 + return None - # Add logging output. - logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_INSTALL=}") - logging.debug(f"{config.PACKAGE_MANAGER_COMMAND_QUERY=}") - logging.debug(f"{config.PACKAGES=}") - logging.debug(f"{config.L9PACKAGES=}") + output = PackageManager( + install=install_command, + download=download_command, + query=query_command, + remove=remove_command, + incompatible_packages=incompatible_packages, + packages=packages, + logos_9_packages=logos_9_packages, + query_prefix=query_prefix + ) + logging.debug("Package Manager: {output}") + return output def get_runmode(): @@ -399,11 +433,12 @@ def get_runmode(): return 'script' -def query_packages(packages, mode="install"): +def query_packages(package_manager: PackageManager, packages, mode="install") -> list[str]: #noqa: E501 result = "" missing_packages = [] conflicting_packages = [] - command = config.PACKAGE_MANAGER_COMMAND_QUERY + + command = package_manager.query try: result = run_command(command) @@ -421,7 +456,7 @@ def query_packages(packages, mode="install"): for line in package_list.split('\n'): # logging.debug(f"{line=}") l_num += 1 - if config.PACKAGE_MANAGER_COMMAND_QUERY[0] == 'dpkg': + if package_manager.query[0] == 'dpkg': parts = line.strip().split() if l_num < 6 or len(parts) < 2: # skip header, etc. continue @@ -435,7 +470,7 @@ def query_packages(packages, mode="install"): status[p] = 'Conflicting' break else: - if line.strip().startswith(f"{config.QUERY_PREFIX}{p}") and mode == "install": # noqa: E501 + if line.strip().startswith(f"{package_manager.query_prefix}{p}") and mode == "install": # noqa: E501 logging.debug(f"'{p}' installed: {line}") status[p] = "Installed" break @@ -464,6 +499,8 @@ def query_packages(packages, mode="install"): txt = f"Conflicting packages: {' '.join(conflicting_packages)}" logging.info(f"Conflicting packages: {txt}") return conflicting_packages + else: + raise ValueError(f"Invalid query mode: {mode}") def have_dep(cmd): @@ -514,7 +551,7 @@ def parse_date(version): def remove_appimagelauncher(app=None): pkg = "appimagelauncher" - cmd = [config.SUPERUSER_COMMAND, *config.PACKAGE_MANAGER_COMMAND_REMOVE, pkg] # noqa: E501 + cmd = [config.SUPERUSER_COMMAND, *package_manager.remove, pkg] # noqa: E501 # FIXME: should this status be higher? (the caller of this function) msg.status("Removing AppImageLauncher…", app) try: @@ -579,12 +616,13 @@ def postinstall_dependencies(): # XXX: move this to control, prompts additional values from app -def install_dependencies(app: App, packages, bad_packages, logos9_packages=None): # noqa: E501 +def install_dependencies(app: App, target_version=10): # noqa: E501 if app.conf.skip_install_system_dependencies: return install_deps_failed = False manual_install_required = False + reboot_required = False message = None no_message = None secondary = None @@ -593,170 +631,173 @@ def install_dependencies(app: App, packages, bad_packages, logos9_packages=None) install_command = [] remove_command = [] postinstall_command = [] - missing_packages = {} - conflicting_packages = {} + missing_packages = [] + conflicting_packages = [] package_list = [] bad_package_list = [] - if packages: - package_list = packages.split() - - if bad_packages: - bad_package_list = bad_packages.split() + package_manager = get_package_manager() - if logos9_packages: - package_list.extend(logos9_packages.split()) + os_name, _ = get_os() - if config.PACKAGE_MANAGER_COMMAND_QUERY: - logging.debug("Querying packages…") - missing_packages = query_packages( - package_list, - ) - conflicting_packages = query_packages( - bad_package_list, - mode="remove", + if not package_manager: + msg.logos_error( + f"The script could not determine your {os_name} install's package manager or it is unsupported." # noqa: E501 ) + # XXX: raise error or exit? + return - os_name, _ = get_os() - if config.PACKAGE_MANAGER_COMMAND_INSTALL: - if os_name in ['fedora', 'arch']: - message = False - no_message = False - secondary = False - elif missing_packages and conflicting_packages: - message = f"Your {os_name} computer requires installing and removing some software.\nProceed?" # noqa: E501 - no_message = "User refused to install and remove software via the application" # noqa: E501 - secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}\nand will remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 - elif missing_packages: - message = f"Your {os_name} computer requires installing some software.\nProceed?" # noqa: E501 - no_message = "User refused to install software via the application." # noqa: E501 - secondary = f"To continue, the program will attempt to install the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_INSTALL}':\n{missing_packages}" # noqa: E501 - elif conflicting_packages: - message = f"Your {os_name} computer requires removing some software.\nProceed?" # noqa: E501 - no_message = "User refused to remove software via the application." # noqa: E501 - secondary = f"To continue, the program will attempt to remove the following package(s) by using '{config.PACKAGE_MANAGER_COMMAND_REMOVE}':\n{conflicting_packages}" # noqa: E501 - else: - message = None + package_list = package_manager.packages.split() - if message is None: - logging.debug("No missing or conflicting dependencies found.") - elif not message: - m = "Your distro requires manual dependency installation." - logging.error(m) - else: - msg.logos_continue_question(message, no_message, secondary, app) - if config.DIALOG == "curses": - app.confirm_e.wait() + bad_package_list = package_manager.incompatible_packages.split() + + if target_version == 9: + package_list.extend(package_manager.logos_9_packages.split()) + + logging.debug("Querying packages…") + missing_packages = query_packages( + package_manager, + package_list, + ) + conflicting_packages = query_packages( + package_manager, + bad_package_list, + mode="remove", + ) - # TODO: Need to send continue question to user based on DIALOG. - # All we do above is create a message that we never send. - # Do we need a TK continue question? I see we have a CLI and curses one - # in msg.py + if os_name in ['fedora', 'arch']: + message = False + no_message = False + secondary = False + elif missing_packages and conflicting_packages: + message = f"Your {os_name} computer requires installing and removing some software.\nProceed?" # noqa: E501 + no_message = "User refused to install and remove software via the application" # noqa: E501 + secondary = f"To continue, the program will attempt to install the following package(s) by using '{package_manager.install}':\n{missing_packages}\nand will remove the following package(s) by using '{package_manager.remove}':\n{conflicting_packages}" # noqa: E501 + elif missing_packages: + message = f"Your {os_name} computer requires installing some software.\nProceed?" # noqa: E501 + no_message = "User refused to install software via the application." # noqa: E501 + secondary = f"To continue, the program will attempt to install the following package(s) by using '{package_manager.install}':\n{missing_packages}" # noqa: E501 + elif conflicting_packages: + message = f"Your {os_name} computer requires removing some software.\nProceed?" # noqa: E501 + no_message = "User refused to remove software via the application." # noqa: E501 + secondary = f"To continue, the program will attempt to remove the following package(s) by using '{package_manager.remove}':\n{conflicting_packages}" # noqa: E501 + else: + message = None - preinstall_command = preinstall_dependencies() + if message is None: + logging.debug("No missing or conflicting dependencies found.") + elif not message: + m = "Your distro requires manual dependency installation." + logging.error(m) + else: + msg.logos_continue_question(message, no_message, secondary, app) + if config.DIALOG == "curses": + app.confirm_e.wait() - if missing_packages: - install_command = config.PACKAGE_MANAGER_COMMAND_INSTALL + missing_packages # noqa: E501 - else: - logging.debug("No missing packages detected.") + # TODO: Need to send continue question to user based on DIALOG. + # All we do above is create a message that we never send. + # Do we need a TK continue question? I see we have a CLI and curses one + # in msg.py - if conflicting_packages: - # TODO: Verify with user before executing - # AppImage Launcher is the only known conflicting package. - remove_command = config.PACKAGE_MANAGER_COMMAND_REMOVE + conflicting_packages # noqa: E501 - config.REBOOT_REQUIRED = True - logging.info("System reboot required.") - else: - logging.debug("No conflicting packages detected.") - - postinstall_command = postinstall_dependencies() - - if preinstall_command: - command.extend(preinstall_command) - if install_command: - if command: - command.append('&&') - command.extend(install_command) - if remove_command: - if command: - command.append('&&') - command.extend(remove_command) - if postinstall_command: - if command: - command.append('&&') - command.extend(postinstall_command) - if not command: # nothing to run; avoid running empty pkexec command - if app: - msg.status("All dependencies are met.", app) - if config.DIALOG == "curses": - app.installdeps_e.set() - return - - if app and config.DIALOG == 'tk': - app.root.event_generate('<>') - msg.status("Installing dependencies…", app) - final_command = [ - f"{config.SUPERUSER_COMMAND}", 'sh', '-c', "'", *command, "'" - ] - command_str = ' '.join(final_command) - # TODO: Fix fedora/arch handling. - if os_name in ['fedora', 'arch']: - manual_install_required = True - sudo_command = command_str.replace("pkexec", "sudo") - message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 - detail = ( - "Please run the following command in a terminal, then restart " - f"{constants.APP_NAME}:\n{sudo_command}\n" - ) - if config.DIALOG == "tk": - if hasattr(app, 'root'): - detail += "\nThe command has been copied to the clipboard." # noqa: E501 - app.root.clipboard_clear() - app.root.clipboard_append(sudo_command) - app.root.update() - msg.logos_error( - message, - detail=detail, - app=app, - parent='installer_win' - ) - elif config.DIALOG == 'cli': - msg.logos_error(message + "\n" + detail) - install_deps_failed = True + preinstall_command = preinstall_dependencies() - if manual_install_required and app and config.DIALOG == "curses": - app.screen_q.put( - app.stack_confirm( - 17, - app.manualinstall_q, - app.manualinstall_e, - f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{constants.APP_NAME}:\n{sudo_command}\n", # noqa: E501 - "User cancelled dependency installation.", # noqa: E501 - message, - options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 - app.manualinstall_e.wait() - - if not install_deps_failed and not manual_install_required: - if config.DIALOG == 'cli': - command_str = command_str.replace("pkexec", "sudo") - try: - logging.debug(f"Attempting to run this command: {command_str}") - run_command(command_str, shell=True) - except subprocess.CalledProcessError as e: - if e.returncode == 127: - logging.error("User cancelled dependency installation.") - else: - logging.error(f"An error occurred in install_dependencies(): {e}") # noqa: E501 - logging.error(f"Command output: {e.output}") - install_deps_failed = True + if missing_packages: + install_command = package_manager.install + missing_packages # noqa: E501 else: - msg.logos_error( - f"The script could not determine your {os_name} install's package manager or it is unsupported. " # noqa: E501 - f"Your computer is missing the command(s) {missing_packages}. " - f"Please install your distro's package(s) associated with {missing_packages} for {os_name}." # noqa: E501 + logging.debug("No missing packages detected.") + + if conflicting_packages: + # TODO: Verify with user before executing + # AppImage Launcher is the only known conflicting package. + remove_command = package_manager.remove + conflicting_packages # noqa: E501 + reboot_required = True + logging.info("System reboot required.") + else: + logging.debug("No conflicting packages detected.") + + postinstall_command = postinstall_dependencies() + + if preinstall_command: + command.extend(preinstall_command) + if install_command: + if command: + command.append('&&') + command.extend(install_command) + if remove_command: + if command: + command.append('&&') + command.extend(remove_command) + if postinstall_command: + if command: + command.append('&&') + command.extend(postinstall_command) + if not command: # nothing to run; avoid running empty pkexec command + if app: + msg.status("All dependencies are met.", app) + if config.DIALOG == "curses": + app.installdeps_e.set() + return + + if app and config.DIALOG == 'tk': + app.root.event_generate('<>') + msg.status("Installing dependencies…", app) + final_command = [ + f"{config.SUPERUSER_COMMAND}", 'sh', '-c', "'", *command, "'" + ] + command_str = ' '.join(final_command) + # TODO: Fix fedora/arch handling. + if os_name in ['fedora', 'arch']: + manual_install_required = True + sudo_command = command_str.replace("pkexec", "sudo") + message = "The system needs to install/remove packages, but it requires manual intervention." # noqa: E501 + detail = ( + "Please run the following command in a terminal, then restart " + f"{constants.APP_NAME}:\n{sudo_command}\n" ) + if config.DIALOG == "tk": + if hasattr(app, 'root'): + detail += "\nThe command has been copied to the clipboard." # noqa: E501 + app.root.clipboard_clear() + app.root.clipboard_append(sudo_command) + app.root.update() + msg.logos_error( + message, + detail=detail, + app=app, + parent='installer_win' + ) + elif config.DIALOG == 'cli': + msg.logos_error(message + "\n" + detail) + install_deps_failed = True + + if manual_install_required and app and config.DIALOG == "curses": + app.screen_q.put( + app.stack_confirm( + 17, + app.manualinstall_q, + app.manualinstall_e, + f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{constants.APP_NAME}:\n{sudo_command}\n", # noqa: E501 + "User cancelled dependency installation.", # noqa: E501 + message, + options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 + app.manualinstall_e.wait() + + if not install_deps_failed and not manual_install_required: + if config.DIALOG == 'cli': + command_str = command_str.replace("pkexec", "sudo") + try: + logging.debug(f"Attempting to run this command: {command_str}") + run_command(command_str, shell=True) + except subprocess.CalledProcessError as e: + if e.returncode == 127: + logging.error("User cancelled dependency installation.") + else: + logging.error(f"An error occurred in install_dependencies(): {e}") # noqa: E501 + logging.error(f"Command output: {e.output}") + install_deps_failed = True + - if config.REBOOT_REQUIRED: + if reboot_required: question = "Should the program reboot the host now?" # noqa: E501 no_text = "The user has chosen not to reboot." secondary = "The system has installed or removed a package that requires a reboot." # noqa: E501 diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 8fd3539b..b176abe1 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -180,13 +180,11 @@ def install_dependencies(app: App): msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) if targetversion == 10: - system.install_dependencies(app, config.PACKAGES, config.BADPACKAGES) # noqa: E501 + system.install_dependencies(app, target_version=10) # noqa: E501 elif targetversion == 9: system.install_dependencies( app, - config.PACKAGES, - config.BADPACKAGES, - config.L9PACKAGES + target_version=9 ) else: logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") From a0253ef1c00bd7fb386bc556f90a20b4366cc921 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:39:13 -0800 Subject: [PATCH 045/137] refactor: move globally scoped tui variables into TUI --- ou_dedetai/config.py | 8 ---- ou_dedetai/tui_app.py | 30 +++++++----- ou_dedetai/tui_curses.py | 57 ++++++++++++---------- ou_dedetai/tui_screen.py | 100 ++++++++++++++++++++------------------- 4 files changed, 100 insertions(+), 95 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 0cce2233..a3691944 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -31,15 +31,7 @@ SUPERUSER_COMMAND: Optional[str] = None wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") -install_finished = False console_log = [] -margin = 2 -console_log_lines = 1 -current_option = 0 -current_page = 0 -total_pages = 0 -options_per_page = 8 -resizing = False processes = {} threads = [] logos_linux_installer_status = None diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 2ebbe662..88b6a4d4 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -30,7 +30,7 @@ # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. class TUI(App): - def __init__(self, stdscr, ephemeral_config: EphemeralConfiguration): + def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfiguration): super().__init__(ephemeral_config) self.stdscr = stdscr # if config.current_logos_version is not None: @@ -86,6 +86,12 @@ def __init__(self, stdscr, ephemeral_config: EphemeralConfiguration): self.install_logos_q = Queue() self.install_logos_e = threading.Event() + self.terminal_margin = 0 + self.resizing = False + # These two are updated in set_window_dimensions + self.console_log_lines = 0 + self.options_per_page = 0 + # Window and Screen Management self.tui_screens = [] self.menu_options = [] @@ -109,8 +115,8 @@ def set_window_dimensions(self): self.menu_window_min = 3 self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) - config.console_log_lines = max(self.main_window_height - self.main_window_min, 1) - config.options_per_page = max(self.window_height - self.main_window_height - 6, 1) + self.console_log_lines = max(self.main_window_height - self.main_window_min, 1) + self.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) resize_lines = tui_curses.wrap_text(self, "Screen too small.") @@ -226,7 +232,7 @@ def update_main_window_contents(self): # even though the resize signal is sent. See tui_curses, line #251 and # tui_screen, line #98. def resize_curses(self): - config.resizing = True + self.resizing = True curses.endwin() self.update_tty_dimensions() self.set_window_dimensions() @@ -234,7 +240,7 @@ def resize_curses(self): self.init_curses() self.refresh() msg.status("Window resized.", self) - config.resizing = False + self.resizing = False def signal_resize(self, signum, frame): self.resize_curses() @@ -254,14 +260,14 @@ def signal_resize(self, signum, frame): def draw_resize_screen(self): self.clear() if self.window_width > 10: - margin = config.margin + margin = self.terminal_margin else: margin = 0 resize_lines = tui_curses.wrap_text(self, "Screen too small.") self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) for i, line in enumerate(resize_lines): if i < self.window_height: - tui_curses.write_line(self, self.resize_window, i, margin, line, self.window_width - config.margin, curses.A_BOLD) + tui_curses.write_line(self, self.resize_window, i, margin, line, self.window_width - self.terminal_margin, curses.A_BOLD) self.refresh() def display(self): @@ -275,8 +281,8 @@ def display(self): while self.llirunning: if self.window_height >= 10 and self.window_width >= 35: - config.margin = 2 - if not config.resizing: + self.terminal_margin = 2 + if not self.resizing: self.update_windows() self.active_screen.display() @@ -310,10 +316,10 @@ def display(self): self.refresh() elif self.window_width >= 10: if self.window_width < 10: - config.margin = 1 # Avoid drawing errors on very small screens + self.terminal_margin = 1 # Avoid drawing errors on very small screens self.draw_resize_screen() elif self.window_width < 10: - config.margin = 0 # Avoid drawing errors on very small screens + self.terminal_margin = 0 # Avoid drawing errors on very small screens def run(self): try: @@ -918,6 +924,6 @@ def get_menu_window(self): return self.menu_window -def control_panel_app(stdscr, ephemeral_config: EphemeralConfiguration): +def control_panel_app(stdscr: curses.window, ephemeral_config: EphemeralConfiguration): os.environ.setdefault('ESCDELAY', '100') TUI(stdscr, ephemeral_config).run() diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 26fdf00e..cf1e351f 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -11,10 +11,10 @@ def wrap_text(app, text): # Turn text into wrapped text, line by line, centered if "\n" in text: lines = text.splitlines() - wrapped_lines = [textwrap.fill(line, app.window_width - (config.margin * 2)) for line in lines] + wrapped_lines = [textwrap.fill(line, app.window_width - (app.terminal_margin * 2)) for line in lines] lines = '\n'.join(wrapped_lines) else: - wrapped_text = textwrap.fill(text, app.window_width - (config.margin * 2)) + wrapped_text = textwrap.fill(text, app.window_width - (app.terminal_margin * 2)) lines = wrapped_text.split('\n') return lines @@ -85,8 +85,9 @@ def confirm(app, question_text, height=None, width=None): class CursesDialog: def __init__(self, app): - self.app = app - self.stdscr = self.app.get_menu_window() + from ou_dedetai.tui_app import TUI + self.app: TUI = app + self.stdscr: curses.window = self.app.get_menu_window() def __str__(self): return f"Curses Dialog" @@ -197,19 +198,23 @@ def __init__(self, app, question_text, options): self.question_start_y = None self.question_lines = None + self.current_option: int = 0 + self.current_page: int = 0 + self.total_pages: int = 0 + def __str__(self): return f"Menu Curses Dialog" def draw(self): self.stdscr.erase() self.app.active_screen.set_options(self.options) - config.total_pages = (len(self.options) - 1) // config.options_per_page + 1 + self.total_pages = (len(self.options) - 1) // self.app.options_per_page + 1 self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) # Display the options, centered options_start_y = self.question_start_y + len(self.question_lines) + 2 - for i in range(config.options_per_page): - index = config.current_page * config.options_per_page + i + for i in range(self.app.options_per_page): + index = self.current_page * self.app.options_per_page + i if index < len(self.options): option = self.options[index] if type(option) is list: @@ -240,7 +245,7 @@ def draw(self): y = options_start_y + i + j x = max(0, self.app.window_width // 2 - len(line) // 2) if y < self.app.menu_window_height: - if index == config.current_option: + if index == self.current_option: write_line(self.app, self.stdscr, y, x, line, self.app.window_width, curses.A_REVERSE) else: write_line(self.app, self.stdscr, y, x, line, self.app.window_width) @@ -250,33 +255,33 @@ def draw(self): options_start_y += (len(option_lines)) # Display pagination information - page_info = f"Page {config.current_page + 1}/{config.total_pages} | Selected Option: {config.current_option + 1}/{len(self.options)}" + page_info = f"Page {self.current_page + 1}/{self.total_pages} | Selected Option: {self.current_option + 1}/{len(self.options)}" write_line(self.app, self.stdscr, max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) def do_menu_up(self): - if config.current_option == config.current_page * config.options_per_page and config.current_page > 0: + if self.current_option == self.current_page * self.app.options_per_page and self.current_page > 0: # Move to the previous page - config.current_page -= 1 - config.current_option = min(len(self.app.menu_options) - 1, (config.current_page + 1) * config.options_per_page - 1) - elif config.current_option == 0: - if config.total_pages == 1: - config.current_option = len(self.app.menu_options) - 1 + self.current_page -= 1 + self.current_option = min(len(self.app.menu_options) - 1, (self.current_page + 1) * self.app.options_per_page - 1) + elif self.current_option == 0: + if self.total_pages == 1: + self.current_option = len(self.app.menu_options) - 1 else: - config.current_page = config.total_pages - 1 - config.current_option = len(self.app.menu_options) - 1 + self.current_page = self.total_pages - 1 + self.current_option = len(self.app.menu_options) - 1 else: - config.current_option = max(0, config.current_option - 1) + self.current_option = max(0, self.current_option - 1) def do_menu_down(self): - if config.current_option == (config.current_page + 1) * config.options_per_page - 1 and config.current_page < config.total_pages - 1: + if self.current_option == (self.current_page + 1) * self.app.options_per_page - 1 and self.current_page < self.total_pages - 1: # Move to the next page - config.current_page += 1 - config.current_option = min(len(self.app.menu_options) - 1, config.current_page * config.options_per_page) - elif config.current_option == len(self.app.menu_options) - 1: - config.current_page = 0 - config.current_option = 0 + self.current_page += 1 + self.current_option = min(len(self.app.menu_options) - 1, self.current_page * self.app.options_per_page) + elif self.current_option == len(self.app.menu_options) - 1: + self.current_page = 0 + self.current_option = 0 else: - config.current_option = min(len(self.app.menu_options) - 1, config.current_option + 1) + self.current_option = min(len(self.app.menu_options) - 1, self.current_option + 1) def input(self): if len(self.app.tui_screens) > 0: @@ -303,7 +308,7 @@ def input(self): elif final_key == 66: self.do_menu_down() elif key == ord('\n') or key == 10: # Enter key - self.user_input = self.options[config.current_option] + self.user_input = self.options[self.current_option] elif key == ord('\x1b'): signal.signal(signal.SIGINT, self.app.end) else: diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index 764c8322..dc6c85f3 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -25,15 +25,15 @@ def __init__(self, app: App, screen_id, queue, event): # running: # This var indicates either whether: # A CursesScreen has already submitted its choice to the choice_q, or - # The var indicates whether a Dialog has already started. If the dialog has already started, - # then the program will not display the dialog again in order to prevent phantom key presses. + # The var indicates whether a Dialog has already started. If the dialog has already started, #noqa: E501 + # then the program will not display the dialog again in order to prevent phantom key presses. #noqa: E501 # 0 = not submitted or not started # 1 = submitted or started # 2 = none or finished self.running = 0 def __str__(self): - return f"Curses Screen" + return "Curses Screen" def display(self): pass @@ -77,7 +77,7 @@ def __init__(self, app, screen_id, queue, event, title, subtitle, title_start_y) self.title_start_y = title_start_y def __str__(self): - return f"Curses Console Screen" + return "Curses Console Screen" def display(self): self.stdscr.erase() @@ -86,21 +86,21 @@ def display(self): console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( tui_curses.wrap_text(self.app, self.subtitle)) + 1 - tui_curses.write_line(self.app, self.stdscr, console_start_y, config.margin, f"---Console---", self.app.window_width - (config.margin * 2)) - recent_messages = config.console_log[-config.console_log_lines:] + tui_curses.write_line(self.app, self.stdscr, console_start_y, self.app.terminal_margin, "---Console---", self.app.window_width - (self.app.terminal_margin * 2)) #noqa: E501 + recent_messages = config.console_log[-self.app.console_log_lines:] for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): if 2 + j < self.app.window_height: - truncated = message[:self.app.window_width - (config.margin * 2)] - tui_curses.write_line(self.app, self.stdscr, console_start_y + i, config.margin, truncated, self.app.window_width - (config.margin * 2)) + truncated = message[:self.app.window_width - (self.app.terminal_margin * 2)] #noqa: E501 + tui_curses.write_line(self.app, self.stdscr, console_start_y + i, self.app.terminal_margin, truncated, self.app.window_width - (self.app.terminal_margin * 2)) #noqa: E501 self.stdscr.noutrefresh() curses.doupdate() class MenuScreen(CursesScreen): - def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): + def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): #noqa: E501 super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -110,7 +110,7 @@ def __init__(self, app, screen_id, queue, event, question, options, height=None, self.menu_height = menu_height def __str__(self): - return f"Curses Menu Screen" + return "Curses Menu Screen" def display(self): self.stdscr.erase() @@ -119,9 +119,7 @@ def display(self): self.question, self.options ).run() - if self.choice is not None and not self.choice == "" and not self.choice == "Processing": - config.current_option = 0 - config.current_page = 0 + if self.choice is not None and not self.choice == "" and not self.choice == "Processing": #noqa: E501 self.submit_choice_to_queue() self.stdscr.noutrefresh() curses.doupdate() @@ -135,14 +133,14 @@ def set_options(self, new_options): class ConfirmScreen(MenuScreen): - def __init__(self, app, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"]): + def __init__(self, app, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"]): #noqa: E501 super().__init__(app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8) self.no_text = no_text self.secondary = secondary def __str__(self): - return f"Curses Confirm Screen" + return "Curses Confirm Screen" def display(self): self.stdscr.erase() @@ -151,9 +149,7 @@ def display(self): self.secondary + "\n" + self.question, self.options ).run() - if self.choice is not None and not self.choice == "" and not self.choice == "Processing": - config.current_option = 0 - config.current_page = 0 + if self.choice is not None and not self.choice == "" and not self.choice == "Processing": #noqa: E501 if self.choice == "No": logging.critical(self.no_text) self.submit_choice_to_queue() @@ -174,7 +170,7 @@ def __init__(self, app, screen_id, queue, event, question, default): ) def __str__(self): - return f"Curses Input Screen" + return "Curses Input Screen" def display(self): self.stdscr.erase() @@ -201,7 +197,7 @@ def __init__(self, app, screen_id, queue, event, question, default): ) def __str__(self): - return f"Curses Password Screen" + return "Curses Password Screen" def display(self): self.stdscr.erase() @@ -222,13 +218,13 @@ def __init__(self, app, screen_id, queue, event, text, wait): self.spinner_index = 0 def __str__(self): - return f"Curses Text Screen" + return "Curses Text Screen" def display(self): self.stdscr.erase() text_start_y, text_lines = tui_curses.text_centered(self.app, self.text) if self.wait: - self.spinner_index = tui_curses.spinner(self.app, self.spinner_index, text_start_y + len(text_lines) + 1) + self.spinner_index = tui_curses.spinner(self.app, self.spinner_index, text_start_y + len(text_lines) + 1) #noqa: E501 time.sleep(0.1) self.stdscr.noutrefresh() curses.doupdate() @@ -238,7 +234,7 @@ def get_text(self): class MenuDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): + def __init__(self, app, screen_id, queue, event, question, options, height=None, width=None, menu_height=8): #noqa: E501 super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -248,13 +244,14 @@ def __init__(self, app, screen_id, queue, event, question, options, height=None, self.menu_height = menu_height def __str__(self): - return f"PyDialog Menu Screen" + return "PyDialog Menu Screen" def display(self): if self.running == 0: self.running = 1 - _, _, self.choice = tui_dialog.menu(self.app, self.question, self.options, self.height, self.width, - self.menu_height) + _, _, self.choice = tui_dialog.menu(self.app, self.question, self.options, + self.height, self.width, + self.menu_height) self.submit_choice_to_queue() def get_question(self): @@ -272,7 +269,7 @@ def __init__(self, app, screen_id, queue, event, question, default): self.default = default def __str__(self): - return f"PyDialog Input Screen" + return "PyDialog Input Screen" def display(self): if self.running == 0: @@ -294,18 +291,18 @@ def __init__(self, app, screen_id, queue, event, question, default): super().__init__(app, screen_id, queue, event, question, default) def __str__(self): - return f"PyDialog Password Screen" + return "PyDialog Password Screen" def display(self): if self.running == 0: self.running = 1 - _, self.choice = tui_dialog.password(self.app, self.question, init=self.default) + _, self.choice = tui_dialog.password(self.app, self.question, init=self.default) #noqa: E501 self.submit_choice_to_queue() utils.send_task(self.app, "INSTALLING_PW") class ConfirmDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, question, no_text, secondary, yes_label="Yes", no_label="No"): + def __init__(self, app, screen_id, queue, event, question, no_text, secondary, yes_label="Yes", no_label="No"): #noqa: E501 super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -315,7 +312,7 @@ def __init__(self, app, screen_id, queue, event, question, no_text, secondary, y self.no_label = no_label def __str__(self): - return f"PyDialog Confirm Screen" + return "PyDialog Confirm Screen" def display(self): if self.running == 0: @@ -334,8 +331,8 @@ def get_question(self): class TextDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, height=None, width=None, - title=None, backtitle=None, colors=True): + def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, + height=None, width=None, title=None, backtitle=None, colors=True): super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.text = text @@ -350,13 +347,13 @@ def __init__(self, app, screen_id, queue, event, text, wait=False, percent=None, self.dialog = "" def __str__(self): - return f"PyDialog Text Screen" + return "PyDialog Text Screen" def display(self): if self.running == 0: if self.wait: if self.app.installer_step_count > 0: - self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) + self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) #noqa: E501 else: self.percent = 0 @@ -368,7 +365,7 @@ def display(self): elif self.running == 1: if self.wait: if self.app.installer_step_count > 0: - self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) + self.percent = installer.get_progress_pct(self.app.installer_step, self.app.installer_step_count) #noqa: E501 else: self.percent = 0 @@ -402,17 +399,20 @@ def __init__(self, app, screen_id, queue, event, text, elements, percent, self.updated = False def __str__(self): - return f"PyDialog Task List Screen" + return "PyDialog Task List Screen" def display(self): if self.running == 0: - tui_dialog.tasklist_progress_bar(self, self.text, self.percent, self.elements, - self.height, self.width, self.title, self.backtitle, self.colors) + tui_dialog.tasklist_progress_bar(self, self.text, self.percent, + self.elements, self.height, self.width, + self.title, self.backtitle, self.colors) self.running = 1 elif self.running == 1: if self.updated: - tui_dialog.tasklist_progress_bar(self, self.text, self.percent, self.elements, - self.height, self.width, self.title, self.backtitle, self.colors) + tui_dialog.tasklist_progress_bar(self, self.text, self.percent, + self.elements, self.height, self.width, + self.title, self.backtitle, + self.colors) else: pass @@ -435,7 +435,7 @@ def get_text(self): class BuildListDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): + def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): #noqa: E501 super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -445,13 +445,14 @@ def __init__(self, app, screen_id, queue, event, question, options, list_height= self.list_height = list_height def __str__(self): - return f"PyDialog Build List Screen" + return "PyDialog Build List Screen" def display(self): if self.running == 0: self.running = 1 - code, self.choice = tui_dialog.buildlist(self.app, self.question, self.options, self.height, self.width, - self.list_height) + code, self.choice = tui_dialog.buildlist(self.app, self.question, + self.options, self.height, + self.width, self.list_height) self.running = 2 def get_question(self): @@ -462,7 +463,7 @@ def set_options(self, new_options): class CheckListDialog(DialogScreen): - def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): + def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): #noqa: E501 super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -472,13 +473,14 @@ def __init__(self, app, screen_id, queue, event, question, options, list_height= self.list_height = list_height def __str__(self): - return f"PyDialog Check List Screen" + return "PyDialog Check List Screen" def display(self): if self.running == 0: self.running = 1 - code, self.choice = tui_dialog.checklist(self.app, self.question, self.options, self.height, self.width, - self.list_height) + code, self.choice = tui_dialog.checklist(self.app, self.question, + self.options, self.height, + self.width, self.list_height) self.running = 2 def get_question(self): From 965d19713fd1b4de5aa886965b810e4cdba11ee8 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:41:17 -0800 Subject: [PATCH 046/137] fix: remove check if indexing variable did nothing, replaced with a string --- ou_dedetai/config.py | 1 - ou_dedetai/logos.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index a3691944..f0bb1f16 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -41,7 +41,6 @@ 2: "no", None: "constants.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set.", # noqa: E501 } -check_if_indexing = None # XXX: remove this diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 061eb538..7c9090cb 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -193,8 +193,8 @@ def wait_on_indexing(): wait_thread = utils.start_thread(wait_on_indexing, daemon_bool=False) main.threads.extend([index_thread, check_thread, wait_thread]) config.processes[self.app.conf.logos_indexer_exe] = index_thread - config.processes[config.check_if_indexing] = check_thread - config.processes[wait_on_indexing] = wait_thread + config.processes["check_if_indexing"] = check_thread + config.processes["wait_on_indexing"] = wait_thread def stop_indexing(self): self.indexing_state = State.STOPPING From 77e4065249065947b644e2b69ddde6d2b6bcdfcd Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:57:02 -0800 Subject: [PATCH 047/137] refactor: migrate current_logos_version --- ou_dedetai/config.py | 1 - ou_dedetai/installer.py | 1 - ou_dedetai/logos.py | 2 +- ou_dedetai/network.py | 2 -- ou_dedetai/new_config.py | 11 ++++++++--- ou_dedetai/tui_app.py | 5 ++--- ou_dedetai/utils.py | 7 ++++--- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index f0bb1f16..ac8fa167 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -8,7 +8,6 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these -current_logos_version = None LOGS = None LAST_UPDATED = None LLI_LATEST_VERSION = None diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index e7f36164..febd3623 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -420,7 +420,6 @@ def ensure_product_installed(app: App): if not app.is_installed(): process = wine.install_msi(app) wine.wait_pid(process) - config.current_logos_version = app.conf.faithlife_product_release # Clean up temp files, etc. utils.clean_all() diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 7c9090cb..7a338b0a 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -86,7 +86,7 @@ def run_logos(): # Ensure wine version is compatible with Logos release version. good_wine, reason = wine.check_wine_rules( wine_release, - config.current_logos_version, + self.app.conf.installed_faithlife_product_release, self.app.conf.faithlife_product_version ) if not good_wine: diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 6e048b7e..f91eae3d 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -438,8 +438,6 @@ def check_for_updates(install_dir: Optional[str], force: bool = False): # order to avoid GitHub API limits. This sets the check to once every 12 # hours. - if install_dir is not None: - config.current_logos_version = utils.get_current_logos_version(install_dir) utils.write_config(config.CONFIG_FILE) # TODO: Check for New Logos Versions. See #116. diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 38041723..fdaa2fd5 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -377,8 +377,6 @@ def write_config(self) -> None: # XXX: Move these into the cache & store -# Used to be called current_logos_version, but actually could be used in Verbium too. -installed_faithlife_product_release: Optional[str] = None # Whether or not the installed faithlife product is configured for additional logging. # Used to be called "LOGS" installed_faithlife_logging: Optional[bool] = None @@ -422,6 +420,7 @@ class Config: _logos_exe: Optional[str] = None _download_dir: Optional[str] = None _wine_output_encoding: Optional[str] = None + _installed_faithlife_product_release: Optional[str] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -837,4 +836,10 @@ def skip_install_fonts(self, val: bool): def download_dir(self) -> str: if self._download_dir is None: self._download_dir = utils.get_user_downloads_dir() - return self._download_dir \ No newline at end of file + return self._download_dir + + @property + def installed_faithlife_product_release(self) -> Optional[str]: + if self._installed_faithlife_product_release is None: + self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 + return self._installed_faithlife_product_release \ No newline at end of file diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 88b6a4d4..c1879f47 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -33,9 +33,8 @@ class TUI(App): def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfiguration): super().__init__(ephemeral_config) self.stdscr = stdscr - # if config.current_logos_version is not None: self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 - self.subtitle = f"Logos Version: {config.current_logos_version} ({self.conf.faithlife_product_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 # else: # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" @@ -217,7 +216,7 @@ def end(self, signal, frame): def update_main_window_contents(self): self.clear() self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 - self.subtitle = f"Logos Version: {config.current_logos_version} ({self.conf.faithlife_product_release_channel})" # noqa: E501 + self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) # self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index b176abe1..f25c6622 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -202,10 +202,10 @@ def file_exists(file_path: Optional[str | bytes | Path]) -> bool: return False -def get_current_logos_version(install_dir: str): +def get_current_logos_version(install_dir: str) -> Optional[str]: path_regex = f"{install_dir}/data/wine64_bottle/drive_c/users/*/AppData/Local/Logos/System/Logos.deps.json" # noqa: E501 file_paths = glob.glob(path_regex) - logos_version_number = None + logos_version_number: Optional[str] = None if file_paths: logos_version_file = file_paths[0] with open(logos_version_file, 'r') as json_file: @@ -222,6 +222,7 @@ def get_current_logos_version(install_dir: str): return None else: logging.debug("Logos.deps.json not found.") + return None def convert_logos_release(logos_release): @@ -615,7 +616,7 @@ def check_appimage(filestr): def find_appimage_files(app: App): - release_version = config.current_logos_version or app.conf.faithlife_product_version + release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version appimages = [] directories = [ os.path.expanduser("~") + "/bin", From e0aa905b604ecb594a5272345f228a32a3874ebd Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:05:13 -0800 Subject: [PATCH 048/137] refactor: migrate LOGS --- ou_dedetai/config.py | 1 - ou_dedetai/gui.py | 1 - ou_dedetai/logos.py | 2 +- ou_dedetai/new_config.py | 27 +++++++++++++++++++-------- ou_dedetai/tui_app.py | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index ac8fa167..536cb651 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -8,7 +8,6 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these -LOGS = None LAST_UPDATED = None LLI_LATEST_VERSION = None lli_release_channel = None diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index bb5d432a..3e67f348 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -188,7 +188,6 @@ def __init__(self, root, app: App, *args, **kwargs): # XXX: remove these # Initialize vars from ENV. - self.logs = config.LOGS self.config_file = config.CONFIG_FILE # Run/install app button diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 7a338b0a..91fcca53 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -269,7 +269,7 @@ def switch_logging(self, action=None): ) wine.wait_pid(process) wine.wineserver_wait(app=self.app) - config.LOGS = state + self.app.conf.faithlife_product_logging = state if config.DIALOG in ['curses', 'dialog', 'tk']: self.app.logging_q.put(state) self.app.root.event_generate(self.app.logging_event) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index fdaa2fd5..f0eec2e0 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -293,6 +293,7 @@ class PersistentConfiguration: faithlife_product: Optional[str] = None faithlife_product_version: Optional[str] = None faithlife_product_release: Optional[str] = None + faithlife_product_logging: Optional[bool] = None install_dir: Optional[Path] = None winetricks_binary: Optional[str] = None wine_binary: Optional[str] = None @@ -349,7 +350,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": installer_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, - winetricks_binary=legacy.WINETRICKSBIN + winetricks_binary=legacy.WINETRICKSBIN, + faithlife_product_logging=legacy.LOGS ) def write_config(self) -> None: @@ -377,9 +379,6 @@ def write_config(self) -> None: # XXX: Move these into the cache & store -# Whether or not the installed faithlife product is configured for additional logging. -# Used to be called "LOGS" -installed_faithlife_logging: Optional[bool] = None last_updated: Optional[datetime] = None recommended_wine_url: Optional[str] = None latest_installer_version: Optional[str] = None @@ -420,7 +419,6 @@ class Config: _logos_exe: Optional[str] = None _download_dir: Optional[str] = None _wine_output_encoding: Optional[str] = None - _installed_faithlife_product_release: Optional[str] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -519,6 +517,19 @@ def faithlife_product_release(self, value: str): def faithlife_product_icon_path(self) -> str: return str(constants.APP_IMAGE_DIR / f"{self.faithlife_product}-128-icon.png") + @property + def faithlife_product_logging(self) -> bool: + """Whether or not the installed faithlife product is configured to log""" + if self._raw.faithlife_product_logging is not None: + return self._raw.faithlife_product_logging + return False + + @faithlife_product_logging.setter + def faithlife_product_logging(self, value: bool): + if self._raw.faithlife_product_logging != value: + self._raw.faithlife_product_logging = value + self._write() + @property def faithlife_installer_name(self) -> str: if self._overrides.faithlife_installer_name is not None: @@ -840,6 +851,6 @@ def download_dir(self) -> str: @property def installed_faithlife_product_release(self) -> Optional[str]: - if self._installed_faithlife_product_release is None: - self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 - return self._installed_faithlife_product_release \ No newline at end of file + if self._faithlife_product_logging is None: + self._faithlife_product_logging = utils.get_current_logos_version(self.install_dir) # noqa: E501 + return self._faithlife_product_logging \ No newline at end of file diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index c1879f47..9fbedf4f 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -831,7 +831,7 @@ def set_utilities_menu_options(self, dialog=False): ] labels.extend(labels_utils_installed) - label = "Enable Logging" if config.LOGS == "DISABLED" else "Disable Logging" + label = "Enable Logging" if self.conf.faithlife_product_logging else "Disable Logging" #noqa: E501 labels.append(label) labels.append("Return to Main Menu") From 912fded0c4c0413fa6379f6a9232a18eeca284b0 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:07:39 -0800 Subject: [PATCH 049/137] fix: installed_faithlife_product_release --- ou_dedetai/new_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index f0eec2e0..ec94c265 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -419,6 +419,7 @@ class Config: _logos_exe: Optional[str] = None _download_dir: Optional[str] = None _wine_output_encoding: Optional[str] = None + _installed_faithlife_product_release: Optional[str] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -719,7 +720,6 @@ def wine_output_encoding(self) -> Optional[str]: return wine.get_winecmd_encoding(self.app) return None - @property def app_wine_log_path(self) -> str: if self._overrides.app_wine_log_path is not None: @@ -851,6 +851,6 @@ def download_dir(self) -> str: @property def installed_faithlife_product_release(self) -> Optional[str]: - if self._faithlife_product_logging is None: - self._faithlife_product_logging = utils.get_current_logos_version(self.install_dir) # noqa: E501 - return self._faithlife_product_logging \ No newline at end of file + if self._installed_faithlife_product_release is None: + self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 + return self._installed_faithlife_product_release \ No newline at end of file From b4b33c46f68630464f2d8df3c1cde431a63dec14 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:29:11 -0800 Subject: [PATCH 050/137] refactor: migrate latest version --- ou_dedetai/cli.py | 2 +- ou_dedetai/config.py | 10 ------- ou_dedetai/gui_app.py | 10 +++---- ou_dedetai/network.py | 34 +++++++++++++---------- ou_dedetai/new_config.py | 28 ++++++++++++++----- ou_dedetai/tui_app.py | 19 +++++++------ ou_dedetai/utils.py | 58 +++++++++++++++++----------------------- 7 files changed, 80 insertions(+), 81 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 0ec3a437..06da58a3 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -86,7 +86,7 @@ def update_latest_appimage(self): utils.update_to_latest_recommended_appimage(self) def update_self(self): - utils.update_to_latest_lli_release() + utils.update_to_latest_lli_release(self) def winetricks(self): import config diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 536cb651..74791996 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -9,7 +9,6 @@ # Define and set variables that are required in the config file. # XXX: slowly kill these LAST_UPDATED = None -LLI_LATEST_VERSION = None lli_release_channel = None # Define and set additional variables that can be set in the env. @@ -24,21 +23,12 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' -LOGOS_LATEST_VERSION_FILENAME = constants.APP_NAME -LOGOS_LATEST_VERSION_URL: Optional[str] = None SUPERUSER_COMMAND: Optional[str] = None wine_user = None WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") console_log = [] processes = {} threads = [] -logos_linux_installer_status = None -logos_linux_installer_status_info = { - 0: "yes", - 1: "uptodate", - 2: "no", - None: "constants.LLI_CURRENT_VERSION or config.LLI_LATEST_VERSION is not set.", # noqa: E501 -} # XXX: remove this diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 27479883..4ddbfc7c 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -558,8 +558,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar text = self.gui.update_lli_label.cget('text') ver = constants.LLI_CURRENT_VERSION - new = config.LLI_LATEST_VERSION - text = f"{text}\ncurrent: v{ver}\nlatest: v{new}" + text = f"{text}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" self.gui.update_lli_label.config(text=text) self.configure_app_button() self.gui.run_indexing_radio.config( @@ -806,15 +805,16 @@ def update_app_button(self, evt=None): def update_latest_lli_release_button(self, evt=None): msg = None + result = utils.compare_logos_linux_installer_version(self) if system.get_runmode() != 'binary': state = 'disabled' msg = "This button is disabled. Can't run self-update from script." - elif config.logos_linux_installer_status == 0: + elif result == utils.VersionComparison.OUT_OF_DATE: state = '!disabled' - elif config.logos_linux_installer_status == 1: + elif result == utils.VersionComparison.UP_TO_DATE: state = 'disabled' msg = f"This button is disabled. {constants.APP_NAME} is up-to-date." # noqa: E501 - elif config.logos_linux_installer_status == 2: + elif result == utils.VersionComparison.DEVELOPMENT: state = 'disabled' msg = f"This button is disabled. {constants.APP_NAME} is newer than the latest release." # noqa: E501 if msg: diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index f91eae3d..02208804 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -405,22 +405,28 @@ def get_tag_name(json_data) -> Optional[str]: return tag_name -def set_logoslinuxinstaller_latest_release_config(): - if config.lli_release_channel is None or config.lli_release_channel == "stable": # noqa: E501 +def get_oudedetai_latest_release_config(channel: str = "stable") -> tuple[str, str]: + """Get latest release information + + Returns: + url + version + """ + if channel == "stable": repo = "FaithLife-Community/LogosLinuxInstaller" else: repo = "FaithLife-Community/test-builds" json_data = get_latest_release_data(repo) - logoslinuxinstaller_url = get_first_asset_url(json_data) - if logoslinuxinstaller_url is None: + oudedetai_url = get_first_asset_url(json_data) + if oudedetai_url is None: logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 - return - config.LOGOS_LATEST_VERSION_URL = logoslinuxinstaller_url - config.LOGOS_LATEST_VERSION_FILENAME = os.path.basename(logoslinuxinstaller_url) # noqa: #501 + raise ValueError("Failed to find latest installer version") # Getting version relies on the the tag_name field in the JSON data. This # is already parsed down to vX.X.X. Therefore we must strip the v. - config.LLI_LATEST_VERSION = get_tag_name(json_data).lstrip('v') - logging.info(f"{config.LLI_LATEST_VERSION=}") + latest_version = get_tag_name(json_data).lstrip('v') + logging.info(f"config.LLI_LATEST_VERSION={latest_version}") + + return oudedetai_url, latest_version def get_recommended_appimage_url() -> str: @@ -458,8 +464,8 @@ def check_for_updates(install_dir: Optional[str], force: bool = False): # FIXME: refresh network config cache? logging.debug("Running self-update.") - set_logoslinuxinstaller_latest_release_config() - utils.compare_logos_linux_installer_version() + # XXX: can't run this here without a network cache + # utils.compare_logos_linux_installer_version() # wine.enforce_icu_data_files() config.LAST_UPDATED = now.isoformat() @@ -531,7 +537,7 @@ def get_logos_releases(app: App) -> list[str]: return filtered_releases -def update_lli_binary(app=None): +def update_lli_binary(app: App): lli_file_path = os.path.realpath(sys.argv[0]) lli_download_path = Path(app.conf.download_dir) / constants.BINARY_NAME temp_path = Path(app.conf.download_dir) / f"{constants.BINARY_NAME}.tmp" @@ -542,13 +548,13 @@ def update_lli_binary(app=None): if lli_download_path.is_file(): logging.info("Checking if existing LLI binary is latest version.") lli_download_ver = utils.get_lli_release_version(lli_download_path) - if not lli_download_ver or lli_download_ver != config.LLI_LATEST_VERSION: # noqa: E501 + if not lli_download_ver or lli_download_ver != app.conf.app_latest_version: # noqa: E501 logging.info(f"Removing \"{lli_download_path}\", version: {lli_download_ver}") # noqa: E501 # Remove incompatible file. lli_download_path.unlink() logos_reuse_download( - config.LOGOS_LATEST_VERSION_URL, + app.conf.app_latest_version_url, constants.BINARY_NAME, app.conf.download_dir, app=app, diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index ec94c265..1e88477e 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -268,6 +268,8 @@ class NetworkCache: # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? # in legacy refresh wasn't handled properly wine_appimage_url: Optional[str] = None + app_latest_version_url: Optional[str] = None + app_latest_version: Optional[str] = None # XXX: add @property defs to automatically retrieve if not found @@ -306,7 +308,7 @@ class PersistentConfiguration: # Faithlife's release channel. Either "stable" or "beta" faithlife_product_release_channel: str = "stable" # The Installer's release channel. Either "stable" or "beta" - installer_release_channel: str = "stable" + app_release_channel: str = "stable" @classmethod def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": @@ -347,7 +349,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": faithlife_product_release_channel=legacy.logos_release_channel or 'stable', faithlife_product_version=legacy.TARGETVERSION, install_dir=install_dir, - installer_release_channel=legacy.lli_release_channel or 'stable', + app_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, winetricks_binary=legacy.WINETRICKSBIN, @@ -549,8 +551,8 @@ def faithlife_product_release_channel(self) -> str: return self._raw.faithlife_product_release_channel @property - def installer_release_channel(self) -> str: - return self._raw.installer_release_channel + def app_release_channel(self) -> str: + return self._raw.app_release_channel @property def winetricks_binary(self) -> str: @@ -748,11 +750,11 @@ def toggle_faithlife_product_release_channel(self): self._write() def toggle_installer_release_channel(self): - if self._raw.installer_release_channel == "stable": + if self._raw.app_release_channel == "stable": new_channel = "dev" else: new_channel = "stable" - self._raw.installer_release_channel = new_channel + self._raw.app_release_channel = new_channel self._write() @property @@ -853,4 +855,16 @@ def download_dir(self) -> str: def installed_faithlife_product_release(self) -> Optional[str]: if self._installed_faithlife_product_release is None: self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 - return self._installed_faithlife_product_release \ No newline at end of file + return self._installed_faithlife_product_release + + @property + def app_latest_version_url(self) -> str: + if self._cache.app_latest_version_url is None: + self._cache.app_latest_version_url, self._cache.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + return self._cache.app_latest_version_url + + @property + def app_latest_version(self) -> str: + if self._cache.app_latest_version is None: + self._cache.app_latest_version_url, self._cache.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + return self._cache.app_latest_version \ No newline at end of file diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 9fbedf4f..758d8a4a 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -33,7 +33,7 @@ class TUI(App): def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfiguration): super().__init__(ephemeral_config) self.stdscr = stdscr - self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 # else: # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 @@ -215,7 +215,7 @@ def end(self, signal, frame): def update_main_window_contents(self): self.clear() - self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.installer_release_channel})" # noqa: E501 + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) @@ -402,7 +402,7 @@ def main_menu_select(self, choice): app=self, ) elif choice.startswith(f"Update {constants.APP_NAME}"): - utils.update_to_latest_lli_release() + utils.update_to_latest_lli_release(self) elif choice == f"Run {self.conf.faithlife_product}": self.reset_screen() self.logos.start() @@ -706,19 +706,18 @@ def which_dialog_options(self, labels, dialog=False): def set_tui_menu_options(self, dialog=False): labels = [] - if config.LLI_LATEST_VERSION and system.get_runmode() == 'binary': - status = config.logos_linux_installer_status - error_message = config.logos_linux_installer_status_info.get(status) # noqa: E501 - if status == 0: + if system.get_runmode() == 'binary': + status = utils.compare_logos_linux_installer_version(self) + if status == utils.VersionComparison.OUT_OF_DATE: labels.append(f"Update {constants.APP_NAME}") - elif status == 1: + elif status == utils.VersionComparison.UP_TO_DATE: # logging.debug("Logos Linux Installer is up-to-date.") pass - elif status == 2: + elif status == utils.VersionComparison.DEVELOPMENT: # logging.debug("Logos Linux Installer is newer than the latest release.") # noqa: E501 pass else: - logging.error(f"{error_message}") + logging.error(f"Unknown result: {status}") if self.is_installed(): if self.logos.logos_state in [logos.State.STARTING, logos.State.RUNNING]: # noqa: E501 diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index f25c6622..d89b76ed 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -1,5 +1,6 @@ import atexit from datetime import datetime +import enum import glob import inspect import json @@ -483,39 +484,28 @@ def install_premade_wine_bottle(srcdir, appdir): appdir ) +class VersionComparison(enum.Enum): + OUT_OF_DATE = enum.auto() + UP_TO_DATE = enum.auto() + DEVELOPMENT = enum.auto() -def compare_logos_linux_installer_version( - current=constants.LLI_CURRENT_VERSION, - latest=config.LLI_LATEST_VERSION, -): - # NOTE: The above params evaluate the variables when the module is - # imported. The following re-evaluates when the function is called. - if latest is None: - latest = config.LLI_LATEST_VERSION - # Check if status has already been evaluated. - if config.logos_linux_installer_status is not None: - status = config.logos_linux_installer_status - message = config.logos_linux_installer_status_info.get(status) - return status, message +def compare_logos_linux_installer_version(app: App) -> Optional[VersionComparison]: + current = constants.LLI_CURRENT_VERSION + latest = app.conf.app_latest_version - status = None - message = None - if current is not None and latest is not None: - if version.parse(current) < version.parse(latest): - # Current release is older than recommended. - status = 0 - elif version.parse(current) == version.parse(latest): - # Current release is latest. - status = 1 - elif version.parse(current) > version.parse(latest): - # Installed version is custom. - status = 2 + if version.parse(current) < version.parse(latest): + # Current release is older than recommended. + output = VersionComparison.OUT_OF_DATE + elif version.parse(current) > version.parse(latest): + # Installed version is custom. + output = VersionComparison.DEVELOPMENT + elif version.parse(current) == version.parse(latest): + # Current release is latest. + output = VersionComparison.UP_TO_DATE - config.logos_linux_installer_status = status - message = config.logos_linux_installer_status_info.get(status) - logging.debug(f"LLI self-update check: {status=}; {message=}") - return status, message + logging.debug(f"LLI self-update check: {output=}") + return output def compare_recommended_appimage_version(app: App): @@ -757,16 +747,16 @@ def set_appimage_symlink(app: App): app.root.event_generate("<>") -def update_to_latest_lli_release(app=None): - status, _ = compare_logos_linux_installer_version() +def update_to_latest_lli_release(app: App): + result = compare_logos_linux_installer_version(app) if system.get_runmode() != 'binary': logging.error(f"Can't update {constants.APP_NAME} when run as a script.") - elif status == 0: + elif result == VersionComparison.OUT_OF_DATE: network.update_lli_binary(app=app) - elif status == 1: + elif result == VersionComparison.UP_TO_DATE: logging.debug(f"{constants.APP_NAME} is already at the latest version.") - elif status == 2: + elif result == VersionComparison.DEVELOPMENT: logging.debug(f"{constants.APP_NAME} is at a newer version than the latest.") # noqa: 501 From 13ed0d008e604220a42139d6b51a5c16d546fc76 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:48:55 -0800 Subject: [PATCH 051/137] refactor: move network cache into persistent config automatically invalidate network cache after it expires keep in mind this doesn't run enforce_icu_date_files on update --- ou_dedetai/config.py | 5 --- ou_dedetai/constants.py | 3 ++ ou_dedetai/network.py | 35 --------------- ou_dedetai/new_config.py | 97 ++++++++++++++++++++++------------------ ou_dedetai/wine.py | 2 + 5 files changed, 58 insertions(+), 84 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 74791996..9f4f6463 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -6,11 +6,6 @@ from . import constants -# Define and set variables that are required in the config file. -# XXX: slowly kill these -LAST_UPDATED = None -lli_release_channel = None - # Define and set additional variables that can be set in the env. extended_config = { 'CONFIG_FILE': None, diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index df016a15..76bbdc19 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -11,6 +11,9 @@ # This is relative to this file itself APP_IMAGE_DIR = Path(__file__).parent / "img" +CACHE_LIFETIME_HOURS = 12 +"""How long to wait before considering our version cache invalid""" + # Set other run-time variables not set in the env. DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 DEFAULT_APP_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 02208804..4a131919 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -439,41 +439,6 @@ def get_recommended_appimage_url() -> str: return appimage_url -def check_for_updates(install_dir: Optional[str], force: bool = False): - # We limit the number of times set_recommended_appimage_config is run in - # order to avoid GitHub API limits. This sets the check to once every 12 - # hours. - - utils.write_config(config.CONFIG_FILE) - - # TODO: Check for New Logos Versions. See #116. - - now = datetime.now().replace(microsecond=0) - if force: - check_again = now - elif config.LAST_UPDATED is not None: - check_again = datetime.strptime( - config.LAST_UPDATED.strip(), - '%Y-%m-%dT%H:%M:%S' - ) - check_again += timedelta(hours=12) - else: - check_again = now - - if now >= check_again: - # FIXME: refresh network config cache? - logging.debug("Running self-update.") - - # XXX: can't run this here without a network cache - # utils.compare_logos_linux_installer_version() - # wine.enforce_icu_data_files() - - config.LAST_UPDATED = now.isoformat() - utils.write_config(config.CONFIG_FILE) - else: - logging.debug("Skipping self-update.") - - def get_recommended_appimage(app: App): wine64_appimage_full_filename = Path(app.conf.wine_appimage_recommended_file_name) # noqa: E501 dest_path = Path(app.conf.installer_binary_dir) / wine64_appimage_full_filename diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index 1e88477e..dad74026 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta import json import logging import os @@ -254,25 +254,6 @@ def load_from_path(cls, path: str) -> "EphemeralConfiguration": return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) # noqa: E501 -@dataclass -class NetworkCache: - """Separate class to store values that while they can be retrieved programmatically - it would take additional time or network connectivity. - - This class handles freshness, does whatever conditional logic it needs to determine if it's values are still up to date""" #noqa: E501 - - # XXX: consider storing this and enforce freshness - - # Start cache - _faithlife_product_releases: Optional[list[str]] = None - # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? - # in legacy refresh wasn't handled properly - wine_appimage_url: Optional[str] = None - app_latest_version_url: Optional[str] = None - app_latest_version: Optional[str] = None - - # XXX: add @property defs to automatically retrieve if not found - @dataclass class PersistentConfiguration: """This class stores the options the user chose @@ -310,6 +291,18 @@ class PersistentConfiguration: # The Installer's release channel. Either "stable" or "beta" app_release_channel: str = "stable" + # Start Cache + # Some of these values are cached to avoid github api rate-limits + faithlife_product_releases: Optional[list[str]] = None + # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? + # in legacy refresh wasn't handled properly + wine_appimage_url: Optional[str] = None + app_latest_version_url: Optional[str] = None + app_latest_version: Optional[str] = None + + last_updated: Optional[datetime] = None + # End Cache + @classmethod def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": # XXX: handle legacy migration @@ -341,6 +334,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": install_dir = None if legacy.INSTALLDIR is not None: install_dir = Path(legacy.INSTALLDIR) + faithlife_product_logging = None + if legacy.LOGS is not None: + faithlife_product_logging = utils.parse_bool(legacy.LOGS) return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, backup_dir=backup_dir, @@ -353,7 +349,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, winetricks_binary=legacy.WINETRICKSBIN, - faithlife_product_logging=legacy.LOGS + faithlife_product_logging=faithlife_product_logging ) def write_config(self) -> None: @@ -380,12 +376,6 @@ def write_config(self) -> None: # Continue, the installer can still operate even if it fails to write. -# XXX: Move these into the cache & store -last_updated: Optional[datetime] = None -recommended_wine_url: Optional[str] = None -latest_installer_version: Optional[str] = None - - # Needed this logic outside this class too for before when when the app is initialized def get_wine_prefix_path(install_dir: str) -> str: return f"{install_dir}/data/wine64_bottle" @@ -413,9 +403,6 @@ class Config: # Overriding programmatically generated values from ENV _overrides: EphemeralConfiguration - # Cache, may or may not be stale, freshness logic is stored within - _cache: NetworkCache - # Start Cache of values unlikely to change during operation. # i.e. filesystem traversals _logos_exe: Optional[str] = None @@ -437,6 +424,23 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: self.app: "App" = app self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) self._overrides = ephemeral_config + + # Now check to see if the persistent cache is still valid + if ( + ephemeral_config.check_updates_now + or self._raw.last_updated is None + or self._raw.last_updated + timedelta(hours=constants.CACHE_LIFETIME_HOURS) <= datetime.now() #noqa: E501 + ): + logging.debug("Cleaning out old cache.") + self._raw.faithlife_product_releases = None + self._raw.app_latest_version = None + self._raw.app_latest_version_url = None + self._raw.wine_appimage_url = None + self._raw.last_updated = datetime.now() + self._write() + else: + logging.debug("Cache is valid.") + logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") @@ -505,9 +509,10 @@ def faithlife_product_version(self, value: Optional[str]): @property def faithlife_product_release(self) -> str: question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: " # noqa: E501 - if self._cache._faithlife_product_releases is None: - self._cache._faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 - options = self._cache._faithlife_product_releases + if self._raw.faithlife_product_releases is None: + self._raw.faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 + self._write() + options = self._raw.faithlife_product_releases return self._ask_if_not_found("faithlife_product_release", question, options) @faithlife_product_release.setter @@ -639,6 +644,7 @@ def wine_binary_code(self) -> str: """""" if self._raw.wine_binary_code is None: self._raw.wine_binary_code = utils.get_winebin_code_and_desc(self.app, self.wine_binary)[0] # noqa: E501 + self._write() return self._raw.wine_binary_code @property @@ -683,9 +689,10 @@ def wine_appimage_recommended_url(self) -> str: """URL to recommended appimage. Talks to the network if required""" - if self._cache.wine_appimage_url is None: - self._cache.wine_appimage_url = network.get_recommended_appimage_url() - return self._cache.wine_appimage_url + if self._raw.wine_appimage_url is None: + self._raw.wine_appimage_url = network.get_recommended_appimage_url() + self._write() + return self._raw.wine_appimage_url @property def wine_appimage_recommended_file_name(self) -> str: @@ -719,8 +726,8 @@ def wine_output_encoding(self) -> Optional[str]: if self._overrides.wine_output_encoding is not None: return self._overrides.wine_output_encoding if self._wine_output_encoding is None: - return wine.get_winecmd_encoding(self.app) - return None + self._wine_output_encoding = wine.get_winecmd_encoding(self.app) + return self._wine_output_encoding @property def app_wine_log_path(self) -> str: @@ -859,12 +866,14 @@ def installed_faithlife_product_release(self) -> Optional[str]: @property def app_latest_version_url(self) -> str: - if self._cache.app_latest_version_url is None: - self._cache.app_latest_version_url, self._cache.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - return self._cache.app_latest_version_url + if self._raw.app_latest_version_url is None: + self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + self._write() + return self._raw.app_latest_version_url @property def app_latest_version(self) -> str: - if self._cache.app_latest_version is None: - self._cache.app_latest_version_url, self._cache.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - return self._cache.app_latest_version \ No newline at end of file + if self._raw.app_latest_version is None: + self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + self._write() + return self._raw.app_latest_version diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 33a1a553..86201b8e 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -411,7 +411,9 @@ def set_win_version(app: App, exe: str, windows_version: str): wait_pid(process) +# FIXME: consider when to run this (in the update case) def enforce_icu_data_files(app: App): + # XXX: consider moving the version and url information into config (and cached) repo = "FaithLife-Community/icu" json_data = network.get_latest_release_data(repo) icu_url = network.get_first_asset_url(json_data) From b7147ad2ee8b04f60a06bd93b0ec8e813344de84 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:54:08 -0800 Subject: [PATCH 052/137] refactor: limit temp workdir to scope --- ou_dedetai/config.py | 2 -- ou_dedetai/installer.py | 2 -- ou_dedetai/utils.py | 2 -- ou_dedetai/wine.py | 5 ++++- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 9f4f6463..2f69ecc6 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -19,8 +19,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' SUPERUSER_COMMAND: Optional[str] = None -wine_user = None -WORKDIR = tempfile.mkdtemp(prefix="/tmp/LBS.") console_log = [] processes = {} threads = [] diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index febd3623..3df4a6e9 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -354,8 +354,6 @@ def ensure_winetricks_applied(app: App): if not app.conf.skip_winetricks: usr_reg = None sys_reg = None - workdir = Path(f"{config.WORKDIR}") - workdir.mkdir(parents=True, exist_ok=True) usr_reg = Path(f"{app.conf.wine_prefix}/user.reg") sys_reg = Path(f"{app.conf.wine_prefix}/system.reg") diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index d89b76ed..045e84e5 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -132,8 +132,6 @@ def restart_lli(): def clean_all(): logging.info("Cleaning all temp files…") - os.system("rm -fr /tmp/LBS.*") - os.system(f"rm -fr {config.WORKDIR}") os.system(f"rm -f {os.getcwd()}/wget-log*") logging.info("done") diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 86201b8e..b8c2261f 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -5,6 +5,7 @@ import signal import subprocess from pathlib import Path +import tempfile from typing import Optional from ou_dedetai.app import App @@ -242,13 +243,15 @@ def wine_reg_install(app: App, reg_file, wine64_binary): def disable_winemenubuilder(app: App, wine64_binary: str): - reg_file = Path(config.WORKDIR) / 'disable-winemenubuilder.reg' + workdir = tempfile.mkdtemp() + reg_file = Path(workdir) / 'disable-winemenubuilder.reg' reg_file.write_text(r'''REGEDIT4 [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "winemenubuilder.exe"="" ''') wine_reg_install(app, reg_file, wine64_binary) + os.remove(workdir) def install_msi(app: App): From 40ad90d514c9d4baff60bf17cec54018e697a67e Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:31:36 -0800 Subject: [PATCH 053/137] fix: misc --- ou_dedetai/installer.py | 2 -- ou_dedetai/main.py | 28 ++++++++++++---------------- ou_dedetai/new_config.py | 30 +++++++++++++++++++++--------- ou_dedetai/utils.py | 7 ++++--- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 3df4a6e9..922f6b2d 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -42,8 +42,6 @@ def ensure_release_choice(app: App): app.installer_step += 1 update_install_feedback("Choose product release…", app=app) logging.debug('- config.TARGET_RELEASE_VERSION') - # accessing this sets the config - app.conf.faithlife_product_release logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index f424bbf8..bcfb9057 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -228,8 +228,8 @@ def parse_args(args, parser) -> EphemeralConfiguration: # FIXME: Should this have been args.check_for_updates? # Should this even be an option? - if network.check_for_updates: - ephemeral_config.check_updates_now = True + # if network.check_for_updates: + # ephemeral_config.check_updates_now = True if args.skip_dependencies: ephemeral_config.install_dependencies_skip = True @@ -339,16 +339,16 @@ def setup_config() -> EphemeralConfiguration: # XXX: do this in the new scheme (read then write the config). # We also want to remove the old file, (stored in CONFIG_FILE?) - # Update config from CONFIG_FILE. - if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 - for legacy_config in constants.LEGACY_CONFIG_FILES: - if utils.file_exists(legacy_config): - config.set_config_env(legacy_config) - utils.write_config(config.CONFIG_FILE) - os.remove(legacy_config) - break - else: - config.set_config_env(config.CONFIG_FILE) + # # Update config from CONFIG_FILE. + # if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 + # for legacy_config in constants.LEGACY_CONFIG_FILES: + # if utils.file_exists(legacy_config): + # config.set_config_env(legacy_config) + # utils.write_config(config.CONFIG_FILE) + # os.remove(legacy_config) + # break + # else: + # config.set_config_env(config.CONFIG_FILE) # Parse CLI args and update affected config vars. return parse_args(cli_args, parser) @@ -469,10 +469,6 @@ def main(): check_incompatibilities() - # XXX: Consider how to get the install dir from here, we'd have to read the config...which isn't done yet. - # I suppose we could read the persistent config at this point - network.check_for_updates(None, bool(ephemeral_config.check_updates_now)) - run(ephemeral_config) diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py index dad74026..b7765156 100644 --- a/ou_dedetai/new_config.py +++ b/ou_dedetai/new_config.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from datetime import datetime, timedelta import json import logging import os from pathlib import Path +import time from typing import Optional from ou_dedetai import msg, network, utils, constants, wine @@ -88,6 +88,10 @@ def load(cls) -> "LegacyConfiguration": @classmethod def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": config_dict = {} + + if not Path(config_file_path).exists(): + return LegacyConfiguration(CONFIG_FILE=config_file_path) + if config_file_path.endswith('.json'): try: with open(config_file_path, 'r') as config_file: @@ -123,15 +127,19 @@ def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": value = vparts[0].strip().strip('"').strip("'") config_dict[parts[0]] = value + # Now restrict the key values pairs to just those found in LegacyConfiguration + output = {} # Now update from ENV for var in LegacyConfiguration().__dict__.keys(): if os.getenv(var) is not None: config_dict[var] = os.getenv(var) + if var in config_dict: + output[var] = config_dict[var] # Populate the path this config was loaded from - config_dict["CONFIG_FILE"] = config_file_path + output["CONFIG_FILE"] = config_file_path - return LegacyConfiguration(**config_dict) + return LegacyConfiguration(**output) @dataclass @@ -300,7 +308,7 @@ class PersistentConfiguration: app_latest_version_url: Optional[str] = None app_latest_version: Optional[str] = None - last_updated: Optional[datetime] = None + last_updated: Optional[float] = None # End Cache @classmethod @@ -314,7 +322,7 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": config_dict = new_config.__dict__ - if config_file_path.endswith('.json'): + if config_file_path.endswith('.json') and Path(config_file_path).exists(): with open(config_file_path, 'r') as config_file: cfg = json.load(config_file) @@ -363,6 +371,8 @@ def write_config(self) -> None: if self.install_dir is not None: # Ensure all paths stored are relative to install_dir for k, v in output.items(): + if k == "install_dir": + continue # XXX: test this if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 output[k] = utils.get_relative_path(v, str(self.install_dir)) @@ -429,14 +439,14 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: if ( ephemeral_config.check_updates_now or self._raw.last_updated is None - or self._raw.last_updated + timedelta(hours=constants.CACHE_LIFETIME_HOURS) <= datetime.now() #noqa: E501 + or self._raw.last_updated + constants.CACHE_LIFETIME_HOURS * 60 * 60 <= time.time() #noqa: E501 ): logging.debug("Cleaning out old cache.") self._raw.faithlife_product_releases = None self._raw.app_latest_version = None self._raw.app_latest_version_url = None self._raw.wine_appimage_url = None - self._raw.last_updated = datetime.now() + self._raw.last_updated = time.time() self._write() else: logging.debug("Cache is valid.") @@ -491,11 +501,13 @@ def faithlife_product_version(self) -> str: return self._overrides.faithlife_product_version question = f"Which version of {self.faithlife_product} should the script install?: " # noqa: E501 options = ["10", "9"] - return self._ask_if_not_found("faithlife_product_version", question, options, ["faithlife_product_version"]) # noqa: E501 + return self._ask_if_not_found("faithlife_product_version", question, options, []) # noqa: E501 @faithlife_product_version.setter def faithlife_product_version(self, value: Optional[str]): if self._raw.faithlife_product_version != value: + self._raw.faithlife_product_version = value + # Set dependents self._raw.faithlife_product_release = None # Install Dir has the name of the product and it's version. Reset it too self._raw.install_dir = None @@ -542,7 +554,7 @@ def faithlife_product_logging(self, value: bool): def faithlife_installer_name(self) -> str: if self._overrides.faithlife_installer_name is not None: return self._overrides.faithlife_installer_name - return f"{self.faithlife_product}_v{self.faithlife_product_version}-x64.msi" + return f"{self.faithlife_product}_v{self.faithlife_product_release}-x64.msi" @property def faithlife_installer_download_url(self) -> str: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 045e84e5..19071312 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -632,9 +632,10 @@ def find_appimage_files(app: App): else: logging.info(f"AppImage file {p} not added: {output2}") - if app: - app.appimage_q.put(appimages) - app.root.event_generate(app.appimage_evt) + # FIXME: consider if this messaging is needed + # if app: + # app.appimage_q.put(appimages) + # app.root.event_generate(app.appimage_evt) return appimages From e903d57788868c7838904a6ae693203069b4c322 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:33:20 -0800 Subject: [PATCH 054/137] refactor: move new_config into config.py --- ou_dedetai/config.py | 938 ++++++++++++++++++++++++++++++++++++--- ou_dedetai/main.py | 2 +- ou_dedetai/new_config.py | 891 ------------------------------------- 3 files changed, 883 insertions(+), 948 deletions(-) delete mode 100644 ou_dedetai/new_config.py diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 2f69ecc6..8ca3dd77 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -1,10 +1,16 @@ +import os +from typing import Optional +from dataclasses import dataclass import json import logging import os -import tempfile +from pathlib import Path +import time from typing import Optional -from . import constants +from ou_dedetai import msg, network, utils, constants, wine + +from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY # Define and set additional variables that can be set in the env. extended_config = { @@ -24,63 +30,883 @@ threads = [] -# XXX: remove this -def get_config_file_dict(config_file_path): - config_dict = {} - if config_file_path.endswith('.json'): - try: +# Begin new config + +@dataclass +class LegacyConfiguration: + """Configuration and it's keys from before the user configuration class existed. + + Useful for one directional compatibility""" + # Legacy Core Configuration + FLPRODUCT: Optional[str] = None + TARGETVERSION: Optional[str] = None + TARGET_RELEASE_VERSION: Optional[str] = None + current_logos_version: Optional[str] = None + curses_colors: Optional[str] = None + INSTALLDIR: Optional[str] = None + WINETRICKSBIN: Optional[str] = None + WINEBIN_CODE: Optional[str] = None + WINE_EXE: Optional[str] = None + WINECMD_ENCODING: Optional[str] = None + LOGS: Optional[str] = None + BACKUPDIR: Optional[str] = None + LAST_UPDATED: Optional[str] = None + RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None + LLI_LATEST_VERSION: Optional[str] = None + logos_release_channel: Optional[str] = None + lli_release_channel: Optional[str] = None + + # Legacy Extended Configuration + APPIMAGE_LINK_SELECTION_NAME: Optional[str] = None + APPDIR_BINDIR: Optional[str] = None + CHECK_UPDATES: Optional[bool] = None + CONFIG_FILE: Optional[str] = None + CUSTOMBINPATH: Optional[str] = None + DEBUG: Optional[bool] = None + DELETE_LOG: Optional[str] = None + DIALOG: Optional[str] = None + LOGOS_LOG: Optional[str] = None + wine_log: Optional[str] = None + LOGOS_EXE: Optional[str] = None + # This is the logos installer executable name (NOT path) + LOGOS_EXECUTABLE: Optional[str] = None + LOGOS_VERSION: Optional[str] = None + # This wasn't overridable in the bash version of this installer (at 554c9a6), + # nor was it used in the python version (at 8926435) + # LOGOS64_MSI: Optional[str] + LOGOS64_URL: Optional[str] = None + SELECTED_APPIMAGE_FILENAME: Optional[str] = None + SKIP_DEPENDENCIES: Optional[bool] = None + SKIP_FONTS: Optional[bool] = None + SKIP_WINETRICKS: Optional[bool] = None + use_python_dialog: Optional[str] = None + VERBOSE: Optional[bool] = None + WINEDEBUG: Optional[str] = None + WINEDLLOVERRIDES: Optional[str] = None + WINEPREFIX: Optional[str] = None + WINESERVER_EXE: Optional[str] = None + WINETRICKS_UNATTENDED: Optional[str] = None + + @classmethod + def config_file_path(cls) -> str: + # XXX: consider legacy config files + return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH + + @classmethod + def load(cls) -> "LegacyConfiguration": + """Find the relevant config file and load it""" + # Update config from CONFIG_FILE. + config_file_path = LegacyConfiguration.config_file_path() + if not utils.file_exists(config_file_path): # noqa: E501 + for legacy_config in constants.LEGACY_CONFIG_FILES: + if utils.file_exists(legacy_config): + return LegacyConfiguration.load_from_path(legacy_config) + else: + return LegacyConfiguration.load_from_path(config_file_path) + logging.debug("Couldn't find config file, loading defaults...") + return LegacyConfiguration() + + @classmethod + def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": + config_dict = {} + + if not Path(config_file_path).exists(): + return LegacyConfiguration(CONFIG_FILE=config_file_path) + + if config_file_path.endswith('.json'): + try: + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) + + for key, value in cfg.items(): + config_dict[key] = value + except TypeError as e: + logging.error("Error opening Config file.") + logging.error(e) + raise e + except FileNotFoundError: + logging.info(f"No config file not found at {config_file_path}") + except json.JSONDecodeError as e: + logging.error("Config file could not be read.") + logging.error(e) + raise e + elif config_file_path.endswith('.conf'): + # Legacy config from bash script. + logging.info("Reading from legacy config file.") + with open(config_file_path, 'r') as config_file: + for line in config_file: + line = line.strip() + if len(line) == 0: # skip blank lines + continue + if line[0] == '#': # skip commented lines + continue + parts = line.split('=') + if len(parts) == 2: + value = parts[1].strip('"').strip("'") # remove quotes + vparts = value.split('#') # get rid of potential comment + if len(vparts) > 1: + value = vparts[0].strip().strip('"').strip("'") + config_dict[parts[0]] = value + + # Now restrict the key values pairs to just those found in LegacyConfiguration + output = {} + # Now update from ENV + for var in LegacyConfiguration().__dict__.keys(): + if os.getenv(var) is not None: + config_dict[var] = os.getenv(var) + if var in config_dict: + output[var] = config_dict[var] + + # Populate the path this config was loaded from + output["CONFIG_FILE"] = config_file_path + + return LegacyConfiguration(**output) + + +@dataclass +class EphemeralConfiguration: + """A set of overrides that don't need to be stored. + + Populated from environment/command arguments/etc + + Changes to this are not saved to disk, but remain while the program runs + """ + + # See naming conventions in Config + + # Start user overridable via env or cli arg + installer_binary_dir: Optional[str] + wineserver_binary: Optional[str] + faithlife_product_version: Optional[str] + faithlife_installer_name: Optional[str] + faithlife_installer_download_url: Optional[str] + log_level: Optional[str | int] + app_log_path: Optional[str] + app_wine_log_path: Optional[str] + """Path to log wine's output to""" + app_winetricks_unattended: Optional[bool] + """Whether or not to send -q to winetricks for all winetricks commands. + + Some commands always send -q""" + + winetricks_skip: Optional[bool] + install_dependencies_skip: Optional[bool] + """Whether to skip installing system package dependencies""" + install_fonts_skip: Optional[bool] + """Whether to skip installing fonts in the wineprefix""" + + wine_dll_overrides: Optional[str] + """Corresponds to wine's WINEDLLOVERRIDES""" + wine_debug: Optional[str] + """Corresponds to wine's WINEDEBUG""" + wine_prefix: Optional[str] + """Corresponds to wine's WINEPREFIX""" + wine_output_encoding: Optional[str] + """Override for what encoding wine's output is using""" + + # FIXME: seems like the wine appimage logic can be simplified + wine_appimage_link_file_name: Optional[str] + """Syslink file name to the active wine appimage.""" + + wine_appimage_path: Optional[str] + """Path to the selected appimage""" + + # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) # noqa: E501 + custom_binary_path: Optional[str] + """Additional path to look for when searching for binaries.""" + + delete_log: Optional[bool] + """Whether to clear the log on startup""" + + check_updates_now: Optional[bool] + """Whether or not to check updates regardless of if one's due""" + + # Start internal values + config_path: str + """Path this config was loaded from""" + + # Start of values just set via cli arg + faithlife_install_passive: bool = False + app_run_as_root_permitted: bool = False + + @classmethod + def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": + log_level = None + wine_debug = legacy.WINEDEBUG + if legacy.DEBUG: + log_level = logging.DEBUG + # FIXME: shouldn't this leave it untouched or fall back to default: `fixme-all,err-all`? # noqa: E501 + wine_debug = "" + elif legacy.VERBOSE: + log_level = logging.INFO + wine_debug = "" + app_winetricks_unattended = None + if legacy.WINETRICKS_UNATTENDED is not None: + app_winetricks_unattended = utils.parse_bool(legacy.WINETRICKS_UNATTENDED) + delete_log = None + if legacy.DELETE_LOG is not None: + delete_log = utils.parse_bool(legacy.DELETE_LOG) + config_file = constants.DEFAULT_CONFIG_PATH + if legacy.CONFIG_FILE is not None: + config_file = legacy.CONFIG_FILE + return EphemeralConfiguration( + installer_binary_dir=legacy.APPDIR_BINDIR, + wineserver_binary=legacy.WINESERVER_EXE, + custom_binary_path=legacy.CUSTOMBINPATH, + faithlife_product_version=legacy.LOGOS_VERSION, + faithlife_installer_name=legacy.LOGOS_EXECUTABLE, + faithlife_installer_download_url=legacy.LOGOS64_URL, + winetricks_skip=legacy.SKIP_WINETRICKS, + log_level=log_level, + wine_debug=wine_debug, + wine_dll_overrides=legacy.WINEDLLOVERRIDES, + wine_prefix=legacy.WINEPREFIX, + app_wine_log_path=legacy.wine_log, + app_log_path=legacy.LOGOS_LOG, + app_winetricks_unattended=app_winetricks_unattended, + config_path=config_file, + check_updates_now=legacy.CHECK_UPDATES, + delete_log=delete_log, + install_dependencies_skip=legacy.SKIP_DEPENDENCIES, + install_fonts_skip=legacy.SKIP_FONTS, + wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, + wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, + wine_output_encoding=legacy.WINECMD_ENCODING + ) + + @classmethod + def load(cls) -> "EphemeralConfiguration": + return EphemeralConfiguration.from_legacy(LegacyConfiguration.load()) + + @classmethod + def load_from_path(cls, path: str) -> "EphemeralConfiguration": + return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) # noqa: E501 + + +@dataclass +class PersistentConfiguration: + """This class stores the options the user chose + + Normally shouldn't be used directly, as it's types may be None, + doesn't handle updates. Use through the `App`'s `Config` instead. + + Easy reading to/from JSON and supports legacy keys + + These values should be stored across invocations + + MUST be saved explicitly + """ + + # See naming conventions in Config + + # XXX: store a version in this config? + # Just in case we need to do conditional logic reading old version's configurations + + faithlife_product: Optional[str] = None + faithlife_product_version: Optional[str] = None + faithlife_product_release: Optional[str] = None + faithlife_product_logging: Optional[bool] = None + install_dir: Optional[Path] = None + winetricks_binary: Optional[str] = None + wine_binary: Optional[str] = None + # This is where to search for wine + wine_binary_code: Optional[str] = None + backup_dir: Optional[Path] = None + + # Color to use in curses. Either "Logos", "Light", or "Dark" + curses_colors: str = "Logos" + # Faithlife's release channel. Either "stable" or "beta" + faithlife_product_release_channel: str = "stable" + # The Installer's release channel. Either "stable" or "beta" + app_release_channel: str = "stable" + + # Start Cache + # Some of these values are cached to avoid github api rate-limits + faithlife_product_releases: Optional[list[str]] = None + # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? + # in legacy refresh wasn't handled properly + wine_appimage_url: Optional[str] = None + app_latest_version_url: Optional[str] = None + app_latest_version: Optional[str] = None + + last_updated: Optional[float] = None + # End Cache + + @classmethod + def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": + # XXX: handle legacy migration + + # First read in the legacy configuration + new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) #noqa: E501 + + new_keys = new_config.__dict__.keys() + + config_dict = new_config.__dict__ + + if config_file_path.endswith('.json') and Path(config_file_path).exists(): with open(config_file_path, 'r') as config_file: cfg = json.load(config_file) for key, value in cfg.items(): - config_dict[key] = value - return config_dict - except TypeError as e: - logging.error("Error opening Config file.") - logging.error(e) - return None - except FileNotFoundError: - logging.info(f"No config file not found at {config_file_path}") - return config_dict - except json.JSONDecodeError as e: - logging.error("Config file could not be read.") - logging.error(e) - return None - elif config_file_path.endswith('.conf'): - # Legacy config from bash script. - logging.info("Reading from legacy config file.") - with open(config_file_path, 'r') as config_file: - for line in config_file: - line = line.strip() - if len(line) == 0: # skip blank lines - continue - if line[0] == '#': # skip commented lines + if key in new_keys: + config_dict[key] = value + else: + logging.info("Not reading new values from non-json config") + + return PersistentConfiguration(**config_dict) + + @classmethod + def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": + backup_dir = None + if legacy.BACKUPDIR is not None: + backup_dir = Path(legacy.BACKUPDIR) + install_dir = None + if legacy.INSTALLDIR is not None: + install_dir = Path(legacy.INSTALLDIR) + faithlife_product_logging = None + if legacy.LOGS is not None: + faithlife_product_logging = utils.parse_bool(legacy.LOGS) + return PersistentConfiguration( + faithlife_product=legacy.FLPRODUCT, + backup_dir=backup_dir, + curses_colors=legacy.curses_colors or 'Logos', + faithlife_product_release=legacy.TARGET_RELEASE_VERSION, + faithlife_product_release_channel=legacy.logos_release_channel or 'stable', + faithlife_product_version=legacy.TARGETVERSION, + install_dir=install_dir, + app_release_channel=legacy.lli_release_channel or 'stable', + wine_binary=legacy.WINE_EXE, + wine_binary_code=legacy.WINEBIN_CODE, + winetricks_binary=legacy.WINETRICKSBIN, + faithlife_product_logging=faithlife_product_logging + ) + + def write_config(self) -> None: + config_file_path = LegacyConfiguration.config_file_path() + # XXX: we may need to merge this dict with the legacy configuration's extended config (as we don't store that persistently anymore) #noqa: E501 + output = self.__dict__ + + logging.info(f"Writing config to {config_file_path}") + os.makedirs(os.path.dirname(config_file_path), exist_ok=True) + + if self.install_dir is not None: + # Ensure all paths stored are relative to install_dir + for k, v in output.items(): + if k == "install_dir": continue - parts = line.split('=') - if len(parts) == 2: - value = parts[1].strip('"').strip("'") # remove quotes - vparts = value.split('#') # get rid of potential comment - if len(vparts) > 1: - value = vparts[0].strip().strip('"').strip("'") - config_dict[parts[0]] = value - return config_dict - - -# XXX: remove this -def set_config_env(config_file_path): - config_dict = get_config_file_dict(config_file_path) - if config_dict is None: - return - # msg.logos_error(f"Error: Unable to get config at {config_file_path}") - logging.info(f"Setting {len(config_dict)} variables from config file.") - for key, value in config_dict.items(): - globals()[key] = value - -# XXX: remove this -def get_env_config(): - for var in globals().keys(): - val = os.getenv(var) - if val is not None: - logging.info(f"Setting '{var}' to '{val}'") - globals()[var] = val + # XXX: test this + if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 + output[k] = utils.get_relative_path(v, str(self.install_dir)) + + try: + with open(config_file_path, 'w') as config_file: + json.dump(output, config_file, indent=4, sort_keys=True) + config_file.write('\n') + except IOError as e: + msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + # Continue, the installer can still operate even if it fails to write. + + +# Needed this logic outside this class too for before when when the app is initialized +def get_wine_prefix_path(install_dir: str) -> str: + return f"{install_dir}/data/wine64_bottle" + +class Config: + """Set of configuration values. + + If the user hasn't selected a particular value yet, they will be prompted in the UI. + """ + + # Naming conventions: + # Use `dir` instead of `directory` + # Use snake_case + # prefix with faithlife_ if it's theirs + # 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 _path if it's a file path + # suffix with _file_name if it's a file's name (with extension) + + # Storage for the keys + _raw: PersistentConfiguration + + # Overriding programmatically generated values from ENV + _overrides: EphemeralConfiguration + + # Start Cache of values unlikely to change during operation. + # i.e. filesystem traversals + _logos_exe: Optional[str] = None + _download_dir: Optional[str] = None + _wine_output_encoding: Optional[str] = None + _installed_faithlife_product_release: Optional[str] = None + + # Start constants + _curses_colors_valid_values = ["Light", "Dark", "Logos"] + + # Singleton logic, this enforces that only one config object exists at a time. + def __new__(cls, *args, **kwargs) -> "Config": + if not hasattr(cls, '_instance'): + cls._instance = super(Config, cls).__new__(cls) + return cls._instance + + def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: + from ou_dedetai.app import App + self.app: "App" = app + self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) + self._overrides = ephemeral_config + + # Now check to see if the persistent cache is still valid + if ( + ephemeral_config.check_updates_now + or self._raw.last_updated is None + or self._raw.last_updated + constants.CACHE_LIFETIME_HOURS * 60 * 60 <= time.time() #noqa: E501 + ): + logging.debug("Cleaning out old cache.") + self._raw.faithlife_product_releases = None + self._raw.app_latest_version = None + self._raw.app_latest_version_url = None + self._raw.wine_appimage_url = None + self._raw.last_updated = time.time() + self._write() + else: + logging.debug("Cache is valid.") + + logging.debug("Current persistent config:") + for k, v in self._raw.__dict__.items(): + logging.debug(f"{k}: {v}") + + def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 + # XXX: should this also update the feedback? + if not getattr(self._raw, parameter): + if dependent_parameters is not None: + for dependent_config_key in dependent_parameters: + setattr(self._raw, dependent_config_key, None) + answer = self.app.ask(question, options) + # Use the setter on this class if found, otherwise set in self._user + if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: # noqa: E501 + getattr(Config, parameter).fset(self, answer) + else: + setattr(self._raw, parameter, answer) + self._write() + # parameter given should be a string + return str(getattr(self._raw, parameter)) + + def _write(self) -> None: + """Writes configuration to file and lets the app know something changed""" + self._raw.write_config() + self.app._config_updated() + + @property + def config_file_path(self) -> str: + return LegacyConfiguration.config_file_path() + + @property + def faithlife_product(self) -> str: + question = "Choose which FaithLife product the script should install: " # noqa: E501 + options = ["Logos", "Verbum"] + return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) # noqa: E501 + + @faithlife_product.setter + def faithlife_product(self, value: Optional[str]): + if self._raw.faithlife_product != value: + self._raw.faithlife_product = value + # Reset dependent variables + self._raw.faithlife_product_release = None + + self._write() + + @property + def faithlife_product_version(self) -> str: + if self._overrides.faithlife_product_version is not None: + return self._overrides.faithlife_product_version + question = f"Which version of {self.faithlife_product} should the script install?: " # noqa: E501 + options = ["10", "9"] + return self._ask_if_not_found("faithlife_product_version", question, options, []) # noqa: E501 + + @faithlife_product_version.setter + def faithlife_product_version(self, value: Optional[str]): + if self._raw.faithlife_product_version != value: + self._raw.faithlife_product_version = value + # Set dependents + self._raw.faithlife_product_release = None + # Install Dir has the name of the product and it's version. Reset it too + self._raw.install_dir = None + # Wine is dependent on the product/version selected + self._raw.wine_binary = None + self._raw.wine_binary_code = None + self._raw.winetricks_binary = None + + self._write() + + @property + def faithlife_product_release(self) -> str: + question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: " # noqa: E501 + if self._raw.faithlife_product_releases is None: + self._raw.faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 + self._write() + options = self._raw.faithlife_product_releases + return self._ask_if_not_found("faithlife_product_release", question, options) + + @faithlife_product_release.setter + def faithlife_product_release(self, value: str): + if self._raw.faithlife_product_release != value: + self._raw.faithlife_product_release = value + self._write() + + @property + def faithlife_product_icon_path(self) -> str: + return str(constants.APP_IMAGE_DIR / f"{self.faithlife_product}-128-icon.png") + + @property + def faithlife_product_logging(self) -> bool: + """Whether or not the installed faithlife product is configured to log""" + if self._raw.faithlife_product_logging is not None: + return self._raw.faithlife_product_logging + return False + + @faithlife_product_logging.setter + def faithlife_product_logging(self, value: bool): + if self._raw.faithlife_product_logging != value: + self._raw.faithlife_product_logging = value + self._write() + + @property + def faithlife_installer_name(self) -> str: + if self._overrides.faithlife_installer_name is not None: + return self._overrides.faithlife_installer_name + return f"{self.faithlife_product}_v{self.faithlife_product_release}-x64.msi" + + @property + def faithlife_installer_download_url(self) -> str: + if self._overrides.faithlife_installer_download_url is not None: + return self._overrides.faithlife_installer_download_url + after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" # noqa: E501 + return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 + + @property + def faithlife_product_release_channel(self) -> str: + return self._raw.faithlife_product_release_channel + + @property + def app_release_channel(self) -> str: + return self._raw.app_release_channel + + @property + def winetricks_binary(self) -> str: + """This may be a path to the winetricks binary or it may be "Download" + """ + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 + options = utils.get_winetricks_options() + return self._ask_if_not_found("winetricks_binary", question, options) + + @winetricks_binary.setter + def winetricks_binary(self, value: Optional[str | Path]): + if value is not None: + value = str(value) + if value is not None and value != "Download": + if not Path(value).exists(): + raise ValueError("Winetricks binary must exist") + if self._raw.winetricks_binary != value: + self._raw.winetricks_binary = value + self._write() + + @property + def install_dir(self) -> str: + default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 + question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 + options = [default, PROMPT_OPTION_DIRECTORY] + output = self._ask_if_not_found("install_dir", question, options) + return output + + @property + # This used to be called APPDIR_BINDIR + def installer_binary_dir(self) -> str: + if self._overrides.installer_binary_dir is not None: + return self._overrides.installer_binary_dir + return f"{self.install_dir}/data/bin" + + @property + # This used to be called WINEPREFIX + def wine_prefix(self) -> str: + if self._overrides.wine_prefix is not None: + return self._overrides.wine_prefix + return get_wine_prefix_path(self.install_dir) + + @property + def wine_binary(self) -> str: + """Returns absolute path to the wine binary""" + output = self._raw.wine_binary + if output is None: + question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: " # noqa: E501 + options = utils.get_wine_options( + self.app, + utils.find_appimage_files(self.app), + utils.find_wine_binary_files(self.app, self.faithlife_product_release) + ) + + choice = self.app.ask(question, options) + + output = choice + self.wine_binary = choice + # Return the full path so we the callee doesn't need to think about it + if self._raw.wine_binary is not None and not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): # noqa: E501 + return str(Path(self.install_dir) / self._raw.wine_binary) + return output + + @wine_binary.setter + def wine_binary(self, value: str): + """Takes in a path to the wine binary and stores it as relative for storage""" + # XXX: change the logic to make ^ true + if (Path(self.install_dir) / value).exists(): + value = str((Path(self.install_dir) / Path(value)).absolute()) + if not Path(value).is_file(): + raise ValueError("Wine Binary path must be a valid file") + + if self._raw.wine_binary != value: + if value is not None: + value = str(Path(value).absolute()) + self._raw.wine_binary = value + # Reset dependents + self._raw.wine_binary_code = None + self._overrides.wine_appimage_path = None + self._write() + + @property + def wine_binary_code(self) -> str: + """""" + if self._raw.wine_binary_code is None: + self._raw.wine_binary_code = utils.get_winebin_code_and_desc(self.app, self.wine_binary)[0] # noqa: E501 + self._write() + return self._raw.wine_binary_code + + @property + def wine64_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wine64') + + @property + # This used to be called WINESERVER_EXE + def wineserver_binary(self) -> str: + return str(Path(self.wine_binary).parent / 'wineserver') + + # FIXME: seems like the logic around wine appimages can be simplified + # Should this be folded into wine_binary? + @property + def wine_appimage_path(self) -> Optional[str]: + """Path to the wine appimage + + Returns: + Path if wine is set to use an appimage, otherwise returns None""" + if self._overrides.wine_appimage_path is not None: + return self._overrides.wine_appimage_path + if self.wine_binary.lower().endswith("appimage"): + return self.wine_binary + return None + + @wine_appimage_path.setter + def wine_appimage_path(self, value: Optional[str]): + if self._overrides.wine_appimage_path != value: + self._overrides.wine_appimage_path = value + # Reset dependents + self._raw.wine_binary_code = None + # XXX: Should we save? There should be something here we should store + + @property + def wine_appimage_link_file_name(self) -> str: + if self._overrides.wine_appimage_link_file_name is not None: + return self._overrides.wine_appimage_link_file_name + return 'selected_wine.AppImage' + + @property + def wine_appimage_recommended_url(self) -> str: + """URL to recommended appimage. + + Talks to the network if required""" + if self._raw.wine_appimage_url is None: + self._raw.wine_appimage_url = network.get_recommended_appimage_url() + self._write() + return self._raw.wine_appimage_url + + @property + def wine_appimage_recommended_file_name(self) -> str: + """Returns the file name of the recommended appimage with extension""" + return os.path.basename(self.wine_appimage_recommended_url) + + @property + def wine_appimage_recommended_version(self) -> str: + # Getting version and branch rely on the filename having this format: + # wine-[branch]_[version]-[arch] + return self.wine_appimage_recommended_file_name.split('-')[1].split('_')[1] + + @property + def wine_dll_overrides(self) -> str: + """Used to set WINEDLLOVERRIDES""" + if self._overrides.wine_dll_overrides is not None: + return self._overrides.wine_dll_overrides + # Default is no overrides + return '' + + @property + def wine_debug(self) -> str: + """Used to set WINEDEBUG""" + if self._overrides.wine_debug is not None: + return self._overrides.wine_debug + return constants.DEFAULT_WINEDEBUG + + @property + def wine_output_encoding(self) -> Optional[str]: + """Attempt to guess the encoding of the wine output""" + if self._overrides.wine_output_encoding is not None: + return self._overrides.wine_output_encoding + if self._wine_output_encoding is None: + self._wine_output_encoding = wine.get_winecmd_encoding(self.app) + return self._wine_output_encoding + + @property + def app_wine_log_path(self) -> str: + if self._overrides.app_wine_log_path is not None: + return self._overrides.app_wine_log_path + return constants.DEFAULT_APP_WINE_LOG_PATH + + @property + def app_log_path(self) -> str: + if self._overrides.app_log_path is not None: + return self._overrides.app_log_path + return constants.DEFAULT_APP_LOG_PATH + + @property + def app_winetricks_unattended(self) -> bool: + """If true, pass -q to winetricks""" + if self._overrides.app_winetricks_unattended is not None: + return self._overrides.app_winetricks_unattended + return False + + def toggle_faithlife_product_release_channel(self): + if self._raw.faithlife_product_release_channel == "stable": + new_channel = "beta" + else: + new_channel = "stable" + self._raw.faithlife_product_release_channel = new_channel + self._write() + + def toggle_installer_release_channel(self): + if self._raw.app_release_channel == "stable": + new_channel = "dev" + else: + new_channel = "stable" + self._raw.app_release_channel = new_channel + self._write() + + @property + def backup_dir(self) -> Path: + question = "New or existing folder to store backups in: " + options = [PROMPT_OPTION_DIRECTORY] + output = Path(self._ask_if_not_found("backup_dir", question, options)) + output.mkdir(parents=True) + return output + + @property + def curses_colors(self) -> str: + """Color for the curses dialog + + returns one of: Logos, Light or Dark""" + return self._raw.curses_colors + + @curses_colors.setter + def curses_colors(self, value: str): + if value not in self._curses_colors_valid_values: + raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") # noqa: E501 + self._raw.curses_colors = value + self._write() + + def cycle_curses_color_scheme(self): + new_index = self._curses_colors_valid_values.index(self.curses_colors) + 1 + if new_index == len(self._curses_colors_valid_values): + new_index = 0 + self.curses_colors = self._curses_colors_valid_values[new_index] + + @property + def logos_exe(self) -> Optional[str]: + # Cache a successful result + if self._logos_exe is None: + self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) # noqa: E501 + return self._logos_exe + + @property + def wine_user(self) -> Optional[str]: + path: Optional[str] = self.logos_exe + if path is None: + return None + normalized_path: str = os.path.normpath(path) + path_parts = normalized_path.split(os.sep) + return path_parts[path_parts.index('users') + 1] + + @property + def logos_cef_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 + return None + + @property + def logos_indexer_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 + return None + + @property + def logos_login_exe(self) -> Optional[str]: + if self.wine_user is not None: + return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 + return None + + @property + def log_level(self) -> str | int: + if self._overrides.log_level is not None: + return self._overrides.log_level + return constants.DEFAULT_LOG_LEVEL + + @property + def skip_winetricks(self) -> bool: + return bool(self._overrides.winetricks_skip) + + @property + def skip_install_system_dependencies(self) -> bool: + return bool(self._overrides.install_dependencies_skip) + + @skip_install_system_dependencies.setter + def skip_install_system_dependencies(self, val: bool): + self._overrides.install_dependencies_skip = val + + @property + def skip_install_fonts(self) -> bool: + return bool(self._overrides.install_fonts_skip) + + @skip_install_fonts.setter + def skip_install_fonts(self, val: bool): + self._overrides.install_fonts_skip = val + + @property + def download_dir(self) -> str: + if self._download_dir is None: + self._download_dir = utils.get_user_downloads_dir() + return self._download_dir + + @property + def installed_faithlife_product_release(self) -> Optional[str]: + if self._installed_faithlife_product_release is None: + self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 + return self._installed_faithlife_product_release + + @property + def app_latest_version_url(self) -> str: + if self._raw.app_latest_version_url is None: + self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + self._write() + return self._raw.app_latest_version_url + @property + def app_latest_version(self) -> str: + if self._raw.app_latest_version is None: + self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 + self._write() + return self._raw.app_latest_version diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index bcfb9057..afde5275 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -2,7 +2,7 @@ import argparse import curses -from ou_dedetai.new_config import EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path +from ou_dedetai.config import EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path try: import dialog # noqa: F401 diff --git a/ou_dedetai/new_config.py b/ou_dedetai/new_config.py deleted file mode 100644 index b7765156..00000000 --- a/ou_dedetai/new_config.py +++ /dev/null @@ -1,891 +0,0 @@ -from dataclasses import dataclass -import json -import logging -import os -from pathlib import Path -import time -from typing import Optional - -from ou_dedetai import msg, network, utils, constants, wine - -from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY - -# XXX: move these configs into config.py once it's cleared out -@dataclass -class LegacyConfiguration: - """Configuration and it's keys from before the user configuration class existed. - - Useful for one directional compatibility""" - # Legacy Core Configuration - FLPRODUCT: Optional[str] = None - TARGETVERSION: Optional[str] = None - TARGET_RELEASE_VERSION: Optional[str] = None - current_logos_version: Optional[str] = None - curses_colors: Optional[str] = None - INSTALLDIR: Optional[str] = None - WINETRICKSBIN: Optional[str] = None - WINEBIN_CODE: Optional[str] = None - WINE_EXE: Optional[str] = None - WINECMD_ENCODING: Optional[str] = None - LOGS: Optional[str] = None - BACKUPDIR: Optional[str] = None - LAST_UPDATED: Optional[str] = None - RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None - LLI_LATEST_VERSION: Optional[str] = None - logos_release_channel: Optional[str] = None - lli_release_channel: Optional[str] = None - - # Legacy Extended Configuration - APPIMAGE_LINK_SELECTION_NAME: Optional[str] = None - APPDIR_BINDIR: Optional[str] = None - CHECK_UPDATES: Optional[bool] = None - CONFIG_FILE: Optional[str] = None - CUSTOMBINPATH: Optional[str] = None - DEBUG: Optional[bool] = None - DELETE_LOG: Optional[str] = None - DIALOG: Optional[str] = None - LOGOS_LOG: Optional[str] = None - wine_log: Optional[str] = None - LOGOS_EXE: Optional[str] = None - # This is the logos installer executable name (NOT path) - LOGOS_EXECUTABLE: Optional[str] = None - LOGOS_VERSION: Optional[str] = None - # This wasn't overridable in the bash version of this installer (at 554c9a6), - # nor was it used in the python version (at 8926435) - # LOGOS64_MSI: Optional[str] - LOGOS64_URL: Optional[str] = None - SELECTED_APPIMAGE_FILENAME: Optional[str] = None - SKIP_DEPENDENCIES: Optional[bool] = None - SKIP_FONTS: Optional[bool] = None - SKIP_WINETRICKS: Optional[bool] = None - use_python_dialog: Optional[str] = None - VERBOSE: Optional[bool] = None - WINEDEBUG: Optional[str] = None - WINEDLLOVERRIDES: Optional[str] = None - WINEPREFIX: Optional[str] = None - WINESERVER_EXE: Optional[str] = None - WINETRICKS_UNATTENDED: Optional[str] = None - - @classmethod - def config_file_path(cls) -> str: - # XXX: consider legacy config files - return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH - - @classmethod - def load(cls) -> "LegacyConfiguration": - """Find the relevant config file and load it""" - # Update config from CONFIG_FILE. - config_file_path = LegacyConfiguration.config_file_path() - if not utils.file_exists(config_file_path): # noqa: E501 - for legacy_config in constants.LEGACY_CONFIG_FILES: - if utils.file_exists(legacy_config): - return LegacyConfiguration.load_from_path(legacy_config) - else: - return LegacyConfiguration.load_from_path(config_file_path) - logging.debug("Couldn't find config file, loading defaults...") - return LegacyConfiguration() - - @classmethod - def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": - config_dict = {} - - if not Path(config_file_path).exists(): - return LegacyConfiguration(CONFIG_FILE=config_file_path) - - if config_file_path.endswith('.json'): - try: - with open(config_file_path, 'r') as config_file: - cfg = json.load(config_file) - - for key, value in cfg.items(): - config_dict[key] = value - except TypeError as e: - logging.error("Error opening Config file.") - logging.error(e) - raise e - except FileNotFoundError: - logging.info(f"No config file not found at {config_file_path}") - except json.JSONDecodeError as e: - logging.error("Config file could not be read.") - logging.error(e) - raise e - elif config_file_path.endswith('.conf'): - # Legacy config from bash script. - logging.info("Reading from legacy config file.") - with open(config_file_path, 'r') as config_file: - for line in config_file: - line = line.strip() - if len(line) == 0: # skip blank lines - continue - if line[0] == '#': # skip commented lines - continue - parts = line.split('=') - if len(parts) == 2: - value = parts[1].strip('"').strip("'") # remove quotes - vparts = value.split('#') # get rid of potential comment - if len(vparts) > 1: - value = vparts[0].strip().strip('"').strip("'") - config_dict[parts[0]] = value - - # Now restrict the key values pairs to just those found in LegacyConfiguration - output = {} - # Now update from ENV - for var in LegacyConfiguration().__dict__.keys(): - if os.getenv(var) is not None: - config_dict[var] = os.getenv(var) - if var in config_dict: - output[var] = config_dict[var] - - # Populate the path this config was loaded from - output["CONFIG_FILE"] = config_file_path - - return LegacyConfiguration(**output) - - -@dataclass -class EphemeralConfiguration: - """A set of overrides that don't need to be stored. - - Populated from environment/command arguments/etc - - Changes to this are not saved to disk, but remain while the program runs - """ - - # See naming conventions in Config - - # Start user overridable via env or cli arg - installer_binary_dir: Optional[str] - wineserver_binary: Optional[str] - faithlife_product_version: Optional[str] - faithlife_installer_name: Optional[str] - faithlife_installer_download_url: Optional[str] - log_level: Optional[str | int] - app_log_path: Optional[str] - app_wine_log_path: Optional[str] - """Path to log wine's output to""" - app_winetricks_unattended: Optional[bool] - """Whether or not to send -q to winetricks for all winetricks commands. - - Some commands always send -q""" - - winetricks_skip: Optional[bool] - install_dependencies_skip: Optional[bool] - """Whether to skip installing system package dependencies""" - install_fonts_skip: Optional[bool] - """Whether to skip installing fonts in the wineprefix""" - - wine_dll_overrides: Optional[str] - """Corresponds to wine's WINEDLLOVERRIDES""" - wine_debug: Optional[str] - """Corresponds to wine's WINEDEBUG""" - wine_prefix: Optional[str] - """Corresponds to wine's WINEPREFIX""" - wine_output_encoding: Optional[str] - """Override for what encoding wine's output is using""" - - # FIXME: seems like the wine appimage logic can be simplified - wine_appimage_link_file_name: Optional[str] - """Syslink file name to the active wine appimage.""" - - wine_appimage_path: Optional[str] - """Path to the selected appimage""" - - # FIXME: consider using PATH instead? (and storing this legacy env in PATH for this process) # noqa: E501 - custom_binary_path: Optional[str] - """Additional path to look for when searching for binaries.""" - - delete_log: Optional[bool] - """Whether to clear the log on startup""" - - check_updates_now: Optional[bool] - """Whether or not to check updates regardless of if one's due""" - - # Start internal values - config_path: str - """Path this config was loaded from""" - - # Start of values just set via cli arg - faithlife_install_passive: bool = False - app_run_as_root_permitted: bool = False - - @classmethod - def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": - log_level = None - wine_debug = legacy.WINEDEBUG - if legacy.DEBUG: - log_level = logging.DEBUG - # FIXME: shouldn't this leave it untouched or fall back to default: `fixme-all,err-all`? # noqa: E501 - wine_debug = "" - elif legacy.VERBOSE: - log_level = logging.INFO - wine_debug = "" - app_winetricks_unattended = None - if legacy.WINETRICKS_UNATTENDED is not None: - app_winetricks_unattended = utils.parse_bool(legacy.WINETRICKS_UNATTENDED) - delete_log = None - if legacy.DELETE_LOG is not None: - delete_log = utils.parse_bool(legacy.DELETE_LOG) - config_file = constants.DEFAULT_CONFIG_PATH - if legacy.CONFIG_FILE is not None: - config_file = legacy.CONFIG_FILE - return EphemeralConfiguration( - installer_binary_dir=legacy.APPDIR_BINDIR, - wineserver_binary=legacy.WINESERVER_EXE, - custom_binary_path=legacy.CUSTOMBINPATH, - faithlife_product_version=legacy.LOGOS_VERSION, - faithlife_installer_name=legacy.LOGOS_EXECUTABLE, - faithlife_installer_download_url=legacy.LOGOS64_URL, - winetricks_skip=legacy.SKIP_WINETRICKS, - log_level=log_level, - wine_debug=wine_debug, - wine_dll_overrides=legacy.WINEDLLOVERRIDES, - wine_prefix=legacy.WINEPREFIX, - app_wine_log_path=legacy.wine_log, - app_log_path=legacy.LOGOS_LOG, - app_winetricks_unattended=app_winetricks_unattended, - config_path=config_file, - check_updates_now=legacy.CHECK_UPDATES, - delete_log=delete_log, - install_dependencies_skip=legacy.SKIP_DEPENDENCIES, - install_fonts_skip=legacy.SKIP_FONTS, - wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, - wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, - wine_output_encoding=legacy.WINECMD_ENCODING - ) - - @classmethod - def load(cls) -> "EphemeralConfiguration": - return EphemeralConfiguration.from_legacy(LegacyConfiguration.load()) - - @classmethod - def load_from_path(cls, path: str) -> "EphemeralConfiguration": - return EphemeralConfiguration.from_legacy(LegacyConfiguration.load_from_path(path)) # noqa: E501 - - -@dataclass -class PersistentConfiguration: - """This class stores the options the user chose - - Normally shouldn't be used directly, as it's types may be None, - doesn't handle updates. Use through the `App`'s `Config` instead. - - Easy reading to/from JSON and supports legacy keys - - These values should be stored across invocations - - MUST be saved explicitly - """ - - # See naming conventions in Config - - # XXX: store a version in this config? - # Just in case we need to do conditional logic reading old version's configurations - - faithlife_product: Optional[str] = None - faithlife_product_version: Optional[str] = None - faithlife_product_release: Optional[str] = None - faithlife_product_logging: Optional[bool] = None - install_dir: Optional[Path] = None - winetricks_binary: Optional[str] = None - wine_binary: Optional[str] = None - # This is where to search for wine - wine_binary_code: Optional[str] = None - backup_dir: Optional[Path] = None - - # Color to use in curses. Either "Logos", "Light", or "Dark" - curses_colors: str = "Logos" - # Faithlife's release channel. Either "stable" or "beta" - faithlife_product_release_channel: str = "stable" - # The Installer's release channel. Either "stable" or "beta" - app_release_channel: str = "stable" - - # Start Cache - # Some of these values are cached to avoid github api rate-limits - faithlife_product_releases: Optional[list[str]] = None - # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? - # in legacy refresh wasn't handled properly - wine_appimage_url: Optional[str] = None - app_latest_version_url: Optional[str] = None - app_latest_version: Optional[str] = None - - last_updated: Optional[float] = None - # End Cache - - @classmethod - def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": - # XXX: handle legacy migration - - # First read in the legacy configuration - new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) #noqa: E501 - - new_keys = new_config.__dict__.keys() - - config_dict = new_config.__dict__ - - if config_file_path.endswith('.json') and Path(config_file_path).exists(): - with open(config_file_path, 'r') as config_file: - cfg = json.load(config_file) - - for key, value in cfg.items(): - if key in new_keys: - config_dict[key] = value - else: - logging.info("Not reading new values from non-json config") - - return PersistentConfiguration(**config_dict) - - @classmethod - def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": - backup_dir = None - if legacy.BACKUPDIR is not None: - backup_dir = Path(legacy.BACKUPDIR) - install_dir = None - if legacy.INSTALLDIR is not None: - install_dir = Path(legacy.INSTALLDIR) - faithlife_product_logging = None - if legacy.LOGS is not None: - faithlife_product_logging = utils.parse_bool(legacy.LOGS) - return PersistentConfiguration( - faithlife_product=legacy.FLPRODUCT, - backup_dir=backup_dir, - curses_colors=legacy.curses_colors or 'Logos', - faithlife_product_release=legacy.TARGET_RELEASE_VERSION, - faithlife_product_release_channel=legacy.logos_release_channel or 'stable', - faithlife_product_version=legacy.TARGETVERSION, - install_dir=install_dir, - app_release_channel=legacy.lli_release_channel or 'stable', - wine_binary=legacy.WINE_EXE, - wine_binary_code=legacy.WINEBIN_CODE, - winetricks_binary=legacy.WINETRICKSBIN, - faithlife_product_logging=faithlife_product_logging - ) - - def write_config(self) -> None: - config_file_path = LegacyConfiguration.config_file_path() - # XXX: we may need to merge this dict with the legacy configuration's extended config (as we don't store that persistently anymore) #noqa: E501 - output = self.__dict__ - - logging.info(f"Writing config to {config_file_path}") - os.makedirs(os.path.dirname(config_file_path), exist_ok=True) - - if self.install_dir is not None: - # Ensure all paths stored are relative to install_dir - for k, v in output.items(): - if k == "install_dir": - continue - # XXX: test this - if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 - output[k] = utils.get_relative_path(v, str(self.install_dir)) - - try: - with open(config_file_path, 'w') as config_file: - json.dump(output, config_file, indent=4, sort_keys=True) - config_file.write('\n') - except IOError as e: - msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 - # Continue, the installer can still operate even if it fails to write. - - -# Needed this logic outside this class too for before when when the app is initialized -def get_wine_prefix_path(install_dir: str) -> str: - return f"{install_dir}/data/wine64_bottle" - -class Config: - """Set of configuration values. - - If the user hasn't selected a particular value yet, they will be prompted in the UI. - """ - - # Naming conventions: - # Use `dir` instead of `directory` - # Use snake_case - # prefix with faithlife_ if it's theirs - # 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 _path if it's a file path - # suffix with _file_name if it's a file's name (with extension) - - # Storage for the keys - _raw: PersistentConfiguration - - # Overriding programmatically generated values from ENV - _overrides: EphemeralConfiguration - - # Start Cache of values unlikely to change during operation. - # i.e. filesystem traversals - _logos_exe: Optional[str] = None - _download_dir: Optional[str] = None - _wine_output_encoding: Optional[str] = None - _installed_faithlife_product_release: Optional[str] = None - - # Start constants - _curses_colors_valid_values = ["Light", "Dark", "Logos"] - - # Singleton logic, this enforces that only one config object exists at a time. - def __new__(cls, *args, **kwargs) -> "Config": - if not hasattr(cls, '_instance'): - cls._instance = super(Config, cls).__new__(cls) - return cls._instance - - def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: - from ou_dedetai.app import App - self.app: "App" = app - self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) - self._overrides = ephemeral_config - - # Now check to see if the persistent cache is still valid - if ( - ephemeral_config.check_updates_now - or self._raw.last_updated is None - or self._raw.last_updated + constants.CACHE_LIFETIME_HOURS * 60 * 60 <= time.time() #noqa: E501 - ): - logging.debug("Cleaning out old cache.") - self._raw.faithlife_product_releases = None - self._raw.app_latest_version = None - self._raw.app_latest_version_url = None - self._raw.wine_appimage_url = None - self._raw.last_updated = time.time() - self._write() - else: - logging.debug("Cache is valid.") - - logging.debug("Current persistent config:") - for k, v in self._raw.__dict__.items(): - logging.debug(f"{k}: {v}") - - def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 - # XXX: should this also update the feedback? - if not getattr(self._raw, parameter): - if dependent_parameters is not None: - for dependent_config_key in dependent_parameters: - setattr(self._raw, dependent_config_key, None) - answer = self.app.ask(question, options) - # Use the setter on this class if found, otherwise set in self._user - if getattr(Config, parameter) and getattr(Config, parameter).fset is not None: # noqa: E501 - getattr(Config, parameter).fset(self, answer) - else: - setattr(self._raw, parameter, answer) - self._write() - # parameter given should be a string - return str(getattr(self._raw, parameter)) - - def _write(self) -> None: - """Writes configuration to file and lets the app know something changed""" - self._raw.write_config() - self.app._config_updated() - - @property - def config_file_path(self) -> str: - return LegacyConfiguration.config_file_path() - - @property - def faithlife_product(self) -> str: - question = "Choose which FaithLife product the script should install: " # noqa: E501 - options = ["Logos", "Verbum"] - return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) # noqa: E501 - - @faithlife_product.setter - def faithlife_product(self, value: Optional[str]): - if self._raw.faithlife_product != value: - self._raw.faithlife_product = value - # Reset dependent variables - self._raw.faithlife_product_release = None - - self._write() - - @property - def faithlife_product_version(self) -> str: - if self._overrides.faithlife_product_version is not None: - return self._overrides.faithlife_product_version - question = f"Which version of {self.faithlife_product} should the script install?: " # noqa: E501 - options = ["10", "9"] - return self._ask_if_not_found("faithlife_product_version", question, options, []) # noqa: E501 - - @faithlife_product_version.setter - def faithlife_product_version(self, value: Optional[str]): - if self._raw.faithlife_product_version != value: - self._raw.faithlife_product_version = value - # Set dependents - self._raw.faithlife_product_release = None - # Install Dir has the name of the product and it's version. Reset it too - self._raw.install_dir = None - # Wine is dependent on the product/version selected - self._raw.wine_binary = None - self._raw.wine_binary_code = None - self._raw.winetricks_binary = None - - self._write() - - @property - def faithlife_product_release(self) -> str: - question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: " # noqa: E501 - if self._raw.faithlife_product_releases is None: - self._raw.faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 - self._write() - options = self._raw.faithlife_product_releases - return self._ask_if_not_found("faithlife_product_release", question, options) - - @faithlife_product_release.setter - def faithlife_product_release(self, value: str): - if self._raw.faithlife_product_release != value: - self._raw.faithlife_product_release = value - self._write() - - @property - def faithlife_product_icon_path(self) -> str: - return str(constants.APP_IMAGE_DIR / f"{self.faithlife_product}-128-icon.png") - - @property - def faithlife_product_logging(self) -> bool: - """Whether or not the installed faithlife product is configured to log""" - if self._raw.faithlife_product_logging is not None: - return self._raw.faithlife_product_logging - return False - - @faithlife_product_logging.setter - def faithlife_product_logging(self, value: bool): - if self._raw.faithlife_product_logging != value: - self._raw.faithlife_product_logging = value - self._write() - - @property - def faithlife_installer_name(self) -> str: - if self._overrides.faithlife_installer_name is not None: - return self._overrides.faithlife_installer_name - return f"{self.faithlife_product}_v{self.faithlife_product_release}-x64.msi" - - @property - def faithlife_installer_download_url(self) -> str: - if self._overrides.faithlife_installer_download_url is not None: - return self._overrides.faithlife_installer_download_url - after_version_url_part = "/Verbum/" if self.faithlife_product == "Verbum" else "/" # noqa: E501 - return f"https://downloads.logoscdn.com/LBS{self.faithlife_product_version}{after_version_url_part}Installer/{self.faithlife_product_release}/{self.faithlife_product}-x64.msi" # noqa: E501 - - @property - def faithlife_product_release_channel(self) -> str: - return self._raw.faithlife_product_release_channel - - @property - def app_release_channel(self) -> str: - return self._raw.app_release_channel - - @property - def winetricks_binary(self) -> str: - """This may be a path to the winetricks binary or it may be "Download" - """ - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 - options = utils.get_winetricks_options() - return self._ask_if_not_found("winetricks_binary", question, options) - - @winetricks_binary.setter - def winetricks_binary(self, value: Optional[str | Path]): - if value is not None: - value = str(value) - if value is not None and value != "Download": - if not Path(value).exists(): - raise ValueError("Winetricks binary must exist") - if self._raw.winetricks_binary != value: - self._raw.winetricks_binary = value - self._write() - - @property - def install_dir(self) -> str: - default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 - question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 - options = [default, PROMPT_OPTION_DIRECTORY] - output = self._ask_if_not_found("install_dir", question, options) - return output - - @property - # This used to be called APPDIR_BINDIR - def installer_binary_dir(self) -> str: - if self._overrides.installer_binary_dir is not None: - return self._overrides.installer_binary_dir - return f"{self.install_dir}/data/bin" - - @property - # This used to be called WINEPREFIX - def wine_prefix(self) -> str: - if self._overrides.wine_prefix is not None: - return self._overrides.wine_prefix - return get_wine_prefix_path(self.install_dir) - - @property - def wine_binary(self) -> str: - """Returns absolute path to the wine binary""" - output = self._raw.wine_binary - if output is None: - question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: " # noqa: E501 - options = utils.get_wine_options( - self.app, - utils.find_appimage_files(self.app), - utils.find_wine_binary_files(self.app, self.faithlife_product_release) - ) - - choice = self.app.ask(question, options) - - output = choice - self.wine_binary = choice - # Return the full path so we the callee doesn't need to think about it - if self._raw.wine_binary is not None and not Path(self._raw.wine_binary).exists() and (Path(self.install_dir) / self._raw.wine_binary).exists(): # noqa: E501 - return str(Path(self.install_dir) / self._raw.wine_binary) - return output - - @wine_binary.setter - def wine_binary(self, value: str): - """Takes in a path to the wine binary and stores it as relative for storage""" - # XXX: change the logic to make ^ true - if (Path(self.install_dir) / value).exists(): - value = str((Path(self.install_dir) / Path(value)).absolute()) - if not Path(value).is_file(): - raise ValueError("Wine Binary path must be a valid file") - - if self._raw.wine_binary != value: - if value is not None: - value = str(Path(value).absolute()) - self._raw.wine_binary = value - # Reset dependents - self._raw.wine_binary_code = None - self._overrides.wine_appimage_path = None - self._write() - - @property - def wine_binary_code(self) -> str: - """""" - if self._raw.wine_binary_code is None: - self._raw.wine_binary_code = utils.get_winebin_code_and_desc(self.app, self.wine_binary)[0] # noqa: E501 - self._write() - return self._raw.wine_binary_code - - @property - def wine64_binary(self) -> str: - return str(Path(self.wine_binary).parent / 'wine64') - - @property - # This used to be called WINESERVER_EXE - def wineserver_binary(self) -> str: - return str(Path(self.wine_binary).parent / 'wineserver') - - # FIXME: seems like the logic around wine appimages can be simplified - # Should this be folded into wine_binary? - @property - def wine_appimage_path(self) -> Optional[str]: - """Path to the wine appimage - - Returns: - Path if wine is set to use an appimage, otherwise returns None""" - if self._overrides.wine_appimage_path is not None: - return self._overrides.wine_appimage_path - if self.wine_binary.lower().endswith("appimage"): - return self.wine_binary - return None - - @wine_appimage_path.setter - def wine_appimage_path(self, value: Optional[str]): - if self._overrides.wine_appimage_path != value: - self._overrides.wine_appimage_path = value - # Reset dependents - self._raw.wine_binary_code = None - # XXX: Should we save? There should be something here we should store - - @property - def wine_appimage_link_file_name(self) -> str: - if self._overrides.wine_appimage_link_file_name is not None: - return self._overrides.wine_appimage_link_file_name - return 'selected_wine.AppImage' - - @property - def wine_appimage_recommended_url(self) -> str: - """URL to recommended appimage. - - Talks to the network if required""" - if self._raw.wine_appimage_url is None: - self._raw.wine_appimage_url = network.get_recommended_appimage_url() - self._write() - return self._raw.wine_appimage_url - - @property - def wine_appimage_recommended_file_name(self) -> str: - """Returns the file name of the recommended appimage with extension""" - return os.path.basename(self.wine_appimage_recommended_url) - - @property - def wine_appimage_recommended_version(self) -> str: - # Getting version and branch rely on the filename having this format: - # wine-[branch]_[version]-[arch] - return self.wine_appimage_recommended_file_name.split('-')[1].split('_')[1] - - @property - def wine_dll_overrides(self) -> str: - """Used to set WINEDLLOVERRIDES""" - if self._overrides.wine_dll_overrides is not None: - return self._overrides.wine_dll_overrides - # Default is no overrides - return '' - - @property - def wine_debug(self) -> str: - """Used to set WINEDEBUG""" - if self._overrides.wine_debug is not None: - return self._overrides.wine_debug - return constants.DEFAULT_WINEDEBUG - - @property - def wine_output_encoding(self) -> Optional[str]: - """Attempt to guess the encoding of the wine output""" - if self._overrides.wine_output_encoding is not None: - return self._overrides.wine_output_encoding - if self._wine_output_encoding is None: - self._wine_output_encoding = wine.get_winecmd_encoding(self.app) - return self._wine_output_encoding - - @property - def app_wine_log_path(self) -> str: - if self._overrides.app_wine_log_path is not None: - return self._overrides.app_wine_log_path - return constants.DEFAULT_APP_WINE_LOG_PATH - - @property - def app_log_path(self) -> str: - if self._overrides.app_log_path is not None: - return self._overrides.app_log_path - return constants.DEFAULT_APP_LOG_PATH - - @property - def app_winetricks_unattended(self) -> bool: - """If true, pass -q to winetricks""" - if self._overrides.app_winetricks_unattended is not None: - return self._overrides.app_winetricks_unattended - return False - - def toggle_faithlife_product_release_channel(self): - if self._raw.faithlife_product_release_channel == "stable": - new_channel = "beta" - else: - new_channel = "stable" - self._raw.faithlife_product_release_channel = new_channel - self._write() - - def toggle_installer_release_channel(self): - if self._raw.app_release_channel == "stable": - new_channel = "dev" - else: - new_channel = "stable" - self._raw.app_release_channel = new_channel - self._write() - - @property - def backup_dir(self) -> Path: - question = "New or existing folder to store backups in: " - options = [PROMPT_OPTION_DIRECTORY] - output = Path(self._ask_if_not_found("backup_dir", question, options)) - output.mkdir(parents=True) - return output - - @property - def curses_colors(self) -> str: - """Color for the curses dialog - - returns one of: Logos, Light or Dark""" - return self._raw.curses_colors - - @curses_colors.setter - def curses_colors(self, value: str): - if value not in self._curses_colors_valid_values: - raise ValueError(f"Invalid curses theme, expected one of: {", ".join(self._curses_colors_valid_values)} but got: {value}") # noqa: E501 - self._raw.curses_colors = value - self._write() - - def cycle_curses_color_scheme(self): - new_index = self._curses_colors_valid_values.index(self.curses_colors) + 1 - if new_index == len(self._curses_colors_valid_values): - new_index = 0 - self.curses_colors = self._curses_colors_valid_values[new_index] - - @property - def logos_exe(self) -> Optional[str]: - # Cache a successful result - if self._logos_exe is None: - self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) # noqa: E501 - return self._logos_exe - - @property - def wine_user(self) -> Optional[str]: - path: Optional[str] = self.logos_exe - if path is None: - return None - normalized_path: str = os.path.normpath(path) - path_parts = normalized_path.split(os.sep) - return path_parts[path_parts.index('users') + 1] - - @property - def logos_cef_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosCEF.exe' # noqa: E501 - return None - - @property - def logos_indexer_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\LogosIndexer.exe' # noqa: E501 - return None - - @property - def logos_login_exe(self) -> Optional[str]: - if self.wine_user is not None: - return f'C:\\users\\{self.wine_user}\\AppData\\Local\\Logos\\System\\Logos.exe' # noqa: E501 - return None - - @property - def log_level(self) -> str | int: - if self._overrides.log_level is not None: - return self._overrides.log_level - return constants.DEFAULT_LOG_LEVEL - - @property - def skip_winetricks(self) -> bool: - return bool(self._overrides.winetricks_skip) - - @property - def skip_install_system_dependencies(self) -> bool: - return bool(self._overrides.install_dependencies_skip) - - @skip_install_system_dependencies.setter - def skip_install_system_dependencies(self, val: bool): - self._overrides.install_dependencies_skip = val - - @property - def skip_install_fonts(self) -> bool: - return bool(self._overrides.install_fonts_skip) - - @skip_install_fonts.setter - def skip_install_fonts(self, val: bool): - self._overrides.install_fonts_skip = val - - @property - def download_dir(self) -> str: - if self._download_dir is None: - self._download_dir = utils.get_user_downloads_dir() - return self._download_dir - - @property - def installed_faithlife_product_release(self) -> Optional[str]: - if self._installed_faithlife_product_release is None: - self._installed_faithlife_product_release = utils.get_current_logos_version(self.install_dir) # noqa: E501 - return self._installed_faithlife_product_release - - @property - def app_latest_version_url(self) -> str: - if self._raw.app_latest_version_url is None: - self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - self._write() - return self._raw.app_latest_version_url - - @property - def app_latest_version(self) -> str: - if self._raw.app_latest_version is None: - self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - self._write() - return self._raw.app_latest_version From c9cf534a3ea92b33eb57dfccec2c1f76a1b8ecdd Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:35:36 -0800 Subject: [PATCH 055/137] refactor: migrate config.SUPERUSER_COMMAND and starting to migrate from msg to the abstract class --- ou_dedetai/app.py | 41 ++++++++-- ou_dedetai/cli.py | 2 +- ou_dedetai/config.py | 1 - ou_dedetai/gui_app.py | 40 ++++++++-- ou_dedetai/installer.py | 2 - ou_dedetai/msg.py | 1 + ou_dedetai/system.py | 164 +++++++++++++++------------------------- ou_dedetai/tui_app.py | 8 +- ou_dedetai/utils.py | 1 - 9 files changed, 136 insertions(+), 124 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 9de27c42..11c4e240 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -1,6 +1,9 @@ import abc +import logging import os +import shutil +import sys from typing import Optional from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -14,7 +17,7 @@ class App(abc.ABC): def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise it would be a circular import - from ou_dedetai.new_config import Config + from ou_dedetai.config import Config self.conf = Config(config, self) pass @@ -43,6 +46,22 @@ def ask(self, question: str, options: list[str]) -> str: return answer + def approve_or_exit(self, question: str, context: Optional[str] = None): + """Asks the user a question, if they refuse, shutdown""" + if not self._confirm(question, context): + self.exit(f"User refused the prompt: {question}") + + def _confirm(self, question: str, context: Optional[str] = None) -> bool: + """Asks the user a y/n question""" + question = f"{context}\n" if context is not None else "" + question + options = ["Yes", "No"] + return self.ask(question, options) == "Yes" + + def exit(self, reason: str): + """Exits the application cleanly with a reason""" + logging.error(f"Cannot continue because {reason}") + sys.exit(1) + _exit_option: Optional[str] = "Exit" @abc.abstractmethod @@ -73,8 +92,18 @@ def is_installed(self) -> bool: return os.access(self.conf.logos_exe, os.X_OK) return False - # XXX: unused at present - # @abc.abstractmethod - # def update_progress(self, message: str, percent: Optional[int] = None): - # """Updates the progress of the current operation""" - # pass + def update_progress(self, message: str, percent: Optional[int] = None): + """Updates the progress of the current operation""" + # XXX: reformat to the cli's normal format (probably in msg.py) + print(f"{percent}% - {message}") + + @property + def superuser_command(self) -> str: + """Command when root privileges are needed. + + Raises: + SuperuserCommandNotFound + + May be sudo or pkexec for example""" + from ou_dedetai.system import get_superuser_command + return get_superuser_command() \ No newline at end of file diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 06da58a3..2013f643 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -2,7 +2,7 @@ import threading from ou_dedetai.app import App -from ou_dedetai.new_config import EphemeralConfiguration +from ou_dedetai.config import EphemeralConfiguration from . import control from . import installer diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 8ca3dd77..71f829da 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -24,7 +24,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' -SUPERUSER_COMMAND: Optional[str] = None console_log = [] processes = {} threads = [] diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 4ddbfc7c..92291106 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -7,8 +7,9 @@ from pathlib import Path from queue import Queue +import shutil from threading import Event -from tkinter import PhotoImage +from tkinter import PhotoImage, messagebox from tkinter import Tk from tkinter import Toplevel from tkinter import filedialog as fd @@ -17,7 +18,7 @@ from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE -from ou_dedetai.new_config import EphemeralConfiguration +from ou_dedetai.config import EphemeralConfiguration from . import config from . import constants @@ -37,7 +38,7 @@ class GuiApp(App): def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwargs): super().__init__(ephemeral_config) - self.root_to_destory_on_none = root + self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: answer_q = Queue() @@ -54,25 +55,45 @@ def spawn_dialog(): answer_event.wait() answer = answer_q.get() if answer is None: - self.root_to_destory_on_none.destroy() + self.root.destroy() return None elif isinstance(options, str): answer = options if answer == PROMPT_OPTION_DIRECTORY: answer = fd.askdirectory( - parent=self.root_to_destory_on_none, + parent=self.root, title=question, initialdir=Path().home(), ) elif answer == PROMPT_OPTION_FILE: answer = fd.askopenfilename( - parent=self.root_to_destory_on_none, + parent=self.root, title=question, initialdir=Path().home(), ) return answer + def _confirm(self, question: str, context: str | None = None) -> bool: + return messagebox.askquestion(question, context) == 'yes' + + def exit(self, reason: str): + self.root.destroy() + return super().exit(reason) + + + def superuser_command(self) -> str: + """Command when root privileges are needed. + + Raises: + SuperuserCommandNotFound - if no command is found + + pkexec if found""" + if shutil.which('pkexec'): + return "pkexec" + else: + raise system.SuperuserCommandNotFound("No superuser command found. Please install pkexec.") # noqa: E501 + class Root(Tk): def __init__(self, *args, **kwargs): super().__init__(**kwargs) @@ -462,6 +483,13 @@ def start_install_thread(self, evt=None): self.gui.progress.config(mode='determinate') utils.start_thread(installer.ensure_launcher_shortcuts, app=self) + def update_progress(self, message: str, percent: Optional[int] = None): + self.gui.progress.state(['!disabled']) + self.gui.progressvar.set(percent or 0) + self.gui.progress.config(mode='indeterminate') + self.gui.progress.start() + self.gui.statusvar.set(message) + def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) self.gui.progressvar.set(0) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 922f6b2d..dabaa22a 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -175,8 +175,6 @@ def ensure_sys_deps(app: App): if not app.conf.skip_install_system_dependencies: utils.install_dependencies(app) - if config.DIALOG == "curses": - app.installdeps_e.wait() logging.debug("> Done.") else: logging.debug("> Skipped.") diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 7f80cf25..a4e43a08 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -331,6 +331,7 @@ def progress(percent, app=None): logos_msg(get_progress_str(percent)) # provisional +# XXX: move this to app.update_progress def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index bdbdcf69..adf20e2e 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -207,9 +207,9 @@ def get_logos_pids(app: App): config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) # noqa: E501 -def reboot(): +def reboot(superuser_command: str): logging.info("Rebooting system.") - command = f"{config.SUPERUSER_COMMAND} reboot now" + command = f"{superuser_command} reboot now" subprocess.run( command, stdout=subprocess.PIPE, @@ -253,22 +253,19 @@ def get_os() -> Tuple[str, str]: return os_name, os_release -def get_superuser_command(): - if config.DIALOG == 'tk': - if shutil.which('pkexec'): - config.SUPERUSER_COMMAND = "pkexec" - else: - msg.logos_error("No superuser command found. Please install pkexec.") # noqa: E501 +class SuperuserCommandNotFound(Exception): + """Superuser command not found. Install pkexec or sudo or doas""" + + +def get_superuser_command() -> str: + if shutil.which('pkexec'): + return "pkexec" + elif shutil.which('sudo'): + return "sudo" + elif shutil.which('doas'): + return "doas" else: - if shutil.which('pkexec'): - config.SUPERUSER_COMMAND = "pkexec" - elif shutil.which('sudo'): - config.SUPERUSER_COMMAND = "sudo" - elif shutil.which('doas'): - config.SUPERUSER_COMMAND = "doas" - else: - msg.logos_error("No superuser command found. Please install sudo or doas.") # noqa: E501 - logging.debug(f"{config.SUPERUSER_COMMAND=}") + raise SuperuserCommandNotFound @dataclass @@ -549,11 +546,14 @@ def parse_date(version): return None -def remove_appimagelauncher(app=None): - pkg = "appimagelauncher" - cmd = [config.SUPERUSER_COMMAND, *package_manager.remove, pkg] # noqa: E501 - # FIXME: should this status be higher? (the caller of this function) +def remove_appimagelauncher(app: App): msg.status("Removing AppImageLauncher…", app) + pkg = "appimagelauncher" + package_manager = get_package_manager() + if package_manager is None: + msg.logos_error("Failed to find the package manager to uninstall AppImageLauncher.") + sys.exit(1) + cmd = [app.superuser_command, *package_manager.remove, pkg] # noqa: E501 try: logging.debug(f"Running command: {cmd}") run_command(cmd) @@ -569,47 +569,47 @@ def remove_appimagelauncher(app=None): sys.exit() -def preinstall_dependencies_steamos(): +def preinstall_dependencies_steamos(superuser_command: str): logging.debug("Disabling read only, updating pacman keys…") command = [ - config.SUPERUSER_COMMAND, "steamos-readonly", "disable", "&&", - config.SUPERUSER_COMMAND, "pacman-key", "--init", "&&", - config.SUPERUSER_COMMAND, "pacman-key", "--populate", "archlinux", + superuser_command, "steamos-readonly", "disable", "&&", + superuser_command, "pacman-key", "--init", "&&", + superuser_command, "pacman-key", "--populate", "archlinux", ] return command -def postinstall_dependencies_steamos(): +def postinstall_dependencies_steamos(superuser_command: str): logging.debug("Updating DNS settings & locales, enabling services & read-only system…") # noqa: E501 command = [ - config.SUPERUSER_COMMAND, "sed", '-i', + superuser_command, "sed", '-i', 's/mymachines resolve/mymachines mdns_minimal [NOTFOUND=return] resolve/', # noqa: E501 '/etc/nsswitch.conf', '&&', - config.SUPERUSER_COMMAND, "locale-gen", '&&', - config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "avahi-daemon", "&&", # noqa: E501 - config.SUPERUSER_COMMAND, "systemctl", "enable", "--now", "cups", "&&", # noqa: E501 - config.SUPERUSER_COMMAND, "steamos-readonly", "enable", + superuser_command, "locale-gen", '&&', + superuser_command, "systemctl", "enable", "--now", "avahi-daemon", "&&", # noqa: E501 + superuser_command, "systemctl", "enable", "--now", "cups", "&&", # noqa: E501 + superuser_command, "steamos-readonly", "enable", ] return command -def preinstall_dependencies(): +def preinstall_dependencies(superuser_command: str): command = [] logging.debug("Performing pre-install dependencies…") os_name, _ = get_os() if os_name == "Steam": - command = preinstall_dependencies_steamos() + command = preinstall_dependencies_steamos(superuser_command) else: logging.debug("No pre-install dependencies required.") return command -def postinstall_dependencies(): +def postinstall_dependencies(superuser_command: str): command = [] logging.debug("Performing post-install dependencies…") os_name, _ = get_os() if os_name == "Steam": - command = postinstall_dependencies_steamos() + command = postinstall_dependencies_steamos(superuser_command) else: logging.debug("No post-install dependencies required.") return command @@ -623,9 +623,8 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 install_deps_failed = False manual_install_required = False reboot_required = False - message = None - no_message = None - secondary = None + message: Optional[str] = None + secondary: Optional[str] = None command = [] preinstall_command = [] install_command = [] @@ -666,23 +665,19 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 ) if os_name in ['fedora', 'arch']: - message = False - no_message = False - secondary = False + # XXX: move the handling up here, possibly simplify? + m = "Your distro requires manual dependency installation." + logging.error(m) + return elif missing_packages and conflicting_packages: message = f"Your {os_name} computer requires installing and removing some software.\nProceed?" # noqa: E501 - no_message = "User refused to install and remove software via the application" # noqa: E501 secondary = f"To continue, the program will attempt to install the following package(s) by using '{package_manager.install}':\n{missing_packages}\nand will remove the following package(s) by using '{package_manager.remove}':\n{conflicting_packages}" # noqa: E501 elif missing_packages: message = f"Your {os_name} computer requires installing some software.\nProceed?" # noqa: E501 - no_message = "User refused to install software via the application." # noqa: E501 secondary = f"To continue, the program will attempt to install the following package(s) by using '{package_manager.install}':\n{missing_packages}" # noqa: E501 elif conflicting_packages: message = f"Your {os_name} computer requires removing some software.\nProceed?" # noqa: E501 - no_message = "User refused to remove software via the application." # noqa: E501 secondary = f"To continue, the program will attempt to remove the following package(s) by using '{package_manager.remove}':\n{conflicting_packages}" # noqa: E501 - else: - message = None if message is None: logging.debug("No missing or conflicting dependencies found.") @@ -690,16 +685,11 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 m = "Your distro requires manual dependency installation." logging.error(m) else: - msg.logos_continue_question(message, no_message, secondary, app) - if config.DIALOG == "curses": - app.confirm_e.wait() + if not app.approve_or_exit(message, secondary): + logging.debug("User refused to install packages. Exiting...") + return - # TODO: Need to send continue question to user based on DIALOG. - # All we do above is create a message that we never send. - # Do we need a TK continue question? I see we have a CLI and curses one - # in msg.py - - preinstall_command = preinstall_dependencies() + preinstall_command = preinstall_dependencies(app.superuser_command) if missing_packages: install_command = package_manager.install + missing_packages # noqa: E501 @@ -715,7 +705,7 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 else: logging.debug("No conflicting packages detected.") - postinstall_command = postinstall_dependencies() + postinstall_command = postinstall_dependencies(app.superuser_command) if preinstall_command: command.extend(preinstall_command) @@ -734,15 +724,11 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 if not command: # nothing to run; avoid running empty pkexec command if app: msg.status("All dependencies are met.", app) - if config.DIALOG == "curses": - app.installdeps_e.set() return - if app and config.DIALOG == 'tk': - app.root.event_generate('<>') - msg.status("Installing dependencies…", app) + app.update_progress("Installing dependencies…") final_command = [ - f"{config.SUPERUSER_COMMAND}", 'sh', '-c', "'", *command, "'" + f"{app.superuser_command}", 'sh', '-c', "'", *command, "'" ] command_str = ' '.join(final_command) # TODO: Fix fedora/arch handling. @@ -754,35 +740,16 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 "Please run the following command in a terminal, then restart " f"{constants.APP_NAME}:\n{sudo_command}\n" ) - if config.DIALOG == "tk": - if hasattr(app, 'root'): - detail += "\nThe command has been copied to the clipboard." # noqa: E501 - app.root.clipboard_clear() - app.root.clipboard_append(sudo_command) - app.root.update() - msg.logos_error( - message, - detail=detail, - app=app, - parent='installer_win' - ) - elif config.DIALOG == 'cli': - msg.logos_error(message + "\n" + detail) - install_deps_failed = True - - if manual_install_required and app and config.DIALOG == "curses": - app.screen_q.put( - app.stack_confirm( - 17, - app.manualinstall_q, - app.manualinstall_e, - f"Please run the following command in a terminal, then select \"Continue\" when finished.\n\n{constants.APP_NAME}:\n{sudo_command}\n", # noqa: E501 - "User cancelled dependency installation.", # noqa: E501 - message, - options=["Continue", "Return to Main Menu"], dialog=config.use_python_dialog)) # noqa: E501 - app.manualinstall_e.wait() + from ou_dedetai import gui_app + if isinstance(app, gui_app.GuiApp): + detail += "\nThe command has been copied to the clipboard." # noqa: E501 + app.root.clipboard_clear() + app.root.clipboard_append(sudo_command) + app.root.update() + app.approve_or_exit(message + " \n" + detail) if not install_deps_failed and not manual_install_required: + # FIXME: why only for this dialog? if config.DIALOG == 'cli': command_str = command_str.replace("pkexec", "sudo") try: @@ -798,24 +765,13 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 if reboot_required: - question = "Should the program reboot the host now?" # noqa: E501 - no_text = "The user has chosen not to reboot." - secondary = "The system has installed or removed a package that requires a reboot." # noqa: E501 - if msg.logos_continue_question(question, no_text, secondary): - reboot() + question = "The system has installed or removed a package that requires a reboot. Do you want to restart now?" # noqa: E501 + if app.approve_or_exit(question): + reboot(app.superuser_command()) else: - logging.error("Cannot proceed until reboot. Exiting.") + logging.error("Please reboot then launch the installer again.") sys.exit(1) - if install_deps_failed: - if app: - if config.DIALOG == "curses": - app.choice_q.put("Return to Main Menu") - else: - if app: - if config.DIALOG == "curses": - app.installdeps_e.set() - def install_winetricks( installdir, diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 758d8a4a..e9609151 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -10,7 +10,7 @@ from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE -from ou_dedetai.new_config import EphemeralConfiguration +from ou_dedetai.config import EphemeralConfiguration from . import config from . import control @@ -66,8 +66,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati # Install and Options self.manualinstall_q = Queue() self.manualinstall_e = threading.Event() - self.installdeps_q = Queue() - self.installdeps_e = threading.Event() self.deps_q = Queue() self.deps_e = threading.Event() self.finished_q = Queue() @@ -654,6 +652,10 @@ def handle_ask_directory_response(self, choice: Optional[str]): if choice is not None and Path(choice).exists() and Path(choice).is_dir(): self.handle_ask_response(choice) + def update_progress(self, message: str, percent: int | None = None): + # XXX: update some screen? Something like get_waiting? + pass + def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 19071312..7d400016 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -56,7 +56,6 @@ def append_unique(list, item): # Set "global" variables. # XXX: fold this into config def set_default_config(): - system.get_superuser_command() system.get_package_manager() if config.CONFIG_FILE is None: config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH From dd4ef56a5deebb2f64b4bc4980303c8a2edc67f9 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:54:58 -0800 Subject: [PATCH 056/137] refactor: remove more DIALOG removed curses.KEY_RESIZE, there wasn't a RESIZE at refactor time start using app.status rather than msg.status --- ou_dedetai/app.py | 22 ++++++---- ou_dedetai/cli.py | 18 ++++++-- ou_dedetai/config.py | 2 +- ou_dedetai/control.py | 8 +--- ou_dedetai/gui_app.py | 90 ++++++++++++++-------------------------- ou_dedetai/installer.py | 32 ++++---------- ou_dedetai/network.py | 13 +----- ou_dedetai/system.py | 2 +- ou_dedetai/tui_app.py | 29 ++++++------- ou_dedetai/tui_curses.py | 2 - ou_dedetai/tui_screen.py | 9 +++- ou_dedetai/utils.py | 20 +-------- ou_dedetai/wine.py | 11 +++-- 13 files changed, 97 insertions(+), 161 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 11c4e240..5ac769a2 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -2,7 +2,6 @@ import abc import logging import os -import shutil import sys from typing import Optional @@ -80,11 +79,6 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: """ raise NotImplementedError() - def _config_updated(self) -> None: - """A hook for any changes the individual apps want to do when the config changes - """ - pass - def is_installed(self) -> bool: """Returns whether the install was successful by checking if the installed exe exists and is executable""" @@ -92,8 +86,8 @@ def is_installed(self) -> bool: return os.access(self.conf.logos_exe, os.X_OK) return False - def update_progress(self, message: str, percent: Optional[int] = None): - """Updates the progress of the current operation""" + def status(self, message: str, percent: Optional[int] = None): + """A status update""" # XXX: reformat to the cli's normal format (probably in msg.py) print(f"{percent}% - {message}") @@ -106,4 +100,14 @@ def superuser_command(self) -> str: May be sudo or pkexec for example""" from ou_dedetai.system import get_superuser_command - return get_superuser_command() \ No newline at end of file + return get_superuser_command() + + # Start hooks + def _config_updated_hook(self) -> None: + """Function run when the config changes""" + + def _install_complete_hook(self): + """Function run when installation is complete.""" + + def _install_started_hook(self): + """Function run when installation first begins.""" \ No newline at end of file diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 2013f643..58b66594 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -35,7 +35,7 @@ def get_winetricks(self): def install_app(self): self.thread = utils.start_thread( - installer.ensure_launcher_shortcuts, + installer.install, app=self ) self.user_input_processor() @@ -97,8 +97,9 @@ def winetricks(self): def _ask(self, question: str, options: list[str] | str) -> str: """Passes the user input to the user_input_processor thread - The user_input_processor is running on the thread that the user's stdin/stdout is attached to - This function is being called from another thread so we need to pass the information between threads using a queue/event + The user_input_processor is running on the thread that the user's stdin/stdout + is attached to. This function is being called from another thread so we need to + pass the information between threads using a queue/event """ if isinstance(options, str): options = [options] @@ -107,7 +108,16 @@ def _ask(self, question: str, options: list[str] | str) -> str: self.choice_event.wait() self.choice_event.clear() # XXX: This is always a freeform input, perhaps we should have some sort of validation? - return self.choice_q.get() + output: str = self.choice_q.get() + return output + + def exit(self, reason: str): + # Signal CLI.user_input_processor to stop. + self.input_q.put(None) + self.input_event.set() + # Signal CLI itself to stop. + self.stop() + return super().exit(reason) def user_input_processor(self, evt=None): while self.running: diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 71f829da..6fdf579d 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -494,7 +494,7 @@ def _ask_if_not_found(self, parameter: str, question: str, options: list[str], d def _write(self) -> None: """Writes configuration to file and lets the app know something changed""" self._raw.write_config() - self.app._config_updated() + self.app._config_updated_hook() @property def config_file_path(self) -> str: diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index b738bdaa..26668c19 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -37,6 +37,7 @@ def restore(app: App): # FIXME: consider moving this into it's own file/module. def backup_and_restore(mode: str, app: App): + app.status(f"Starting {mode}...") data_dirs = ['Data', 'Documents', 'Users'] backup_dir = Path(app.conf.backup_dir).expanduser().resolve() @@ -116,9 +117,6 @@ def backup_and_restore(mode: str, app: App): print() msg.logos_error("Cancelled with Ctrl+C.", app=app) t.join() - if config.DIALOG == 'tk': - app.root.event_generate('<>') - app.root.event_generate('<>') src_size = q.get() if src_size == 0: msg.logos_warning(f"Nothing to {mode}!", app=app) @@ -221,9 +219,7 @@ def remove_all_index_files(app: App): except OSError as e: logging.error(f"Error removing {file_to_remove}: {e}") - msg.status("======= Removing all LogosBible index files done! =======") - if hasattr(app, 'status_evt'): - app.root.event_generate(app.status_evt) + msg.status("Removed all LogosBible index files!") sys.exit(0) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 92291106..d7e9f0d6 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -253,20 +253,13 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): self.get_q = Queue() self.get_evt = "<>" self.root.bind(self.get_evt, self.update_download_progress) - self.check_evt = "<>" - self.root.bind(self.check_evt, self.update_file_check_progress) self.status_q = Queue() self.status_evt = "<>" self.root.bind(self.status_evt, self.update_status_text) self.progress_q = Queue() self.root.bind( "<>", - self.update_progress - ) - self.todo_q = Queue() - self.root.bind( - "<>", - self.todo + self.step_start ) self.releases_q = Queue() self.wine_q = Queue() @@ -274,7 +267,7 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): # Run commands. self.get_winetricks_options() - def _config_updated(self): + def _config_updated_hook(self): """Update the GUI to reflect changes in the configuration if they were prompted separately""" # The configuration enforces dependencies, if product is unset, so will it's dependents (version and release) # XXX: test this hook. Interesting thing is, this may never be called in production, as it's only called (presently) when the separate prompt returns @@ -319,22 +312,10 @@ def set_input_widgets_state(self, state, widgets='all'): for w in widgets: w.state(state) - def todo(self, evt=None, task=None): - logging.debug(f"GUI todo: {task=}") - widgets = [] - if not task: - if not self.todo_q.empty(): - task = self.todo_q.get() - else: - return - self.set_input_widgets_state('enabled') - if task == 'INSTALL': - self.gui.statusvar.set('Ready to install!') - self.gui.progressvar.set(0) - elif task == 'INSTALLING': - self.set_input_widgets_state('disabled') - elif task == 'DONE': - self.update_install_progress() + def _install_started_hook(self): + self.gui.statusvar.set('Ready to install!') + self.gui.progressvar.set(0) + self.set_input_widgets_state('disabled') def set_product(self, evt=None): if self.gui.productvar.get().startswith('C'): # ignore default text @@ -425,9 +406,14 @@ def start_wine_versions_check(self, release_version): self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Finding available wine binaries…") + + def get_wine_options(app: InstallerWindow, app_images, binaries): + app.wines_q.put(utils.get_wine_options(app, app_images, binaries)) + app.root.event_generate(app.wine_evt) + # Start thread. utils.start_thread( - utils.get_wine_options, + get_wine_options, self, self.appimages, utils.find_wine_binary_files(self, release_version), @@ -481,14 +467,21 @@ def on_cancel_released(self, evt=None): def start_install_thread(self, evt=None): self.gui.progress.config(mode='determinate') - utils.start_thread(installer.ensure_launcher_shortcuts, app=self) - - def update_progress(self, message: str, percent: Optional[int] = None): - self.gui.progress.state(['!disabled']) - self.gui.progressvar.set(percent or 0) - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() + utils.start_thread(installer.install, app=self) + + def status(self, message: str, percent: int | None = None): + if percent: + self.gui.progress.stop() + self.gui.progress.state(['disabled']) + self.gui.progress.config(mode='determinate') + self.gui.progressvar.set(percent) + else: + self.gui.progress.state(['!disabled']) + self.gui.progressvar.set(0) + self.gui.progress.config(mode='indeterminate') + self.gui.progress.start() self.gui.statusvar.set(message) + super().status(message, percent) def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) @@ -530,17 +523,11 @@ def update_wine_check_progress(self, evt=None): self.stop_indeterminate_progress() self.gui.wine_check_button.state(['!disabled']) - def update_file_check_progress(self, evt=None): - self.gui.progress.stop() - self.gui.statusvar.set('') - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - def update_download_progress(self, evt=None): d = self.get_q.get() self.gui.progressvar.set(int(d)) - def update_progress(self, evt=None): + def step_start(self, evt=None): progress = self.progress_q.get() if not type(progress) is int: return @@ -558,7 +545,7 @@ def update_status_text(self, evt=None, status=None): text = status self.gui.statusvar.set(text) - def update_install_progress(self, evt=None): + def _install_complete_hook(self): self.gui.progress.stop() self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) @@ -650,13 +637,9 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar '<>', self.start_indeterminate_progress ) - self.root.bind( - '<>', - self.stop_indeterminate_progress - ) self.root.bind( '<>', - self.update_progress + self.step_start ) self.root.bind( "<>", @@ -666,8 +649,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.get_q = Queue() self.get_evt = "<>" self.root.bind(self.get_evt, self.update_download_progress) - self.check_evt = "<>" - self.root.bind(self.check_evt, self.update_file_check_progress) # Start function to determine app logging state. if self.is_installed(): @@ -884,17 +865,11 @@ def reverse_logging_state_value(self, state): def clear_status_text(self, evt=None): self.gui.statusvar.set('') - def update_file_check_progress(self, evt=None): - self.gui.progress.stop() - self.gui.statusvar.set('') - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - def update_download_progress(self, evt=None): d = self.get_q.get() self.gui.progressvar.set(int(d)) - def update_progress(self, evt=None): + def step_start(self, evt=None): progress = self.progress_q.get() if not type(progress) is int: return @@ -918,11 +893,6 @@ def start_indeterminate_progress(self, evt=None): self.gui.progress.config(mode='indeterminate') self.gui.progress.start() - def stop_indeterminate_progress(self, evt=None): - self.gui.progress.stop() - self.gui.progress.state(['disabled']) - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) def control_panel_app(ephemeral_config: EphemeralConfiguration): diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index dabaa22a..ebaca3ba 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -131,11 +131,8 @@ def ensure_installation_config(app: App): logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") - # XXX: What does the install task do? Shouldn't that logic be here? - if config.DIALOG in ['curses', 'dialog', 'tk']: - utils.send_task(app, 'INSTALL') - else: - msg.logos_msg("Install is running…") + app._install_started_hook() + app.status("Install is running…") def ensure_install_dirs(app: App): @@ -162,10 +159,6 @@ def ensure_install_dirs(app: App): logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") logging.debug(f"> config.WINEPREFIX={app.conf.wine_prefix}") - # XXX: what does this task do? Shouldn't that logic be here? - if config.DIALOG in ['curses', 'dialog', 'tk']: - utils.send_task(app, 'INSTALLING') - def ensure_sys_deps(app: App): app.installer_step_count += 1 @@ -396,9 +389,6 @@ def ensure_icu_data_files(app: App): wine.enforce_icu_data_files(app=app) - if config.DIALOG == "curses": - app.install_icu_e.wait() - logging.debug('> ICU data files installed') @@ -427,12 +417,9 @@ def ensure_config_file(app: App): app.installer_step += 1 update_install_feedback("Ensuring config file is up-to-date…", app=app) - # XXX: Why the platform specific logic? + app.status("Install has finished.", 100) - if config.DIALOG == 'cli': - msg.logos_msg("Install has finished.") - else: - utils.send_task(app, 'DONE') + app._install_complete_hook() logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 @@ -478,14 +465,9 @@ def ensure_launcher_shortcuts(app: App): app=app ) - # XXX: why only for this dialog? - if config.DIALOG == 'cli': - # Signal CLI.user_input_processor to stop. - app.input_q.put(None) - app.input_event.set() - # Signal CLI itself to stop. - app.stop() - +def install(app: App): + """Entrypoint for installing""" + ensure_launcher_shortcuts(app) def update_install_feedback(text, app: App): percent = get_progress_pct(app.installer_step, app.installer_step_count) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 4a131919..ce5187ce 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -323,13 +323,9 @@ def net_get(url, target=None, app=None, evt=None, q=None): msg.logos_error("Killed with Ctrl+C") -def verify_downloaded_file(url, file_path, app=None, evt=None): +def verify_downloaded_file(url, file_path, app: Optional[App]=None): if app: - if config.DIALOG == "tk": - app.root.event_generate('<>') - msg.status(f"Verifying {file_path}…", app) - # if config.DIALOG == "tk": - # app.root.event_generate('<>') + app.status(f"Verifying {file_path}…", 0) res = False txt = f"{file_path} is the wrong size." right_size = same_size(url, file_path) @@ -340,11 +336,6 @@ def verify_downloaded_file(url, file_path, app=None, evt=None): txt = f"{file_path} is verified." res = True logging.info(txt) - if app: - if config.DIALOG == "tk": - if not evt: - evt = app.check_evt - app.root.event_generate(evt) return res diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index adf20e2e..ffdd0ddd 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -726,7 +726,7 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 msg.status("All dependencies are met.", app) return - app.update_progress("Installing dependencies…") + app.status("Installing dependencies…") final_command = [ f"{app.superuser_command}", 'sh', '-c', "'", *command, "'" ] diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index e9609151..97381b0b 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -51,8 +51,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.main_thread = threading.Thread() self.get_q = Queue() self.get_e = threading.Event() - self.input_q = Queue() - self.input_e = threading.Event() self.status_q = Queue() self.status_e = threading.Event() self.progress_q = Queue() @@ -79,7 +77,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.appimage_q = Queue() self.appimage_e = threading.Event() self.install_icu_q = Queue() - self.install_icu_e = threading.Event() self.install_logos_q = Queue() self.install_logos_e = threading.Event() @@ -191,7 +188,7 @@ def init_curses(self): logging.error(f"An error occurred in init_curses(): {e}") raise - def _config_updated(self): + def _config_updated_hook(self): self.set_curses_colors() def end_curses(self): @@ -211,6 +208,10 @@ def end(self, signal, frame): self.llirunning = False curses.endwin() + def _install_complete_hook(self): + # Update the contents going back to the start + self.update_main_window_contents() + def update_main_window_contents(self): self.clear() self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 @@ -307,7 +308,7 @@ def display(self): run_monitor, last_time = utils.stopwatch(last_time, 2.5) if run_monitor: self.logos.monitor() - self.task_processor(self, task="PID") + self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) if isinstance(self.active_screen, tui_screen.CursesScreen): self.refresh() @@ -329,15 +330,8 @@ def run(self): self.end_curses() signal.signal(signal.SIGINT, self.end) - def task_processor(self, evt=None, task=None): - if task == 'INSTALL' or task == 'INSTALLING': - utils.start_thread(self.get_waiting, config.use_python_dialog) - elif task == 'INSTALLING_PW': - utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) - elif task == 'DONE': - self.update_main_window_contents() - elif task == 'PID': - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + def installing_pw_waiting(self): + utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -395,7 +389,7 @@ def main_menu_select(self, choice): self.installer_step = 0 self.installer_step_count = 0 utils.start_thread( - installer.ensure_launcher_shortcuts, + installer.install, daemon_bool=True, app=self, ) @@ -652,10 +646,13 @@ def handle_ask_directory_response(self, choice: Optional[str]): if choice is not None and Path(choice).exists() and Path(choice).is_dir(): self.handle_ask_response(choice) - def update_progress(self, message: str, percent: int | None = None): + def status(self, message: str, percent: int | None = None): # XXX: update some screen? Something like get_waiting? pass + def _install_started_hook(self): + self.get_waiting(self, config.use_python_dialog) + def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index cf1e351f..49f1531e 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -293,8 +293,6 @@ def input(self): try: if key == -1: # If key not found, keep processing. pass - elif key == curses.KEY_RESIZE: - utils.send_task(self.app, 'RESIZE') elif key == curses.KEY_UP or key == 259: # Up arrow self.do_menu_up() elif key == curses.KEY_DOWN or key == 258: # Down arrow diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index dc6c85f3..84270231 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -190,6 +190,9 @@ def get_default(self): class PasswordScreen(InputScreen): def __init__(self, app, screen_id, queue, event, question, default): super().__init__(app, screen_id, queue, event, question, default) + # Update type for type linting + from ou_dedetai.tui_app import TUI + self.app: TUI = app self.dialog = tui_curses.PasswordDialog( self.app, self.question, @@ -204,7 +207,7 @@ def display(self): self.choice = self.dialog.run() if not self.choice == "Processing": self.submit_choice_to_queue() - utils.send_task(self.app, "INSTALLING_PW") + self.app.installing_pw_waiting() self.stdscr.noutrefresh() curses.doupdate() @@ -289,6 +292,8 @@ def get_default(self): class PasswordDialog(InputDialog): def __init__(self, app, screen_id, queue, event, question, default): super().__init__(app, screen_id, queue, event, question, default) + from ou_dedetai.tui_app import TUI + self.app: TUI = app def __str__(self): return "PyDialog Password Screen" @@ -298,7 +303,7 @@ def display(self): self.running = 1 _, self.choice = tui_dialog.password(self.app, self.question, init=self.default) #noqa: E501 self.submit_choice_to_queue() - utils.send_task(self.app, "INSTALLING_PW") + self.app.installing_pw_waiting() class ConfirmDialog(DialogScreen): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 7d400016..9296e9fe 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -175,7 +175,7 @@ def install_dependencies(app: App): targetversion = int(app.conf.faithlife_product_version) else: targetversion = 10 - msg.status(f"Checking Logos {str(targetversion)} dependencies…", app) + app.status(f"Checking Logos {str(targetversion)} dependencies…") if targetversion == 10: system.install_dependencies(app, target_version=10) # noqa: E501 @@ -187,9 +187,7 @@ def install_dependencies(app: App): else: logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") - if config.DIALOG == "tk": - # FIXME: This should get moved to gui_app. - app.root.event_generate('<>') + app.status("Installed dependencies.", 100) def file_exists(file_path: Optional[str | bytes | Path]) -> bool: @@ -336,11 +334,6 @@ def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 # wine_binary_options.append(["Exit", "Exit", "Cancel installation."]) logging.debug(f"{wine_binary_options=}") - if app: - if config.DIALOG != "cli": - app.wines_q.put(wine_binary_options) - if config.DIALOG == 'tk': - app.root.event_generate(app.wine_evt) return wine_binary_options @@ -787,15 +780,6 @@ def get_downloaded_file_path(download_dir: str, filename: str): logging.debug(f"File not found: {filename}") -def send_task(app, task): - # logging.debug(f"{task=}") - app.todo_q.put(task) - if config.DIALOG == 'tk': - app.root.event_generate('<>') - elif config.DIALOG == 'curses': - app.task_processor(app, task=task) - - def grep(regexp, filepath): fp = Path(filepath) found = False diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index b8c2261f..71054606 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -416,6 +416,7 @@ def set_win_version(app: App, exe: str, windows_version: str): # FIXME: consider when to run this (in the update case) def enforce_icu_data_files(app: App): + app.status("Downloading ICU files...") # XXX: consider moving the version and url information into config (and cached) repo = "FaithLife-Community/icu" json_data = network.get_latest_release_data(repo) @@ -434,6 +435,9 @@ def enforce_icu_data_files(app: App): app.conf.download_dir, app=app ) + + app.status("Copying ICU files...") + drive_c = f"{app.conf.wine_prefix}/drive_c" utils.untar_file(f"{app.conf.download_dir}/{icu_filename}", drive_c) @@ -443,13 +447,8 @@ def enforce_icu_data_files(app: App): os.makedirs(icu_win_dir) shutil.copytree(icu_win_dir, f"{drive_c}/windows", dirs_exist_ok=True) - if hasattr(app, 'status_evt'): - app.status_q.put("ICU files copied.") - app.root.event_generate(app.status_evt) + app.status("ICU files copied.", 100) - if app: - if config.DIALOG == "curses": - app.install_icu_e.set() def get_registry_value(reg_path, name, app: App): From 27f3a43c0ebcd5c7015b304ef5ef30aa3b23ec64 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:22:33 -0800 Subject: [PATCH 057/137] refactor: move DIALOG removing --- ou_dedetai/app.py | 11 ++++--- ou_dedetai/cli.py | 9 ++++++ ou_dedetai/control.py | 4 +-- ou_dedetai/gui_app.py | 17 +++++++--- ou_dedetai/network.py | 75 +++++++------------------------------------ ou_dedetai/system.py | 3 -- ou_dedetai/utils.py | 63 ++++-------------------------------- 7 files changed, 48 insertions(+), 134 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 5ac769a2..2e324a16 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -47,10 +47,10 @@ def ask(self, question: str, options: list[str]) -> str: def approve_or_exit(self, question: str, context: Optional[str] = None): """Asks the user a question, if they refuse, shutdown""" - if not self._confirm(question, context): + if not self.approve(question, context): self.exit(f"User refused the prompt: {question}") - def _confirm(self, question: str, context: Optional[str] = None) -> bool: + def approve(self, question: str, context: Optional[str] = None) -> bool: """Asks the user a y/n question""" question = f"{context}\n" if context is not None else "" + question options = ["Yes", "No"] @@ -88,8 +88,11 @@ def is_installed(self) -> bool: def status(self, message: str, percent: Optional[int] = None): """A status update""" - # XXX: reformat to the cli's normal format (probably in msg.py) - print(f"{percent}% - {message}") + if percent: + # XXX: consider using utils.write_progress_bar + # Print out 20 periods or spaces proportional to progress + print("[" + "." * int(percent / 5) + " " * int((100 - percent) / 5) + "] ", end='') #noqa: E501 + print(f"{message}") @property def superuser_command(self) -> str: diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 58b66594..7b99a12a 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -1,8 +1,10 @@ import queue +import shutil import threading from ou_dedetai.app import App from ou_dedetai.config import EphemeralConfiguration +from ou_dedetai.system import SuperuserCommandNotFound from . import control from . import installer @@ -118,6 +120,13 @@ def exit(self, reason: str): # Signal CLI itself to stop. self.stop() return super().exit(reason) + + @property + def superuser_command(self) -> str: + if shutil.which('sudo'): + return "sudo" + else: + raise SuperuserCommandNotFound("sudo command not found. Please install.") def user_input_processor(self, evt=None): while self.running: diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 26668c19..625daf61 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -178,9 +178,7 @@ def backup_and_restore(mode: str, app: App): print() msg.logos_error("Cancelled with Ctrl+C.") t.join() - if config.DIALOG == 'tk': - app.root.event_generate('<>') - logging.info(f"Finished. {src_size} bytes copied to {str(dst_dir)}") + app.status(f"Finished {mode}. {src_size} bytes copied to {str(dst_dir)}") def copy_data(src_dirs, dst_dir): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index d7e9f0d6..05dff788 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -74,7 +74,7 @@ def spawn_dialog(): ) return answer - def _confirm(self, question: str, context: str | None = None) -> bool: + def approve(self, question: str, context: str | None = None) -> bool: return messagebox.askquestion(question, context) == 'yes' def exit(self, reason: str): @@ -341,6 +341,11 @@ def set_version(self, evt=None): self.start_ensure_config() + def get_logos_releases(self): + filtered_releases = network.get_logos_releases(self) + self.releases_q.put(filtered_releases) + self.root.event_generate(self.release_evt) + def start_releases_check(self): # Disable button; clear list. self.gui.release_check_button.state(['disabled']) @@ -357,7 +362,7 @@ def start_releases_check(self): self.gui.progress.start() self.gui.statusvar.set("Downloading Release list…") # Start thread. - utils.start_thread(network.get_logos_releases, app=self) + utils.start_thread(self.get_logos_releases) def set_release(self, evt=None): if self.gui.releasevar.get()[0] == 'C': # ignore default text @@ -739,11 +744,15 @@ def update_to_latest_lli_release(self, evt=None): self.gui.statusvar.set(f"Updating to latest {constants.APP_NAME} version…") # noqa: E501 utils.start_thread(utils.update_to_latest_lli_release, app=self) + def set_appimage_symlink(self): + utils.set_appimage_symlink(self) + self.update_latest_appimage_button() + def update_to_latest_appimage(self, evt=None): self.conf.wine_appimage_path = self.conf.wine_appimage_recommended_file_name # noqa: E501 self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest AppImage…") - utils.start_thread(utils.set_appimage_symlink, app=self) + utils.start_thread(self.set_appimage_symlink) def set_appimage(self, evt=None): # TODO: Separate as advanced feature. @@ -751,7 +760,7 @@ def set_appimage(self, evt=None): if not appimage_filename: return self.conf.wine_appimage_path = appimage_filename - utils.start_thread(utils.set_appimage_symlink, app=self) + utils.start_thread(self.set_appimage_symlink) def get_winetricks(self, evt=None): # TODO: Separate as advanced feature. diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index ce5187ce..0abe7127 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -123,39 +123,6 @@ def get_md5(self): return self.md5 -def cli_download(uri, destination, app=None): - message = f"Downloading '{uri}' to '{destination}'" - msg.status(message) - - # Set target. - if destination != destination.rstrip('/'): - target = os.path.join(destination, os.path.basename(uri)) - if not os.path.isdir(destination): - os.makedirs(destination) - elif os.path.isdir(destination): - target = os.path.join(destination, os.path.basename(uri)) - else: - target = destination - dirname = os.path.dirname(destination) - if not os.path.isdir(dirname): - os.makedirs(dirname) - - # Download from uri in thread while showing progress bar. - cli_queue = queue.Queue() - kwargs = {'q': cli_queue, 'target': target} - t = utils.start_thread(net_get, uri, **kwargs) - try: - while t.is_alive(): - sleep(0.1) - if cli_queue.empty(): - continue - utils.write_progress_bar(cli_queue.get()) - print() - except KeyboardInterrupt: - print() - msg.logos_error('Interrupted with Ctrl+C') - - def logos_reuse_download( sourceurl, file, @@ -191,17 +158,12 @@ def logos_reuse_download( logging.info(f"Incomplete file: {file_path}.") if found == 1: file_path = os.path.join(app.conf.download_dir, file) - if config.DIALOG == 'tk' and app: - # Ensure progress bar. - app.stop_indeterminate_progress() - # Start download. - net_get( - sourceurl, - target=file_path, - app=app, - ) - else: - cli_download(sourceurl, file_path, app=app) + # Start download. + net_get( + sourceurl, + target=file_path, + app=app, + ) if verify_downloaded_file( sourceurl, file_path, @@ -216,16 +178,15 @@ def logos_reuse_download( msg.logos_error(f"Bad file size or checksum: {file_path}") -def net_get(url, target=None, app=None, evt=None, q=None): - +# FIXME: refactor to raise rather than return None +def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): # TODO: # - Check available disk space before starting download logging.debug(f"Download source: {url}") logging.debug(f"Download destination: {target}") target = FileProps(target) # sets path and size attribs if app and target.path: - app.status_q.put(f"Downloading {target.path.name}…") # noqa: E501 - app.root.event_generate('<>') + app.status(f"Downloading {target.path.name}…") parsed_url = urlparse(url) domain = parsed_url.netloc # Gets the requested domain url = UrlProps(url) # uses requests to set headers, size, md5 attribs @@ -305,11 +266,8 @@ def net_get(url, target=None, app=None, evt=None, q=None): percent = round(local_size / total_size * 100) # if None not in [app, evt]: if app: - # Send progress value to tk window. - app.get_q.put(percent) - if not evt: - evt = app.get_evt - app.root.event_generate(evt) + # Send progress value to App + app.status("Downloading...", percent=percent) elif q is not None: # Send progress value to queue param. q.put(percent) @@ -453,13 +411,8 @@ def get_logos_releases(app: App) -> list[str]: url = f"https://clientservices.logos.com/update/v1/feed/logos{app.conf.faithlife_product_version}/stable.xml" # noqa: E501 response_xml_bytes = net_get(url) - # if response_xml is None and None not in [q, app]: if response_xml_bytes is None: - if app: - app.releases_q.put(None) - if config.DIALOG == 'tk': - app.root.event_generate(app.release_evt) - return None + raise Exception("Failed to get logos releases") # Parse XML root = ET.fromstring(response_xml_bytes.decode('utf-8-sig')) @@ -486,10 +439,6 @@ def get_logos_releases(app: App) -> list[str]: # logging.debug(f"Filtered releases: {', '.join(filtered_releases)}") filtered_releases = releases - if app: - if config.DIALOG == 'tk': - app.releases_q.put(filtered_releases) - app.root.event_generate(app.release_evt) return filtered_releases diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index ffdd0ddd..208333ba 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -749,9 +749,6 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 app.approve_or_exit(message + " \n" + detail) if not install_deps_failed and not manual_install_required: - # FIXME: why only for this dialog? - if config.DIALOG == 'cli': - command_str = command_str.replace("pkexec", "sudo") try: logging.debug(f"Attempting to run this command: {command_str}") run_command(command_str, shell=True) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 9296e9fe..3b004c32 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -83,7 +83,7 @@ def update_config_file(config_file_path, key, value): msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 -def die_if_running(): +def die_if_running(app: App): def remove_pid_file(): if os.path.exists(constants.PID_FILE): @@ -93,21 +93,7 @@ def remove_pid_file(): with open(constants.PID_FILE, 'r') as f: pid = f.read().strip() message = f"The script is already running on PID {pid}. Should it be killed to allow this instance to run?" # noqa: E501 - if config.DIALOG == "tk": - # TODO: With the GUI this runs in a thread. It's not clear if - # the messagebox will work correctly. It may need to be - # triggered from here with an event and then opened from the - # main thread. - tk_root = tk.Tk() - tk_root.withdraw() - confirm = tk.messagebox.askquestion("Confirmation", message) - tk_root.destroy() - elif config.DIALOG == "curses": - confirm = tui.confirm("Confirmation", message) - else: - confirm = msg.cli_question(message, "") - - if confirm: + if app.approve(message): os.kill(int(pid), signal.SIGKILL) atexit.register(remove_pid_file) @@ -304,17 +290,8 @@ def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 wine_binary_options = [] # Add AppImages to list - # if config.DIALOG == 'tk': wine_binary_options.append(f"{app.conf.installer_binary_dir}/{app.conf.wine_appimage_recommended_file_name}") # noqa: E501 wine_binary_options.extend(appimages) - # else: - # appimage_entries = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 - # wine_binary_options.append([ - # "Recommended", # Code - # f'{app.conf.installer_binary_directory}/{app.conf.wine_appimage_recommended_file_name}', # noqa: E501 - # f"AppImage of Wine64 {app.conf.wine_appimage_recommended_version}" # noqa: E501 - # ]) - # wine_binary_options.extend(appimage_entries) sorted_binaries = sorted(list(set(binaries))) logging.debug(f"{sorted_binaries=}") @@ -323,16 +300,7 @@ def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 code, description = get_winebin_code_and_desc(app, wine_binary_path) # noqa: E501 # Create wine binary option array - # if config.DIALOG == 'tk': wine_binary_options.append(wine_binary_path) - # else: - # wine_binary_options.append( - # [code, wine_binary_path, description] - # ) - # - # if config.DIALOG != 'tk': - # wine_binary_options.append(["Exit", "Exit", "Cancel installation."]) - logging.debug(f"{wine_binary_options=}") return wine_binary_options @@ -404,10 +372,7 @@ def write_progress_bar(percent, screen_width=80): l_f = int(screen_width * 0.75) # progress bar length l_y = int(l_f * percent / 100) # num. of chars. complete l_n = l_f - l_y # num. of chars. incomplete - if config.DIALOG == 'curses': - msg.status(f" [{y * l_y}{n * l_n}] {percent:>3}%") - else: - print(f" [{y * l_y}{n * l_n}] {percent:>3}%", end='\r') + msg.status(f" [{y * l_y}{n * l_n}] {percent:>3}%",end="\r\n") def find_installed_product(faithlife_product: str, wine_prefix: str) -> Optional[str]: @@ -420,6 +385,7 @@ def find_installed_product(faithlife_product: str, wine_prefix: str) -> Optional exe = str(root / f"{name}.exe") break return exe + return None def enough_disk_space(dest_dir, bytes_required): @@ -702,27 +668,12 @@ def set_appimage_symlink(app: App): msg.logos_error(f"Cannot use {selected_appimage_file_path}.") # Determine if user wants their AppImage in the app bin dir. - copy_message = ( + copy_question = ( f"Should the program copy {selected_appimage_file_path} to the" f" {app.conf.installer_binary_dir} directory?" ) - # XXX: move this to .ask - # FIXME: What if user cancels the confirmation dialog? - if config.DIALOG == "tk": - # TODO: With the GUI this runs in a thread. It's not clear if the - # messagebox will work correctly. It may need to be triggered from - # here with an event and then opened from the main thread. - tk_root = tk.Tk() - tk_root.withdraw() - confirm = tk.messagebox.askquestion("Confirmation", copy_message) - tk_root.destroy() - elif config.DIALOG in ['curses', 'dialog']: - confirm = tui.confirm("Confirmation", copy_message) - elif config.DIALOG == 'cli': - confirm = msg.logos_acknowledge_question(copy_message, '', '') - # Copy AppImage if confirmed. - if confirm is True or confirm == 'yes': + if app.approve(copy_question): logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 dest = appdir_bindir / selected_appimage_file_path.name if not dest.exists(): @@ -734,8 +685,6 @@ def set_appimage_symlink(app: App): app.conf.wine_appimage_path = f"{selected_appimage_file_path.name}" # noqa: E501 write_config(config.CONFIG_FILE) - if config.DIALOG == 'tk': - app.root.event_generate("<>") def update_to_latest_lli_release(app: App): From b4ca5fc88c591e35324b806fda13ef6649928bbe Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:34:36 -0800 Subject: [PATCH 058/137] refactor: more DIALOG --- ou_dedetai/control.py | 10 ++++------ ou_dedetai/gui_app.py | 34 +++++++++++++++++----------------- ou_dedetai/logos.py | 17 ++++------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 625daf61..79acff3a 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -97,12 +97,10 @@ def backup_and_restore(mode: str, app: App): msg.logos_warning(f"No files to {mode}", app=app) return - # FIXME: UI specific code - if config.DIALOG == 'curses': - if mode == 'backup': - app.screen_q.put(app.stack_text(8, app.todo_q, app.todo_e, "Backing up data…", wait=True)) - else: - app.screen_q.put(app.stack_text(8, app.todo_q, app.todo_e, "Restoring data…", wait=True)) + if mode == 'backup': + app.status("Backing up data…") + else: + app.status("Restoring data…") # Get source transfer size. q = queue.Queue() diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 05dff788..fc1f7bdb 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -631,8 +631,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.update_run_winetricks_button() self.logging_q = Queue() - self.logging_event = '<>' - self.root.bind(self.logging_event, self.update_logging_button) self.status_q = Queue() self.status_evt = '<>' self.root.bind(self.status_evt, self.update_status_text) @@ -675,8 +673,8 @@ def configure_app_button(self, evt=None): def run_installer(self, evt=None): classname = constants.BINARY_NAME - self.installer_win = Toplevel() - InstallerWindow(self.installer_win, self.root, app=self, class_=classname) + installer_window_top = Toplevel() + self.installer_window = InstallerWindow(installer_window_top, self.root, app=self, class_=classname) self.root.icon = self.conf.faithlife_product_icon_path def run_logos(self, evt=None): @@ -794,23 +792,19 @@ def switch_logging(self, evt=None): self.logos.switch_logging, action=desired_state.lower() ) - - def initialize_logging_button(self, evt=None): - self.gui.statusvar.set('') - self.gui.progress.stop() - self.gui.progress.state(['disabled']) - state = self.reverse_logging_state_value(self.logging_q.get()) - self.gui.loggingstatevar.set(state[:-1].title()) - self.gui.logging_button.state(['!disabled']) + + def _config_updated_hook(self) -> None: + self.update_logging_button() + if self.installer_window is not None: + self.installer_window._config_updated_hook() + return super()._config_updated_hook() def update_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() self.gui.progress.state(['disabled']) - new_state = self.reverse_logging_state_value(self.logging_q.get()) - new_text = new_state[:-1].title() - logging.debug(f"Updating app logging button text to: {new_text}") - self.gui.loggingstatevar.set(new_text) + state = self.reverse_logging_state_value(self.current_logging_state_value()) + self.gui.loggingstatevar.set(state[:-1].title()) self.gui.logging_button.state(['!disabled']) def update_app_button(self, evt=None): @@ -865,7 +859,13 @@ def update_run_winetricks_button(self, evt=None): state = 'disabled' self.gui.run_winetricks_button.state([state]) - def reverse_logging_state_value(self, state): + def current_logging_state_value(self) -> str: + if self.conf.faithlife_product_logging: + return 'ENABLED' + else: + return 'DISABLED' + + def reverse_logging_state_value(self, state) ->str: if state == 'DISABLED': return 'ENABLED' else: diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 91fcca53..88cde433 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -94,7 +94,8 @@ def run_logos(): else: wine.wineserver_kill(self.app.conf.wineserver_binary) app = self.app - if config.DIALOG == 'tk': + from ou_dedetai.gui_app import GuiApp + if isinstance(self.app, GuiApp): # Don't send "Running" message to GUI b/c it never clears. app = None msg.status(f"Running {app.conf.faithlife_product}…", app=app) @@ -103,7 +104,8 @@ def run_logos(): # Logos, but since wine logging is sent directly to wine.log, # there's no terminal output to see. A user can see that output by: # tail -f ~/.local/state/FaithLife-Community/wine.log - # if config.DIALOG == 'cli': + # from ou_dedetai.cli import CLI + # if isinstance(self.app, CLI): # run_logos() # self.monitor() # while config.processes.get(app.conf.logos_exe) is None: @@ -111,8 +113,6 @@ def run_logos(): # while self.logos_state != State.STOPPED: # time.sleep(0.1) # self.monitor() - # else: - # utils.start_thread(run_logos, daemon_bool=False) def stop(self): logging.debug("Stopping LogosManager.") @@ -228,12 +228,6 @@ def get_app_logging_state(self, init=False): ) if current_value == '0x1': state = 'ENABLED' - if config.DIALOG in ['curses', 'dialog', 'tk']: - self.app.logging_q.put(state) - if init: - self.app.root.event_generate('<>') - else: - self.app.root.event_generate('<>') return state def switch_logging(self, action=None): @@ -270,6 +264,3 @@ def switch_logging(self, action=None): wine.wait_pid(process) wine.wineserver_wait(app=self.app) self.app.conf.faithlife_product_logging = state - if config.DIALOG in ['curses', 'dialog', 'tk']: - self.app.logging_q.put(state) - self.app.root.event_generate(self.app.logging_event) From d18b907d9d6c99efc2de4b9c7fe491a2411413d4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 30 Nov 2024 01:58:24 -0800 Subject: [PATCH 059/137] fix: misc --- ou_dedetai/app.py | 13 +++++----- ou_dedetai/cli.py | 14 +++++----- ou_dedetai/config.py | 11 +++++--- ou_dedetai/gui_app.py | 4 +-- ou_dedetai/installer.py | 56 +++++++++++++++++++--------------------- ou_dedetai/msg.py | 2 +- ou_dedetai/system.py | 12 ++++----- ou_dedetai/tui_app.py | 8 ++++++ ou_dedetai/tui_curses.py | 42 ++++++++++++++---------------- ou_dedetai/utils.py | 14 ++-------- ou_dedetai/wine.py | 4 +-- 11 files changed, 86 insertions(+), 94 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 2e324a16..988cc215 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -56,10 +56,13 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: options = ["Yes", "No"] return self.ask(question, options) == "Yes" - def exit(self, reason: str): + def exit(self, reason: str, intended:bool=False): """Exits the application cleanly with a reason""" - logging.error(f"Cannot continue because {reason}") - sys.exit(1) + if intended: + sys.exit(0) + else: + logging.error(f"Cannot continue because {reason}") + sys.exit(1) _exit_option: Optional[str] = "Exit" @@ -88,10 +91,6 @@ def is_installed(self) -> bool: def status(self, message: str, percent: Optional[int] = None): """A status update""" - if percent: - # XXX: consider using utils.write_progress_bar - # Print out 20 periods or spaces proportional to progress - print("[" + "." * int(percent / 5) + " " * int((100 - percent) / 5) + "] ", end='') #noqa: E501 print(f"{message}") @property diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 7b99a12a..8f3d787e 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -36,8 +36,11 @@ def get_winetricks(self): control.set_winetricks(self) def install_app(self): + def install(app: CLI): + installer.install(app) + app.exit("Install has finished", intended=True) self.thread = utils.start_thread( - installer.install, + install, app=self ) self.user_input_processor() @@ -78,9 +81,6 @@ def run_winetricks(self): def set_appimage(self): utils.set_appimage_symlink(app=self) - def stop(self): - self.running = False - def toggle_app_logging(self): self.logos.switch_logging() @@ -113,13 +113,13 @@ def _ask(self, question: str, options: list[str] | str) -> str: output: str = self.choice_q.get() return output - def exit(self, reason: str): + def exit(self, reason: str, intended: bool = False): # Signal CLI.user_input_processor to stop. self.input_q.put(None) self.input_event.set() # Signal CLI itself to stop. - self.stop() - return super().exit(reason) + self.running = False + return super().exit(reason, intended) @property def superuser_command(self) -> str: diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 6fdf579d..beae2c51 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -205,7 +205,7 @@ class EphemeralConfiguration: # FIXME: seems like the wine appimage logic can be simplified wine_appimage_link_file_name: Optional[str] - """Syslink file name to the active wine appimage.""" + """Symlink file name to the active wine appimage.""" wine_appimage_path: Optional[str] """Path to the selected appimage""" @@ -597,7 +597,10 @@ def winetricks_binary(self) -> str: """ question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 options = utils.get_winetricks_options() - return self._ask_if_not_found("winetricks_binary", question, options) + output = self._ask_if_not_found("winetricks_binary", question, options) + if (Path(self.install_dir) / output).exists(): + return str(Path(self.install_dir) / output) + return output @winetricks_binary.setter def winetricks_binary(self, value: Optional[str | Path]): @@ -673,7 +676,9 @@ def wine_binary(self, value: str): @property def wine_binary_code(self) -> str: - """""" + """Wine binary code. + + One of: Recommended, AppImage, System, Proton, PlayOnLinux, Custom""" if self._raw.wine_binary_code is None: self._raw.wine_binary_code = utils.get_winebin_code_and_desc(self.app, self.wine_binary)[0] # noqa: E501 self._write() diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index fc1f7bdb..0a5c0cbe 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -77,9 +77,9 @@ def spawn_dialog(): def approve(self, question: str, context: str | None = None) -> bool: return messagebox.askquestion(question, context) == 'yes' - def exit(self, reason: str): + def exit(self, reason: str, intended: bool = False): self.root.destroy() - return super().exit(reason) + return super().exit(reason, intended) def superuser_command(self) -> str: diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index ebaca3ba..61713423 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -86,7 +86,6 @@ def ensure_winetricks_choice(app: App): update_install_feedback("Choose winetricks binary…", app=app) logging.debug('- config.WINETRICKSBIN') # Accessing the winetricks_binary variable will do this. - app.conf.winetricks_binary logging.debug(f"> config.WINETRICKSBIN={app.conf.winetricks_binary}") @@ -213,9 +212,7 @@ def ensure_wine_executables(app: App): logging.debug('- wine64') logging.debug('- wineserver') - if not os.access(app.conf.wine_binary, os.X_OK): - msg.status("Creating wine appimage symlinks…", app=app) - create_wine_appimage_symlinks(app=app) + create_wine_appimage_symlinks(app=app) # PATH is modified if wine appimage isn't found, but it's not modified # during a restarted installation, so shutil.which doesn't find the @@ -235,11 +232,8 @@ def ensure_winetricks_executable(app: App): app=app ) - if app.conf.winetricks_binary == constants.DOWNLOAD or not os.access(app.conf.winetricks_binary, os.X_OK): - # Either previous system winetricks is no longer accessible, or the - # or the user has chosen to download it. - msg.status("Downloading winetricks from the Internet…", app=app) - system.install_winetricks(app.conf.installer_binary_dir, app=app) + msg.status("Downloading winetricks from the Internet…", app=app) + system.install_winetricks(app.conf.installer_binary_dir, app=app) logging.debug(f"> {app.conf.winetricks_binary} is executable?: {os.access(app.conf.winetricks_binary, os.X_OK)}") # noqa: E501 return 0 @@ -480,39 +474,41 @@ def get_progress_pct(current, total): return round(current * 100 / total) -# FIXME: Consider moving the condition for whether to run this inside the function -# Right now the condition is outside def create_wine_appimage_symlinks(app: App): + app.status("Creating wine appimage symlinks…") appdir_bindir = Path(app.conf.installer_binary_dir) os.environ['PATH'] = f"{app.conf.installer_binary_dir}:{os.getenv('PATH')}" # Ensure AppImage symlink. appimage_link = appdir_bindir / app.conf.wine_appimage_link_file_name - if app.conf.wine_binary_code in ['AppImage', 'Recommended'] and app.conf.wine_appimage_path is not None: #noqa: E501 - appimage_file = Path(app.conf.wine_appimage_path) - appimage_filename = Path(app.conf.wine_appimage_path).name - # Ensure appimage is copied to appdir_bindir. - downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) #noqa: E501 - if not appimage_file.is_file(): - msg.status( - f"Copying: {downloaded_file} into: {appdir_bindir}", - app=app - ) - shutil.copy(downloaded_file, str(appdir_bindir)) - os.chmod(appimage_file, 0o755) - appimage_filename = appimage_file.name - elif app.conf.wine_binary_code in ["System", "Proton", "PlayOnLinux", "Custom"]: - appimage_filename = "none.AppImage" - else: - msg.logos_error( - f"WINEBIN_CODE error. WINEBIN_CODE is {app.conf.wine_binary_code}. Installation canceled!", # noqa: E501 + if app.conf.wine_binary_code not in ['AppImage', 'Recommended'] or app.conf.wine_appimage_path is None: #noqa: E501 + logging.debug("No need to symlink non-appimages") + return + + appimage_file = Path(app.conf.wine_appimage_path) + appimage_filename = Path(app.conf.wine_appimage_path).name + # Ensure appimage is copied to appdir_bindir. + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) #noqa: E501 + if downloaded_file is None: + logging.critical("Failed to get a valid wine appimage") + return + if Path(downloaded_file).parent != appdir_bindir: + msg.status( + f"Copying: {downloaded_file} into: {appdir_bindir}", app=app ) + shutil.copy(downloaded_file, appdir_bindir) + os.chmod(appimage_file, 0o755) + appimage_filename = appimage_file.name appimage_link.unlink(missing_ok=True) # remove & replace appimage_link.symlink_to(f"./{appimage_filename}") + # NOTE: if we symlink "winetricks" then the log is polluted with: + # "Executing: cd /tmp/.mount_winet.../bin" + (appdir_bindir / "winetricks").unlink(missing_ok=True) + # Ensure wine executables symlinks. - for name in ["wine", "wine64", "wineserver", "winetricks"]: + for name in ["wine", "wine64", "wineserver"]: p = appdir_bindir / name p.unlink(missing_ok=True) p.symlink_to(f"./{app.conf.wine_appimage_link_file_name}") diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index a4e43a08..06cab120 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -331,7 +331,7 @@ def progress(percent, app=None): logos_msg(get_progress_str(percent)) # provisional -# XXX: move this to app.update_progress +# XXX: move this to app.status def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): return msg[timestamp_length:] diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 208333ba..a3856998 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -685,9 +685,7 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 m = "Your distro requires manual dependency installation." logging.error(m) else: - if not app.approve_or_exit(message, secondary): - logging.debug("User refused to install packages. Exiting...") - return + app.approve_or_exit(message, secondary) preinstall_command = preinstall_dependencies(app.superuser_command) @@ -764,16 +762,16 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 if reboot_required: question = "The system has installed or removed a package that requires a reboot. Do you want to restart now?" # noqa: E501 if app.approve_or_exit(question): - reboot(app.superuser_command()) + reboot(app.superuser_command) else: logging.error("Please reboot then launch the installer again.") sys.exit(1) def install_winetricks( - installdir, - app: App, - version=constants.WINETRICKS_VERSION, + installdir, + app: App, + version=constants.WINETRICKS_VERSION, ): msg.status(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 97381b0b..31a40fbf 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -93,8 +93,16 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.main_window_ratio = self.main_window_ratio = self.menu_window_ratio = self.main_window_min = None self.menu_window_min = self.main_window_height = self.menu_window_height = self.main_window = None self.menu_window = self.resize_window = None + + # For menu dialogs. + # a new MenuDialog is created every loop, so we can't store it there. + self.current_option: int = 0 + self.current_page: int = 0 + self.total_pages: int = 0 + self.set_window_dimensions() + def set_window_dimensions(self): self.update_tty_dimensions() curses.resizeterm(self.window_height, self.window_width) diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 49f1531e..fd35aa9e 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -198,10 +198,6 @@ def __init__(self, app, question_text, options): self.question_start_y = None self.question_lines = None - self.current_option: int = 0 - self.current_page: int = 0 - self.total_pages: int = 0 - def __str__(self): return f"Menu Curses Dialog" @@ -214,7 +210,7 @@ def draw(self): # Display the options, centered options_start_y = self.question_start_y + len(self.question_lines) + 2 for i in range(self.app.options_per_page): - index = self.current_page * self.app.options_per_page + i + index = self.app.current_page * self.app.options_per_page + i if index < len(self.options): option = self.options[index] if type(option) is list: @@ -245,7 +241,7 @@ def draw(self): y = options_start_y + i + j x = max(0, self.app.window_width // 2 - len(line) // 2) if y < self.app.menu_window_height: - if index == self.current_option: + if index == self.app.current_option: write_line(self.app, self.stdscr, y, x, line, self.app.window_width, curses.A_REVERSE) else: write_line(self.app, self.stdscr, y, x, line, self.app.window_width) @@ -255,33 +251,33 @@ def draw(self): options_start_y += (len(option_lines)) # Display pagination information - page_info = f"Page {self.current_page + 1}/{self.total_pages} | Selected Option: {self.current_option + 1}/{len(self.options)}" + page_info = f"Page {self.app.current_page + 1}/{self.total_pages} | Selected Option: {self.app.current_option + 1}/{len(self.options)}" write_line(self.app, self.stdscr, max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) def do_menu_up(self): - if self.current_option == self.current_page * self.app.options_per_page and self.current_page > 0: + if self.app.current_option == self.app.current_page * self.app.options_per_page and self.app.current_page > 0: # Move to the previous page - self.current_page -= 1 - self.current_option = min(len(self.app.menu_options) - 1, (self.current_page + 1) * self.app.options_per_page - 1) - elif self.current_option == 0: + self.app.current_page -= 1 + self.app.current_option = min(len(self.app.menu_options) - 1, (self.app.current_page + 1) * self.app.options_per_page - 1) + elif self.app.current_option == 0: if self.total_pages == 1: - self.current_option = len(self.app.menu_options) - 1 + self.app.current_option = len(self.app.menu_options) - 1 else: - self.current_page = self.total_pages - 1 - self.current_option = len(self.app.menu_options) - 1 + self.app.current_page = self.total_pages - 1 + self.app.current_option = len(self.app.menu_options) - 1 else: - self.current_option = max(0, self.current_option - 1) + self.app.current_option = max(0, self.app.current_option - 1) def do_menu_down(self): - if self.current_option == (self.current_page + 1) * self.app.options_per_page - 1 and self.current_page < self.total_pages - 1: + if self.app.current_option == (self.app.current_page + 1) * self.app.options_per_page - 1 and self.app.current_page < self.total_pages - 1: # Move to the next page - self.current_page += 1 - self.current_option = min(len(self.app.menu_options) - 1, self.current_page * self.app.options_per_page) - elif self.current_option == len(self.app.menu_options) - 1: - self.current_page = 0 - self.current_option = 0 + self.app.current_page += 1 + self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_page * self.app.options_per_page) + elif self.app.current_option == len(self.app.menu_options) - 1: + self.app.current_page = 0 + self.app.current_option = 0 else: - self.current_option = min(len(self.app.menu_options) - 1, self.current_option + 1) + self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_option + 1) def input(self): if len(self.app.tui_screens) > 0: @@ -306,7 +302,7 @@ def input(self): elif final_key == 66: self.do_menu_down() elif key == ord('\n') or key == 10: # Enter key - self.user_input = self.options[self.current_option] + self.user_input = self.options[self.app.current_option] elif key == ord('\x1b'): signal.signal(signal.SIGINT, self.app.end) else: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 3b004c32..17fce28d 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -16,11 +16,10 @@ import tarfile import threading import time -import tkinter as tk from ou_dedetai.app import App from packaging import version from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Tuple from . import config from . import constants @@ -366,15 +365,6 @@ def get_procs_using_file(file_path, mode=None): # logging.info("* End of wait_process_using_dir.") -def write_progress_bar(percent, screen_width=80): - y = '.' - n = ' ' - l_f = int(screen_width * 0.75) # progress bar length - l_y = int(l_f * percent / 100) # num. of chars. complete - l_n = l_f - l_y # num. of chars. incomplete - msg.status(f" [{y * l_y}{n * l_n}] {percent:>3}%",end="\r\n") - - def find_installed_product(faithlife_product: str, wine_prefix: str) -> Optional[str]: if faithlife_product and wine_prefix: drive_c = Path(f"{wine_prefix}/drive_c/") @@ -645,7 +635,7 @@ def set_appimage_symlink(app: App): logging.debug("AppImage commands disabled since we're not using an appimage") # noqa: E501 return if app.conf.wine_appimage_path is None: - logging.debug("No need to set appimage syslink, as it wasn't set") + logging.debug("No need to set appimage symlink, as it wasn't set") return logging.debug(f"config.APPIMAGE_FILE_PATH={app.conf.wine_appimage_path}") diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 71054606..615cb42f 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -17,7 +17,6 @@ from . import system from . import utils -from .config import processes def check_wineserver(wineserver_binary: str): try: @@ -43,7 +42,7 @@ def wineserver_wait(wineserver_binary: str): def end_wine_processes(): - for process_name, process in processes.items(): + for process_name, process in config.processes.items(): if isinstance(process, subprocess.Popen): logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 try: @@ -396,6 +395,7 @@ def set_renderer(app: App, renderer: str): def set_win_version(app: App, exe: str, windows_version: str): if exe == "logos": run_winetricks_cmd(app, '-q', 'settings', f'{windows_version}') + elif exe == "indexer": reg = f"HKCU\\Software\\Wine\\AppDefaults\\{app.conf.faithlife_product}Indexer.exe" # noqa: E501 exe_args = [ From 3a28e3fadfe48166c03adf52193659c980a8cb96 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:27:53 -0800 Subject: [PATCH 060/137] refactor: migrate config.processes --- ou_dedetai/app.py | 3 +++ ou_dedetai/cli.py | 1 - ou_dedetai/config.py | 1 - ou_dedetai/gui_app.py | 1 - ou_dedetai/logos.py | 61 ++++++++++++++++++++++++++++--------------- ou_dedetai/main.py | 6 ----- ou_dedetai/system.py | 13 +++------ ou_dedetai/tui_app.py | 1 - ou_dedetai/wine.py | 34 ++++++------------------ 9 files changed, 55 insertions(+), 66 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 988cc215..9081c66e 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -18,6 +18,8 @@ def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise it would be a circular import from ou_dedetai.config import Config self.conf = Config(config, self) + from ou_dedetai.logos import LogosManager + self.logos = LogosManager(app=self) pass def ask(self, question: str, options: list[str]) -> str: @@ -58,6 +60,7 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: def exit(self, reason: str, intended:bool=False): """Exits the application cleanly with a reason""" + self.logos.end_processes() if intended: sys.exit(0) else: diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 8f3d787e..6a96fa0c 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -21,7 +21,6 @@ def __init__(self, ephemeral_config: EphemeralConfiguration): self.input_q = queue.Queue() self.input_event = threading.Event() self.choice_event = threading.Event() - self.logos = logos.LogosManager(app=self) def backup(self): control.backup(app=self) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index beae2c51..6c3aa985 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -25,7 +25,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' console_log = [] -processes = {} threads = [] diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 0a5c0cbe..5245c4ab 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -574,7 +574,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.root.resizable(False, False) self.gui = gui.ControlGui(self.root, app=self) self.actioncmd = None - self.logos = logos.LogosManager(app=self) text = self.gui.update_lli_label.cget('text') ver = constants.LLI_CURRENT_VERSION diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 88cde433..b3a95d2c 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -1,3 +1,6 @@ +import os +import signal +import subprocess import time from enum import Enum import logging @@ -26,19 +29,23 @@ def __init__(self, app: App): self.logos_state = State.STOPPED self.indexing_state = State.STOPPED self.app = app + self.processes: dict[str, subprocess.Popen] = {} + """These are sub-processes we started""" + self.existing_processes: dict[str, list[psutil.Process]] = {} + """These are processes we discovered already running""" def monitor_indexing(self): - if self.app.conf.logos_indexer_exe in config.processes: - indexer = config.processes.get(self.app.conf.logos_indexer_exe) + if self.app.conf.logos_indexer_exe in self.existing_processes: + indexer = self.processes.get(self.app.conf.logos_indexer_exe) if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): # noqa: E501 self.indexing_state = State.RUNNING else: self.indexing_state = State.STOPPED def monitor_logos(self): - splash = config.processes.get(self.app.conf.logos_exe, []) - login = config.processes.get(self.app.conf.logos_login_exe, []) - cef = config.processes.get(self.app.conf.logos_cef_exe, []) + splash = self.existing_processes.get(self.app.conf.logos_exe, []) + login = self.existing_processes.get(self.app.conf.logos_login_exe, []) + cef = self.existing_processes.get(self.app.conf.logos_cef_exe, []) splash_running = splash[0].is_running() if splash else False login_running = login[0].is_running() if login else False @@ -62,9 +69,15 @@ def monitor_logos(self): if cef_running: self.logos_state = State.RUNNING + def get_logos_pids(self): + app = self.app + self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) + self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) # noqa: E501 + self.existing_processes[app.conf.logos_cef_exe] = system.get_pids(app.conf.logos_cef_exe) # noqa: E501 + def monitor(self): if self.app.is_installed(): - system.get_logos_pids(self.app) + self.get_logos_pids() try: self.monitor_indexing() self.monitor_logos() @@ -77,11 +90,13 @@ def start(self): wine_release, _ = wine.get_wine_release(self.app.conf.wine_binary) def run_logos(): - wine.run_wine_proc( + process = wine.run_wine_proc( self.app.conf.wine_binary, self.app, exe=self.app.conf.logos_exe ) + if isinstance(process, subprocess.Popen): + self.processes[self.app.conf.logos_exe] = process # Ensure wine version is compatible with Logos release version. good_wine, reason = wine.check_wine_rules( @@ -108,7 +123,7 @@ def run_logos(): # if isinstance(self.app, CLI): # run_logos() # self.monitor() - # while config.processes.get(app.conf.logos_exe) is None: + # while self.processes.get(app.conf.logos_exe) is None: # time.sleep(0.1) # while self.logos_state != State.STOPPED: # time.sleep(0.1) @@ -124,7 +139,7 @@ def stop(self): self.app.conf.logos_login_exe, self.app.conf.logos_cef_exe ]: - process_list = config.processes.get(process_name) + process_list = self.processes.get(process_name) if process_list: pids.extend([str(process.pid) for process in process_list]) else: @@ -142,15 +157,28 @@ def stop(self): self.logos_state = State.STOPPED wine.wineserver_wait(self.app) + def end_processes(self): + for process_name, process in self.processes.items(): + if isinstance(process, subprocess.Popen): + logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 + try: + process.terminate() + process.wait(timeout=10) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGTERM) + system.wait_pid(process) + def index(self): self.indexing_state = State.STARTING index_finished = threading.Event() def run_indexing(): - wine.run_wine_proc( + process = wine.run_wine_proc( self.app.conf.wine_binary, exe=self.app.conf.logos_indexer_exe ) + if isinstance(process, subprocess.Popen): + self.processes[self.app.conf.logos_indexer_exe] = process def check_if_indexing(process): start_time = time.time() @@ -179,29 +207,20 @@ def wait_on_indexing(): msg.status("Indexing has begun…", self.app) index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING - # If we don't wait the process won't yet be launched when we try to - # pull it from config.processes. - while config.processes.get(self.app.conf.logos_indexer_exe) is None: - time.sleep(0.1) - logging.debug(f"{config.processes=}") - process = config.processes[self.app.conf.logos_indexer_exe] check_thread = utils.start_thread( check_if_indexing, - process, + index_thread, daemon_bool=False ) wait_thread = utils.start_thread(wait_on_indexing, daemon_bool=False) main.threads.extend([index_thread, check_thread, wait_thread]) - config.processes[self.app.conf.logos_indexer_exe] = index_thread - config.processes["check_if_indexing"] = check_thread - config.processes["wait_on_indexing"] = wait_thread def stop_indexing(self): self.indexing_state = State.STOPPING if self.app: pids = [] for process_name in [self.app.conf.logos_indexer_exe]: - process_list = config.processes.get(process_name) + process_list = self.processes.get(process_name) if process_list: pids.extend([str(process.pid) for process in process_list]) else: diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index afde5275..01ee1261 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -478,12 +478,6 @@ def close(): # Only wait on non-daemon threads. if not thread.daemon: thread.join() - # Only kill wine processes if closing the Control Panel. Otherwise, some - # CLI commands get killed as soon as they're started. - if config.ACTION.__name__ == 'run_control_panel' and len(processes) > 0: - wine.end_wine_processes() - else: - logging.debug("No extra processes found.") logging.debug(f"Closing {constants.APP_NAME} finished.") diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index a3856998..5cf3a5bd 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -188,7 +188,7 @@ def popen_command(command, retries=1, delay=0, **kwargs): return None -def get_pids(query): +def get_pids(query) -> list[psutil.Process]: results = [] for process in psutil.process_iter(['pid', 'name', 'cmdline']): try: @@ -199,14 +199,6 @@ def get_pids(query): return results -# XXX: should this be in config? -def get_logos_pids(app: App): - config.processes[app.conf.logos_exe] = get_pids(app.conf.logos_exe) - config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) - config.processes[app.conf.logos_cef_exe] = get_pids(app.conf.logos_cef_exe) - config.processes[app.conf.logos_indexer_exe] = get_pids(app.conf.logos_indexer_exe) # noqa: E501 - - def reboot(superuser_command: str): logging.info("Rebooting system.") command = f"{superuser_command} reboot now" @@ -795,3 +787,6 @@ def install_winetricks( os.chmod(f"{installdir}/winetricks", 0o755) app.conf.winetricks_binary = f"{installdir}/winetricks" logging.debug("Winetricks installed.") + +def wait_pid(process): + os.waitpid(-process.pid, 0) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 31a40fbf..2129a8cd 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -40,7 +40,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.console_message = "Starting TUI…" self.llirunning = True self.active_progress = False - self.logos = logos.LogosManager(app=self) self.tmp = "" # Generic ask/response events/threads diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 615cb42f..9546dbd8 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -23,7 +23,7 @@ def check_wineserver(wineserver_binary: str): # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of WINESERVER_EXE # changed it to use wineserver_binary, this change may alter the behavior, to match what the code intended process = run_wine_proc(wineserver_binary, exe_args=["-p"]) - wait_pid(process) + system.wait_pid(process) return process.returncode == 0 except Exception: return False @@ -32,25 +32,13 @@ def check_wineserver(wineserver_binary: str): def wineserver_kill(wineserver_binary: str): if check_wineserver(wineserver_binary): process = run_wine_proc(wineserver_binary, exe_args=["-k"]) - wait_pid(process) + system.wait_pid(process) def wineserver_wait(wineserver_binary: str): if check_wineserver(wineserver_binary): process = run_wine_proc(wineserver_binary, exe_args=["-w"]) - wait_pid(process) - - -def end_wine_processes(): - for process_name, process in config.processes.items(): - if isinstance(process, subprocess.Popen): - logging.debug(f"Found {process_name} in Processes. Attempting to close {process}.") # noqa: E501 - try: - process.terminate() - process.wait(timeout=10) - except subprocess.TimeoutExpired: - os.killpg(process.pid, signal.SIGTERM) - wait_pid(process) + system.wait_pid(process) # FIXME: consider raising exceptions on error @@ -229,8 +217,8 @@ def wine_reg_install(app: App, reg_file, wine64_binary): exe="regedit.exe", exe_args=[reg_file] ) - # NOTE: For some reason wait_pid results in the reg install failing. - # wait_pid(process) + # NOTE: For some reason system.wait_pid results in the reg install failing. + # system.wait_pid(process) process.wait() if process is None or process.returncode != 0: failed = "Failed to install reg file" @@ -265,10 +253,6 @@ def install_msi(app: App): return process -def wait_pid(process): - os.waitpid(-process.pid, 0) - - def get_winecmd_encoding(app: App) -> Optional[str]: # Get wine system's cmd.exe encoding for proper decoding to UTF8 later. logging.debug("Getting wine system's cmd.exe encoding.") @@ -319,8 +303,6 @@ def run_wine_proc( start_new_session=True ) if process is not None: - if exe is not None and isinstance(process, subprocess.Popen): - config.processes[exe] = process if process.poll() is None and process.stdout is not None: with process.stdout: for line in iter(process.stdout.readline, b''): @@ -346,7 +328,7 @@ def run_wine_proc( def run_winetricks(app: App, cmd=None): process = run_wine_proc(app.conf.winetricks_binary, exe=cmd) - wait_pid(process) + system.wait_pid(process) wineserver_wait(app) # XXX: this function looks similar to the one above. duplicate? @@ -358,7 +340,7 @@ def run_winetricks_cmd(app: App, *args): msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) - wait_pid(process) + system.wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") wineserver_wait(app) logging.debug(f"procs using {app.conf.wine_prefix}:") @@ -411,7 +393,7 @@ def set_win_version(app: App, exe: str, windows_version: str): exe='reg', exe_args=exe_args ) - wait_pid(process) + system.wait_pid(process) # FIXME: consider when to run this (in the update case) From 818b063ef4cf29378724e5391693a12f89e2ea65 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:32:46 -0800 Subject: [PATCH 061/137] chore: remove unused --- ou_dedetai/config.py | 2 -- ou_dedetai/gui_app.py | 35 +++++++++++++++++------------------ ou_dedetai/logos.py | 3 +-- ou_dedetai/main.py | 5 +---- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 6c3aa985..3c50f7fb 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -3,10 +3,8 @@ from dataclasses import dataclass import json import logging -import os from pathlib import Path import time -from typing import Optional from ou_dedetai import msg, network, utils, constants, wine diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 5245c4ab..c76b0839 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -20,12 +20,10 @@ from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE from ou_dedetai.config import EphemeralConfiguration -from . import config from . import constants from . import control from . import gui from . import installer -from . import logos from . import network from . import system from . import utils @@ -53,7 +51,7 @@ def spawn_dialog(): utils.start_thread(spawn_dialog) answer_event.wait() - answer = answer_q.get() + answer: Optional[str] = answer_q.get() if answer is None: self.root.destroy() return None @@ -81,7 +79,7 @@ def exit(self, reason: str, intended: bool = False): self.root.destroy() return super().exit(reason, intended) - + @property def superuser_command(self) -> str: """Command when root privileges are needed. @@ -189,7 +187,7 @@ def on_cancel_released(self, evt=None): class InstallerWindow(GuiApp): def __init__(self, new_win, root: Root, app: App, **kwargs): - super().__init__(root) + super().__init__(root, app.conf._overrides) # Set root parameters. self.win = new_win self.root = root @@ -268,15 +266,17 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): self.get_winetricks_options() def _config_updated_hook(self): - """Update the GUI to reflect changes in the configuration if they were prompted separately""" - # The configuration enforces dependencies, if product is unset, so will it's dependents (version and release) - # XXX: test this hook. Interesting thing is, this may never be called in production, as it's only called (presently) when the separate prompt returns + """Update the GUI to reflect changes in the configuration if they were prompted separately""" #noqa: E501 + # The configuration enforces dependencies, if product is unset, so will it's + # dependents (version and release) + # XXX: test this hook. Interesting thing is, this may never be called in + # production, as it's only called (presently) when the separate prompt returns # Returns either from config or the dropdown - self.gui.productvar.set(self.conf._raw.faithlife_product or self.gui.product_dropdown['values'][0]) - self.gui.versionvar.set(self.conf._raw.faithlife_product_version or self.gui.version_dropdown['values'][-1]) - self.gui.releasevar.set(self.conf._raw.faithlife_product_release or self.gui.release_dropdown['values'][0]) - # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' - self.gui.winevar.set(self.conf._raw.wine_binary or next(iter(self.gui.wine_dropdown['values']), '')) + self.gui.productvar.set(self.conf._raw.faithlife_product or self.gui.product_dropdown['values'][0]) #noqa: E501 + self.gui.versionvar.set(self.conf._raw.faithlife_product_version or self.gui.version_dropdown['values'][-1]) #noqa: E501 + self.gui.releasevar.set(self.conf._raw.faithlife_product_release or self.gui.release_dropdown['values'][0]) #noqa: E501 + # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' #noqa: E501 + self.gui.winevar.set(self.conf._raw.wine_binary or next(iter(self.gui.wine_dropdown['values']), '')) #noqa: E501 def start_ensure_config(self): # Ensure progress counter is reset. @@ -459,7 +459,7 @@ def set_skip_fonts(self, evt=None): def set_skip_dependencies(self, evt=None): self.conf.skip_install_system_dependencies = 1 - self.gui.skipdepsvar.get() # invert True/False # noqa: E501 - logging.debug(f"> config.SKIP_DEPENDENCIES={self.conf.skip_install_system_dependencies}") + logging.debug(f"> config.SKIP_DEPENDENCIES={self.conf.skip_install_system_dependencies}") #noqa: E501 def on_okay_released(self, evt=None): # Update desktop panel icon. @@ -534,7 +534,7 @@ def update_download_progress(self, evt=None): def step_start(self, evt=None): progress = self.progress_q.get() - if not type(progress) is int: + if type(progress) is not int: return if progress >= 100: self.gui.progressvar.set(0) @@ -808,7 +808,7 @@ def update_logging_button(self, evt=None): def update_app_button(self, evt=None): self.gui.app_button.state(['!disabled']) - # XXX: we may need another hook here to update the product version should it change + # XXX: we may need another hook here to update the product version should it change #noqa: E501 self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.configure_app_button() self.update_run_winetricks_button() @@ -879,7 +879,7 @@ def update_download_progress(self, evt=None): def step_start(self, evt=None): progress = self.progress_q.get() - if not type(progress) is int: + if type(progress) is not int: return if progress >= 100: self.gui.progressvar.set(0) @@ -904,7 +904,6 @@ def start_indeterminate_progress(self, evt=None): def control_panel_app(ephemeral_config: EphemeralConfiguration): - utils.set_debug() classname = constants.BINARY_NAME root = Root(className=classname) ControlWindow(root, ephemeral_config, class_=classname) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index b3a95d2c..a649dba6 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -9,7 +9,6 @@ from ou_dedetai.app import App -from . import config from . import main from . import msg from . import system @@ -71,7 +70,7 @@ def monitor_logos(self): def get_logos_pids(self): app = self.app - self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) + self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) # noqa: E501 self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) # noqa: E501 self.existing_processes[app.conf.logos_cef_exe] = system.get_pids(app.conf.logos_cef_exe) # noqa: E501 diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 01ee1261..c92e1840 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -15,17 +15,14 @@ from . import cli from . import config -from . import control from . import constants from . import gui_app from . import msg -from . import network from . import system from . import tui_app from . import utils -from . import wine -from .config import processes, threads +from .config import threads def get_parser(): From 92bf4658c90f37fd89b05eaa13e58784a33b93c0 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:57:40 -0800 Subject: [PATCH 062/137] fix: misc --- ou_dedetai/gui_app.py | 32 ++++++++++++++++++++++++++------ ou_dedetai/installer.py | 6 +++--- ou_dedetai/logos.py | 22 ++++++++++++++-------- ou_dedetai/main.py | 7 +++++++ ou_dedetai/system.py | 3 ++- ou_dedetai/wine.py | 12 +++++++----- 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index c76b0839..b471329c 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -474,6 +474,7 @@ def start_install_thread(self, evt=None): self.gui.progress.config(mode='determinate') utils.start_thread(installer.install, app=self) + # XXX: where should this live? here or ControlWindow? def status(self, message: str, percent: int | None = None): if percent: self.gui.progress.stop() @@ -652,11 +653,9 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.get_evt = "<>" self.root.bind(self.get_evt, self.update_download_progress) - # Start function to determine app logging state. - if self.is_installed(): - self.gui.statusvar.set('Getting current app logging status…') - self.start_indeterminate_progress() - utils.start_thread(self.logos.get_app_logging_state) + self.installer_window = None + + self.update_logging_button() def edit_config(self): control.edit_file(self.conf.config_file_path) @@ -724,7 +723,7 @@ def run_restore(self, evt=None): def install_deps(self, evt=None): self.start_indeterminate_progress() - utils.start_thread(utils.install_dependencies) + utils.start_thread(utils.install_dependencies, self) def open_file_dialog(self, filetype_name, filetype_extension): file_path = fd.askopenfilename( @@ -798,6 +797,21 @@ def _config_updated_hook(self) -> None: self.installer_window._config_updated_hook() return super()._config_updated_hook() + # XXX: should this live here or in installerWindow? + def status(self, message: str, percent: int | None = None): + if percent: + self.gui.progress.stop() + self.gui.progress.state(['disabled']) + self.gui.progress.config(mode='determinate') + self.gui.progressvar.set(percent) + else: + self.gui.progress.state(['!disabled']) + self.gui.progressvar.set(0) + self.gui.progress.config(mode='indeterminate') + self.gui.progress.start() + self.gui.statusvar.set(message) + super().status(message, percent) + def update_logging_button(self, evt=None): self.gui.statusvar.set('') self.gui.progress.stop() @@ -851,6 +865,12 @@ def update_latest_appimage_button(self, evt=None): self.stop_indeterminate_progress() self.gui.latest_appimage_button.state([state]) + def stop_indeterminate_progress(self, evt=None): + self.gui.progress.stop() + self.gui.progress.state(['disabled']) + self.gui.progress.config(mode='determinate') + self.gui.progressvar.set(0) + def update_run_winetricks_button(self, evt=None): if utils.file_exists(self.conf.winetricks_binary): state = '!disabled' diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 61713423..379e51a5 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -313,8 +313,8 @@ def ensure_wineprefix_init(app: App): ) else: logging.debug("Initializing wineprefix.") - process = wine.initializeWineBottle(app.conf.wine64_binary) - wine.wait_pid(process) + process = wine.initializeWineBottle(app.conf.wine64_binary, app) + system.wait_pid(process) # wine.light_wineserver_wait() wine.wineserver_wait(app) logging.debug("Wine init complete.") @@ -397,7 +397,7 @@ def ensure_product_installed(app: App): if not app.is_installed(): process = wine.install_msi(app) - wine.wait_pid(process) + system.wait_pid(process) # Clean up temp files, etc. utils.clean_all() diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index a649dba6..258f3724 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -112,7 +112,7 @@ def run_logos(): if isinstance(self.app, GuiApp): # Don't send "Running" message to GUI b/c it never clears. app = None - msg.status(f"Running {app.conf.faithlife_product}…", app=app) + msg.status(f"Running {self.app.conf.faithlife_product}…", app=app) utils.start_thread(run_logos, daemon_bool=False) # NOTE: The following code would keep the CLI open while running # Logos, but since wine logging is sent directly to wine.log, @@ -174,6 +174,7 @@ def index(self): def run_indexing(): process = wine.run_wine_proc( self.app.conf.wine_binary, + app=self.app, exe=self.app.conf.logos_indexer_exe ) if isinstance(process, subprocess.Popen): @@ -239,11 +240,15 @@ def stop_indexing(self): def get_app_logging_state(self, init=False): state = 'DISABLED' - current_value = wine.get_registry_value( - 'HKCU\\Software\\Logos4\\Logging', - 'Enabled', - self.app - ) + try: + current_value = wine.get_registry_value( + 'HKCU\\Software\\Logos4\\Logging', + 'Enabled', + self.app + ) + except Exception as e: + logging.warning(f"Failed to determine if logging was enabled, assuming no: {e}") #noqa: E501 + current_value = None if current_value == '0x1': state = 'ENABLED' return state @@ -276,9 +281,10 @@ def switch_logging(self, action=None): ] process = wine.run_wine_proc( self.app.conf.wine_binary, + app=self.app, exe='reg', exe_args=exe_args ) - wine.wait_pid(process) - wine.wineserver_wait(app=self.app) + system.wait_pid(process) + wine.wineserver_wait(self.app.conf.wineserver_binary) self.app.conf.faithlife_product_logging = state diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index c92e1840..e69df526 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse import curses +import logging.handlers from ou_dedetai.config import EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path @@ -210,6 +211,12 @@ def parse_args(args, parser) -> EphemeralConfiguration: if args.debug: msg.update_log_level(logging.DEBUG) + # Also add stdout for debugging purposes + stdout_h = logging.StreamHandler(sys.stdout) + stdout_h.name = "terminal" + stdout_h.setLevel(logging.DEBUG) + stdout_h.addFilter(msg.DeduplicateFilter()) + logging.root.addHandler(stdout_h) if args.delete_log: ephemeral_config.delete_log = True diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 5cf3a5bd..96699b75 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -94,7 +94,8 @@ def run_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.Co ) return result except subprocess.CalledProcessError as e: - logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}") # noqa: E501 + logging.error(f"Error occurred in run_command() while executing \"{command}\": {e}.") # noqa: E501 + logging.debug(f"Command failed with output:\n{e.stdout}\nand stderr:\n{e.stderr}") #noqa: E501 if "lock" in str(e): logging.debug(f"Database appears to be locked. Retrying in {delay} seconds…") # noqa: E501 time.sleep(delay) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 9546dbd8..5d790bab 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -17,7 +17,7 @@ from . import system from . import utils - +# XXX: fix lingering lack of refs to app def check_wineserver(wineserver_binary: str): try: # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of WINESERVER_EXE @@ -37,7 +37,7 @@ def wineserver_kill(wineserver_binary: str): def wineserver_wait(wineserver_binary: str): if check_wineserver(wineserver_binary): - process = run_wine_proc(wineserver_binary, exe_args=["-w"]) + process = run_wine_proc(wineserver_binary, app, exe_args=["-w"]) system.wait_pid(process) @@ -193,7 +193,7 @@ def check_wine_version_and_branch(release_version, test_binary, faithlife_produc return True, "None" -def initializeWineBottle(wine64_binary: str): +def initializeWineBottle(wine64_binary: str, app: App): msg.status("Initializing wine bottle…") logging.debug(f"{wine64_binary=}") # Avoid wine-mono window @@ -201,6 +201,7 @@ def initializeWineBottle(wine64_binary: str): logging.debug(f"Running: {wine64_binary} wineboot --init") process = run_wine_proc( wine64_binary, + app=app, exe='wineboot', exe_args=['--init'], init=True, @@ -214,6 +215,7 @@ def wine_reg_install(app: App, reg_file, wine64_binary): msg.status(f"Installing registry file: {reg_file}") process = run_wine_proc( wine64_binary, + app=app, exe="regedit.exe", exe_args=[reg_file] ) @@ -327,7 +329,7 @@ def run_wine_proc( def run_winetricks(app: App, cmd=None): - process = run_wine_proc(app.conf.winetricks_binary, exe=cmd) + process = run_wine_proc(app.conf.winetricks_binary, app=app, exe=cmd) system.wait_pid(process) wineserver_wait(app) @@ -446,7 +448,7 @@ def get_registry_value(reg_path, name, app: App): 'reg', 'query', reg_path, '/v', name, ] err_msg = f"Failed to get registry value: {reg_path}\\{name}" - encoding = app.conf.wine_output_encoding + encoding = app.conf._wine_output_encoding if encoding is None: encoding = 'UTF-8' try: From d07c92fc159de2e52ebc40c9a3a3ce324c451828 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:39:31 -0800 Subject: [PATCH 063/137] chore: add comments for additional work --- ou_dedetai/config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index c17e0ff2..245e04e7 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -433,6 +433,10 @@ class Config: # Overriding programmatically generated values from ENV _overrides: EphemeralConfiguration + # XXX: Move this to it's own class/file. + # And check cache for all operations in network + # (similar to this struct but in network) + # Start Cache of values unlikely to change during operation. # i.e. filesystem traversals _logos_exe: Optional[str] = None @@ -474,7 +478,7 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") - + def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 # XXX: should this also update the feedback? if not getattr(self._raw, parameter): @@ -496,6 +500,8 @@ def _write(self) -> None: self._raw.write_config() self.app._config_updated_hook() + # XXX: Add a reload command to resolve #168 (at least plumb the backend) + @property def config_file_path(self) -> str: return LegacyConfiguration.config_file_path() From be9f468bb68d129e3ea092d029559d361d6b5d87 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:55:49 -0800 Subject: [PATCH 064/137] docs: add comment to architecture code --- ou_dedetai/system.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 6d2933c9..866be7c0 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -233,10 +233,17 @@ def get_dialog(): config.DIALOG = 'tk' -def get_architecture(): +def get_architecture() -> Tuple[str, int]: + """Queries the device and returns which cpu architure and bits is supported + + Returns: + architecture: x86_64 x86_32 or """ machine = platform.machine().lower() bits = struct.calcsize("P") * 8 + # FIXME: consider conforming to a standard for the architecture name + # normally see arm64 in lowercase for example and risc as riscv64 on + # debian's support architectures for example https://wiki.debian.org/SupportedArchitectures if "x86_64" in machine or "amd64" in machine: architecture = "x86_64" elif "i386" in machine or "i686" in machine: From d6c26a45978bb8f1f1a002bdd4f3ac6459d3b536 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 04:57:58 -0800 Subject: [PATCH 065/137] fix: migrate new code to config --- ou_dedetai/config.py | 3 --- ou_dedetai/main.py | 2 -- ou_dedetai/system.py | 19 ++++++++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 245e04e7..3473830c 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -22,9 +22,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' -architecture = None -bits = None -ELFPACKAGES = None console_log = [] threads = [] diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 6d58c964..ca06410f 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -380,8 +380,6 @@ def set_dialog(): logging.debug(f"Use Python Dialog?: {config.use_python_dialog}") # Set Architecture - config.architecture, config.bits = system.get_architecture() - logging.debug(f"Current Architecture: {config.architecture}, {config.bits}bit.") system.check_architecture() diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 866be7c0..ed15b841 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -269,7 +269,8 @@ def install_elf_interpreter(): # ELF interpreter between box64, FEX-EMU, and hangover. That or else we have to pursue a particular interpreter # for the install routine, depending on what's needed logging.critical("ELF interpretation is not yet coded in the installer.") - # if "x86_64" not in config.architecture: + # architecture, bits = get_architecture() + # if "x86_64" not in architecture: # if config.ELFPACKAGES is not None: # utils.install_packages(config.ELFPACKAGES) # else: @@ -280,21 +281,23 @@ def install_elf_interpreter(): def check_architecture(): - if "x86_64" in config.architecture: + architecture, bits = get_architecture() + logging.debug(f"Current Architecture: {architecture}, {bits}bit.") + if "x86_64" in architecture: pass - elif "ARM64" in config.architecture: + elif "ARM64" in architecture: logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") install_elf_interpreter() - elif "RISC-V 64" in config.architecture: + elif "RISC-V 64" in architecture: logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") install_elf_interpreter() - elif "x86_32" in config.architecture: + elif "x86_32" in architecture: logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") install_elf_interpreter() - elif "ARM32" in config.architecture: + elif "ARM32" in architecture: logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") install_elf_interpreter() - elif "RISC-V 32" in config.architecture: + elif "RISC-V 32" in architecture: logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") install_elf_interpreter() else: @@ -347,6 +350,8 @@ class PackageManager: logos_9_packages: str incompatible_packages: str + # For future expansion: + # elf_packages: str def get_package_manager() -> PackageManager | None: From c566637077245b327c5f32902875f00ff224ca10 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 05:36:57 -0800 Subject: [PATCH 066/137] fix: validate responses from _ask --- ou_dedetai/app.py | 53 ++++++++++++++++++++++++++++++++++++------- ou_dedetai/cli.py | 2 +- ou_dedetai/control.py | 23 ++++++++----------- ou_dedetai/gui_app.py | 3 --- ou_dedetai/msg.py | 7 ++++++ ou_dedetai/tui_app.py | 8 +++---- 6 files changed, 65 insertions(+), 31 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 9081c66e..9f4bdb79 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -2,8 +2,9 @@ import abc import logging import os +from pathlib import Path import sys -from typing import Optional +from typing import NoReturn, Optional from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -29,6 +30,31 @@ def ask(self, question: str, options: list[str]) -> str: If the internal ask function returns None, the process will exit with 1 """ + def validate_result(answer: str, options: list[str]) -> Optional[str]: + special_cases = set([PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE]) + # These constants have special meaning, don't worry about them to start with + simple_options = list(set(options) - special_cases) + # This MUST have the same indexes as above + simple_options_lower = [opt.lower() for opt in simple_options] + + # Case sensitive check first + if answer in simple_options: + return answer + # Also do a case insensitive match, no reason to fail due to casing + if answer.lower() in simple_options_lower: + # Return the correct casing to simplify the parsing of the ask result + return simple_options[simple_options.index(answer.lower())] + + # Now check the special cases + if PROMPT_OPTION_FILE in options and Path(answer).is_file(): + return answer + if PROMPT_OPTION_DIRECTORY in options and Path(answer).is_dir(): + return answer + + # Not valid + return None + + passed_options: list[str] | str = options if len(passed_options) == 1 and ( PROMPT_OPTION_DIRECTORY in passed_options @@ -38,12 +64,25 @@ def ask(self, question: str, options: list[str]) -> str: passed_options = options[0] elif passed_options is not None and self._exit_option is not None: passed_options = options + [self._exit_option] + answer = self._ask(question, passed_options) + while answer is None or validate_result(answer, options) is not None: + invalid_response = "That response is not valid, please try again." + new_question = f"{invalid_response}\n{question}" + answer = self._ask(new_question, passed_options) + + if answer is not None: + answer = validate_result(answer, options) + if answer is None: + # Huh? coding error, this should have been checked earlier + logging.critical("An invalid response slipped by, please report this incident to the developers") #noqa: E501 + self.exit("Failed to get a valid value from user") + if answer == self._exit_option: answer = None if answer is None: - exit(1) + self.exit("Failed to get a valid value from user") return answer @@ -58,7 +97,7 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: options = ["Yes", "No"] return self.ask(question, options) == "Yes" - def exit(self, reason: str, intended:bool=False): + def exit(self, reason: str, intended:bool=False) -> NoReturn: """Exits the application cleanly with a reason""" self.logos.end_processes() if intended: @@ -74,14 +113,12 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: """Implementation for asking a question pre-front end Options may include ability to prompt for an additional value. - Implementations MUST handle the follow up prompt before returning + Such as asking for one of strings or a directory. + If the user selects choose a new directory, the + implementations MUST handle the follow up prompt before returning Options may be a single value, Implementations MUST handle this single option being a follow up prompt - - If you would otherwise return None, consider shutting down cleanly, - the calling function will exit the process with an error code of one - if this function returns None """ raise NotImplementedError() diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 6a96fa0c..d3a17b65 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -108,8 +108,8 @@ def _ask(self, question: str, options: list[str] | str) -> str: self.input_event.set() self.choice_event.wait() self.choice_event.clear() - # XXX: This is always a freeform input, perhaps we should have some sort of validation? output: str = self.choice_q.get() + # NOTE: this response is validated in App's .ask return output def exit(self, reason: str, intended: bool = False): diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 79acff3a..235cfaea 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -46,14 +46,10 @@ def backup_and_restore(mode: str, app: App): pass # user confirms in GUI or TUI else: verb = 'Use' if mode == 'backup' else 'Restore backup from' - if not msg.cli_question(f"{verb} existing backups folder \"{app.conf.backup_dir}\"?", ""): # noqa: E501 - answer = None - while answer is None or (mode == 'restore' and not answer.is_dir()): # noqa: E501 - answer = msg.cli_ask_filepath("Please provide a backups folder path:") - answer = Path(answer).expanduser().resolve() - if not answer.is_dir(): - msg.status(f"Not a valid folder path: {answer}", app=app) - config.app.conf.backup_directory = answer + if not app.approve(f"{verb} existing backups folder \"{app.conf.backup_dir}\"?"): #noqa: E501 + # Reset backup dir. + # The app will re-prompt next time the backup_dir is accessed + app.conf._raw.backup_dir = None # Set source folders. backup_dir = Path(app.conf.backup_dir) @@ -186,14 +182,13 @@ def copy_data(src_dirs, dst_dir): def remove_install_dir(app: App): folder = Path(app.conf.install_dir) - if ( - folder.is_dir() - and msg.cli_question(f"Delete \"{folder}\" and all its contents?") - ): + question = f"Delete \"{folder}\" and all its contents?" + if not folder.is_dir(): + logging.info(f"Folder doesn't exist: {folder}") + return + if app.approve(question): shutil.rmtree(folder) logging.warning(f"Deleted folder and all its contents: {folder}") - else: - logging.info(f"Folder doesn't exist: {folder}") def remove_all_index_files(app: App): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index b471329c..7ab65752 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -52,9 +52,6 @@ def spawn_dialog(): answer_event.wait() answer: Optional[str] = answer_q.get() - if answer is None: - self.root.destroy() - return None elif isinstance(options, str): answer = options diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 06cab120..0f8d39ed 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -148,6 +148,7 @@ def logos_msg(message, end='\n'): cli_msg(message, end) +# XXX: remove in favor of app.status("message", percent) def logos_progress(): if config.DIALOG == 'curses': pass @@ -163,12 +164,14 @@ def logos_progress(): def logos_warn(message): + # XXX: shouldn't this always use logging.warning? if config.DIALOG == 'curses': logging.warning(message) else: logos_msg(message) +# XXX: move this to app as... message? def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' @@ -237,6 +240,7 @@ def logos_warning(message, secondary=None, detail=None, app=None, parent=None): logging.error(message) +# XXX: remove in favor of app.ask def cli_question(question_text, secondary=""): while True: try: @@ -281,6 +285,7 @@ def cli_ask_filepath(question_text): logos_error("Cancelled with Ctrl+C") +# XXX: remove in favor of confirm_or_die def logos_continue_question(question_text, no_text, secondary, app=None): if config.DIALOG == 'tk': gui_continue_question(question_text, no_text, secondary) @@ -302,6 +307,7 @@ def logos_continue_question(question_text, no_text, secondary, app=None): logos_error(f"Unhandled question: {question_text}") +# XXX: remove in favor of confirm def logos_acknowledge_question(question_text, no_text, secondary): if config.DIALOG == 'curses': pass @@ -316,6 +322,7 @@ def get_progress_str(percent): return f"[{'*' * part_done}{'-' * part_left}]" +# XXX: remove in favor of app.status def progress(percent, app=None): """Updates progressbar values for TUI and GUI.""" if config.DIALOG == 'tk' and app: diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 2129a8cd..7f68f7da 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -617,11 +617,9 @@ def switch_screen(self, dialog): def _ask(self, question: str, options: list[str] | str) -> Optional[str]: if isinstance(options, str): answer = options - - if isinstance(options, list): - options = self.which_dialog_options(options, config.use_python_dialog) - self.menu_options = options - self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, options, dialog=config.use_python_dialog)) + elif isinstance(options, list): + self.menu_options = self.which_dialog_options(options, config.use_python_dialog) + self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, self.menu_options, dialog=config.use_python_dialog)) #noqa: E501 # Now wait for it to complete self.ask_answer_event.wait() From ffe13623f0a077102b8e5e0c58f447a7601170a4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 05:44:46 -0800 Subject: [PATCH 067/137] refactor: migrate msg questions into app logos_continue_question, logos_acknowledge_question --- ou_dedetai/app.py | 9 ++++-- ou_dedetai/control.py | 27 +++++----------- ou_dedetai/main.py | 16 --------- ou_dedetai/msg.py | 75 ------------------------------------------- ou_dedetai/system.py | 13 ++++++++ 5 files changed, 27 insertions(+), 113 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 9f4bdb79..5802eefe 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -16,12 +16,15 @@ class App(abc.ABC): """Step the installer is on. Starts at 0""" def __init__(self, config, **kwargs) -> None: - # This lazy load is required otherwise it would be a circular import + # This lazy load is required otherwise these would be circular imports from ou_dedetai.config import Config - self.conf = Config(config, self) from ou_dedetai.logos import LogosManager + from ou_dedetai.system import check_incompatibilities + + self.conf = Config(config, self) self.logos = LogosManager(app=self) - pass + # Ensure everything is good to start + check_incompatibilities(self) def ask(self, question: str, options: list[str]) -> str: """Asks the user a question with a list of supplied options diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 235cfaea..64d111fa 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -65,25 +65,14 @@ def backup_and_restore(mode: str, app: App): if mode == 'restore': restore_dir = utils.get_latest_folder(app.conf.backup_dir) restore_dir = Path(restore_dir).expanduser().resolve() - if config.DIALOG == 'tk': - pass - elif config.DIALOG == 'curses': - app.screen_q.put(app.stack_confirm(24, app.todo_q, app.todo_e, - f"Restore most-recent backup?: {restore_dir}", "", "", - dialog=config.use_python_dialog)) - app.todo_e.wait() # Wait for TUI to confirm restore_dir - app.todo_e.clear() - if app.tmp == "No": - question = "Please choose a different restore folder path:" - app.screen_q.put(app.stack_input(25, app.todo_q, app.todo_e, question, f"{restore_dir}", - dialog=config.use_python_dialog)) - app.todo_e.wait() - app.todo_e.clear() - restore_dir = Path(app.tmp).expanduser().resolve() - else: - # Offer to restore the most recent backup. - if not msg.cli_question(f"Restore most-recent backup?: {restore_dir}", ""): # noqa: E501 - restore_dir = msg.cli_ask_filepath("Path to backup set that you want to restore:") # noqa: E501 + # FIXME: Shouldn't this prompt this prompt the list of backups? + # Rather than forcing the latest + # Offer to restore the most recent backup. + if not app.approve(f"Restore most-recent backup?: {restore_dir}", ""): # noqa: E501 + # Reset and re-prompt + app.conf._raw.backup_dir = None + restore_dir = utils.get_latest_folder(app.conf.backup_dir) + restore_dir = Path(restore_dir).expanduser().resolve() source_dir_base = restore_dir else: source_dir_base = Path(app.conf.logos_exe).parent diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index ca06410f..5007a1db 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -383,20 +383,6 @@ def set_dialog(): system.check_architecture() -def check_incompatibilities(): - # Check for AppImageLauncher - if shutil.which('AppImageLauncher'): - question_text = "Remove AppImageLauncher? A reboot will be required." - secondary = ( - "Your system currently has AppImageLauncher installed.\n" - f"{constants.APP_NAME} is not compatible with AppImageLauncher.\n" - f"For more information, see: {constants.REPOSITORY_LINK}/issues/114" - ) - no_text = "User declined to remove AppImageLauncher." - msg.logos_continue_question(question_text, no_text, secondary) - system.remove_appimagelauncher() - - def is_app_installed(ephemeral_config: EphemeralConfiguration): persistent_config = PersistentConfiguration.load_from_path(ephemeral_config.config_path) if persistent_config.faithlife_product is None or persistent_config.install_dir is None: @@ -474,8 +460,6 @@ def main(): # Print terminal banner logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 - check_incompatibilities() - run(ephemeral_config) diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 0f8d39ed..407a2bf3 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -240,81 +240,6 @@ def logos_warning(message, secondary=None, detail=None, app=None, parent=None): logging.error(message) -# XXX: remove in favor of app.ask -def cli_question(question_text, secondary=""): - while True: - try: - cli_msg(secondary) - yn = input(f"{question_text} [Y/n]: ") - except KeyboardInterrupt: - print() - logos_error("Cancelled with Ctrl+C") - - if yn.lower() == 'y' or yn == '': # defaults to "Yes" - return True - elif yn.lower() == 'n': - return False - else: - logos_msg("Type Y[es] or N[o].") - - -def cli_continue_question(question_text, no_text, secondary): - if not cli_question(question_text, secondary): - logos_error(no_text) - - -def gui_continue_question(question_text, no_text, secondary): - if ask_question(question_text, secondary) == 'no': - logos_error(no_text) - - -def cli_acknowledge_question(question_text, no_text, secondary): - if not cli_question(question_text, secondary): - logos_msg(no_text) - return False - else: - return True - - -def cli_ask_filepath(question_text): - try: - answer = input(f"{question_text} ") - return answer.strip('"').strip("'") - except KeyboardInterrupt: - print() - logos_error("Cancelled with Ctrl+C") - - -# XXX: remove in favor of confirm_or_die -def logos_continue_question(question_text, no_text, secondary, app=None): - if config.DIALOG == 'tk': - gui_continue_question(question_text, no_text, secondary) - elif config.DIALOG == 'cli': - cli_continue_question(question_text, no_text, secondary) - elif config.DIALOG == 'curses': - app.screen_q.put( - app.stack_confirm( - 16, - app.confirm_q, - app.confirm_e, - question_text, - no_text, - secondary, - dialog=config.use_python_dialog - ) - ) - else: - logos_error(f"Unhandled question: {question_text}") - - -# XXX: remove in favor of confirm -def logos_acknowledge_question(question_text, no_text, secondary): - if config.DIALOG == 'curses': - pass - else: - return cli_acknowledge_question(question_text, no_text, secondary) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index ed15b841..0005f8bd 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -900,3 +900,16 @@ def install_winetricks( def wait_pid(process): os.waitpid(-process.pid, 0) + + +def check_incompatibilities(app: App): + # Check for AppImageLauncher + if shutil.which('AppImageLauncher'): + question_text = "Remove AppImageLauncher? A reboot will be required." + secondary = ( + "Your system currently has AppImageLauncher installed.\n" + f"{constants.APP_NAME} is not compatible with AppImageLauncher.\n" + f"For more information, see: {constants.REPOSITORY_LINK}/issues/114" + ) + app.approve_or_exit(question_text, secondary) + remove_appimagelauncher(app) \ No newline at end of file From dbcfffa834d7eeb1a7b78ca6c93fc0bedffcbdd7 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:02:34 -0800 Subject: [PATCH 068/137] refactor: msg.logos_error --- ou_dedetai/app.py | 14 ++++++++++++-- ou_dedetai/config.py | 2 +- ou_dedetai/control.py | 7 ++++--- ou_dedetai/gui_app.py | 4 ++++ ou_dedetai/logos.py | 2 +- ou_dedetai/main.py | 9 ++++++--- ou_dedetai/msg.py | 39 +-------------------------------------- ou_dedetai/network.py | 13 +++---------- ou_dedetai/system.py | 19 ++++++++----------- ou_dedetai/utils.py | 11 ++--------- ou_dedetai/wine.py | 2 +- 11 files changed, 43 insertions(+), 79 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 5802eefe..2979b921 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -6,6 +6,7 @@ import sys from typing import NoReturn, Optional +from ou_dedetai import constants from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -101,12 +102,21 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: return self.ask(question, options) == "Yes" def exit(self, reason: str, intended:bool=False) -> NoReturn: - """Exits the application cleanly with a reason""" + """Exits the application cleanly with a reason.""" + # XXX: print out support information + + # Shutdown logos/indexer if we spawned it self.logos.end_processes() + # Remove pid file if exists + try: + os.remove(constants.PID_FILE) + except FileNotFoundError: # no pid file when testing functions + pass + # exit from the process if intended: sys.exit(0) else: - logging.error(f"Cannot continue because {reason}") + logging.critical(f"Cannot continue because {reason}") sys.exit(1) _exit_option: Optional[str] = "Exit" diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 3473830c..5b4142e6 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -399,7 +399,7 @@ def write_config(self) -> None: json.dump(output, config_file, indent=4, sort_keys=True) config_file.write('\n') except IOError as e: - msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + logging.error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 # Continue, the installer can still operate even if it fails to write. diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 64d111fa..5c045e41 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -98,7 +98,7 @@ def backup_and_restore(mode: str, app: App): print() except KeyboardInterrupt: print() - msg.logos_error("Cancelled with Ctrl+C.", app=app) + app.exit("Cancelled with Ctrl+C.") t.join() src_size = q.get() if src_size == 0: @@ -123,7 +123,8 @@ def backup_and_restore(mode: str, app: App): try: dst_dir.mkdir() except FileExistsError: - msg.logos_error(f"Backup already exists: {dst_dir}.") + # This shouldn't happen, there is a timestamp in the backup_dir name + app.exit(f"Backup already exists: {dst_dir}.") # Verify disk space. if not utils.enough_disk_space(dst_dir, src_size): @@ -159,7 +160,7 @@ def backup_and_restore(mode: str, app: App): print() except KeyboardInterrupt: print() - msg.logos_error("Cancelled with Ctrl+C.") + app.exit("Cancelled with Ctrl+C.") t.join() app.status(f"Finished {mode}. {src_size} bytes copied to {str(dst_dir)}") diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 7ab65752..f3548bda 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -73,6 +73,10 @@ def approve(self, question: str, context: str | None = None) -> bool: return messagebox.askquestion(question, context) == 'yes' def exit(self, reason: str, intended: bool = False): + # Create a little dialog before we die so the user can see why this happened + if not intended: + # XXX: add support information + gui.show_error(reason, fatal=True) self.root.destroy() return super().exit(reason, intended) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 368b7059..44b89acd 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -104,7 +104,7 @@ def run_logos(): self.app.conf.faithlife_product_version ) if not good_wine: - msg.logos_error(reason, app=self) + self.app.exit(reason) else: if reason is not None: logging.debug(f"Warning: Wine Check: {reason}") diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 5007a1db..d6a51f98 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -395,7 +395,8 @@ def is_app_installed(ephemeral_config: EphemeralConfiguration): def run(ephemeral_config: EphemeralConfiguration): # Run desired action (requested function, defaults to control_panel) if config.ACTION == "disabled": - msg.logos_error("That option is disabled.", "info") + print("That option is disabled.", file=sys.stderr) + sys.exit(1) if config.ACTION.__name__ == 'run_control_panel': # if utils.app_is_installed(): # wine.set_logos_paths() @@ -430,7 +431,8 @@ def run(ephemeral_config: EphemeralConfiguration): logging.info(f"Running function: {config.ACTION.__name__}") # noqa: E501 config.ACTION(ephemeral_config) else: # install_required, but app not installed - msg.logos_error("App not installed…") + print("App not installed, but required for this operation. Consider installing first.", file=sys.stderr) #noqa: E501 + sys.exit(1) def main(): @@ -455,7 +457,8 @@ def main(): # program. # utils.die_if_running() if os.getuid() == 0 and not ephemeral_config.app_run_as_root_permitted: - msg.logos_error("Running Wine/winetricks as root is highly discouraged. Use -f|--force-root if you must run as root. See https://wiki.winehq.org/FAQ#Should_I_run_Wine_as_root.3F") # noqa: E501 + print("Running Wine/winetricks as root is highly discouraged. Use -f|--force-root if you must run as root. See https://wiki.winehq.org/FAQ#Should_I_run_Wine_as_root.3F", file=sys.stderr) # noqa: E501 + sys.exit(1) # Print terminal banner logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 407a2bf3..c31562c1 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -175,6 +175,7 @@ def logos_warn(message): def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' + # XXX: move these to constants and output them on error WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" TELEGRAM_LINK = "https://t.me/linux_logos" MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" @@ -197,44 +198,6 @@ def ui_message(message, secondary=None, detail=None, app=None, parent=None, fata logos_msg(message) -# TODO: I think detail is doing the same thing as secondary. -def logos_error(message: str, secondary=None, detail=None, app=None, parent=None): - # if detail is None: - # detail = '' - # WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" - # TELEGRAM_LINK = "https://t.me/linux_logos" - # MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" - # help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 - # if config.DIALOG == 'tk': - # show_error( - # message, - # detail=f"{detail}\n\n{help_message}", - # app=app, - # parent=parent - # ) - # elif config.DIALOG == 'curses': - # if secondary != "info": - # status(message) - # status(help_message) - # else: - # logos_msg(message) - # else: - # logos_msg(message) - ui_message(message, secondary=secondary, detail=detail, app=app, parent=parent, fatal=True) # noqa: E501 - - logging.critical(message) - if secondary is None or secondary == "": - try: - os.remove(constants.PID_FILE) - except FileNotFoundError: # no pid file when testing functions - pass - os.kill(os.getpgid(os.getpid()), signal.SIGKILL) - - if hasattr(app, 'destroy'): - app.destroy() - sys.exit(1) - - def logos_warning(message, secondary=None, detail=None, app=None, parent=None): ui_message(message, secondary=secondary, detail=detail, app=app, parent=parent) # noqa: E501 logging.error(message) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 0abe7127..1e94a5fd 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -81,10 +81,8 @@ def get_headers(self): except Exception as e: logging.error(e) return None - except KeyboardInterrupt: - print() - msg.logos_error("Interrupted by Ctrl+C") - return None + # XXX: should we have a more generic catch for KeyboardInterrupt rather than deep in this function? #noqa: E501 + # except KeyboardInterrupt: self.headers = r.headers return self.headers @@ -175,7 +173,7 @@ def logos_reuse_download( except shutil.SameFileError: pass else: - msg.logos_error(f"Bad file size or checksum: {file_path}") + app.exit(f"Bad file size or checksum: {file_path}") # FIXME: refactor to raise rather than return None @@ -274,11 +272,6 @@ def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): except requests.exceptions.RequestException as e: logging.error(f"Error occurred during HTTP request: {e}") return None # Return None values to indicate an error condition - except Exception as e: - msg.logos_error(e) - except KeyboardInterrupt: - print() - msg.logos_error("Killed with Ctrl+C") def verify_downloaded_file(url, file_path, app: Optional[App]=None): diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 0005f8bd..7f026a6b 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -218,14 +218,16 @@ def reboot(superuser_command: str): def get_dialog(): if not os.environ.get('DISPLAY'): - msg.logos_error("The installer does not work unless you are running a display") # noqa: E501 + print("The installer does not work unless you are running a display", file=sys.stderr) # noqa: E501 + sys.exit(1) dialog = os.getenv('DIALOG') # Set config.DIALOG. if dialog is not None: dialog = dialog.lower() if dialog not in ['cli', 'curses', 'tk']: - msg.logos_error("Valid values for DIALOG are 'cli', 'curses' or 'tk'.") # noqa: E501 + print("Valid values for DIALOG are 'cli', 'curses' or 'tk'.", file=sys.stderr) # noqa: E501 + sys.exit(1) config.DIALOG = dialog elif sys.__stdin__.isatty(): config.DIALOG = 'curses' @@ -489,9 +491,7 @@ def get_package_manager() -> PackageManager | None: incompatible_packages = "" # appimagelauncher handled separately else: # Add more conditions for other package managers as needed. - error = "Your package manager is not yet supported. Please contact the developers." - msg.logos_error(error) # noqa: E501 - return None + logging.critical("Your package manager is not yet supported. Please contact the developers.") output = PackageManager( install=install_command, @@ -635,8 +635,7 @@ def remove_appimagelauncher(app: App): pkg = "appimagelauncher" package_manager = get_package_manager() if package_manager is None: - msg.logos_error("Failed to find the package manager to uninstall AppImageLauncher.") - sys.exit(1) + app.exit("Failed to find the package manager to uninstall AppImageLauncher.") cmd = [app.superuser_command, *package_manager.remove, pkg] # noqa: E501 try: logging.debug(f"Running command: {cmd}") @@ -647,8 +646,7 @@ def remove_appimagelauncher(app: App): else: logging.error(f"An error occurred: {e}") logging.error(f"Command output: {e.output}") - msg.logos_error("Failed to uninstall AppImageLauncher.") - sys.exit(1) + app.exit(f"Failed to uninstall AppImageLauncher: {e}") logging.info("System reboot is required.") sys.exit() @@ -740,10 +738,9 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 os_name, _ = get_os() if not package_manager: - msg.logos_error( + app.exit( f"The script could not determine your {os_name} install's package manager or it is unsupported." # noqa: E501 ) - # XXX: raise error or exit? return package_list = package_manager.packages.split() diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 17fce28d..9a9bf4e2 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -79,7 +79,7 @@ def update_config_file(config_file_path, key, value): json.dump(config_data, f, indent=4, sort_keys=True) f.write('\n') except IOError as e: - msg.logos_error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 + raise (f"Error writing to config file {config_file_path}: {e}") from e # noqa: E501 def die_if_running(app: App): @@ -120,13 +120,6 @@ def clean_all(): logging.info("done") -def mkdir_critical(directory): - try: - os.mkdir(directory) - except OSError: - msg.logos_error(f"Can't create the {directory} directory") - - def get_user_downloads_dir() -> str: home = Path.home() xdg_config = Path(os.getenv('XDG_CONFIG_HOME', home / '.config')) @@ -655,7 +648,7 @@ def set_appimage_symlink(app: App): selected_appimage_file_path = appimage_file_path # Verify user-selected AppImage. if not check_appimage(selected_appimage_file_path): - msg.logos_error(f"Cannot use {selected_appimage_file_path}.") + app.exit(f"Cannot use {selected_appimage_file_path}.") # Determine if user wants their AppImage in the app bin dir. copy_question = ( diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 1ea5d82b..cece943c 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -228,7 +228,7 @@ def wine_reg_install(app: App, reg_file, wine64_binary): if process is None or process.returncode != 0: failed = "Failed to install reg file" logging.debug(f"{failed}. {process=}") - msg.logos_error(f"{failed}: {reg_file}") + app.exit(f"{failed}: {reg_file}") elif process.returncode == 0: logging.info(f"{reg_file} installed.") wineserver_wait(app) From 17f6ac9f89e047385f6cd5d2ea2094ceb9577b21 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:19:52 -0800 Subject: [PATCH 069/137] refactor: replace logos_warning with app.exit This is a harder failure than before, but in all cases the operation failed It's possible there is another class of error here for operation failed rather than terminating the entire application. Future work --- ou_dedetai/app.py | 10 ++++++---- ou_dedetai/constants.py | 12 +++++++++--- ou_dedetai/control.py | 10 ++++------ ou_dedetai/gui_app.py | 3 +-- ou_dedetai/installer.py | 16 ++-------------- ou_dedetai/msg.py | 11 +---------- 6 files changed, 23 insertions(+), 39 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 2979b921..bcaad665 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -101,10 +101,8 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: options = ["Yes", "No"] return self.ask(question, options) == "Yes" - def exit(self, reason: str, intended:bool=False) -> NoReturn: + def exit(self, reason: str, intended: bool = False) -> NoReturn: """Exits the application cleanly with a reason.""" - # XXX: print out support information - # Shutdown logos/indexer if we spawned it self.logos.end_processes() # Remove pid file if exists @@ -116,7 +114,7 @@ def exit(self, reason: str, intended:bool=False) -> NoReturn: if intended: sys.exit(0) else: - logging.critical(f"Cannot continue because {reason}") + logging.critical(f"Cannot continue because {reason}\n{constants.SUPPORT_MESSAGE}") #noqa: E501 sys.exit(1) _exit_option: Optional[str] = "Exit" @@ -142,6 +140,10 @@ def is_installed(self) -> bool: return os.access(self.conf.logos_exe, os.X_OK) return False + # def message(self, message: str): + # """Show the user a message in their native UI""" + # print(message) + def status(self, message: str, percent: Optional[int] = None): """A status update""" print(f"{message}") diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 76bbdc19..3d89937f 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -2,14 +2,18 @@ import os from pathlib import Path +# This is relative to this file itself +APP_IMAGE_DIR = Path(__file__).parent / "img" + # Define app name variables. APP_NAME = 'Ou Dedetai' BINARY_NAME = 'oudedetai' PACKAGE_NAME = 'ou_dedetai' -REPOSITORY_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller" -# This is relative to this file itself -APP_IMAGE_DIR = Path(__file__).parent / "img" +REPOSITORY_LINK = "https://github.com/FaithLife-Community/LogosLinuxInstaller" +WIKI_LINK = f"{REPOSITORY_LINK}/wiki" +TELEGRAM_LINK = "https://t.me/linux_logos" +MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" CACHE_LIFETIME_HOURS = 12 """How long to wait before considering our version cache invalid""" @@ -34,6 +38,8 @@ PID_FILE = f'/tmp/{BINARY_NAME}.pid' WINETRICKS_VERSION = '20220411' +SUPPORT_MESSAGE = f"If you need help, please consult:\n{WIKI_LINK}\nIf that doesn't answer your question, please send the following files {DEFAULT_CONFIG_PATH}, {DEFAULT_APP_WINE_LOG_PATH} and {DEFAULT_APP_LOG_PATH} to one of the following group chats:\nTelegram: {TELEGRAM_LINK}\nMatrix: {MATRIX_LINK}" # noqa: E501 + # Strings for choosing a follow up file or directory PROMPT_OPTION_DIRECTORY = "Choose Directory" PROMPT_OPTION_FILE = "Choose File" diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 5c045e41..e1b52dae 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -59,8 +59,7 @@ def backup_and_restore(mode: str, app: App): verb = 'access' if mode == 'backup': verb = 'create' - msg.logos_warning(f"Can't {verb} folder: {backup_dir}") - return + app.exit(f"Can't {verb} folder: {backup_dir}") if mode == 'restore': restore_dir = utils.get_latest_folder(app.conf.backup_dir) @@ -79,7 +78,7 @@ def backup_and_restore(mode: str, app: App): src_dirs = [source_dir_base / d for d in data_dirs if Path(source_dir_base / d).is_dir()] # noqa: E501 logging.debug(f"{src_dirs=}") if not src_dirs: - msg.logos_warning(f"No files to {mode}", app=app) + app.exit(f"No files to {mode}") return if mode == 'backup': @@ -102,7 +101,7 @@ def backup_and_restore(mode: str, app: App): t.join() src_size = q.get() if src_size == 0: - msg.logos_warning(f"Nothing to {mode}!", app=app) + app.exit(f"Nothing to {mode}!") return # Set destination folder. @@ -129,8 +128,7 @@ def backup_and_restore(mode: str, app: App): # Verify disk space. if not utils.enough_disk_space(dst_dir, src_size): dst_dir.rmdir() - msg.logos_warning(f"Not enough free disk space for {mode}.", app=app) - return + app.exit(f"Not enough free disk space for {mode}.") # Run file transfer. if mode == 'restore': diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index f3548bda..15dc3568 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -75,8 +75,7 @@ def approve(self, question: str, context: str | None = None) -> bool: def exit(self, reason: str, intended: bool = False): # Create a little dialog before we die so the user can see why this happened if not intended: - # XXX: add support information - gui.show_error(reason, fatal=True) + gui.show_error(reason, detail=constants.SUPPORT_MESSAGE, fatal=True) self.root.destroy() return super().exit(reason, intended) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 379e51a5..50fd2b75 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -542,22 +542,11 @@ def create_launcher_shortcuts(app: App): # Set variables for use in launcher files. flproduct = app.conf.faithlife_product installdir = Path(app.conf.install_dir) - m = "Can't create launchers" - if flproduct is None: - reason = "because the FaithLife product is not defined." - msg.logos_warning(f"{m} {reason}") # noqa: E501 - return logos_icon_src = constants.APP_IMAGE_DIR / f"{flproduct}-128-icon.png" app_icon_src = constants.APP_IMAGE_DIR / 'icon.png' - if installdir is None: - reason = "because the installation folder is not defined." - msg.logos_warning(f"{m} {reason}") - return if not installdir.is_dir(): - reason = "because the installation folder does not exist." - msg.logos_warning(f"{m} {reason}") - return + app.exit("Can't create launchers because the installation folder does not exist.") app_dir = Path(installdir) / 'data' logos_icon_path = app_dir / logos_icon_src.name app_icon_path = app_dir / app_icon_src.name @@ -575,8 +564,7 @@ def create_launcher_shortcuts(app: App): # Find python in virtual environment. py_bin = next(repo_dir.glob('*/bin/python')) if not py_bin.is_file(): - msg.logos_warning("Could not locate python binary in virtual environment.") # noqa: E501 - return + app.exit("Could not locate python binary in virtual environment.") # noqa: E501 lli_executable = f"env DIALOG=tk {py_bin} {script}" for (src, path) in [(app_icon_src, app_icon_path), (logos_icon_src, logos_icon_path)]: # noqa: E501 diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index c31562c1..4f178fa9 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -175,11 +175,7 @@ def logos_warn(message): def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 if detail is None: detail = '' - # XXX: move these to constants and output them on error - WIKI_LINK = f"{constants.REPOSITORY_LINK}/wiki" - TELEGRAM_LINK = "https://t.me/linux_logos" - MATRIX_LINK = "https://matrix.to/#/#logosbible:matrix.org" - help_message = f"If you need help, please consult:\n{WIKI_LINK}\n{TELEGRAM_LINK}\n{MATRIX_LINK}" # noqa: E501 + help_message = constants.SUPPORT_MESSAGE if config.DIALOG == 'tk': show_error( message, @@ -198,11 +194,6 @@ def ui_message(message, secondary=None, detail=None, app=None, parent=None, fata logos_msg(message) -def logos_warning(message, secondary=None, detail=None, app=None, parent=None): - ui_message(message, secondary=secondary, detail=detail, app=app, parent=parent) # noqa: E501 - logging.error(message) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) From 755df3d84159fbbc0afca67ed6b68e7b76fcc79b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:20:27 -0800 Subject: [PATCH 070/137] refactor: remove unused ui_message --- ou_dedetai/app.py | 4 ---- ou_dedetai/msg.py | 23 ----------------------- 2 files changed, 27 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index bcaad665..995ddc68 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -140,10 +140,6 @@ def is_installed(self) -> bool: return os.access(self.conf.logos_exe, os.X_OK) return False - # def message(self, message: str): - # """Show the user a message in their native UI""" - # print(message) - def status(self, message: str, percent: Optional[int] = None): """A status update""" print(f"{message}") diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 4f178fa9..57321c03 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -171,29 +171,6 @@ def logos_warn(message): logos_msg(message) -# XXX: move this to app as... message? -def ui_message(message, secondary=None, detail=None, app=None, parent=None, fatal=False): # noqa: E501 - if detail is None: - detail = '' - help_message = constants.SUPPORT_MESSAGE - if config.DIALOG == 'tk': - show_error( - message, - detail=f"{detail}\n\n{help_message}", - app=app, - fatal=fatal, - parent=parent - ) - elif config.DIALOG == 'curses': - if secondary != "info": - status(message) - status(help_message) - else: - logos_msg(message) - else: - logos_msg(message) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) From 5e0526c991a832c666c3762ae93fe51f63a7e8a7 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:22:40 -0800 Subject: [PATCH 071/137] refactor: remove msg.progress --- ou_dedetai/control.py | 9 --------- ou_dedetai/gui_app.py | 30 ------------------------------ ou_dedetai/installer.py | 3 +-- ou_dedetai/msg.py | 16 ---------------- 4 files changed, 1 insertion(+), 57 deletions(-) diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index e1b52dae..748d754c 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -145,15 +145,6 @@ def backup_and_restore(mode: str, app: App): while t.is_alive(): logging.debug(f"DEV: Still copying… {counter}") counter = counter + 1 - # progress = utils.get_copy_progress( - # dst_dir, - # src_size, - # dest_size_init=dst_dir_size - # ) - # utils.write_progress_bar(progress) - # if config.DIALOG == 'tk': - # app.progress_q.put(progress) - # app.root.event_generate('<>') time.sleep(1) print() except KeyboardInterrupt: diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 15dc3568..e73a879e 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -254,11 +254,6 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): self.status_q = Queue() self.status_evt = "<>" self.root.bind(self.status_evt, self.update_status_text) - self.progress_q = Queue() - self.root.bind( - "<>", - self.step_start - ) self.releases_q = Queue() self.wine_q = Queue() @@ -533,16 +528,6 @@ def update_download_progress(self, evt=None): d = self.get_q.get() self.gui.progressvar.set(int(d)) - def step_start(self, evt=None): - progress = self.progress_q.get() - if type(progress) is not int: - return - if progress >= 100: - self.gui.progressvar.set(0) - # self.gui.progress.state(['disabled']) - else: - self.gui.progressvar.set(progress) - def update_status_text(self, evt=None, status=None): text = '' if evt: @@ -635,15 +620,10 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.status_evt = '<>' self.root.bind(self.status_evt, self.update_status_text) self.root.bind('<>', self.clear_status_text) - self.progress_q = Queue() self.root.bind( '<>', self.start_indeterminate_progress ) - self.root.bind( - '<>', - self.step_start - ) self.root.bind( "<>", self.update_latest_appimage_button @@ -897,16 +877,6 @@ def update_download_progress(self, evt=None): d = self.get_q.get() self.gui.progressvar.set(int(d)) - def step_start(self, evt=None): - progress = self.progress_q.get() - if type(progress) is not int: - return - if progress >= 100: - self.gui.progressvar.set(0) - # self.gui.progress.state(['disabled']) - else: - self.gui.progressvar.set(progress) - def update_status_text(self, evt=None): if evt: self.gui.statusvar.set(self.status_q.get()) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 50fd2b75..34471b2a 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -466,8 +466,7 @@ def install(app: App): def update_install_feedback(text, app: App): percent = get_progress_pct(app.installer_step, app.installer_step_count) logging.debug(f"Install step {app.installer_step} of {app.installer_step_count}") # noqa: E501 - msg.progress(percent, app=app) - msg.status(text, app=app) + app.status(text, percent) def get_progress_pct(current, total): diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 57321c03..5be58570 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -178,22 +178,6 @@ def get_progress_str(percent): return f"[{'*' * part_done}{'-' * part_left}]" -# XXX: remove in favor of app.status -def progress(percent, app=None): - """Updates progressbar values for TUI and GUI.""" - if config.DIALOG == 'tk' and app: - app.progress_q.put(percent) - app.root.event_generate('<>') - logging.info(f"Progress: {percent}%") - elif config.DIALOG == 'curses': - if app: - status(f"Progress: {percent}%", app) - else: - status(f"Progress: {get_progress_str(percent)}", app) - else: - logos_msg(get_progress_str(percent)) # provisional - - # XXX: move this to app.status def status(text, app=None, end='\n'): def strip_timestamp(msg, timestamp_length=20): From 5fd98d24924369e54ee040c4b149234d0c6cf505 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:38:41 -0800 Subject: [PATCH 072/137] refactor: update_install_feedback to app.status --- ou_dedetai/app.py | 14 +++++++ ou_dedetai/gui_app.py | 8 ++-- ou_dedetai/installer.py | 88 ++++++++++++++--------------------------- ou_dedetai/tui_app.py | 2 +- 4 files changed, 48 insertions(+), 64 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 995ddc68..41bbf4d7 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -11,6 +11,7 @@ class App(abc.ABC): + # FIXME: consider weighting install steps. Different steps take different lengths installer_step_count: int = 0 """Total steps in the installer, only set the installation process has started.""" installer_step: int = 1 @@ -142,6 +143,19 @@ def is_installed(self) -> bool: def status(self, message: str, percent: Optional[int] = None): """A status update""" + # If we're installing + if self.installer_step_count != 0: + current_step_percent = percent or 0 + # We're further than the start of our current step, percent more + installer_percent = round((self.installer_step * 100 + current_step_percent) / self.installer_step_count) # noqa: E501 + logging.debug(f"Install step {self.installer_step} of {self.installer_step_count}") # noqa: E501 + self._status(message, percent=installer_percent) + else: + # Otherwise just print status using the progress given + self._status(message, percent) + + def _status(self, message: str, percent: Optional[int] = None): + """Implementation for updating status pre-front end""" print(f"{message}") @property diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index e73a879e..7ce0bae8 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -470,7 +470,7 @@ def start_install_thread(self, evt=None): utils.start_thread(installer.install, app=self) # XXX: where should this live? here or ControlWindow? - def status(self, message: str, percent: int | None = None): + def _status(self, message: str, percent: int | None = None): if percent: self.gui.progress.stop() self.gui.progress.state(['disabled']) @@ -482,7 +482,7 @@ def status(self, message: str, percent: int | None = None): self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set(message) - super().status(message, percent) + super()._status(message, percent) def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) @@ -778,7 +778,7 @@ def _config_updated_hook(self) -> None: return super()._config_updated_hook() # XXX: should this live here or in installerWindow? - def status(self, message: str, percent: int | None = None): + def _status(self, message: str, percent: int | None = None): if percent: self.gui.progress.stop() self.gui.progress.state(['disabled']) @@ -790,7 +790,7 @@ def status(self, message: str, percent: int | None = None): self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set(message) - super().status(message, percent) + super()._status(message, percent) def update_logging_button(self, evt=None): self.gui.statusvar.set('') diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 34471b2a..d68ce20c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -18,7 +18,7 @@ # XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses def ensure_product_choice(app: App): app.installer_step_count += 1 - update_install_feedback("Choose product…", app=app) + app.status("Choose product…") logging.debug('- config.FLPRODUCT') logging.debug(f"> config.FLPRODUCT={app.conf.faithlife_product}") @@ -29,7 +29,7 @@ def ensure_version_choice(app: App): app.installer_step_count += 1 ensure_product_choice(app=app) app.installer_step += 1 - update_install_feedback("Choose version…", app=app) + app.status("Choose version…") logging.debug('- config.TARGETVERSION') # Accessing this ensures it's set logging.debug(f"> config.TARGETVERSION={app.conf.faithlife_product_version=}") @@ -40,7 +40,7 @@ def ensure_release_choice(app: App): app.installer_step_count += 1 ensure_version_choice(app=app) app.installer_step += 1 - update_install_feedback("Choose product release…", app=app) + app.status("Choose product release…") logging.debug('- config.TARGET_RELEASE_VERSION') logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") @@ -49,7 +49,7 @@ def ensure_install_dir_choice(app: App): app.installer_step_count += 1 ensure_release_choice(app=app) app.installer_step += 1 - update_install_feedback("Choose installation folder…", app=app) + app.status("Choose installation folder…") logging.debug('- config.INSTALLDIR') # Accessing this sets install_dir and bin_dir app.conf.install_dir @@ -61,7 +61,7 @@ def ensure_wine_choice(app: App): app.installer_step_count += 1 ensure_install_dir_choice(app=app) app.installer_step += 1 - update_install_feedback("Choose wine binary…", app=app) + app.status("Choose wine binary…") logging.debug('- config.SELECTED_APPIMAGE_FILENAME') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_URL') logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME') @@ -83,7 +83,7 @@ def ensure_winetricks_choice(app: App): app.installer_step_count += 1 ensure_wine_choice(app=app) app.installer_step += 1 - update_install_feedback("Choose winetricks binary…", app=app) + app.status("Choose winetricks binary…") logging.debug('- config.WINETRICKSBIN') # Accessing the winetricks_binary variable will do this. logging.debug(f"> config.WINETRICKSBIN={app.conf.winetricks_binary}") @@ -94,7 +94,7 @@ def ensure_install_fonts_choice(app: App): app.installer_step_count += 1 ensure_winetricks_choice(app=app) app.installer_step += 1 - update_install_feedback("Ensuring install fonts choice…", app=app) + app.status("Ensuring install fonts choice…") logging.debug('- config.SKIP_FONTS') logging.debug(f"> config.SKIP_FONTS={app.conf.skip_install_fonts}") @@ -105,9 +105,8 @@ def ensure_check_sys_deps_choice(app: App): app.installer_step_count += 1 ensure_install_fonts_choice(app=app) app.installer_step += 1 - update_install_feedback( - "Ensuring check system dependencies choice…", - app=app + app.status( + "Ensuring check system dependencies choice…" ) logging.debug('- config.SKIP_DEPENDENCIES') @@ -118,7 +117,7 @@ def ensure_installation_config(app: App): app.installer_step_count += 1 ensure_check_sys_deps_choice(app=app) app.installer_step += 1 - update_install_feedback("Ensuring installation config is set…", app=app) + app.status("Ensuring installation config is set…") logging.debug('- config.LOGOS_ICON_URL') logging.debug('- config.LOGOS_VERSION') logging.debug('- config.LOGOS64_URL') @@ -138,7 +137,7 @@ def ensure_install_dirs(app: App): app.installer_step_count += 1 ensure_installation_config(app=app) app.installer_step += 1 - update_install_feedback("Ensuring installation directories…", app=app) + app.status("Ensuring installation directories…") logging.debug('- config.INSTALLDIR') logging.debug('- config.WINEPREFIX') logging.debug('- data/bin') @@ -163,7 +162,7 @@ def ensure_sys_deps(app: App): app.installer_step_count += 1 ensure_install_dirs(app=app) app.installer_step += 1 - update_install_feedback("Ensuring system dependencies are met…", app=app) + app.status("Ensuring system dependencies are met…") if not app.conf.skip_install_system_dependencies: utils.install_dependencies(app) @@ -178,10 +177,7 @@ def ensure_appimage_download(app: App): app.installer_step += 1 if app.conf.faithlife_product_version != '9' and not str(app.conf.wine_binary).lower().endswith('appimage'): # noqa: E501 return - update_install_feedback( - "Ensuring wine AppImage is downloaded…", - app=app - ) + app.status("Ensuring wine AppImage is downloaded…") downloaded_file = None appimage_path = app.conf.wine_appimage_path or app.conf.wine_appimage_recommended_file_name @@ -203,10 +199,7 @@ def ensure_wine_executables(app: App): app.installer_step_count += 1 ensure_appimage_download(app=app) app.installer_step += 1 - update_install_feedback( - "Ensuring wine executables are available…", - app=app - ) + app.status("Ensuring wine executables are available…") logging.debug('- config.WINESERVER_EXE') logging.debug('- wine') logging.debug('- wine64') @@ -227,10 +220,7 @@ def ensure_winetricks_executable(app: App): app.installer_step_count += 1 ensure_wine_executables(app=app) app.installer_step += 1 - update_install_feedback( - "Ensuring winetricks executable is available…", - app=app - ) + app.status("Ensuring winetricks executable is available…") msg.status("Downloading winetricks from the Internet…", app=app) system.install_winetricks(app.conf.installer_binary_dir, app=app) @@ -245,10 +235,7 @@ def ensure_premade_winebottle_download(app: App): app.installer_step += 1 if app.conf.faithlife_product_version != '9': return - update_install_feedback( - f"Ensuring {constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME} bottle is downloaded…", # noqa: E501 - app=app - ) + app.status(f"Ensuring {constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME} bottle is downloaded…") # noqa: E501 downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: @@ -274,10 +261,7 @@ def ensure_product_installer_download(app: App): app.installer_step_count += 1 ensure_premade_winebottle_download(app=app) app.installer_step += 1 - update_install_feedback( - f"Ensuring {app.conf.faithlife_product} installer is downloaded…", - app=app - ) + app.status(f"Ensuring {app.conf.faithlife_product} installer is downloaded…") downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, app.conf.faithlife_installer_name) if not downloaded_file: @@ -300,7 +284,7 @@ def ensure_wineprefix_init(app: App): app.installer_step_count += 1 ensure_product_installer_download(app=app) app.installer_step += 1 - update_install_feedback("Ensuring wineprefix is initialized…", app=app) + app.status("Ensuring wineprefix is initialized…") init_file = Path(f"{app.conf.wine_prefix}/system.reg") logging.debug(f"{init_file=}") @@ -325,8 +309,7 @@ def ensure_winetricks_applied(app: App): app.installer_step_count += 1 ensure_wineprefix_init(app=app) app.installer_step += 1 - status = "Ensuring winetricks & other settings are applied…" - update_install_feedback(status, app=app) + app.status("Ensuring winetricks & other settings are applied…") logging.debug('- disable winemenubuilder') logging.debug('- settings renderer=gdi') logging.debug('- corefonts') @@ -377,8 +360,7 @@ def ensure_icu_data_files(app: App): app.installer_step_count += 1 ensure_winetricks_applied(app=app) app.installer_step += 1 - status = "Ensuring ICU data files are installed…" - update_install_feedback(status, app=app) + app.status("Ensuring ICU data files are installed…") logging.debug('- ICU data files') wine.enforce_icu_data_files(app=app) @@ -390,10 +372,7 @@ def ensure_product_installed(app: App): app.installer_step_count += 1 ensure_icu_data_files(app=app) app.installer_step += 1 - update_install_feedback( - f"Ensuring {app.conf.faithlife_product} is installed…", - app=app - ) + app.status(f"Ensuring {app.conf.faithlife_product} is installed…") if not app.is_installed(): process = wine.install_msi(app) @@ -409,7 +388,7 @@ def ensure_config_file(app: App): app.installer_step_count += 1 ensure_product_installed(app=app) app.installer_step += 1 - update_install_feedback("Ensuring config file is up-to-date…", app=app) + app.status("Ensuring config file is up-to-date…") app.status("Install has finished.", 100) @@ -424,10 +403,7 @@ def ensure_launcher_executable(app: App): app.installer_step += 1 runmode = system.get_runmode() if runmode == 'binary': - update_install_feedback( - f"Copying launcher to {app.conf.install_dir}…", - app=app - ) + app.status(f"Copying launcher to {app.conf.install_dir}…") # Copy executable into install dir. launcher_exe = Path(f"{app.conf.install_dir}/{constants.BINARY_NAME}") @@ -438,9 +414,8 @@ def ensure_launcher_executable(app: App): shutil.copy(sys.executable, launcher_exe) logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 else: - update_install_feedback( - "Running from source. Skipping launcher creation.", - app=app + app.status( + "Running from source. Skipping launcher creation." ) @@ -448,26 +423,21 @@ def ensure_launcher_shortcuts(app: App): app.installer_step_count += 1 ensure_launcher_executable(app=app) app.installer_step += 1 - update_install_feedback("Creating launcher shortcuts…", app=app) + app.status("Creating launcher shortcuts…") runmode = system.get_runmode() if runmode == 'binary': - update_install_feedback("Creating launcher shortcuts…", app=app) + app.status("Creating launcher shortcuts…") create_launcher_shortcuts(app) else: - update_install_feedback( + # FIXME: Is this because it's hard to find the python binary? + app.status( "Running from source. Skipping launcher creation.", - app=app ) def install(app: App): """Entrypoint for installing""" ensure_launcher_shortcuts(app) -def update_install_feedback(text, app: App): - percent = get_progress_pct(app.installer_step, app.installer_step_count) - logging.debug(f"Install step {app.installer_step} of {app.installer_step_count}") # noqa: E501 - app.status(text, percent) - def get_progress_pct(current, total): return round(current * 100 / total) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 7f68f7da..da71bf64 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -651,7 +651,7 @@ def handle_ask_directory_response(self, choice: Optional[str]): if choice is not None and Path(choice).exists() and Path(choice).is_dir(): self.handle_ask_response(choice) - def status(self, message: str, percent: int | None = None): + def _status(self, message: str, percent: int | None = None): # XXX: update some screen? Something like get_waiting? pass From 9d95a0e793e851eba24981e912065ea7b8685589 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:51:51 -0800 Subject: [PATCH 073/137] fix: include progress bar in cli output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Looks like: ``` [- ] Choose product… [-- ] Choose version… [--- ] Choose product release… [--- ] Choose installation folder… [---- ] Choose wine binary… [----- ] Choose winetricks binary… [------ ] Ensuring install fonts choice… [------- ] Ensuring check system dependencies choice… [-------- ] Ensuring installation config is set… [-------- ] Install is running… [--------- ] Ensuring installation directories… [---------- ] Ensuring system dependencies are met… [---------- ] Checking Logos 10 dependencies… [---------- ] Installed dependencies. [---------- ] Ensuring wine AppImage is downloaded… [---------- ] Verifying /home/user/wine-devel_9.19-x86_64.AppImage… Copying wine-devel_9.19-x86_64.AppImage into /home/user/ [----------- ] Ensuring wine executables are available… [----------- ] Creating wine appimage symlinks… [------------ ] Ensuring winetricks executable is available… Installing winetricks v20220411… [------------ ] Verifying /home/user/20220411.zip… Copying 20220411.zip into /home/user/ [-------------- ] Ensuring Logos installer is downloaded… [-------------- ] Verifying /home/user/Logos_v37.2.0.0012-x64.msi… Copying Logos_v37.2.0.0012-x64.msi into /home/user/ [--------------- ] Ensuring wineprefix is initialized… [---------------- ] Ensuring winetricks & other settings are applied… Setting Logos Bible Indexing to Win10 Mode… [----------------- ] Ensuring ICU data files are installed… [----------------- ] Downloading ICU files... [----------------- ] Verifying /home/user/icu-win-72.1-custom+4.tar.gz… Copying icu-win-72.1-custom+4.tar.gz into /home/user/ [----------------- ] Copying ICU files... [----------------- ] ICU files copied. [----------------- ] Ensuring Logos is installed… [------------------ ] Ensuring config file is up-to-date… [------------------- ] Install has finished. [------------------- ] Running from source. Skipping launcher creation. [--------------------] Creating launcher shortcuts… [--------------------] Running from source. Skipping launcher creation. ``` --- ou_dedetai/app.py | 2 +- ou_dedetai/cli.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 41bbf4d7..9b5354fb 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -71,7 +71,7 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: passed_options = options + [self._exit_option] answer = self._ask(question, passed_options) - while answer is None or validate_result(answer, options) is not None: + while answer is None or validate_result(answer, options) is None: invalid_response = "That response is not valid, please try again." new_question = f"{invalid_response}\n{question}" answer = self._ask(new_question, passed_options) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index d3a17b65..6a18625b 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -1,6 +1,7 @@ import queue import shutil import threading +from typing import Optional from ou_dedetai.app import App from ou_dedetai.config import EphemeralConfiguration @@ -120,6 +121,16 @@ def exit(self, reason: str, intended: bool = False): self.running = False return super().exit(reason, intended) + def _status(self, message: str, percent: Optional[int] = None): + """Implementation for updating status pre-front end""" + if percent: + percent_per_char = 5 + chars_of_progress = round(percent / percent_per_char) + chars_remaining = round((100 - percent) / percent_per_char) + progress_str = "[" + "-" * chars_of_progress + " " * chars_remaining + "] " + print(progress_str, end="") + print(f"{message}") + @property def superuser_command(self) -> str: if shutil.which('sudo'): @@ -145,8 +156,7 @@ def user_input_processor(self, evt=None): if question is not None and options is not None: # Convert options list to string. default = options[0] - options[0] = f"{options[0]} [default]" - optstr = ', '.join(options) + optstr = f"{options[0]} [default], " + ', '.join(options[1:]) choice = input(f"{question}: {optstr}: ") if len(choice) == 0: choice = default From 976ce2744ba2f0d2c6927070c8a19b6d4a138b95 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:11:42 -0800 Subject: [PATCH 074/137] refactor: msg.status Some of these were demoted to logging.info rather than being user facing --- ou_dedetai/app.py | 4 +++- ou_dedetai/control.py | 13 ++++++------- ou_dedetai/installer.py | 22 +++++++++++----------- ou_dedetai/logos.py | 12 ++++++------ ou_dedetai/msg.py | 29 ----------------------------- ou_dedetai/network.py | 6 +++--- ou_dedetai/system.py | 6 +++--- ou_dedetai/tui_app.py | 21 ++++++++++++++------- ou_dedetai/tui_curses.py | 8 +++++--- ou_dedetai/utils.py | 2 +- ou_dedetai/wine.py | 13 ++++++------- 11 files changed, 58 insertions(+), 78 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 9b5354fb..cad3d685 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -141,8 +141,10 @@ def is_installed(self) -> bool: return os.access(self.conf.logos_exe, os.X_OK) return False - def status(self, message: str, percent: Optional[int] = None): + def status(self, message: str, percent: Optional[int | float] = None): """A status update""" + if isinstance(percent, float): + percent = round(percent * 100) # If we're installing if self.installer_step_count != 0: current_step_percent = percent or 0 diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 748d754c..6c45e402 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -88,7 +88,7 @@ def backup_and_restore(mode: str, app: App): # Get source transfer size. q = queue.Queue() - msg.status("Calculating backup size…", app=app) + app.status("Calculating backup size…") t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): @@ -135,10 +135,10 @@ def backup_and_restore(mode: str, app: App): m = f"Restoring backup from {str(source_dir_base)}…" else: m = f"Backing up to {str(dst_dir)}…" - msg.status(m, app=app) - msg.status("Calculating destination directory size", app=app) + app.status(m) + app.status("Calculating destination directory size") dst_dir_size = utils.get_path_size(dst_dir) - msg.status("Starting backup…", app=app) + app.status("Starting backup…") t = utils.start_thread(copy_data, src_dirs, dst_dir) try: counter = 0 @@ -189,8 +189,7 @@ def remove_all_index_files(app: App): except OSError as e: logging.error(f"Error removing {file_to_remove}: {e}") - msg.status("Removed all LogosBible index files!") - sys.exit(0) + app.status("Removed all LogosBible index files!", 100) def remove_library_catalog(app: App): @@ -205,7 +204,7 @@ def remove_library_catalog(app: App): def set_winetricks(app: App): - msg.status("Preparing winetricks…") + app.status("Preparing winetricks…") if app.conf.winetricks_binary != constants.DOWNLOAD: valid = True # Double check it's a valid winetricks diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index d68ce20c..dbe2d1e9 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -222,7 +222,7 @@ def ensure_winetricks_executable(app: App): app.installer_step += 1 app.status("Ensuring winetricks executable is available…") - msg.status("Downloading winetricks from the Internet…", app=app) + app.status("Downloading winetricks from the Internet…") system.install_winetricks(app.conf.installer_binary_dir, app=app) logging.debug(f"> {app.conf.winetricks_binary} is executable?: {os.access(app.conf.winetricks_binary, os.X_OK)}") # noqa: E501 @@ -249,6 +249,7 @@ def ensure_premade_winebottle_download(app: App): # Install bottle. bottle = Path(app.conf.wine_prefix) if not bottle.is_dir(): + # FIXME: this code seems to be logos 9 specific, why is it here? utils.install_premade_wine_bottle( app.conf.download_dir, f"{app.conf.install_dir}/data" @@ -323,28 +324,30 @@ def ensure_winetricks_applied(app: App): usr_reg = Path(f"{app.conf.wine_prefix}/user.reg") sys_reg = Path(f"{app.conf.wine_prefix}/system.reg") + # FIXME: consider supplying progresses to these sub-steps + if not utils.grep(r'"winemenubuilder.exe"=""', usr_reg): - msg.status("Disabling winemenubuilder…", app) + app.status("Disabling winemenubuilder…") wine.disable_winemenubuilder(app, app.conf.wine64_binary) if not utils.grep(r'"renderer"="gdi"', usr_reg): - msg.status("Setting Renderer to GDI…", app) + app.status("Setting Renderer to GDI…") wine.set_renderer(app, "gdi") if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): - msg.status("Setting Font Smooting to RGB…", app) + app.status("Setting Font Smooting to RGB…") wine.install_font_smoothing(app) if not app.conf.skip_install_fonts and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 - msg.status("Installing fonts…", app) + app.status("Installing fonts…") wine.install_fonts(app) if not utils.grep(r'"\*d3dcompiler_47"="native"', usr_reg): - msg.status("Installing D3D…", app) + app.status("Installing D3D…") wine.install_d3d_compiler(app) if not utils.grep(r'"ProductName"="Microsoft Windows 10"', sys_reg): - msg.status(f"Setting {app.conf.faithlife_product} to Win10 Mode…", app) + app.status(f"Setting {app.conf.faithlife_product} to Win10 Mode…") wine.set_win_version(app, "logos", "win10") # NOTE: Can't use utils.grep check here because the string @@ -461,10 +464,7 @@ def create_wine_appimage_symlinks(app: App): logging.critical("Failed to get a valid wine appimage") return if Path(downloaded_file).parent != appdir_bindir: - msg.status( - f"Copying: {downloaded_file} into: {appdir_bindir}", - app=app - ) + app.status(f"Copying: {downloaded_file} into: {appdir_bindir}") shutil.copy(downloaded_file, appdir_bindir) os.chmod(appimage_file, 0o755) appimage_filename = appimage_file.name diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 44b89acd..64bdb830 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -114,7 +114,7 @@ def run_logos(): if isinstance(self.app, GuiApp): # Don't send "Running" message to GUI b/c it never clears. app = None - msg.status(f"Running {self.app.conf.faithlife_product}…", app=app) + app.status(f"Running {self.app.conf.faithlife_product}…") utils.start_thread(run_logos, daemon_bool=False) # NOTE: The following code would keep the CLI open while running # Logos, but since wine logging is sent directly to wine.log, @@ -150,7 +150,7 @@ def stop(self): try: system.run_command(['kill', '-9'] + pids) self.logos_state = State.STOPPED - msg.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.", self.app) # noqa: E501 + self.app.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.") # noqa: E501 except Exception as e: logging.debug(f"Error while stopping Logos processes: {e}.") # noqa: E501 else: @@ -195,18 +195,18 @@ def check_if_indexing(process): elapsed_min = int(total_elapsed_time // 60) elapsed_sec = int(total_elapsed_time % 60) formatted_time = f"{elapsed_min}m {elapsed_sec}s" - msg.status(f"Indexing is running… (Elapsed Time: {formatted_time})", self.app) # noqa: E501 + self.app.status(f"Indexing is running… (Elapsed Time: {formatted_time})") # noqa: E501 update_send = 0 index_finished.set() def wait_on_indexing(): index_finished.wait() self.indexing_state = State.STOPPED - msg.status("Indexing has finished.", self.app) + self.app.status("Indexing has finished.", percent=100) wine.wineserver_wait(app=self.app) wine.wineserver_kill(self.app.conf.wineserver_binary) - msg.status("Indexing has begun…", self.app) + self.app.status("Indexing has begun…") index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING check_thread = utils.start_thread( @@ -232,7 +232,7 @@ def stop_indexing(self): try: system.run_command(['kill', '-9'] + pids) self.indexing_state = State.STOPPED - msg.status(f"Stopped LogosIndexer processes at PIDs {', '.join(pids)}.", self.app) # noqa: E501 + self.app.status(f"Stopped LogosIndexer processes at PIDs {', '.join(pids)}.") # noqa: E501 except Exception as e: logging.debug(f"Error while stopping LogosIndexer processes: {e}.") # noqa: E501 else: diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 5be58570..b777a01e 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -177,32 +177,3 @@ def get_progress_str(percent): part_left = length - part_done return f"[{'*' * part_done}{'-' * part_left}]" - -# XXX: move this to app.status -def status(text, app=None, end='\n'): - def strip_timestamp(msg, timestamp_length=20): - return msg[timestamp_length:] - - timestamp = utils.get_timestamp() - """Handles status messages for both TUI and GUI.""" - if app is not None: - if config.DIALOG == 'tk': - app.status_q.put(text) - app.root.event_generate(app.status_evt) - logging.info(f"{text}") - elif config.DIALOG == 'curses': - if len(config.console_log) > 0: - last_msg = strip_timestamp(config.console_log[-1]) - if last_msg != text: - app.status_q.put(f"{timestamp} {text}") - app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 - logging.info(f"{text}") - else: - app.status_q.put(f"{timestamp} {text}") - app.report_waiting(f"{app.status_q.get()}", dialog=config.use_python_dialog) # noqa: E501 - logging.info(f"{text}") - else: - logging.info(f"{text}") - else: - # Prints message to stdout regardless of log level. - logos_msg(text, end=end) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 1e94a5fd..a1d0551e 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -145,7 +145,7 @@ def logos_reuse_download( app=app, ): logging.info(f"{file} properties match. Using it…") - msg.status(f"Copying {file} into {targetdir}") + logging.debug(f"Copying {file} into {targetdir}") try: shutil.copy(os.path.join(i, file), targetdir) except shutil.SameFileError: @@ -167,7 +167,7 @@ def logos_reuse_download( file_path, app=app, ): - msg.status(f"Copying: {file} into: {targetdir}") + logging.debug(f"Copying: {file} into: {targetdir}") try: shutil.copy(os.path.join(app.conf.download_dir, file), targetdir) except shutil.SameFileError: @@ -396,7 +396,7 @@ def get_recommended_appimage(app: App): def get_logos_releases(app: App) -> list[str]: # TODO: Use already-downloaded list if requested again. - msg.status(f"Downloading release list for {app.conf.faithlife_product} {app.conf.faithlife_product_version}…") # noqa: E501 + logging.debug(f"Downloading release list for {app.conf.faithlife_product} {app.conf.faithlife_product_version}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. if app.conf.faithlife_product_release_channel == "beta": url = "https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 7f026a6b..d7c7ab5e 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -631,7 +631,7 @@ def parse_date(version): def remove_appimagelauncher(app: App): - msg.status("Removing AppImageLauncher…", app) + app.status("Removing AppImageLauncher…") pkg = "appimagelauncher" package_manager = get_package_manager() if package_manager is None: @@ -818,7 +818,7 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 command.extend(postinstall_command) if not command: # nothing to run; avoid running empty pkexec command if app: - msg.status("All dependencies are met.", app) + app.status("All dependencies are met.", 100) return app.status("Installing dependencies…") @@ -872,7 +872,7 @@ def install_winetricks( app: App, version=constants.WINETRICKS_VERSION, ): - msg.status(f"Installing winetricks v{version}…") + app.status(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 zip_name = f"{version}.zip" network.logos_reuse_download( diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index da71bf64..0edf1264 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -244,7 +244,7 @@ def resize_curses(self): self.clear() self.init_curses() self.refresh() - msg.status("Window resized.", self) + logging.debug("Window resized.", self) self.resizing = False def signal_resize(self, signum, frame): @@ -279,7 +279,12 @@ def display(self): signal.signal(signal.SIGWINCH, self.signal_resize) signal.signal(signal.SIGINT, self.end) msg.initialize_tui_logging() - msg.status(self.console_message, self) + + # Makes sure status stays shown + timestamp = utils.get_timestamp() + self.status_q.put(f"{timestamp} {self.console_message}") + self.report_waiting(f"{self.console_message}", dialog=config.use_python_dialog) # noqa: E501 + self.active_screen = self.menu_screen last_time = time.time() self.logos.monitor() @@ -431,8 +436,8 @@ def main_menu_select(self, choice): self.set_utilities_menu_options(), dialog=config.use_python_dialog)) self.choice_q.put("0") elif choice == "Change Color Scheme": + self.status("Changing color scheme") self.conf.cycle_curses_color_scheme() - msg.status("Changing color scheme", self) self.reset_screen() utils.write_config(config.CONFIG_FILE) @@ -500,7 +505,6 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Install Dependencies": self.reset_screen() - msg.status("Checking dependencies…", self) self.update_windows() utils.install_dependencies(self) self.go_to_main_menu() @@ -576,22 +580,25 @@ def install_dependencies_confirm(self, choice): def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: self.reset_screen() + self.status(f"Changing renderer to {choice}.") wine.set_renderer(self, choice) - msg.status(f"Changed renderer to {choice}.", self) + self.status(f"Changed renderer to {choice}.", 100) self.go_to_main_menu() def win_ver_logos_select(self, choice): if choice in ["vista", "win7", "win8", "win10", "win11"]: self.reset_screen() + self.status(f"Changing Windows version for Logos to {choice}.", 0) wine.set_win_version(self, "logos", choice) - msg.status(f"Changed Windows version for Logos to {choice}.", self) + self.status(f"Changed Windows version for Logos to {choice}.", 100) self.go_to_main_menu() def win_ver_index_select(self, choice): if choice in ["vista", "win7", "win8", "win10", "win11"]: self.reset_screen() + self.status(f"Changing Windows version for Indexer to {choice}.", 0) wine.set_win_version(self, "indexer", choice) - msg.status(f"Changed Windows version for Indexer to {choice}.", self) + self.status(f"Changed Windows version for Indexer to {choice}.", 100) self.go_to_main_menu() def manual_install_confirm(self, choice): diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index fd35aa9e..678ea40d 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -1,4 +1,5 @@ import curses +import logging import signal import textwrap @@ -305,9 +306,10 @@ def input(self): self.user_input = self.options[self.app.current_option] elif key == ord('\x1b'): signal.signal(signal.SIGINT, self.app.end) - else: - msg.status("Input unknown.", self.app) - pass + # FIXME: do we need to log this? + # else: + # logging.debug(f"Input unknown: {key}") + # pass except KeyboardInterrupt: signal.signal(signal.SIGINT, self.app.end) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 9a9bf4e2..3da400e2 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -417,7 +417,7 @@ def get_latest_folder(folder_path): def install_premade_wine_bottle(srcdir, appdir): - msg.status(f"Extracting: '{constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 + logging.info(f"Extracting: '{constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}' into: {appdir}") # noqa: E501 shutil.unpack_archive( f"{srcdir}/{constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME}", appdir diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index cece943c..a6e14dae 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -197,7 +197,7 @@ def check_wine_version_and_branch(release_version, test_binary, faithlife_produc def initializeWineBottle(wine64_binary: str, app: App): - msg.status("Initializing wine bottle…") + app.status("Initializing wine bottle…") logging.debug(f"{wine64_binary=}") # Avoid wine-mono window wine_dll_override="mscoree=" @@ -215,7 +215,7 @@ def initializeWineBottle(wine64_binary: str, app: App): def wine_reg_install(app: App, reg_file, wine64_binary): reg_file = str(reg_file) - msg.status(f"Installing registry file: {reg_file}") + app.status(f"Installing registry file: {reg_file}") process = run_wine_proc( wine64_binary, app=app, @@ -247,7 +247,7 @@ def disable_winemenubuilder(app: App, wine64_binary: str): def install_msi(app: App): - msg.status(f"Running MSI installer: {app.conf.faithlife_installer_name}.", app) + app.status(f"Running MSI installer: {app.conf.faithlife_installer_name}.") # Execute the .MSI wine_exe = app.conf.wine64_binary exe_args = ["/i", f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}"] @@ -342,7 +342,6 @@ def run_winetricks_cmd(app: App, *args): # FIXME: test this to ensure it behaves as expected if "-q" not in args and app.conf.winetricks_binary: cmd.insert(0, "-q") - msg.status(f"Running winetricks \"{args[-1]}\"") logging.info(f"running \"winetricks {' '.join(cmd)}\"") process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) system.wait_pid(process) @@ -361,16 +360,16 @@ def install_d3d_compiler(app: App): def install_fonts(app: App): - msg.status("Configuring fonts…") fonts = ['corefonts', 'tahoma'] if not app.conf.skip_fonts: - for f in fonts: + for i, f in enumerate(fonts): + app.status("Configuring fonts, this step may take several minutes…", i / len(fonts)) # noqa: E501 args = [f] run_winetricks_cmd(app, *args) def install_font_smoothing(app: App): - msg.status("Setting font smoothing…") + logging.info("Setting font smoothing…") args = ['settings', 'fontsmooth=rgb'] run_winetricks_cmd(app, *args) From 0361d08f22ccfca8783276df1cbe6aa89ace8c4b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:13:20 -0800 Subject: [PATCH 075/137] refactor: msg.logos_warn installer.create_config_file is unused --- ou_dedetai/installer.py | 10 ---------- ou_dedetai/msg.py | 8 -------- ou_dedetai/tui_app.py | 2 +- ou_dedetai/utils.py | 2 +- 4 files changed, 2 insertions(+), 20 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index dbe2d1e9..72c53d6e 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -483,16 +483,6 @@ def create_wine_appimage_symlinks(app: App): p.symlink_to(f"./{app.conf.wine_appimage_link_file_name}") -def create_config_file(): - config_dir = Path(constants.DEFAULT_CONFIG_PATH).parent - config_dir.mkdir(exist_ok=True, parents=True) - if config_dir.is_dir(): - utils.write_config(config.CONFIG_FILE) - logging.info(f"A config file was created at {config.CONFIG_FILE}.") - else: - msg.logos_warn(f"{config_dir} does not exist. Failed to create config file.") # noqa: E501 - - def create_desktop_file(name, contents): launcher_path = Path(f"~/.local/share/applications/{name}").expanduser() if launcher_path.is_file(): diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index b777a01e..5378b9fc 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -163,14 +163,6 @@ def logos_progress(): # time.sleep(0.1) -def logos_warn(message): - # XXX: shouldn't this always use logging.warning? - if config.DIALOG == 'curses': - logging.warning(message) - else: - logos_msg(message) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 0edf1264..2a5999fd 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -393,7 +393,7 @@ def go_to_main_menu(self): def main_menu_select(self, choice): if choice is None or choice == "Exit": - msg.logos_warn("Exiting installation.") + logging.info("Exiting installation.") self.tui_screens = [] self.llirunning = False elif choice.startswith("Install"): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 3da400e2..5b3042bb 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -49,7 +49,7 @@ def append_unique(list, item): if item not in list: list.append(item) else: - msg.logos_warn(f"{item} already in {list}.") + logging.debug(f"{item} already in {list}.") # Set "global" variables. From 8fcfeaf4ab83c30aa9666d72ae67efbbf798ce31 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:16:58 -0800 Subject: [PATCH 076/137] refactor: remove logos_progress It was used, and this does remove a sign of life on the cli, no other UIs were implemented --- ou_dedetai/control.py | 4 +++- ou_dedetai/msg.py | 15 --------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 6c45e402..0d20b8ef 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -35,6 +35,8 @@ def restore(app: App): backup_and_restore(mode='restore', app=app) +# FIXME: almost seems like this is long enough to reuse the install_step count in app +# for a more detailed progress bar # FIXME: consider moving this into it's own file/module. def backup_and_restore(mode: str, app: App): app.status(f"Starting {mode}...") @@ -92,7 +94,7 @@ def backup_and_restore(mode: str, app: App): t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): - msg.logos_progress() + # FIXME: consider showing a sign of life to the app time.sleep(0.5) print() except KeyboardInterrupt: diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 5378b9fc..e31ddb98 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -148,21 +148,6 @@ def logos_msg(message, end='\n'): cli_msg(message, end) -# XXX: remove in favor of app.status("message", percent) -def logos_progress(): - if config.DIALOG == 'curses': - pass - else: - sys.stdout.write('.') - sys.stdout.flush() - # i = 0 - # spinner = "|/-\\" - # sys.stdout.write(f"\r{text} {spinner[i]}") - # sys.stdout.flush() - # i = (i + 1) % len(spinner) - # time.sleep(0.1) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) From d9a0685b1ff84b85bdd0976a29fcb3d70d05a1b3 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:17:33 -0800 Subject: [PATCH 077/137] refactor: msg.logos_msg --- ou_dedetai/installer.py | 2 +- ou_dedetai/msg.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 72c53d6e..dc07cb71 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -352,7 +352,7 @@ def ensure_winetricks_applied(app: App): # NOTE: Can't use utils.grep check here because the string # "Version"="win10" might appear elsewhere in the registry. - msg.logos_msg(f"Setting {app.conf.faithlife_product} Bible Indexing to Win10 Mode…") # noqa: E501 + app.status(f"Setting {app.conf.faithlife_product} Bible Indexing to Win10 Mode…") # noqa: E501 wine.set_win_version(app, "indexer", "win10") # wine.light_wineserver_wait() wine.wineserver_wait(app) diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index e31ddb98..ba60b9e5 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -141,13 +141,6 @@ def cli_msg(message, end='\n'): print(message, end=end) -def logos_msg(message, end='\n'): - if config.DIALOG == 'curses': - pass - else: - cli_msg(message, end) - - def get_progress_str(percent): length = 40 part_done = round(percent * length / 100) From 5c46fd768e1b7266d263ca27f7f6630c1e931112 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:18:01 -0800 Subject: [PATCH 078/137] chore: remove unused functions --- ou_dedetai/msg.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index ba60b9e5..aa3c3582 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -2,18 +2,11 @@ import logging from logging.handlers import RotatingFileHandler import os -import signal import shutil import sys from pathlib import Path -from . import config -from . import constants -from . import utils -from .gui import ask_question -from .gui import show_error - class GzippedRotatingFileHandler(RotatingFileHandler): def doRollover(self): @@ -135,15 +128,3 @@ def update_log_level(new_level): h.setLevel(new_level) logging.info(f"Terminal log level set to {get_log_level_name(new_level)}") - -def cli_msg(message, end='\n'): - '''Prints message to stdout regardless of log level.''' - print(message, end=end) - - -def get_progress_str(percent): - length = 40 - part_done = round(percent * length / 100) - part_left = length - part_done - return f"[{'*' * part_done}{'-' * part_left}]" - From 923b5312d5f372e28e480df7eb84d4d87d53620f Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:47:47 -0800 Subject: [PATCH 079/137] fix: resolve misc warning and errors --- ou_dedetai/cli.py | 3 +- ou_dedetai/config.py | 2 +- ou_dedetai/control.py | 14 ++--- ou_dedetai/gui.py | 5 +- ou_dedetai/gui_app.py | 4 +- ou_dedetai/installer.py | 14 ++--- ou_dedetai/logos.py | 5 +- ou_dedetai/main.py | 5 +- ou_dedetai/msg.py | 2 +- ou_dedetai/network.py | 22 ++++---- ou_dedetai/system.py | 21 ++++---- ou_dedetai/tui_app.py | 111 +++++++++++++++++---------------------- ou_dedetai/tui_curses.py | 61 ++++++++++----------- ou_dedetai/tui_dialog.py | 26 ++++----- ou_dedetai/wine.py | 39 +++++++------- 15 files changed, 159 insertions(+), 175 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 6a18625b..b3ab8333 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -9,7 +9,6 @@ from . import control from . import installer -from . import logos from . import wine from . import utils @@ -91,7 +90,7 @@ def update_self(self): utils.update_to_latest_lli_release(self) def winetricks(self): - import config + from ou_dedetai import config wine.run_winetricks_cmd(self, *config.winetricks_args) _exit_option: str = "Exit" diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 5b4142e6..e602755d 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -6,7 +6,7 @@ from pathlib import Path import time -from ou_dedetai import msg, network, utils, constants, wine +from ou_dedetai import network, utils, constants, wine from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 0d20b8ef..690c02d9 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -8,7 +8,6 @@ import os import shutil import subprocess -import sys import time from pathlib import Path @@ -16,10 +15,7 @@ from . import config from . import constants -from . import msg -from . import network from . import system -from . import tui_curses from . import utils @@ -76,12 +72,13 @@ def backup_and_restore(mode: str, app: App): restore_dir = Path(restore_dir).expanduser().resolve() source_dir_base = restore_dir else: + if not app.conf.logos_exe: + app.exit("Cannot backup, Logos is not installed") source_dir_base = Path(app.conf.logos_exe).parent src_dirs = [source_dir_base / d for d in data_dirs if Path(source_dir_base / d).is_dir()] # noqa: E501 logging.debug(f"{src_dirs=}") if not src_dirs: app.exit(f"No files to {mode}") - return if mode == 'backup': app.status("Backing up data…") @@ -104,10 +101,11 @@ def backup_and_restore(mode: str, app: App): src_size = q.get() if src_size == 0: app.exit(f"Nothing to {mode}!") - return # Set destination folder. if mode == 'restore': + if not app.conf.logos_exe: + app.exit("Cannot restore, Logos is not installed") dst_dir = Path(app.conf.logos_exe).parent # Remove existing data. for d in data_dirs: @@ -173,6 +171,8 @@ def remove_install_dir(app: App): def remove_all_index_files(app: App): + if not app.conf.logos_exe: + app.exit("Cannot remove index files, Logos is not installed") logos_dir = os.path.dirname(app.conf.logos_exe) index_paths = [ os.path.join(logos_dir, "Data", "*", "BibleIndex"), @@ -195,6 +195,8 @@ def remove_all_index_files(app: App): def remove_library_catalog(app: App): + if not app.conf.logos_exe: + app.exit("Cannot remove library catalog, Logos is not installed") logos_dir = os.path.dirname(app.conf.logos_exe) files_to_remove = glob.glob(f"{logos_dir}/Data/*/LibraryCatalog/*") for file_to_remove in files_to_remove: diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index 3e67f348..a30c3fff 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -17,7 +17,6 @@ from ou_dedetai.app import App from . import config -from . import utils from . import constants @@ -125,12 +124,12 @@ def __init__(self, root, app: App, **kwargs): # Fonts row. self.fonts_label = Label(self, text="Install Fonts: ") - self.fontsvar = BooleanVar(value=1-self.app.conf.skip_install_fonts) + self.fontsvar = BooleanVar(value=not self.app.conf.skip_install_fonts) self.fonts_checkbox = Checkbutton(self, variable=self.fontsvar) # Skip Dependencies row. self.skipdeps_label = Label(self, text="Install Dependencies: ") - self.skipdepsvar = BooleanVar(value=1-self.app.conf.skip_install_system_dependencies) + self.skipdepsvar = BooleanVar(value=self.app.conf.skip_install_system_dependencies) #noqa: E501 self.skipdeps_checkbox = Checkbutton(self, variable=self.skipdepsvar) # Cancel/Okay buttons row. diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 7ce0bae8..62376181 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -34,7 +34,7 @@ class GuiApp(App): _exit_option: Optional[str] = None - def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwargs): + def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwargs): #noqa: E501 super().__init__(ephemeral_config) self.root = root @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): class ChoicePopUp(Tk): """Creates a pop-up with a choice""" - def __init__(self, question: str, options: list[str], answer_q: Queue, answer_event: Event, **kwargs): + def __init__(self, question: str, options: list[str], answer_q: Queue, answer_event: Event, **kwargs): #noqa: E501 # Set root parameters. super().__init__() self.title(f"Quesiton: {question.strip().strip(':')}") diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index dc07cb71..7997d831 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -113,6 +113,7 @@ def ensure_check_sys_deps_choice(app: App): logging.debug(f"> config.SKIP_DEPENDENCIES={app.conf._overrides.winetricks_skip}") +# XXX: should this be it's own step? faithlife_product_version is asked def ensure_installation_config(app: App): app.installer_step_count += 1 ensure_check_sys_deps_choice(app=app) @@ -122,9 +123,6 @@ def ensure_installation_config(app: App): logging.debug('- config.LOGOS_VERSION') logging.debug('- config.LOGOS64_URL') - # XXX: This doesn't prompt the user for anything, all values are derived from other user-supplied values - # these "config" values probably don't need to be stored independently of the values they're derived from - logging.debug(f"> config.LOGOS_ICON_URL={app.conf.faithlife_product_icon_path}") logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") @@ -180,7 +178,7 @@ def ensure_appimage_download(app: App): app.status("Ensuring wine AppImage is downloaded…") downloaded_file = None - appimage_path = app.conf.wine_appimage_path or app.conf.wine_appimage_recommended_file_name + appimage_path = app.conf.wine_appimage_path or app.conf.wine_appimage_recommended_file_name #noqa: E501 filename = Path(appimage_path).name downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, filename) if not downloaded_file: @@ -239,7 +237,7 @@ def ensure_premade_winebottle_download(app: App): downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME) # noqa: E501 if not downloaded_file: - downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name + downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name #noqa: E501 network.logos_reuse_download( constants.LOGOS9_WINE64_BOTTLE_TARGZ_URL, constants.LOGOS9_WINE64_BOTTLE_TARGZ_NAME, @@ -264,9 +262,9 @@ def ensure_product_installer_download(app: App): app.installer_step += 1 app.status(f"Ensuring {app.conf.faithlife_product} installer is downloaded…") - downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, app.conf.faithlife_installer_name) + downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, app.conf.faithlife_installer_name) #noqa: E501 if not downloaded_file: - downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name + downloaded_file = Path(app.conf.download_dir) / app.conf.faithlife_installer_name #noqa: E501 network.logos_reuse_download( app.conf.faithlife_installer_download_url, app.conf.faithlife_installer_name, @@ -520,6 +518,8 @@ def create_launcher_shortcuts(app: App): if c.name == '.git': repo_dir = p break + if repo_dir is None: + app.exit("Could not find .git directory from arg 0") # noqa: E501 # Find python in virtual environment. py_bin = next(repo_dir.glob('*/bin/python')) if not py_bin.is_file(): diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 64bdb830..cce18b77 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -10,7 +10,6 @@ from ou_dedetai.app import App from . import main -from . import msg from . import system from . import utils from . import wine @@ -108,7 +107,7 @@ def run_logos(): else: if reason is not None: logging.debug(f"Warning: Wine Check: {reason}") - wine.wineserver_kill(self.app.conf.wineserver_binary) + wine.wineserver_kill(self.app) app = self.app from ou_dedetai.gui_app import GuiApp if isinstance(self.app, GuiApp): @@ -205,7 +204,7 @@ def wait_on_indexing(): self.app.status("Indexing has finished.", percent=100) wine.wineserver_wait(app=self.app) - wine.wineserver_kill(self.app.conf.wineserver_binary) + wine.wineserver_kill(self.app) self.app.status("Indexing has begun…") index_thread = utils.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index d6a51f98..7a946898 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -3,7 +3,9 @@ import curses import logging.handlers -from ou_dedetai.config import EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path +from ou_dedetai.config import ( + EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path +) try: import dialog # noqa: F401 @@ -11,7 +13,6 @@ pass import logging import os -import shutil import sys from . import cli diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index aa3c3582..30120ebe 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -98,7 +98,7 @@ def initialize_logging(log_level: str | int, app_log_path: str): stderr_h.name = "terminal" stderr_h.setLevel(log_level) stderr_h.addFilter(DeduplicateFilter()) - handlers = [ + handlers: list[logging.Handler] = [ file_h, # stdout_h, stderr_h, diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index a1d0551e..34d808d0 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -2,24 +2,18 @@ import json import logging import os -import queue from typing import Optional import requests import shutil import sys from base64 import b64encode -from datetime import datetime, timedelta from pathlib import Path -from time import sleep from urllib.parse import urlparse from xml.etree import ElementTree as ET -from ou_dedetai import wine from ou_dedetai.app import App -from . import config from . import constants -from . import msg from . import utils @@ -340,10 +334,13 @@ def get_first_asset_url(json_data) -> Optional[str]: def get_tag_name(json_data) -> Optional[str]: - tag_name = None + """Gets tag name from json data, strips leading v if exists""" + tag_name: Optional[str] = None if json_data: tag_name = json_data.get('tag_name') logging.info(f"Release URL Tag Name: {tag_name}") + if tag_name is not None: + tag_name = tag_name.lstrip("v") return tag_name @@ -363,9 +360,10 @@ def get_oudedetai_latest_release_config(channel: str = "stable") -> tuple[str, s if oudedetai_url is None: logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 raise ValueError("Failed to find latest installer version") - # Getting version relies on the the tag_name field in the JSON data. This - # is already parsed down to vX.X.X. Therefore we must strip the v. - latest_version = get_tag_name(json_data).lstrip('v') + latest_version = get_tag_name(json_data) + if latest_version is None: + logging.critical(f"Unable to set {constants.APP_NAME} release without the tag.") # noqa: E501 + raise ValueError("Failed to find latest installer version") logging.info(f"config.LLI_LATEST_VERSION={latest_version}") return oudedetai_url, latest_version @@ -420,8 +418,8 @@ def get_logos_releases(app: App) -> list[str]: releases = [] # Obtain all listed releases. for entry in root.findall('.//ns1:version', namespaces): - release = entry.text - releases.append(release) + if entry.text: + releases.append(entry.text) # if len(releases) == 5: # break diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index d7c7ab5e..5e01e448 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -267,8 +267,9 @@ def get_architecture() -> Tuple[str, int]: def install_elf_interpreter(): - # TODO: This probably needs to be changed to another install step that requests the user to choose a specific - # ELF interpreter between box64, FEX-EMU, and hangover. That or else we have to pursue a particular interpreter + # TODO: This probably needs to be changed to another install step that requests the + # user to choose a specific ELF interpreter between box64, FEX-EMU, and hangover. + # That or else we have to pursue a particular interpreter # for the install routine, depending on what's needed logging.critical("ELF interpretation is not yet coded in the installer.") # architecture, bits = get_architecture() @@ -288,19 +289,19 @@ def check_architecture(): if "x86_64" in architecture: pass elif "ARM64" in architecture: - logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") + logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") #noqa: E501 install_elf_interpreter() elif "RISC-V 64" in architecture: - logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") + logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") #noqa: E501 install_elf_interpreter() elif "x86_32" in architecture: - logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") + logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") #noqa: E501 install_elf_interpreter() elif "ARM32" in architecture: - logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") + logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") #noqa: E501 install_elf_interpreter() elif "RISC-V 32" in architecture: - logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") + logging.critical("Unsupported architecture. Requires box64 or FEX-EMU or Wine Hangover to be integrated.") #noqa: E501 install_elf_interpreter() else: logging.critical("System archictecture unknown.") @@ -491,7 +492,8 @@ def get_package_manager() -> PackageManager | None: incompatible_packages = "" # appimagelauncher handled separately else: # Add more conditions for other package managers as needed. - logging.critical("Your package manager is not yet supported. Please contact the developers.") + logging.critical("Your package manager is not yet supported. Please contact the developers.") #noqa: E501 + return None output = PackageManager( install=install_command, @@ -741,7 +743,6 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 app.exit( f"The script could not determine your {os_name} install's package manager or it is unsupported." # noqa: E501 ) - return package_list = package_manager.packages.split() @@ -761,7 +762,7 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 mode="remove", ) - if os_name in ['fedora', 'arch', 'alpine']: + if os_name in bad_os: # XXX: move the handling up here, possibly simplify? m = "Your distro requires manual dependency installation." logging.error(m) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 2a5999fd..af6e0823 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -114,12 +114,12 @@ def set_window_dimensions(self): tui_curses.wrap_text(self, self.subtitle)) + min_console_height self.menu_window_ratio = 0.75 self.menu_window_min = 3 - self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) - self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) + self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) #noqa: E501#noqa: E501 + self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) #noqa: E501 self.console_log_lines = max(self.main_window_height - self.main_window_min, 1) self.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) + self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) #noqa: E501 resize_lines = tui_curses.wrap_text(self, "Screen too small.") self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) @@ -182,11 +182,9 @@ def init_curses(self): curses.cbreak() self.stdscr.keypad(True) - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) - self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, - "Main Menu", self.set_tui_menu_options(dialog=False)) - #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", - # self.set_tui_menu_options(dialog=True)) + self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) #noqa: E501 + self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(dialog=False)) #noqa: E501 + #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(dialog=True)) #noqa: E501 self.refresh() except curses.error as e: logging.error(f"Curses error in init_curses: {e}") @@ -252,7 +250,10 @@ def signal_resize(self, signum, frame): self.choice_q.put("resize") if config.use_python_dialog: - if isinstance(self.active_screen, tui_screen.TextDialog) and self.active_screen.text == "Screen Too Small": + if ( + isinstance(self.active_screen, tui_screen.TextDialog) + and self.active_screen.text == "Screen Too Small" + ): self.choice_q.put("Return to Main Menu") else: if self.active_screen.get_screen_id == 14: @@ -326,7 +327,8 @@ def display(self): self.refresh() elif self.window_width >= 10: if self.window_width < 10: - self.terminal_margin = 1 # Avoid drawing errors on very small screens + # Avoid drawing errors on very small screens + self.terminal_margin = 1 self.draw_resize_screen() elif self.window_width < 10: self.terminal_margin = 0 # Avoid drawing errors on very small screens @@ -373,9 +375,10 @@ def choice_processor(self, stdscr, screen_id, choice): if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): self.reset_screen() self.switch_q.put(1) - #FIXME: There is some kind of graphical glitch that activates on returning to Main Menu, - # but not from all submenus. - # Further, there appear to be issues with how the program exits on Ctrl+C as part of this. + # FIXME: There is some kind of graphical glitch that activates on returning + # to Main Menu, but not from all submenus. + # Further, there appear to be issues with how the program exits on Ctrl+C as + # part of this. else: action = screen_actions.get(screen_id) if action: @@ -427,13 +430,11 @@ def main_menu_select(self, choice): control.remove_library_catalog(self) elif choice.startswith("Winetricks"): self.reset_screen() - self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", - self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) #noqa: E501 self.choice_q.put("0") elif choice.startswith("Utilities"): self.reset_screen() - self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", - self.set_utilities_menu_options(), dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", self.set_utilities_menu_options(), dialog=config.use_python_dialog)) #noqa: E501 self.choice_q.put("0") elif choice == "Change Color Scheme": self.status("Changing color scheme") @@ -521,12 +522,11 @@ def utilities_menu_select(self, choice): elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File appimages = utils.find_appimage_files() - appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in - appimages] # noqa: E501 + appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) self.menu_options = appimage_choices question = "Which AppImage should be used?" - self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) + self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) #noqa: E501 elif choice == "Install ICU": self.reset_screen() wine.enforce_icu_data_files() @@ -539,7 +539,7 @@ def utilities_menu_select(self, choice): def custom_appimage_select(self, choice): #FIXME if choice == "Input Custom AppImage": - appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") + appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") #noqa: E501 else: appimage_filename = choice self.conf.wine_appimage_path = appimage_filename @@ -573,9 +573,7 @@ def install_dependencies_confirm(self, choice): else: self.menu_screen.choice = "Processing" self.confirm_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) #noqa: E501 def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: @@ -606,12 +604,10 @@ def manual_install_confirm(self, choice): if choice == "Continue": self.menu_screen.choice = "Processing" self.manualinstall_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, - "Installing dependencies…\n", wait=True, - dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) #noqa: E501 def switch_screen(self, dialog): - if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: + if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: #noqa: E501 self.tui_screens.pop(0) if self.active_screen == self.menu_screen: self.menu_screen.choice = "Processing" @@ -625,7 +621,7 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: if isinstance(options, str): answer = options elif isinstance(options, list): - self.menu_options = self.which_dialog_options(options, config.use_python_dialog) + self.menu_options = self.which_dialog_options(options, config.use_python_dialog) #noqa: E501 self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, self.menu_options, dialog=config.use_python_dialog)) #noqa: E501 # Now wait for it to complete @@ -634,8 +630,7 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: stack_index = 3 if answer == PROMPT_OPTION_FILE else 4 - self.screen_q.put(self.stack_input(stack_index, Queue(), threading.Event(), question, - os.path.expanduser(f"~/"), dialog=config.use_python_dialog)) + self.screen_q.put(self.stack_input(stack_index, Queue(), threading.Event(), question, os.path.expanduser(f"~/"), dialog=config.use_python_dialog)) #noqa: E501 # Now wait for it to complete self.ask_answer_event.wait() answer = self.ask_answer_queue.get() @@ -669,14 +664,13 @@ def get_waiting(self, dialog, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) - percent = installer.get_progress_pct(self.installer_step, self.installer_step_count) - self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, - wait=True, percent=percent, dialog=dialog)) + percent = installer.get_progress_pct(self.installer_step, self.installer_step_count) #noqa: E501 + self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, dialog=dialog)) #noqa: E501 # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " # f"Please provide your password to provide escalation privileges.") - # self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) + # self.screen_q.put(self.stack_password(15, self.password_q, self.password_e, question, dialog=dialog)) #noqa: E501 def confirm_restore_dir(self, choice): if choice: @@ -701,7 +695,7 @@ def do_backup(self): self.go_to_main_menu() def report_waiting(self, text, dialog): - #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) + #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) #noqa: E501 config.console_log.append(text) def which_dialog_options(self, labels, dialog=False): @@ -850,65 +844,59 @@ def set_utilities_menu_options(self, dialog=False): return options - def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): + def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): #noqa: E501 if dialog: utils.append_unique(self.tui_screens, - tui_screen.MenuDialog(self, screen_id, queue, event, question, options, - height, width, menu_height)) + tui_screen.MenuDialog(self, screen_id, queue, event, question, options, height, width, menu_height)) #noqa: E501 else: utils.append_unique(self.tui_screens, - tui_screen.MenuScreen(self, screen_id, queue, event, question, options, - height, width, menu_height)) + tui_screen.MenuScreen(self, screen_id, queue, event, question, options, height, width, menu_height)) #noqa: E501 def stack_input(self, screen_id, queue, event, question, default, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.InputDialog(self, screen_id, queue, event, question, default)) + tui_screen.InputDialog(self, screen_id, queue, event, question, default)) #noqa: E501 else: utils.append_unique(self.tui_screens, - tui_screen.InputScreen(self, screen_id, queue, event, question, default)) + tui_screen.InputScreen(self, screen_id, queue, event, question, default)) #noqa: E501 - def stack_password(self, screen_id, queue, event, question, default="", dialog=False): + def stack_password(self, screen_id, queue, event, question, default="", dialog=False): #noqa: E501 if dialog: utils.append_unique(self.tui_screens, - tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) + tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) #noqa: E501 else: utils.append_unique(self.tui_screens, - tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) + tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) #noqa: E501 - def stack_confirm(self, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"], dialog=False): + def stack_confirm(self, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"], dialog=False): #noqa: E501 if dialog: yes_label = options[0] no_label = options[1] utils.append_unique(self.tui_screens, - tui_screen.ConfirmDialog(self, screen_id, queue, event, question, no_text, secondary, - yes_label=yes_label, no_label=no_label)) + tui_screen.ConfirmDialog(self, screen_id, queue, event, question, no_text, secondary, yes_label=yes_label, no_label=no_label)) #noqa: E501 else: utils.append_unique(self.tui_screens, - tui_screen.ConfirmScreen(self, screen_id, queue, event, question, no_text, secondary, - options)) + tui_screen.ConfirmScreen(self, screen_id, queue, event, question, no_text, secondary, options)) #noqa: E501 - def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): + def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): #noqa: E501 if dialog: utils.append_unique(self.tui_screens, - tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) + tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) #noqa: E501 else: - utils.append_unique(self.tui_screens, tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) + utils.append_unique(self.tui_screens, tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) #noqa: E501 - def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialog=False): + def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialog=False): #noqa: E501 logging.debug(f"Elements stacked: {elements}") if dialog: - utils.append_unique(self.tui_screens, tui_screen.TaskListDialog(self, screen_id, queue, event, text, - elements, percent)) + utils.append_unique(self.tui_screens, tui_screen.TaskListDialog(self, screen_id, queue, event, text, elements, percent)) #noqa: E501 else: #TODO: curses version pass - def stack_buildlist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): + def stack_buildlist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): #noqa: E501 if dialog: utils.append_unique(self.tui_screens, - tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, - height, width, list_height)) + tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) #noqa: E501 else: # TODO pass @@ -917,8 +905,7 @@ def stack_checklist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): if dialog: utils.append_unique(self.tui_screens, - tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, - height, width, list_height)) + tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) #noqa: E501 else: # TODO pass diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 678ea40d..37c1960f 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -1,18 +1,13 @@ import curses -import logging import signal import textwrap -from . import config -from . import msg -from . import utils - def wrap_text(app, text): # Turn text into wrapped text, line by line, centered if "\n" in text: lines = text.splitlines() - wrapped_lines = [textwrap.fill(line, app.window_width - (app.terminal_margin * 2)) for line in lines] + wrapped_lines = [textwrap.fill(line, app.window_width - (app.terminal_margin * 2)) for line in lines] #noqa: E501 lines = '\n'.join(wrapped_lines) else: wrapped_text = textwrap.fill(text, app.window_width - (app.terminal_margin * 2)) @@ -20,7 +15,7 @@ def wrap_text(app, text): return lines -def write_line(app, stdscr, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): +def write_line(app, stdscr, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): #noqa: E501 try: stdscr.addnstr(start_y, start_x, text, char_limit, attributes) except curses.error: @@ -30,11 +25,11 @@ def write_line(app, stdscr, start_y, start_x, text, char_limit, attributes=curse def title(app, title_text, title_start_y_adj): stdscr = app.get_main_window() title_lines = wrap_text(app, title_text) - title_start_y = max(0, app.window_height // 2 - len(title_lines) // 2) + # title_start_y = max(0, app.window_height // 2 - len(title_lines) // 2) last_index = 0 for i, line in enumerate(title_lines): if i < app.window_height: - write_line(app, stdscr, i + title_start_y_adj, 2, line, app.window_width, curses.A_BOLD) + write_line(app, stdscr, i + title_start_y_adj, 2, line, app.window_width, curses.A_BOLD) #noqa: E501 last_index = i return last_index @@ -51,7 +46,7 @@ def text_centered(app, text, start_y=0): for i, line in enumerate(text_lines): if text_start_y + i < app.window_height: x = app.window_width // 2 - text_width // 2 - write_line(app, stdscr, text_start_y + i, x, line, app.window_width, curses.A_BOLD) + write_line(app, stdscr, text_start_y + i, x, line, app.window_width, curses.A_BOLD) #noqa: E501 return text_start_y, text_lines @@ -81,7 +76,7 @@ def confirm(app, question_text, height=None, width=None): elif key.lower() == 'n': return False - write_line(app, stdscr, y, 0, "Type Y[es] or N[o]. ", app.window_width, curses.A_BOLD) + write_line(app, stdscr, y, 0, "Type Y[es] or N[o]. ", app.window_width, curses.A_BOLD) #noqa: E501 class CursesDialog: @@ -91,7 +86,7 @@ def __init__(self, app): self.stdscr: curses.window = self.app.get_menu_window() def __str__(self): - return f"Curses Dialog" + return "Curses Dialog" def draw(self): pass @@ -114,21 +109,21 @@ def __init__(self, app, question_text, default_text): self.question_lines = None def __str__(self): - return f"UserInput Curses Dialog" + return "UserInput Curses Dialog" def draw(self): curses.echo() curses.curs_set(1) self.stdscr.clear() - self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) + self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) #noqa: E501 self.input() curses.curs_set(0) curses.noecho() self.stdscr.refresh() def input(self): - write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) - key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) + write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) #noqa: E501 + key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) #noqa: E501 try: if key == -1: # If key not found, keep processing. @@ -169,9 +164,8 @@ def run(self): return self.user_input def input(self): - write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, - self.app.window_width) - key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) + write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, self.app.window_width) #noqa: E501 + key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) #noqa: E501 try: if key == -1: # If key not found, keep processing. @@ -200,14 +194,14 @@ def __init__(self, app, question_text, options): self.question_lines = None def __str__(self): - return f"Menu Curses Dialog" + return "Menu Curses Dialog" def draw(self): self.stdscr.erase() self.app.active_screen.set_options(self.options) self.total_pages = (len(self.options) - 1) // self.app.options_per_page + 1 - self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) + self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) #noqa: E501 # Display the options, centered options_start_y = self.question_start_y + len(self.question_lines) + 2 for i in range(self.app.options_per_page): @@ -221,10 +215,10 @@ def draw(self): wine_binary_path = option[1] wine_binary_description = option[2] wine_binary_path_wrapped = textwrap.wrap( - f"Binary Path: {wine_binary_path}", self.app.window_width - 4) + f"Binary Path: {wine_binary_path}", self.app.window_width - 4) #noqa: E501 option_lines.extend(wine_binary_path_wrapped) wine_binary_desc_wrapped = textwrap.wrap( - f"Description: {wine_binary_description}", self.app.window_width - 4) + f"Description: {wine_binary_description}", self.app.window_width - 4) #noqa: E501 option_lines.extend(wine_binary_desc_wrapped) else: wine_binary_path = option[1] @@ -243,23 +237,23 @@ def draw(self): x = max(0, self.app.window_width // 2 - len(line) // 2) if y < self.app.menu_window_height: if index == self.app.current_option: - write_line(self.app, self.stdscr, y, x, line, self.app.window_width, curses.A_REVERSE) + write_line(self.app, self.stdscr, y, x, line, self.app.window_width, curses.A_REVERSE) #noqa: E501 else: - write_line(self.app, self.stdscr, y, x, line, self.app.window_width) + write_line(self.app, self.stdscr, y, x, line, self.app.window_width) #noqa: E501 menu_bottom = y if type(option) is list: options_start_y += (len(option_lines)) # Display pagination information - page_info = f"Page {self.app.current_page + 1}/{self.total_pages} | Selected Option: {self.app.current_option + 1}/{len(self.options)}" - write_line(self.app, self.stdscr, max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) + page_info = f"Page {self.app.current_page + 1}/{self.total_pages} | Selected Option: {self.app.current_option + 1}/{len(self.options)}" #noqa: E501 + write_line(self.app, self.stdscr, max(menu_bottom, self.app.menu_window_height) - 3, 2, page_info, self.app.window_width, curses.A_BOLD) #noqa: E501 def do_menu_up(self): - if self.app.current_option == self.app.current_page * self.app.options_per_page and self.app.current_page > 0: + if self.app.current_option == self.app.current_page * self.app.options_per_page and self.app.current_page > 0: #noqa: E501 # Move to the previous page self.app.current_page -= 1 - self.app.current_option = min(len(self.app.menu_options) - 1, (self.app.current_page + 1) * self.app.options_per_page - 1) + self.app.current_option = min(len(self.app.menu_options) - 1, (self.app.current_page + 1) * self.app.options_per_page - 1) #noqa: E501 elif self.app.current_option == 0: if self.total_pages == 1: self.app.current_option = len(self.app.menu_options) - 1 @@ -270,15 +264,15 @@ def do_menu_up(self): self.app.current_option = max(0, self.app.current_option - 1) def do_menu_down(self): - if self.app.current_option == (self.app.current_page + 1) * self.app.options_per_page - 1 and self.app.current_page < self.total_pages - 1: + if self.app.current_option == (self.app.current_page + 1) * self.app.options_per_page - 1 and self.app.current_page < self.total_pages - 1: #noqa: E501 # Move to the next page self.app.current_page += 1 - self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_page * self.app.options_per_page) + self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_page * self.app.options_per_page) #noqa: E501 elif self.app.current_option == len(self.app.menu_options) - 1: self.app.current_page = 0 self.app.current_option = 0 else: - self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_option + 1) + self.app.current_option = min(len(self.app.menu_options) - 1, self.app.current_option + 1) #noqa: E501 def input(self): if len(self.app.tui_screens) > 0: @@ -294,7 +288,8 @@ def input(self): self.do_menu_up() elif key == curses.KEY_DOWN or key == 258: # Down arrow self.do_menu_down() - elif key == 27: # Sometimes the up/down arrow key is represented by a series of three keys. + elif key == 27: + # Sometimes the up/down arrow key is represented by a series of 3 keys. next_key = self.stdscr.getch() if next_key == 91: final_key = self.stdscr.getch() diff --git a/ou_dedetai/tui_dialog.py b/ou_dedetai/tui_dialog.py index 44a838dc..5c80a636 100644 --- a/ou_dedetai/tui_dialog.py +++ b/ou_dedetai/tui_dialog.py @@ -7,7 +7,7 @@ -def text(screen, text, height=None, width=None, title=None, backtitle=None, colors=True): +def text(screen, text, height=None, width=None, title=None, backtitle=None, colors=True): # noqa: E501 dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -22,7 +22,7 @@ def text(screen, text, height=None, width=None, title=None, backtitle=None, colo dialog.infobox(text, **options) -def progress_bar(screen, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): +def progress_bar(screen, text, percent, height=None, width=None, title=None, backtitle=None, colors=True): # noqa: E501 screen.dialog = Dialog() screen.dialog.autowidgetsize = True options = {'colors': colors} @@ -49,7 +49,7 @@ def stop_progress_bar(screen): screen.dialog.gauge_stop() -def tasklist_progress_bar(screen, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): +def tasklist_progress_bar(screen, text, percent, elements, height=None, width=None, title=None, backtitle=None, colors=None): # noqa: E501 dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -73,7 +73,7 @@ def tasklist_progress_bar(screen, text, percent, elements, height=None, width=No raise -def input(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): +def input(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): # noqa: E501 dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -89,7 +89,7 @@ def input(screen, question_text, height=None, width=None, init="", title=None, return code, input -def password(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): +def password(screen, question_text, height=None, width=None, init="", title=None, backtitle=None, colors=True): # noqa: E501 dialog = Dialog() dialog.autowidgetsize = True options = {'colors': colors} @@ -101,7 +101,7 @@ def password(screen, question_text, height=None, width=None, init="", title=Non options['title'] = title if backtitle is not None: options['backtitle'] = backtitle - code, password = dialog.passwordbox(question_text, init=init, insecure=True, **options) + code, password = dialog.passwordbox(question_text, init=init, insecure=True, **options) # noqa: E501 return code, password @@ -118,11 +118,11 @@ def confirm(screen, question_text, yes_label="Yes", no_label="No", options['title'] = title if backtitle is not None: options['backtitle'] = backtitle - check = dialog.yesno(question_text, height, width, yes_label=yes_label, no_label=no_label, **options) + check = dialog.yesno(question_text, height, width, yes_label=yes_label, no_label=no_label, **options) # noqa: E501 return check # Returns "ok" or "cancel" -def directory_picker(screen, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): +def directory_picker(screen, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): # noqa: E501 str_dir = str(path_dir) try: @@ -147,7 +147,7 @@ def directory_picker(screen, path_dir, height=None, width=None, title=None, back return path -def menu(screen, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): +def menu(screen, question_text, choices, height=None, width=None, menu_height=8, title=None, backtitle=None, colors=True): # noqa: E501 tag_to_description = {tag: description for tag, description in choices} dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True @@ -158,7 +158,7 @@ def menu(screen, question_text, choices, height=None, width=None, menu_height=8, options['backtitle'] = backtitle menu_options = [(tag, description) for i, (tag, description) in enumerate(choices)] - code, tag = dialog.menu(question_text, height, width, menu_height, menu_options, **options) + code, tag = dialog.menu(question_text, height, width, menu_height, menu_options, **options) # noqa: E501 selected_description = tag_to_description.get(tag) if code == dialog.OK: @@ -167,7 +167,7 @@ def menu(screen, question_text, choices, height=None, width=None, menu_height=8, return None, None, "Return to Main Menu" -def buildlist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): +def buildlist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # noqa: E501 # items is an interable of (tag, item, status) dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True @@ -189,7 +189,7 @@ def buildlist(screen, text, items=[], height=None, width=None, list_height=None, return None -def checklist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): +def checklist(screen, text, items=[], height=None, width=None, list_height=None, title=None, backtitle=None, colors=True): # noqa: E501 # items is an iterable of (tag, item, status) dialog = Dialog(dialog="dialog") dialog.autowidgetsize = True @@ -203,7 +203,7 @@ def checklist(screen, text, items=[], height=None, width=None, list_height=None, if backtitle is not None: options['backtitle'] = backtitle - code, tags = dialog.checklist(text, choices=items, list_height=list_height, **options) + code, tags = dialog.checklist(text, choices=items, list_height=list_height, **options) # noqa: E501 if code == dialog.OK: return code, tags diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index a6e14dae..1a7c2662 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -10,34 +10,32 @@ from ou_dedetai.app import App -from . import config from . import constants -from . import msg from . import network from . import system from . import utils -# XXX: fix lingering lack of refs to app -def check_wineserver(wineserver_binary: str): +def check_wineserver(app: App): try: - # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of WINESERVER_EXE - # changed it to use wineserver_binary, this change may alter the behavior, to match what the code intended - process = run_wine_proc(wineserver_binary, exe_args=["-p"]) + # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of + # WINESERVER_EXE changed it to use wineserver_binary, this change may alter the + # behavior, to match what the code intended + process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-p"]) system.wait_pid(process) return process.returncode == 0 except Exception: return False -def wineserver_kill(wineserver_binary: str): - if check_wineserver(wineserver_binary): - process = run_wine_proc(wineserver_binary, exe_args=["-k"]) +def wineserver_kill(app: App): + if check_wineserver(app): + process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-k"]) system.wait_pid(process) -def wineserver_wait(wineserver_binary: str): - if check_wineserver(wineserver_binary): - process = run_wine_proc(wineserver_binary, app, exe_args=["-w"]) +def wineserver_wait(app: App): + if check_wineserver(app): + process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-w"]) system.wait_pid(process) @@ -172,7 +170,8 @@ def check_wine_rules(wine_release, release_version, faithlife_product_version: s return True, "Default to trusting user override" -def check_wine_version_and_branch(release_version, test_binary, faithlife_product_version): +def check_wine_version_and_branch(release_version, test_binary, + faithlife_product_version): if not os.path.exists(test_binary): reason = "Binary does not exist." return False, reason @@ -186,7 +185,11 @@ def check_wine_version_and_branch(release_version, test_binary, faithlife_produc if wine_release is False and error_message is not None: return False, error_message - result, message = check_wine_rules(wine_release, release_version, faithlife_product_version) + result, message = check_wine_rules( + wine_release, + release_version, + faithlife_product_version + ) if not result: return result, message @@ -250,7 +253,7 @@ def install_msi(app: App): app.status(f"Running MSI installer: {app.conf.faithlife_installer_name}.") # Execute the .MSI wine_exe = app.conf.wine64_binary - exe_args = ["/i", f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}"] + exe_args = ["/i", f"{app.conf.install_dir}/data/{app.conf.faithlife_installer_name}"] #noqa: E501 if app.conf._overrides.faithlife_install_passive is True: exe_args.append('/passive') logging.info(f"Running: {wine_exe} msiexec {' '.join(exe_args)}") @@ -361,7 +364,7 @@ def install_d3d_compiler(app: App): def install_fonts(app: App): fonts = ['corefonts', 'tahoma'] - if not app.conf.skip_fonts: + if not app.conf.skip_install_fonts: for i, f in enumerate(fonts): app.status("Configuring fonts, this step may take several minutes…", i / len(fonts)) # noqa: E501 args = [f] @@ -407,7 +410,7 @@ def enforce_icu_data_files(app: App): repo = "FaithLife-Community/icu" json_data = network.get_latest_release_data(repo) icu_url = network.get_first_asset_url(json_data) - icu_latest_version = network.get_tag_name(json_data).lstrip('v') + icu_latest_version = network.get_tag_name(json_data) if icu_url is None: logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 From 3b8273efff5422cafefe586629bc6b71a40937be Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:28:10 -0800 Subject: [PATCH 080/137] refactor: DIALOG and use_python_dialog it should be noted this does expose some possibly untested code paths Before some function calls didn't pass config.use_python_dialog (falling back to False), now it all respects use_python_dialog some menus may open in dialog that didn't before. --- ou_dedetai/app.py | 2 +- ou_dedetai/config.py | 11 +- ou_dedetai/control.py | 14 +- ou_dedetai/main.py | 37 +-- ou_dedetai/system.py | 18 +- ou_dedetai/tui_app.py | 590 ++++++++++++++++++++++++++++++------------ 6 files changed, 459 insertions(+), 213 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index cad3d685..3f2a5208 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -179,4 +179,4 @@ def _install_complete_hook(self): """Function run when installation is complete.""" def _install_started_hook(self): - """Function run when installation first begins.""" \ No newline at end of file + """Function run when installation first begins.""" diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e602755d..c40bbd81 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -13,9 +13,6 @@ # Define and set additional variables that can be set in the env. extended_config = { 'CONFIG_FILE': None, - 'DIALOG': None, - # Dependent on DIALOG with env override - 'use_python_dialog': None, } for key, default in extended_config.items(): globals()[key] = os.getenv(key, default) @@ -221,6 +218,8 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" + terminal_app_prefer_dialog: Optional[bool] = None + # Start of values just set via cli arg faithlife_install_passive: bool = False app_run_as_root_permitted: bool = False @@ -245,6 +244,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": config_file = constants.DEFAULT_CONFIG_PATH if legacy.CONFIG_FILE is not None: config_file = legacy.CONFIG_FILE + terminal_app_prefer_dialog = None + if legacy.use_python_dialog is not None: + terminal_app_prefer_dialog = utils.parse_bool(legacy.use_python_dialog) return EphemeralConfiguration( installer_binary_dir=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, @@ -267,7 +269,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": install_fonts_skip=legacy.SKIP_FONTS, wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, - wine_output_encoding=legacy.WINECMD_ENCODING + wine_output_encoding=legacy.WINECMD_ENCODING, + terminal_app_prefer_dialog=terminal_app_prefer_dialog ) @classmethod diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 690c02d9..cddffb3d 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -39,15 +39,11 @@ def backup_and_restore(mode: str, app: App): data_dirs = ['Data', 'Documents', 'Users'] backup_dir = Path(app.conf.backup_dir).expanduser().resolve() - # FIXME: Why is this different per UI? Should this always accept? - if config.DIALOG == 'tk' or config.DIALOG == 'curses': - pass # user confirms in GUI or TUI - else: - verb = 'Use' if mode == 'backup' else 'Restore backup from' - if not app.approve(f"{verb} existing backups folder \"{app.conf.backup_dir}\"?"): #noqa: E501 - # Reset backup dir. - # The app will re-prompt next time the backup_dir is accessed - app.conf._raw.backup_dir = None + verb = 'Use' if mode == 'backup' else 'Restore backup from' + if not app.approve(f"{verb} existing backups folder \"{app.conf.backup_dir}\"?"): #noqa: E501 + # Reset backup dir. + # The app will re-prompt next time the backup_dir is accessed + app.conf._raw.backup_dir = None # Set source folders. backup_dir = Path(app.conf.backup_dir) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 7a946898..f00fd894 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -300,8 +300,9 @@ def parse_args(args, parser) -> EphemeralConfiguration: def run_control_panel(ephemeral_config: EphemeralConfiguration): - logging.info(f"Using DIALOG: {config.DIALOG}") - if config.DIALOG is None or config.DIALOG == 'tk': + dialog = system.get_dialog() + logging.info(f"Using DIALOG: {dialog}") + if dialog == 'tk': gui_app.control_panel_app(ephemeral_config) else: try: @@ -359,31 +360,6 @@ def setup_config() -> EphemeralConfiguration: return parse_args(cli_args, parser) -def set_dialog(): - # Set DIALOG and GUI variables. - if config.DIALOG is None: - system.get_dialog() - else: - config.DIALOG = config.DIALOG.lower() - - if config.DIALOG == 'curses' and "dialog" in sys.modules and config.use_python_dialog is None: # noqa: E501 - config.use_python_dialog = system.test_dialog_version() - - if config.use_python_dialog is None: - logging.debug("The 'dialog' package was not found. Falling back to Python Curses.") # noqa: E501 - config.use_python_dialog = False - elif config.use_python_dialog: - logging.debug("Dialog version is up-to-date.") - config.use_python_dialog = True - else: - logging.error("Dialog version is outdated. The program will fall back to Curses.") # noqa: E501 - config.use_python_dialog = False - logging.debug(f"Use Python Dialog?: {config.use_python_dialog}") - # Set Architecture - - system.check_architecture() - - def is_app_installed(ephemeral_config: EphemeralConfiguration): persistent_config = PersistentConfiguration.load_from_path(ephemeral_config.config_path) if persistent_config.faithlife_product is None or persistent_config.install_dir is None: @@ -403,9 +379,8 @@ def run(ephemeral_config: EphemeralConfiguration): # wine.set_logos_paths() config.ACTION(ephemeral_config) # run control_panel right away return - - # Only control_panel ACTION uses TUI/GUI interface; all others are CLI. - config.DIALOG = 'cli' + + # Proceeding with the CLI interface install_required = [ 'backup', @@ -438,7 +413,7 @@ def run(ephemeral_config: EphemeralConfiguration): def main(): ephemeral_config = setup_config() - set_dialog() + system.check_architecture() # XXX: consider configuration migration from legacy to new diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 5e01e448..d3e6e8b3 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -216,7 +216,15 @@ def reboot(superuser_command: str): sys.exit(0) -def get_dialog(): +def get_dialog() -> str: + """Returns which frontend the user prefers + + Uses "DIALOG" from environment if found, + otherwise opens curses if the user has a tty + + Returns: + dialog - tk (graphical), curses (terminal ui), or cli (command line) + """ if not os.environ.get('DISPLAY'): print("The installer does not work unless you are running a display", file=sys.stderr) # noqa: E501 sys.exit(1) @@ -228,11 +236,11 @@ def get_dialog(): if dialog not in ['cli', 'curses', 'tk']: print("Valid values for DIALOG are 'cli', 'curses' or 'tk'.", file=sys.stderr) # noqa: E501 sys.exit(1) - config.DIALOG = dialog - elif sys.__stdin__.isatty(): - config.DIALOG = 'curses' + return dialog + elif sys.__stdin__ is not None and sys.__stdin__.isatty(): + return 'curses' else: - config.DIALOG = 'tk' + return 'tk' def get_architecture() -> Tuple[str, int]: diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index af6e0823..940a2ebb 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -1,6 +1,7 @@ import logging import os import signal +import sys import threading import time import curses @@ -88,9 +89,15 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati # Window and Screen Management self.tui_screens = [] self.menu_options = [] - self.window_height = self.window_width = self.console = self.menu_screen = self.active_screen = None - self.main_window_ratio = self.main_window_ratio = self.menu_window_ratio = self.main_window_min = None - self.menu_window_min = self.main_window_height = self.menu_window_height = self.main_window = None + self.window_height = self.window_width = self.console = self.menu_screen = ( + self.active_screen + ) = None + self.main_window_ratio = self.main_window_ratio = self.menu_window_ratio = ( + self.main_window_min + ) = None + self.menu_window_min = self.main_window_height = self.menu_window_height = ( + self.main_window + ) = None self.menu_window = self.resize_window = None # For menu dialogs. @@ -99,8 +106,33 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.current_page: int = 0 self.total_pages: int = 0 - self.set_window_dimensions() + # Note to reviewers: + # This does expose some possibly untested code paths + # + # Before some function calls didn't pass use_python_dialog falling back to False + # now it all respects use_python_dialog + # some menus may open in dialog that didn't before. + # + # XXX: consider hard coding this to false for the time being + # Is there value in supportting both curses and dialog? + self.use_python_dialog: bool = False + if "dialog" in sys.modules and ephemeral_config.terminal_app_prefer_dialog is not False: #noqa: E501 + result = system.test_dialog_version() + + if result is None: + logging.debug( + "The 'dialog' package was not found. Falling back to Python Curses." + ) # noqa: E501 + elif result: + logging.debug("Dialog version is up-to-date.") + self.use_python_dialog = True + else: + logging.error( + "Dialog version is outdated. The program will fall back to Curses." + ) # noqa: E501 + logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") + self.set_window_dimensions() def set_window_dimensions(self): self.update_tty_dimensions() @@ -110,16 +142,27 @@ def set_window_dimensions(self): min_console_height = len(tui_curses.wrap_text(self, config.console_log[-1])) else: min_console_height = 2 - self.main_window_min = len(tui_curses.wrap_text(self, self.title)) + len( - tui_curses.wrap_text(self, self.subtitle)) + min_console_height + self.main_window_min = ( + len(tui_curses.wrap_text(self, self.title)) + + len(tui_curses.wrap_text(self, self.subtitle)) + + min_console_height + ) self.menu_window_ratio = 0.75 self.menu_window_min = 3 - self.main_window_height = max(int(self.window_height * self.main_window_ratio), self.main_window_min) #noqa: E501#noqa: E501 - self.menu_window_height = max(self.window_height - self.main_window_height, int(self.window_height * self.menu_window_ratio), self.menu_window_min) #noqa: E501 + self.main_window_height = max( + int(self.window_height * self.main_window_ratio), self.main_window_min + ) # noqa: E501#noqa: E501 + self.menu_window_height = max( + self.window_height - self.main_window_height, + int(self.window_height * self.menu_window_ratio), + self.menu_window_min, + ) # noqa: E501 self.console_log_lines = max(self.main_window_height - self.main_window_min, 1) self.options_per_page = max(self.window_height - self.main_window_height - 6, 1) self.main_window = curses.newwin(self.main_window_height, curses.COLS, 0, 0) - self.menu_window = curses.newwin(self.menu_window_height, curses.COLS, self.main_window_height + 1, 0) #noqa: E501 + self.menu_window = curses.newwin( + self.menu_window_height, curses.COLS, self.main_window_height + 1, 0 + ) # noqa: E501 resize_lines = tui_curses.wrap_text(self, "Screen too small.") self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) @@ -127,9 +170,9 @@ def set_window_dimensions(self): def set_curses_style(): curses.start_color() curses.use_default_colors() - curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue - curses.init_color(curses.COLOR_CYAN, 906, 906, 906) # Logos Gray - curses.init_color(curses.COLOR_WHITE, 988, 988, 988) # Logos White + curses.init_color(curses.COLOR_BLUE, 0, 510, 1000) # Logos Blue + curses.init_color(curses.COLOR_CYAN, 906, 906, 906) # Logos Gray + curses.init_color(curses.COLOR_WHITE, 988, 988, 988) # Logos White curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_CYAN) curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLUE) @@ -140,17 +183,17 @@ def set_curses_style(): def set_curses_colors(self): if self.conf.curses_colors == "Logos": - self.stdscr.bkgd(' ', curses.color_pair(3)) - self.main_window.bkgd(' ', curses.color_pair(3)) - self.menu_window.bkgd(' ', curses.color_pair(3)) + self.stdscr.bkgd(" ", curses.color_pair(3)) + self.main_window.bkgd(" ", curses.color_pair(3)) + self.menu_window.bkgd(" ", curses.color_pair(3)) elif self.conf.curses_colors == "Light": - self.stdscr.bkgd(' ', curses.color_pair(6)) - self.main_window.bkgd(' ', curses.color_pair(6)) - self.menu_window.bkgd(' ', curses.color_pair(6)) + self.stdscr.bkgd(" ", curses.color_pair(6)) + self.main_window.bkgd(" ", curses.color_pair(6)) + self.menu_window.bkgd(" ", curses.color_pair(6)) elif self.conf.curses_colors == "Dark": - self.stdscr.bkgd(' ', curses.color_pair(7)) - self.main_window.bkgd(' ', curses.color_pair(7)) - self.menu_window.bkgd(' ', curses.color_pair(7)) + self.stdscr.bkgd(" ", curses.color_pair(7)) + self.main_window.bkgd(" ", curses.color_pair(7)) + self.menu_window.bkgd(" ", curses.color_pair(7)) def update_windows(self): if isinstance(self.active_screen, tui_screen.CursesScreen): @@ -182,9 +225,18 @@ def init_curses(self): curses.cbreak() self.stdscr.keypad(True) - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) #noqa: E501 - self.menu_screen = tui_screen.MenuScreen(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(dialog=False)) #noqa: E501 - #self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(dialog=True)) #noqa: E501 + self.console = tui_screen.ConsoleScreen( + self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0 + ) # noqa: E501 + self.menu_screen = tui_screen.MenuScreen( + self, + 0, + self.status_q, + self.status_e, + "Main Menu", + self.set_tui_menu_options(), + ) # noqa: E501 + # self.menu_screen = tui_screen.MenuDialog(self, 0, self.status_q, self.status_e, "Main Menu", self.set_tui_menu_options(dialog=True)) #noqa: E501 self.refresh() except curses.error as e: logging.error(f"Curses error in init_curses: {e}") @@ -221,8 +273,10 @@ def update_main_window_contents(self): self.clear() self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 - self.console = tui_screen.ConsoleScreen(self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0) # noqa: E501 - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.console = tui_screen.ConsoleScreen( + self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0 + ) # noqa: E501 + self.menu_screen.set_options(self.set_tui_menu_options()) # self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) self.refresh() @@ -249,7 +303,7 @@ def signal_resize(self, signum, frame): self.resize_curses() self.choice_q.put("resize") - if config.use_python_dialog: + if self.use_python_dialog: if ( isinstance(self.active_screen, tui_screen.TextDialog) and self.active_screen.text == "Screen Too Small" @@ -273,7 +327,15 @@ def draw_resize_screen(self): self.resize_window = curses.newwin(len(resize_lines) + 1, curses.COLS, 0, 0) for i, line in enumerate(resize_lines): if i < self.window_height: - tui_curses.write_line(self, self.resize_window, i, margin, line, self.window_width - self.terminal_margin, curses.A_BOLD) + tui_curses.write_line( + self, + self.resize_window, + i, + margin, + line, + self.window_width - self.terminal_margin, + curses.A_BOLD, + ) self.refresh() def display(self): @@ -284,7 +346,7 @@ def display(self): # Makes sure status stays shown timestamp = utils.get_timestamp() self.status_q.put(f"{timestamp} {self.console_message}") - self.report_waiting(f"{self.console_message}", dialog=config.use_python_dialog) # noqa: E501 + self.report_waiting(f"{self.console_message}") # noqa: E501 self.active_screen = self.menu_screen last_time = time.time() @@ -302,7 +364,8 @@ def display(self): self.choice_processor( self.menu_window, self.active_screen.get_screen_id(), - self.choice_q.get()) + self.choice_q.get(), + ) if self.screen_q.qsize() > 0: self.screen_q.get() @@ -310,7 +373,7 @@ def display(self): if self.switch_q.qsize() > 0: self.switch_q.get() - self.switch_screen(config.use_python_dialog) + self.switch_screen() if len(self.tui_screens) == 0: self.active_screen = self.menu_screen @@ -321,7 +384,7 @@ def display(self): run_monitor, last_time = utils.stopwatch(last_time, 2.5) if run_monitor: self.logos.monitor() - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.menu_screen.set_options(self.set_tui_menu_options()) if isinstance(self.active_screen, tui_screen.CursesScreen): self.refresh() @@ -345,7 +408,7 @@ def run(self): signal.signal(signal.SIGINT, self.end) def installing_pw_waiting(self): - utils.start_thread(self.get_waiting, config.use_python_dialog, screen_id=15) + utils.start_thread(self.get_waiting, screen_id=15) def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -368,7 +431,7 @@ def choice_processor(self, stdscr, screen_id, choice): 20: self.win_ver_logos_select, 21: self.win_ver_index_select, 24: self.confirm_restore_dir, - 25: self.choose_restore_dir + 25: self.choose_restore_dir, } # Capture menu exiting before processing in the rest of the handler @@ -413,12 +476,12 @@ def main_menu_select(self, choice): elif choice == f"Run {self.conf.faithlife_product}": self.reset_screen() self.logos.start() - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.menu_screen.set_options(self.set_tui_menu_options()) self.switch_q.put(1) elif choice == f"Stop {self.conf.faithlife_product}": self.reset_screen() self.logos.stop() - self.menu_screen.set_options(self.set_tui_menu_options(dialog=False)) + self.menu_screen.set_options(self.set_tui_menu_options()) self.switch_q.put(1) elif choice == "Run Indexing": self.active_screen.running = 0 @@ -430,11 +493,27 @@ def main_menu_select(self, choice): control.remove_library_catalog(self) elif choice.startswith("Winetricks"): self.reset_screen() - self.screen_q.put(self.stack_menu(11, self.todo_q, self.todo_e, "Winetricks Menu", self.set_winetricks_menu_options(), dialog=config.use_python_dialog)) #noqa: E501 + self.screen_q.put( + self.stack_menu( + 11, + self.todo_q, + self.todo_e, + "Winetricks Menu", + self.set_winetricks_menu_options(), + ) + ) # noqa: E501 self.choice_q.put("0") elif choice.startswith("Utilities"): self.reset_screen() - self.screen_q.put(self.stack_menu(18, self.todo_q, self.todo_e, "Utilities Menu", self.set_utilities_menu_options(), dialog=config.use_python_dialog)) #noqa: E501 + self.screen_q.put( + self.stack_menu( + 18, + self.todo_q, + self.todo_e, + "Utilities Menu", + self.set_utilities_menu_options(), + ) + ) # noqa: E501 self.choice_q.put("0") elif choice == "Change Color Scheme": self.status("Changing color scheme") @@ -460,24 +539,39 @@ def winetricks_menu_select(self, choice): self.go_to_main_menu() elif choice == "Set Renderer": self.reset_screen() - self.screen_q.put(self.stack_menu(19, self.todo_q, self.todo_e, - "Choose Renderer", - self.set_renderer_menu_options(), - dialog=config.use_python_dialog)) + self.screen_q.put( + self.stack_menu( + 19, + self.todo_q, + self.todo_e, + "Choose Renderer", + self.set_renderer_menu_options(), + ) + ) self.choice_q.put("0") elif choice == "Set Windows Version for Logos": self.reset_screen() - self.screen_q.put(self.stack_menu(20, self.todo_q, self.todo_e, - "Set Windows Version for Logos", - self.set_win_ver_menu_options(), - dialog=config.use_python_dialog)) + self.screen_q.put( + self.stack_menu( + 20, + self.todo_q, + self.todo_e, + "Set Windows Version for Logos", + self.set_win_ver_menu_options(), + ) + ) self.choice_q.put("0") elif choice == "Set Windows Version for Indexer": self.reset_screen() - self.screen_q.put(self.stack_menu(21, self.todo_q, self.todo_e, - "Set Windows Version for Indexer", - self.set_win_ver_menu_options(), - dialog=config.use_python_dialog)) + self.screen_q.put( + self.stack_menu( + 21, + self.todo_q, + self.todo_e, + "Set Windows Version for Indexer", + self.set_win_ver_menu_options(), + ) + ) self.choice_q.put("0") def utilities_menu_select(self, choice): @@ -522,11 +616,17 @@ def utilities_menu_select(self, choice): elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File appimages = utils.find_appimage_files() - appimage_choices = [["AppImage", filename, "AppImage of Wine64"] for filename in appimages] # noqa: E501 + appimage_choices = [ + ["AppImage", filename, "AppImage of Wine64"] for filename in appimages + ] # noqa: E501 appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) self.menu_options = appimage_choices question = "Which AppImage should be used?" - self.screen_q.put(self.stack_menu(1, self.appimage_q, self.appimage_e, question, appimage_choices)) #noqa: E501 + self.screen_q.put( + self.stack_menu( + 1, self.appimage_q, self.appimage_e, question, appimage_choices + ) + ) # noqa: E501 elif choice == "Install ICU": self.reset_screen() wine.enforce_icu_data_files() @@ -537,9 +637,11 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() def custom_appimage_select(self, choice): - #FIXME + # FIXME if choice == "Input Custom AppImage": - appimage_filename = tui_curses.get_user_input(self, "Enter AppImage filename: ", "") #noqa: E501 + appimage_filename = tui_curses.get_user_input( + self, "Enter AppImage filename: ", "" + ) # noqa: E501 else: appimage_filename = choice self.conf.wine_appimage_path = appimage_filename @@ -573,7 +675,15 @@ def install_dependencies_confirm(self, choice): else: self.menu_screen.choice = "Processing" self.confirm_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) #noqa: E501 + self.screen_q.put( + self.stack_text( + 13, + self.todo_q, + self.todo_e, + "Installing dependencies…\n", + wait=True, + ) + ) # noqa: E501 def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: @@ -604,10 +714,22 @@ def manual_install_confirm(self, choice): if choice == "Continue": self.menu_screen.choice = "Processing" self.manualinstall_e.set() - self.screen_q.put(self.stack_text(13, self.todo_q, self.todo_e, "Installing dependencies…\n", wait=True, dialog=config.use_python_dialog)) #noqa: E501 - - def switch_screen(self, dialog): - if self.active_screen is not None and self.active_screen != self.menu_screen and len(self.tui_screens) > 0: #noqa: E501 + self.screen_q.put( + self.stack_text( + 13, + self.todo_q, + self.todo_e, + "Installing dependencies…\n", + wait=True, + ) + ) # noqa: E501 + + def switch_screen(self): + if ( + self.active_screen is not None + and self.active_screen != self.menu_screen + and len(self.tui_screens) > 0 + ): # noqa: E501 self.tui_screens.pop(0) if self.active_screen == self.menu_screen: self.menu_screen.choice = "Processing" @@ -621,16 +743,28 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: if isinstance(options, str): answer = options elif isinstance(options, list): - self.menu_options = self.which_dialog_options(options, config.use_python_dialog) #noqa: E501 - self.screen_q.put(self.stack_menu(2, Queue(), threading.Event(), question, self.menu_options, dialog=config.use_python_dialog)) #noqa: E501 + self.menu_options = self.which_dialog_options(options) + self.screen_q.put( + self.stack_menu( + 2, Queue(), threading.Event(), question, self.menu_options + ) + ) # noqa: E501 # Now wait for it to complete self.ask_answer_event.wait() answer = self.ask_answer_queue.get() - if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: + if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: stack_index = 3 if answer == PROMPT_OPTION_FILE else 4 - self.screen_q.put(self.stack_input(stack_index, Queue(), threading.Event(), question, os.path.expanduser(f"~/"), dialog=config.use_python_dialog)) #noqa: E501 + self.screen_q.put( + self.stack_input( + stack_index, + Queue(), + threading.Event(), + question, + os.path.expanduser("~/"), + ) + ) # noqa: E501 # Now wait for it to complete self.ask_answer_event.wait() answer = self.ask_answer_queue.get() @@ -641,7 +775,7 @@ def handle_ask_response(self, choice: Optional[str]): if choice is not None: self.ask_answer_queue.put(choice) self.ask_answer_event.set() - self.switch_screen(config.use_python_dialog) + self.switch_screen() def handle_ask_file_response(self, choice: Optional[str]): # XXX: can there be some sort of feedback if this file path isn't valid? @@ -658,14 +792,25 @@ def _status(self, message: str, percent: int | None = None): pass def _install_started_hook(self): - self.get_waiting(self, config.use_python_dialog) + self.get_waiting(self) - def get_waiting(self, dialog, screen_id=8): + def get_waiting(self, screen_id=8): text = ["Install is running…\n"] processed_text = utils.str_array_to_string(text) - - percent = installer.get_progress_pct(self.installer_step, self.installer_step_count) #noqa: E501 - self.screen_q.put(self.stack_text(screen_id, self.status_q, self.status_e, processed_text, wait=True, percent=percent, dialog=dialog)) #noqa: E501 + + percent = installer.get_progress_pct( + self.installer_step, self.installer_step_count + ) # noqa: E501 + self.screen_q.put( + self.stack_text( + screen_id, + self.status_q, + self.status_e, + processed_text, + wait=True, + percent=percent, + ) + ) # noqa: E501 # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " @@ -688,30 +833,30 @@ def choose_restore_dir(self, choice): def do_backup(self): self.todo_e.wait() self.todo_e.clear() - if self.tmp == 'backup': + if self.tmp == "backup": control.backup(self) else: control.restore(self) self.go_to_main_menu() - def report_waiting(self, text, dialog): - #self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) #noqa: E501 + def report_waiting(self, text): + # self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) #noqa: E501 config.console_log.append(text) - def which_dialog_options(self, labels, dialog=False): + def which_dialog_options(self, labels): options = [] option_number = 1 for label in labels: - if dialog: + if self.use_python_dialog: options.append((str(option_number), label)) option_number += 1 else: options.append(label) return options - def set_tui_menu_options(self, dialog=False): + def set_tui_menu_options(self): labels = [] - if system.get_runmode() == 'binary': + if system.get_runmode() == "binary": status = utils.compare_logos_linux_installer_version(self) if status == utils.VersionComparison.OUT_OF_DATE: labels.append(f"Update {constants.APP_NAME}") @@ -734,32 +879,24 @@ def set_tui_menu_options(self, dialog=False): indexing = "Stop Indexing" elif self.logos.indexing_state == logos.State.STOPPED: indexing = "Run Indexing" - labels_default = [ - run, - indexing - ] + labels_default = [run, indexing] else: labels_default = ["Install Logos Bible Software"] labels.extend(labels_default) - labels_support = [ - "Utilities →", - "Winetricks →" - ] + labels_support = ["Utilities →", "Winetricks →"] labels.extend(labels_support) - labels_options = [ - "Change Color Scheme" - ] + labels_options = ["Change Color Scheme"] labels.extend(labels_options) labels.append("Exit") - options = self.which_dialog_options(labels, dialog=False) + options = self.which_dialog_options(labels) return options - def set_winetricks_menu_options(self, dialog=False): + def set_winetricks_menu_options(self): labels = [] labels_support = [ "Download or Update Winetricks", @@ -768,62 +905,49 @@ def set_winetricks_menu_options(self, dialog=False): "Install Fonts", "Set Renderer", "Set Windows Version for Logos", - "Set Windows Version for Indexer" + "Set Windows Version for Indexer", ] labels.extend(labels_support) labels.append("Return to Main Menu") - options = self.which_dialog_options(labels, dialog=False) + options = self.which_dialog_options(labels) return options - def set_renderer_menu_options(self, dialog=False): + def set_renderer_menu_options(self): labels = [] - labels_support = [ - "gdi", - "gl", - "vulkan" - ] + labels_support = ["gdi", "gl", "vulkan"] labels.extend(labels_support) labels.append("Return to Main Menu") - options = self.which_dialog_options(labels, dialog=False) + options = self.which_dialog_options(labels) return options - def set_win_ver_menu_options(self, dialog=False): + def set_win_ver_menu_options(self): labels = [] - labels_support = [ - "vista", - "win7", - "win8", - "win10", - "win11" - ] + labels_support = ["vista", "win7", "win8", "win10", "win11"] labels.extend(labels_support) labels.append("Return to Main Menu") - options = self.which_dialog_options(labels, dialog=False) + options = self.which_dialog_options(labels) return options - def set_utilities_menu_options(self, dialog=False): + def set_utilities_menu_options(self): labels = [] if self.is_installed(): labels_catalog = [ "Remove Library Catalog", "Remove All Index Files", - "Install ICU" + "Install ICU", ] labels.extend(labels_catalog) - labels_utilities = [ - "Install Dependencies", - "Edit Config" - ] + labels_utilities = ["Install Dependencies", "Edit Config"] labels.extend(labels_utilities) if self.is_installed(): @@ -835,77 +959,217 @@ def set_utilities_menu_options(self, dialog=False): ] labels.extend(labels_utils_installed) - label = "Enable Logging" if self.conf.faithlife_product_logging else "Disable Logging" #noqa: E501 + label = ( + "Enable Logging" + if self.conf.faithlife_product_logging + else "Disable Logging" + ) # noqa: E501 labels.append(label) labels.append("Return to Main Menu") - options = self.which_dialog_options(labels, dialog=False) + options = self.which_dialog_options(labels) return options - def stack_menu(self, screen_id, queue, event, question, options, height=None, width=None, menu_height=8, dialog=False): #noqa: E501 - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.MenuDialog(self, screen_id, queue, event, question, options, height, width, menu_height)) #noqa: E501 + def stack_menu( + self, + screen_id, + queue, + event, + question, + options, + height=None, + width=None, + menu_height=8, + ): # noqa: E501 + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.MenuDialog( + self, + screen_id, + queue, + event, + question, + options, + height, + width, + menu_height, + ), + ) # noqa: E501 else: - utils.append_unique(self.tui_screens, - tui_screen.MenuScreen(self, screen_id, queue, event, question, options, height, width, menu_height)) #noqa: E501 - - def stack_input(self, screen_id, queue, event, question, default, dialog=False): - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.InputDialog(self, screen_id, queue, event, question, default)) #noqa: E501 + utils.append_unique( + self.tui_screens, + tui_screen.MenuScreen( + self, + screen_id, + queue, + event, + question, + options, + height, + width, + menu_height, + ), + ) # noqa: E501 + + def stack_input(self, screen_id, queue, event, question, default): + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.InputDialog( + self, screen_id, queue, event, question, default + ), + ) # noqa: E501 else: - utils.append_unique(self.tui_screens, - tui_screen.InputScreen(self, screen_id, queue, event, question, default)) #noqa: E501 - - def stack_password(self, screen_id, queue, event, question, default="", dialog=False): #noqa: E501 - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.PasswordDialog(self, screen_id, queue, event, question, default)) #noqa: E501 + utils.append_unique( + self.tui_screens, + tui_screen.InputScreen( + self, screen_id, queue, event, question, default + ), + ) # noqa: E501 + + def stack_password( + self, screen_id, queue, event, question, default="" + ): # noqa: E501 + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.PasswordDialog( + self, screen_id, queue, event, question, default + ), + ) # noqa: E501 else: - utils.append_unique(self.tui_screens, - tui_screen.PasswordScreen(self, screen_id, queue, event, question, default)) #noqa: E501 - - def stack_confirm(self, screen_id, queue, event, question, no_text, secondary, options=["Yes", "No"], dialog=False): #noqa: E501 - if dialog: + utils.append_unique( + self.tui_screens, + tui_screen.PasswordScreen( + self, screen_id, queue, event, question, default + ), + ) # noqa: E501 + + def stack_confirm( + self, + screen_id, + queue, + event, + question, + no_text, + secondary, + options=["Yes", "No"], + ): # noqa: E501 + if self.use_python_dialog: yes_label = options[0] no_label = options[1] - utils.append_unique(self.tui_screens, - tui_screen.ConfirmDialog(self, screen_id, queue, event, question, no_text, secondary, yes_label=yes_label, no_label=no_label)) #noqa: E501 + utils.append_unique( + self.tui_screens, + tui_screen.ConfirmDialog( + self, + screen_id, + queue, + event, + question, + no_text, + secondary, + yes_label=yes_label, + no_label=no_label, + ), + ) # noqa: E501 else: - utils.append_unique(self.tui_screens, - tui_screen.ConfirmScreen(self, screen_id, queue, event, question, no_text, secondary, options)) #noqa: E501 - - def stack_text(self, screen_id, queue, event, text, wait=False, percent=None, dialog=False): #noqa: E501 - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.TextDialog(self, screen_id, queue, event, text, wait, percent)) #noqa: E501 + utils.append_unique( + self.tui_screens, + tui_screen.ConfirmScreen( + self, screen_id, queue, event, question, no_text, secondary, options + ), + ) # noqa: E501 + + def stack_text( + self, screen_id, queue, event, text, wait=False, percent=None + ): # noqa: E501 + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.TextDialog( + self, screen_id, queue, event, text, wait, percent + ), + ) # noqa: E501 else: - utils.append_unique(self.tui_screens, tui_screen.TextScreen(self, screen_id, queue, event, text, wait)) #noqa: E501 - - def stack_tasklist(self, screen_id, queue, event, text, elements, percent, dialog=False): #noqa: E501 + utils.append_unique( + self.tui_screens, + tui_screen.TextScreen(self, screen_id, queue, event, text, wait), + ) # noqa: E501 + + def stack_tasklist( + self, screen_id, queue, event, text, elements, percent + ): # noqa: E501 logging.debug(f"Elements stacked: {elements}") - if dialog: - utils.append_unique(self.tui_screens, tui_screen.TaskListDialog(self, screen_id, queue, event, text, elements, percent)) #noqa: E501 + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.TaskListDialog( + self, screen_id, queue, event, text, elements, percent + ), + ) # noqa: E501 else: - #TODO: curses version + # TODO: curses version pass - def stack_buildlist(self, screen_id, queue, event, question, options, height=None, width=None, list_height=None, dialog=False): #noqa: E501 - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.BuildListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) #noqa: E501 + def stack_buildlist( + self, + screen_id, + queue, + event, + question, + options, + height=None, + width=None, + list_height=None, + ): # noqa: E501 + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.BuildListDialog( + self, + screen_id, + queue, + event, + question, + options, + height, + width, + list_height, + ), + ) # noqa: E501 else: # TODO pass - def stack_checklist(self, screen_id, queue, event, question, options, - height=None, width=None, list_height=None, dialog=False): - if dialog: - utils.append_unique(self.tui_screens, - tui_screen.CheckListDialog(self, screen_id, queue, event, question, options, height, width, list_height)) #noqa: E501 + def stack_checklist( + self, + screen_id, + queue, + event, + question, + options, + height=None, + width=None, + list_height=None, + ): + if self.use_python_dialog: + utils.append_unique( + self.tui_screens, + tui_screen.CheckListDialog( + self, + screen_id, + queue, + event, + question, + options, + height, + width, + list_height, + ), + ) # noqa: E501 else: # TODO pass @@ -921,5 +1185,5 @@ def get_menu_window(self): def control_panel_app(stdscr: curses.window, ephemeral_config: EphemeralConfiguration): - os.environ.setdefault('ESCDELAY', '100') + os.environ.setdefault("ESCDELAY", "100") TUI(stdscr, ephemeral_config).run() From 4d732fcadeb1a353d1483eec7bb1521123e320a1 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:39:24 -0800 Subject: [PATCH 081/137] refactor: migrate config.threads to the app --- ou_dedetai/app.py | 29 +++++++++++++++++++++++++++++ ou_dedetai/cli.py | 10 +++++++--- ou_dedetai/config.py | 1 - ou_dedetai/control.py | 4 ++-- ou_dedetai/gui_app.py | 40 ++++++++++++++++++++-------------------- ou_dedetai/logos.py | 14 ++++++-------- ou_dedetai/main.py | 19 +------------------ ou_dedetai/tui_app.py | 8 ++++---- ou_dedetai/utils.py | 12 ------------ 9 files changed, 69 insertions(+), 68 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 3f2a5208..69ccd232 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -4,6 +4,7 @@ import os from pathlib import Path import sys +import threading from typing import NoReturn, Optional from ou_dedetai import constants @@ -17,6 +18,12 @@ class App(abc.ABC): installer_step: int = 1 """Step the installer is on. Starts at 0""" + _threads: list[threading.Thread] + """List of threads + + Non-daemon threads will be joined before shutdown + """ + def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise these would be circular imports from ou_dedetai.config import Config @@ -25,6 +32,7 @@ def __init__(self, config, **kwargs) -> None: self.conf = Config(config, self) self.logos = LogosManager(app=self) + self._threads = [] # Ensure everything is good to start check_incompatibilities(self) @@ -104,8 +112,14 @@ def approve(self, question: str, context: Optional[str] = None) -> bool: def exit(self, reason: str, intended: bool = False) -> NoReturn: """Exits the application cleanly with a reason.""" + logging.debug(f"Closing {constants.APP_NAME}.") # Shutdown logos/indexer if we spawned it self.logos.end_processes() + # Join threads + for thread in self._threads: + # Only wait on non-daemon threads. + if not thread.daemon: + thread.join() # Remove pid file if exists try: os.remove(constants.PID_FILE) @@ -180,3 +194,18 @@ def _install_complete_hook(self): def _install_started_hook(self): """Function run when installation first begins.""" + + def start_thread(self, task, *args, daemon_bool: bool = True, **kwargs): + """Starts a new thread + + Non-daemon threads be joined before shutdown""" + thread = threading.Thread( + name=f"{constants.APP_NAME} {task}", + target=task, + daemon=daemon_bool, + args=args, + kwargs=kwargs + ) + self._threads.append(thread) + thread.start() + return thread \ No newline at end of file diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index b3ab8333..a7db7e5b 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -3,6 +3,7 @@ import threading from typing import Optional +from ou_dedetai import constants from ou_dedetai.app import App from ou_dedetai.config import EphemeralConfiguration from ou_dedetai.system import SuperuserCommandNotFound @@ -38,10 +39,13 @@ def install_app(self): def install(app: CLI): installer.install(app) app.exit("Install has finished", intended=True) - self.thread = utils.start_thread( - install, - app=self + self.thread = threading.Thread( + name=f"{constants.APP_NAME} install", + target=install, + daemon=False, + args=[self] ) + self.thread.start() self.user_input_processor() def install_d3d_compiler(self): diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index c40bbd81..f6f1bf72 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -20,7 +20,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' console_log = [] -threads = [] # Begin new config diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index cddffb3d..7d913ab6 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -84,7 +84,7 @@ def backup_and_restore(mode: str, app: App): # Get source transfer size. q = queue.Queue() app.status("Calculating backup size…") - t = utils.start_thread(utils.get_folder_group_size, src_dirs, q) + t = app.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): # FIXME: consider showing a sign of life to the app @@ -135,7 +135,7 @@ def backup_and_restore(mode: str, app: App): app.status("Calculating destination directory size") dst_dir_size = utils.get_path_size(dst_dir) app.status("Starting backup…") - t = utils.start_thread(copy_data, src_dirs, dst_dir) + t = app.start_thread(copy_data, src_dirs, dst_dir) try: counter = 0 while t.is_alive(): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 62376181..c6401d95 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -48,7 +48,7 @@ def spawn_dialog(): # Run the mainloop in this thread pop_up.mainloop() if isinstance(options, list): - utils.start_thread(spawn_dialog) + self.start_thread(spawn_dialog) answer_event.wait() answer: Optional[str] = answer_q.get() @@ -277,7 +277,7 @@ def start_ensure_config(self): # Ensure progress counter is reset. self.installer_step = 0 self.installer_step_count = 0 - self.config_thread = utils.start_thread( + self.config_thread = self.start_thread( installer.ensure_installation_config, app=self, ) @@ -357,7 +357,7 @@ def start_releases_check(self): self.gui.progress.start() self.gui.statusvar.set("Downloading Release list…") # Start thread. - utils.start_thread(self.get_logos_releases) + self.start_thread(self.get_logos_releases) def set_release(self, evt=None): if self.gui.releasevar.get()[0] == 'C': # ignore default text @@ -384,7 +384,7 @@ def start_find_appimage_files(self, release_version): self.gui.progress.start() self.gui.statusvar.set("Finding available wine AppImages…") # Start thread. - utils.start_thread( + self.start_thread( utils.find_appimage_files, release_version=release_version, app=self, @@ -412,7 +412,7 @@ def get_wine_options(app: InstallerWindow, app_images, binaries): app.root.event_generate(app.wine_evt) # Start thread. - utils.start_thread( + self.start_thread( get_wine_options, self, self.appimages, @@ -467,7 +467,7 @@ def on_cancel_released(self, evt=None): def start_install_thread(self, evt=None): self.gui.progress.config(mode='determinate') - utils.start_thread(installer.install, app=self) + self.start_thread(installer.install, app=self) # XXX: where should this live? here or ControlWindow? def _status(self, message: str, percent: int | None = None): @@ -656,7 +656,7 @@ def run_installer(self, evt=None): self.root.icon = self.conf.faithlife_product_icon_path def run_logos(self, evt=None): - utils.start_thread(self.logos.start) + self.start_thread(self.logos.start) def run_action_cmd(self, evt=None): self.actioncmd() @@ -675,18 +675,18 @@ def on_action_radio_clicked(self, evt=None): self.actioncmd = self.install_icu def run_indexing(self, evt=None): - utils.start_thread(self.logos.index) + self.start_thread(self.logos.index) def remove_library_catalog(self, evt=None): control.remove_library_catalog(self) def remove_indexes(self, evt=None): self.gui.statusvar.set("Removing indexes…") - utils.start_thread(control.remove_all_index_files, app=self) + self.start_thread(control.remove_all_index_files, app=self) def install_icu(self, evt=None): self.gui.statusvar.set("Installing ICU files…") - utils.start_thread(wine.enforce_icu_data_files, app=self) + self.start_thread(wine.enforce_icu_data_files, app=self) def run_backup(self, evt=None): # Prepare progress bar. @@ -694,16 +694,16 @@ def run_backup(self, evt=None): self.gui.progress.config(mode='determinate') self.gui.progressvar.set(0) # Start backup thread. - utils.start_thread(control.backup, app=self) + self.start_thread(control.backup, app=self) def run_restore(self, evt=None): # FIXME: Allow user to choose restore source? # Start restore thread. - utils.start_thread(control.restore, app=self) + self.start_thread(control.restore, app=self) def install_deps(self, evt=None): self.start_indeterminate_progress() - utils.start_thread(utils.install_dependencies, self) + self.start_thread(utils.install_dependencies, self) def open_file_dialog(self, filetype_name, filetype_extension): file_path = fd.askopenfilename( @@ -718,7 +718,7 @@ def open_file_dialog(self, filetype_name, filetype_extension): def update_to_latest_lli_release(self, evt=None): self.start_indeterminate_progress() self.gui.statusvar.set(f"Updating to latest {constants.APP_NAME} version…") # noqa: E501 - utils.start_thread(utils.update_to_latest_lli_release, app=self) + self.start_thread(utils.update_to_latest_lli_release, app=self) def set_appimage_symlink(self): utils.set_appimage_symlink(self) @@ -728,7 +728,7 @@ def update_to_latest_appimage(self, evt=None): self.conf.wine_appimage_path = self.conf.wine_appimage_recommended_file_name # noqa: E501 self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest AppImage…") - utils.start_thread(self.set_appimage_symlink) + self.start_thread(self.set_appimage_symlink) def set_appimage(self, evt=None): # TODO: Separate as advanced feature. @@ -736,12 +736,12 @@ def set_appimage(self, evt=None): if not appimage_filename: return self.conf.wine_appimage_path = appimage_filename - utils.start_thread(self.set_appimage_symlink) + self.start_thread(self.set_appimage_symlink) def get_winetricks(self, evt=None): # TODO: Separate as advanced feature. self.gui.statusvar.set("Installing Winetricks…") - utils.start_thread( + self.start_thread( system.install_winetricks, self.conf.installer_binary_dir, app=self @@ -751,10 +751,10 @@ def get_winetricks(self, evt=None): def launch_winetricks(self, evt=None): self.gui.statusvar.set("Launching Winetricks…") # Start winetricks in thread. - utils.start_thread(self.run_winetricks) + self.start_thread(self.run_winetricks) # Start thread to clear status after delay. args = [12000, self.root.event_generate, '<>'] - utils.start_thread(self.root.after, *args) + self.start_thread(self.root.after, *args) def run_winetricks(self): wine.run_winetricks(self) @@ -766,7 +766,7 @@ def switch_logging(self, evt=None): self.gui.progress.state(['!disabled']) self.gui.progress.start() self.gui.logging_button.state(['disabled']) - utils.start_thread( + self.start_thread( self.logos.switch_logging, action=desired_state.lower() ) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index cce18b77..e6c44093 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -110,11 +110,10 @@ def run_logos(): wine.wineserver_kill(self.app) app = self.app from ou_dedetai.gui_app import GuiApp - if isinstance(self.app, GuiApp): + if not isinstance(self.app, GuiApp): # Don't send "Running" message to GUI b/c it never clears. - app = None - app.status(f"Running {self.app.conf.faithlife_product}…") - utils.start_thread(run_logos, daemon_bool=False) + app.status(f"Running {self.app.conf.faithlife_product}…") + self.app.start_thread(run_logos, daemon_bool=False) # NOTE: The following code would keep the CLI open while running # Logos, but since wine logging is sent directly to wine.log, # there's no terminal output to see. A user can see that output by: @@ -206,15 +205,14 @@ def wait_on_indexing(): wine.wineserver_kill(self.app) self.app.status("Indexing has begun…") - index_thread = utils.start_thread(run_indexing, daemon_bool=False) + index_thread = self.app.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING - check_thread = utils.start_thread( + self.app.start_thread( check_if_indexing, index_thread, daemon_bool=False ) - wait_thread = utils.start_thread(wait_on_indexing, daemon_bool=False) - main.threads.extend([index_thread, check_thread, wait_thread]) + self.app.start_thread(wait_on_indexing, daemon_bool=False) def stop_indexing(self): self.indexing_state = State.STOPPING diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index f00fd894..2888ce83 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -24,8 +24,6 @@ from . import tui_app from . import utils -from .config import threads - def get_parser(): desc = "Installs FaithLife Bible Software with Wine." @@ -311,10 +309,6 @@ def run_control_panel(ephemeral_config: EphemeralConfiguration): raise except SystemExit: logging.info("Caught SystemExit, exiting gracefully...") - try: - close() - except Exception as e: - raise e raise except curses.error as e: logging.error(f"Curses error in run_control_panel(): {e}") @@ -442,19 +436,8 @@ def main(): run(ephemeral_config) -def close(): - logging.debug(f"Closing {constants.APP_NAME}.") - for thread in threads: - # Only wait on non-daemon threads. - if not thread.daemon: - thread.join() - logging.debug(f"Closing {constants.APP_NAME} finished.") - - if __name__ == '__main__': try: main() except KeyboardInterrupt: - close() - - close() + pass diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 940a2ebb..37ef3a1f 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -408,7 +408,7 @@ def run(self): signal.signal(signal.SIGINT, self.end) def installing_pw_waiting(self): - utils.start_thread(self.get_waiting, screen_id=15) + self.start_thread(self.get_waiting, screen_id=15) def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -466,7 +466,7 @@ def main_menu_select(self, choice): self.reset_screen() self.installer_step = 0 self.installer_step_count = 0 - utils.start_thread( + self.start_thread( installer.install, daemon_bool=True, app=self, @@ -605,10 +605,10 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Back Up Data": self.reset_screen() - utils.start_thread(self.do_backup) + self.start_thread(self.do_backup) elif choice == "Restore Data": self.reset_screen() - utils.start_thread(self.do_backup) + self.start_thread(self.do_backup) elif choice == "Update to Latest AppImage": self.reset_screen() utils.update_to_latest_recommended_appimage(self) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 5b3042bb..99d3e454 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -727,18 +727,6 @@ def grep(regexp, filepath): return found -def start_thread(task, *args, daemon_bool=True, **kwargs): - thread = threading.Thread( - name=f"{task}", - target=task, - daemon=daemon_bool, - args=args, - kwargs=kwargs - ) - config.threads.append(thread) - thread.start() - return thread - def str_array_to_string(text, delimeter="\n"): try: From 703198dd6f304224aa92637ef1279f111784e51b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:47:14 -0800 Subject: [PATCH 082/137] refactor: console_log --- ou_dedetai/config.py | 1 - ou_dedetai/tui_app.py | 14 +++++++++++--- ou_dedetai/tui_screen.py | 7 +++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index f6f1bf72..c3f9ddec 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -19,7 +19,6 @@ # Set other run-time variables not set in the env. ACTION: str = 'app' -console_log = [] # Begin new config diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 37ef3a1f..a2cfcf67 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -106,6 +106,9 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.current_page: int = 0 self.total_pages: int = 0 + # Lines for the on-screen console log + self.console_log: list[str] = [] + # Note to reviewers: # This does expose some possibly untested code paths # @@ -134,12 +137,17 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() + @property + def recent_console_log(self) -> list[str]: + """Outputs console log trimmed by the maximum length""" + return self.console_log[-self.console_log_lines:] + def set_window_dimensions(self): self.update_tty_dimensions() curses.resizeterm(self.window_height, self.window_width) self.main_window_ratio = 0.25 - if config.console_log: - min_console_height = len(tui_curses.wrap_text(self, config.console_log[-1])) + if self.console_log: + min_console_height = len(tui_curses.wrap_text(self, self.console_log[-1])) else: min_console_height = 2 self.main_window_min = ( @@ -841,7 +849,7 @@ def do_backup(self): def report_waiting(self, text): # self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) #noqa: E501 - config.console_log.append(text) + self.console_log.append(text) def which_dialog_options(self, labels): options = [] diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index 84270231..8cb0880c 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -16,7 +16,10 @@ class Screen: def __init__(self, app: App, screen_id, queue, event): - self.app = app + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("Cannot start TUI screen with non-TUI app") + self.app: TUI = app self.stdscr = "" self.screen_id = screen_id self.choice = "Processing" @@ -87,7 +90,7 @@ def display(self): console_start_y = len(tui_curses.wrap_text(self.app, self.title)) + len( tui_curses.wrap_text(self.app, self.subtitle)) + 1 tui_curses.write_line(self.app, self.stdscr, console_start_y, self.app.terminal_margin, "---Console---", self.app.window_width - (self.app.terminal_margin * 2)) #noqa: E501 - recent_messages = config.console_log[-self.app.console_log_lines:] + recent_messages = self.app.recent_console_log for i, message in enumerate(recent_messages, 1): message_lines = tui_curses.wrap_text(self.app, message) for j, line in enumerate(message_lines): From 7edb53a0c35d9affa10e94a46dbf0fd57a2d64e0 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:52:36 -0800 Subject: [PATCH 083/137] refactor: migrate ACTION --- ou_dedetai/config.py | 3 --- ou_dedetai/main.py | 42 ++++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index c3f9ddec..d0243933 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -17,9 +17,6 @@ for key, default in extended_config.items(): globals()[key] = os.getenv(key, default) -# Set other run-time variables not set in the env. -ACTION: str = 'app' - # Begin new config diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 2888ce83..42748797 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -2,6 +2,7 @@ import argparse import curses import logging.handlers +from typing import Callable, Tuple from ou_dedetai.config import ( EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path @@ -198,7 +199,7 @@ def get_parser(): return parser -def parse_args(args, parser) -> EphemeralConfiguration: +def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfiguration], None]]: #noqa: E501 if args.config: ephemeral_config = EphemeralConfiguration.load_from_path(args.config) else: @@ -251,7 +252,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: if args.passive: ephemeral_config.faithlife_install_passive = True - # Set ACTION function. + # Set action return function. actions = { 'backup': cli.backup, 'create_shortcuts': cli.create_shortcuts, @@ -276,7 +277,7 @@ def parse_args(args, parser) -> EphemeralConfiguration: 'winetricks': cli.winetricks, } - config.ACTION = None + run_action = None for arg, action in actions.items(): if getattr(args, arg): if arg == "set_appimage": @@ -288,13 +289,14 @@ def parse_args(args, parser) -> EphemeralConfiguration: e = f"{ephemeral_config.wine_appimage_path} is not an AppImage." raise argparse.ArgumentTypeError(e) if arg == 'winetricks': + # XXX: lingering config ref config.winetricks_args = getattr(args, 'winetricks') - config.ACTION = action + run_action = action break - if config.ACTION is None: - config.ACTION = run_control_panel - logging.debug(f"{config.ACTION=}") - return ephemeral_config + if run_action is None: + run_action = run_control_panel + logging.debug(f"{run_action=}") + return ephemeral_config, run_action def run_control_panel(ephemeral_config: EphemeralConfiguration): @@ -318,7 +320,7 @@ def run_control_panel(ephemeral_config: EphemeralConfiguration): raise e -def setup_config() -> EphemeralConfiguration: +def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfiguration], None]]: #noqa: E501 parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately @@ -363,15 +365,15 @@ def is_app_installed(ephemeral_config: EphemeralConfiguration): return utils.find_installed_product(persistent_config.faithlife_product, wine_prefix) -def run(ephemeral_config: EphemeralConfiguration): +def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralConfiguration], None]): #noqa: E501 # Run desired action (requested function, defaults to control_panel) - if config.ACTION == "disabled": + if action == "disabled": print("That option is disabled.", file=sys.stderr) sys.exit(1) - if config.ACTION.__name__ == 'run_control_panel': + if action.__name__ == 'run_control_panel': # if utils.app_is_installed(): # wine.set_logos_paths() - config.ACTION(ephemeral_config) # run control_panel right away + action(ephemeral_config) # run control_panel right away return # Proceeding with the CLI interface @@ -392,21 +394,21 @@ def run(ephemeral_config: EphemeralConfiguration): 'toggle_app_logging', 'winetricks', ] - if config.ACTION.__name__ not in install_required: - logging.info(f"Running function: {config.ACTION.__name__}") - config.ACTION(ephemeral_config) + if action.__name__ not in install_required: + logging.info(f"Running function: {action.__name__}") + action(ephemeral_config) elif is_app_installed(ephemeral_config): # install_required; checking for app # wine.set_logos_paths() # Run the desired Logos action. - logging.info(f"Running function: {config.ACTION.__name__}") # noqa: E501 - config.ACTION(ephemeral_config) + logging.info(f"Running function: {action.__name__}") # noqa: E501 + action(ephemeral_config) else: # install_required, but app not installed print("App not installed, but required for this operation. Consider installing first.", file=sys.stderr) #noqa: E501 sys.exit(1) def main(): - ephemeral_config = setup_config() + ephemeral_config, action = setup_config() system.check_architecture() # XXX: consider configuration migration from legacy to new @@ -433,7 +435,7 @@ def main(): # Print terminal banner logging.info(f"{constants.APP_NAME}, {constants.LLI_CURRENT_VERSION} by {constants.LLI_AUTHOR}.") # noqa: E501 - run(ephemeral_config) + run(ephemeral_config, action) if __name__ == '__main__': From 6dc5a074bbd7f0712fd03184a2e2bdb6caec6f3f Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 08:55:32 -0800 Subject: [PATCH 084/137] refactor: migrate CONFIG_FILE --- ou_dedetai/config.py | 10 ---------- ou_dedetai/gui.py | 4 ---- ou_dedetai/installer.py | 2 -- ou_dedetai/main.py | 3 --- ou_dedetai/tui_app.py | 1 - ou_dedetai/utils.py | 10 ---------- 6 files changed, 30 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index d0243933..e5f34cde 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -10,16 +10,6 @@ from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY -# Define and set additional variables that can be set in the env. -extended_config = { - 'CONFIG_FILE': None, -} -for key, default in extended_config.items(): - globals()[key] = os.getenv(key, default) - - -# Begin new config - @dataclass class LegacyConfiguration: """Configuration and it's keys from before the user configuration class existed. diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index a30c3fff..ee6f698d 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -185,10 +185,6 @@ def __init__(self, root, app: App, *args, **kwargs): self.app = app - # XXX: remove these - # Initialize vars from ENV. - self.config_file = config.CONFIG_FILE - # Run/install app button self.app_buttonvar = StringVar() self.app_buttonvar.set("Install") diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 7997d831..e2d005e0 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -395,8 +395,6 @@ def ensure_config_file(app: App): app._install_complete_hook() - logging.debug(f"> File exists?: {config.CONFIG_FILE}: {Path(config.CONFIG_FILE).is_file()}") # noqa: E501 - def ensure_launcher_executable(app: App): app.installer_step_count += 1 diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 42748797..5106dd34 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -335,9 +335,6 @@ def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfigura # Initialize logging. msg.initialize_logging(log_level, app_log_path) - # Set default config; incl. defining CONFIG_FILE. - utils.set_default_config() - # XXX: do this in the new scheme (read then write the config). # We also want to remove the old file, (stored in CONFIG_FILE?) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index a2cfcf67..39f05f06 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -527,7 +527,6 @@ def main_menu_select(self, choice): self.status("Changing color scheme") self.conf.cycle_curses_color_scheme() self.reset_screen() - utils.write_config(config.CONFIG_FILE) def winetricks_menu_select(self, choice): if choice == "Download or Update Winetricks": diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 99d3e454..9baa20c5 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -52,14 +52,6 @@ def append_unique(list, item): logging.debug(f"{item} already in {list}.") -# Set "global" variables. -# XXX: fold this into config -def set_default_config(): - system.get_package_manager() - if config.CONFIG_FILE is None: - config.CONFIG_FILE = constants.DEFAULT_CONFIG_PATH - - # XXX: remove, no need. def write_config(config_file_path): pass @@ -667,8 +659,6 @@ def set_appimage_symlink(app: App): os.symlink(selected_appimage_file_path, appimage_symlink_path) app.conf.wine_appimage_path = f"{selected_appimage_file_path.name}" # noqa: E501 - write_config(config.CONFIG_FILE) - def update_to_latest_lli_release(app: App): result = compare_logos_linux_installer_version(app) From cdfd46e14b4eaced76ed7147d936a2e54a67b223 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 09:56:14 -0800 Subject: [PATCH 085/137] fix: resolve remaining mypy/ruff lints --- ou_dedetai/cli.py | 17 ++++---- ou_dedetai/config.py | 4 ++ ou_dedetai/control.py | 7 +-- ou_dedetai/gui.py | 1 - ou_dedetai/gui_app.py | 59 ++++++-------------------- ou_dedetai/installer.py | 8 ++-- ou_dedetai/logos.py | 3 +- ou_dedetai/main.py | 20 +++------ ou_dedetai/system.py | 14 +++--- ou_dedetai/tui_app.py | 80 +++++++--------------------------- ou_dedetai/tui_dialog.py | 2 +- ou_dedetai/tui_screen.py | 2 - ou_dedetai/utils.py | 19 +++------ ou_dedetai/wine.py | 92 +++++++++++++++++++++++----------------- 14 files changed, 117 insertions(+), 211 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index a7db7e5b..691f5d17 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -1,7 +1,7 @@ import queue import shutil import threading -from typing import Optional +from typing import Optional, Tuple from ou_dedetai import constants from ou_dedetai.app import App @@ -18,8 +18,8 @@ class CLI(App): def __init__(self, ephemeral_config: EphemeralConfiguration): super().__init__(ephemeral_config) self.running: bool = True - self.choice_q = queue.Queue() - self.input_q = queue.Queue() + self.choice_q: queue.Queue[str] = queue.Queue() + self.input_q: queue.Queue[Tuple[str, list[str]] | None] = queue.Queue() self.input_event = threading.Event() self.choice_event = threading.Event() @@ -94,8 +94,7 @@ def update_self(self): utils.update_to_latest_lli_release(self) def winetricks(self): - from ou_dedetai import config - wine.run_winetricks_cmd(self, *config.winetricks_args) + wine.run_winetricks_cmd(self, *self.conf._overrides.winetricks_args) _exit_option: str = "Exit" @@ -141,12 +140,12 @@ def superuser_command(self) -> str: else: raise SuperuserCommandNotFound("sudo command not found. Please install.") - def user_input_processor(self, evt=None): + def user_input_processor(self, evt=None) -> None: while self.running: prompt = None - question = None + question: Optional[str] = None options = None - choice = None + choice: Optional[str] = None # Wait for next input queue item. self.input_event.wait() self.input_event.clear() @@ -174,7 +173,7 @@ def user_input_processor(self, evt=None): # instantiated at the moment the subcommand is run. This lets any CLI-specific # code get executed along with the subcommand. -# NOTE: we should be able to achieve the same effect without re-declaring these functions +# NOTE: we should be able to achieve the same effect without re-declaring these def backup(ephemeral_config: EphemeralConfiguration): CLI(ephemeral_config).backup() diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index e5f34cde..69ac08bb 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -203,6 +203,10 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" + + winetricks_args: Optional[str] = None + """Arguments to winetricks if the action is running winetricks""" + terminal_app_prefer_dialog: Optional[bool] = None # Start of values just set via cli arg diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 7d913ab6..55a88e32 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -13,7 +13,6 @@ from ou_dedetai.app import App -from . import config from . import constants from . import system from . import utils @@ -82,7 +81,7 @@ def backup_and_restore(mode: str, app: App): app.status("Restoring data…") # Get source transfer size. - q = queue.Queue() + q: queue.Queue[int] = queue.Queue() app.status("Calculating backup size…") t = app.start_thread(utils.get_folder_group_size, src_dirs, q) try: @@ -132,8 +131,6 @@ def backup_and_restore(mode: str, app: App): else: m = f"Backing up to {str(dst_dir)}…" app.status(m) - app.status("Calculating destination directory size") - dst_dir_size = utils.get_path_size(dst_dir) app.status("Starting backup…") t = app.start_thread(copy_data, src_dirs, dst_dir) try: @@ -212,7 +209,7 @@ def set_winetricks(app: App): logging.warning("Winetricks path does not exist, downloading instead...") valid = False if not os.access(app.conf.winetricks_binary, os.X_OK): - logging.warning("Winetricks path given is not executable, downloading instead...") + logging.warning("Winetricks path given is not executable, downloading instead...") #noqa: E501 valid = False if not utils.check_winetricks_version(app.conf.winetricks_binary): logging.warning("Winetricks version mismatch, downloading instead...") diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index ee6f698d..fa4e76a5 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -16,7 +16,6 @@ from ou_dedetai.app import App -from . import config from . import constants diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index c6401d95..9128b78a 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -39,7 +39,7 @@ def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwa self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: - answer_q = Queue() + answer_q: Queue[str] = Queue() answer_event = Event() def spawn_dialog(): # Create a new popup (with it's own event loop) @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): class ChoicePopUp(Tk): """Creates a pop-up with a choice""" - def __init__(self, question: str, options: list[str], answer_q: Queue, answer_event: Event, **kwargs): #noqa: E501 + def __init__(self, question: str, options: list[str], answer_q: Queue[str], answer_event: Event, **kwargs): #noqa: E501 # Set root parameters. super().__init__() self.title(f"Quesiton: {question.strip().strip(':')}") @@ -248,14 +248,8 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): "<>", self.update_wine_check_progress ) - self.get_q = Queue() - self.get_evt = "<>" - self.root.bind(self.get_evt, self.update_download_progress) - self.status_q = Queue() - self.status_evt = "<>" - self.root.bind(self.status_evt, self.update_status_text) - self.releases_q = Queue() - self.wine_q = Queue() + self.releases_q: Queue[list[str]] = Queue() + self.wine_q: Queue[str] = Queue() # Run commands. self.get_winetricks_options() @@ -284,7 +278,7 @@ def start_ensure_config(self): def get_winetricks_options(self): self.conf.winetricks_binary = None # override config file b/c "Download" accounts for that # noqa: E501 - self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + ['Return to Main Menu'] + self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + ['Return to Main Menu'] #noqa: E501 self.gui.tricksvar.set(self.gui.tricks_dropdown['values'][0]) def set_input_widgets_state(self, state, widgets='all'): @@ -318,7 +312,7 @@ def set_product(self, evt=None): self.conf.faithlife_product = self.gui.productvar.get() self.gui.product_dropdown.selection_clear() if evt: # manual override; reset dependent variables - logging.debug(f"User changed faithlife_product to '{self.conf.faithlife_product}'") + logging.debug(f"User changed faithlife_product to '{self.conf.faithlife_product}'") #noqa: E501 self.gui.versionvar.set('') self.gui.releasevar.set('') self.gui.winevar.set('') @@ -428,10 +422,10 @@ def set_wine(self, evt=None): self.start_ensure_config() else: self.wine_q.put( - utils.get_relative_path( + str(utils.get_relative_path( utils.get_config_var(self.conf.wine_binary), self.conf.install_dir - ) + )) ) def set_winetricks(self, evt=None): @@ -524,18 +518,6 @@ def update_wine_check_progress(self, evt=None): self.stop_indeterminate_progress() self.gui.wine_check_button.state(['!disabled']) - def update_download_progress(self, evt=None): - d = self.get_q.get() - self.gui.progressvar.set(int(d)) - - def update_status_text(self, evt=None, status=None): - text = '' - if evt: - text = self.status_q.get() - elif status: - text = status - self.gui.statusvar.set(text) - def _install_complete_hook(self): self.gui.progress.stop() self.gui.progress.config(mode='determinate') @@ -615,10 +597,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.gui.run_winetricks_button.config(command=self.launch_winetricks) self.update_run_winetricks_button() - self.logging_q = Queue() - self.status_q = Queue() - self.status_evt = '<>' - self.root.bind(self.status_evt, self.update_status_text) self.root.bind('<>', self.clear_status_text) self.root.bind( '<>', @@ -629,9 +607,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.update_latest_appimage_button ) self.root.bind('<>', self.update_app_button) - self.get_q = Queue() - self.get_evt = "<>" - self.root.bind(self.get_evt, self.update_download_progress) self.installer_window = None @@ -652,7 +627,7 @@ def configure_app_button(self, evt=None): def run_installer(self, evt=None): classname = constants.BINARY_NAME installer_window_top = Toplevel() - self.installer_window = InstallerWindow(installer_window_top, self.root, app=self, class_=classname) + self.installer_window = InstallerWindow(installer_window_top, self.root, app=self, class_=classname) #noqa: E501 self.root.icon = self.conf.faithlife_product_icon_path def run_logos(self, evt=None): @@ -774,7 +749,9 @@ def switch_logging(self, evt=None): def _config_updated_hook(self) -> None: self.update_logging_button() if self.installer_window is not None: - self.installer_window._config_updated_hook() + # XXX: for some reason mypy thinks this is unreachable. + # consider the relationship between these too classes anyway + self.installer_window._config_updated_hook() #type: ignore[unreachable] return super()._config_updated_hook() # XXX: should this live here or in installerWindow? @@ -873,18 +850,6 @@ def reverse_logging_state_value(self, state) ->str: def clear_status_text(self, evt=None): self.gui.statusvar.set('') - def update_download_progress(self, evt=None): - d = self.get_q.get() - self.gui.progressvar.set(int(d)) - - def update_status_text(self, evt=None): - if evt: - self.gui.statusvar.set(self.status_q.get()) - self.root.after(3000, self.update_status_text) - else: # clear status text if called manually and no progress shown - if self.gui.progressvar.get() == 0: - self.gui.statusvar.set('') - def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) self.gui.progressvar.set(0) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index e2d005e0..176f8f91 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -6,16 +6,14 @@ from ou_dedetai.app import App -from . import config from . import constants -from . import msg from . import network from . import system from . import utils from . import wine -# XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses +# XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses #noqa: E501 def ensure_product_choice(app: App): app.installer_step_count += 1 app.status("Choose product…") @@ -42,7 +40,7 @@ def ensure_release_choice(app: App): app.installer_step += 1 app.status("Choose product release…") logging.debug('- config.TARGET_RELEASE_VERSION') - logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") + logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") #noqa: E501 def ensure_install_dir_choice(app: App): @@ -501,7 +499,7 @@ def create_launcher_shortcuts(app: App): app_icon_src = constants.APP_IMAGE_DIR / 'icon.png' if not installdir.is_dir(): - app.exit("Can't create launchers because the installation folder does not exist.") + app.exit("Can't create launchers because the installation folder does not exist.") #noqa: E501 app_dir = Path(installdir) / 'data' logos_icon_path = app_dir / logos_icon_src.name app_icon_path = app_dir / app_icon_src.name diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index e6c44093..71639d0a 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -9,7 +9,6 @@ from ou_dedetai.app import App -from . import main from . import system from . import utils from . import wine @@ -85,7 +84,7 @@ def monitor(self): def start(self): self.logos_state = State.STARTING - wine_release, _ = wine.get_wine_release(self.app.conf.wine_binary) + wine_release, _ = wine.get_wine_release() def run_logos(): process = wine.run_wine_proc( diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 5106dd34..c4323107 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -8,16 +8,11 @@ EphemeralConfiguration, PersistentConfiguration, get_wine_prefix_path ) -try: - import dialog # noqa: F401 -except ImportError: - pass import logging import os import sys from . import cli -from . import config from . import constants from . import gui_app from . import msg @@ -289,8 +284,7 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera e = f"{ephemeral_config.wine_appimage_path} is not an AppImage." raise argparse.ArgumentTypeError(e) if arg == 'winetricks': - # XXX: lingering config ref - config.winetricks_args = getattr(args, 'winetricks') + ephemeral_config.winetricks_args = getattr(args, 'winetricks') run_action = action break if run_action is None: @@ -324,8 +318,8 @@ def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfigura parser = get_parser() cli_args = parser.parse_args() # parsing early lets 'help' run immediately - # Get config based on env and configuration file temporarily just to load a couple values out - # We'll load this fully later. + # Get config based on env and configuration file temporarily just to load a couple + # values out. We'll load this fully later. temp = EphemeralConfiguration.load() log_level = temp.log_level or constants.DEFAULT_LOG_LEVEL app_log_path = temp.app_log_path or constants.DEFAULT_APP_LOG_PATH @@ -354,12 +348,12 @@ def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfigura def is_app_installed(ephemeral_config: EphemeralConfiguration): - persistent_config = PersistentConfiguration.load_from_path(ephemeral_config.config_path) - if persistent_config.faithlife_product is None or persistent_config.install_dir is None: + persistent_config = PersistentConfiguration.load_from_path(ephemeral_config.config_path) #noqa: E501 + if persistent_config.faithlife_product is None or persistent_config.install_dir is None: #noqa: E501 # Not enough information stored to find the product return False - wine_prefix = ephemeral_config.wine_prefix or get_wine_prefix_path(persistent_config.install_dir) - return utils.find_installed_product(persistent_config.faithlife_product, wine_prefix) + wine_prefix = ephemeral_config.wine_prefix or get_wine_prefix_path(str(persistent_config.install_dir)) #noqa: E501 + return utils.find_installed_product(persistent_config.faithlife_product, wine_prefix) #noqa: E501 def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralConfiguration], None]): #noqa: E501 diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index d3e6e8b3..378d0897 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -16,15 +16,12 @@ from ou_dedetai.app import App -from . import config from . import constants -from . import msg from . import network -from . import utils # TODO: Replace functions in control.py and wine.py with Popen command. -def run_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.CompletedProcess[any]]: # noqa: E501 +def run_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.CompletedProcess]: # noqa: E501 check = kwargs.get("check", True) text = kwargs.get("text", True) capture_output = kwargs.get("capture_output", True) @@ -63,7 +60,7 @@ def run_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.Co for attempt in range(retries): try: - result = subprocess.run( + result: subprocess.CompletedProcess = subprocess.run( command, check=check, text=text, @@ -525,7 +522,7 @@ def get_runmode(): def query_packages(package_manager: PackageManager, packages, mode="install") -> list[str]: #noqa: E501 - result = "" + result = None missing_packages = [] conflicting_packages = [] @@ -535,7 +532,10 @@ def query_packages(package_manager: PackageManager, packages, mode="install") -> result = run_command(command) except Exception as e: logging.error(f"Error occurred while executing command: {e}") - logging.error(e.output) + # FIXME: consider raising an exception + if result is None: + logging.error("Failed to query packages") + return [] package_list = result.stdout logging.debug(f"packages to check: {packages}") diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 39f05f06..bc3e0230 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -7,13 +7,12 @@ import curses from pathlib import Path from queue import Queue -from typing import Optional +from typing import Any, Optional from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE from ou_dedetai.config import EphemeralConfiguration -from . import config from . import control from . import constants from . import installer @@ -44,41 +43,24 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.tmp = "" # Generic ask/response events/threads - self.ask_answer_queue = Queue() + self.ask_answer_queue: Queue[str] = Queue() self.ask_answer_event = threading.Event() # Queues self.main_thread = threading.Thread() - self.get_q = Queue() - self.get_e = threading.Event() - self.status_q = Queue() + self.status_q: Queue[str] = Queue() self.status_e = threading.Event() - self.progress_q = Queue() - self.progress_e = threading.Event() - self.todo_q = Queue() + self.todo_q: Queue[str] = Queue() self.todo_e = threading.Event() - self.screen_q = Queue() - self.choice_q = Queue() - self.switch_q = Queue() + self.screen_q: Queue[None] = Queue() + self.choice_q: Queue[str] = Queue() + self.switch_q: Queue[int] = Queue() # Install and Options - self.manualinstall_q = Queue() - self.manualinstall_e = threading.Event() - self.deps_q = Queue() - self.deps_e = threading.Event() - self.finished_q = Queue() - self.finished_e = threading.Event() - self.config_q = Queue() - self.config_e = threading.Event() - self.confirm_q = Queue() - self.confirm_e = threading.Event() - self.password_q = Queue() + self.password_q: Queue[str] = Queue() self.password_e = threading.Event() - self.appimage_q = Queue() + self.appimage_q: Queue[str] = Queue() self.appimage_e = threading.Event() - self.install_icu_q = Queue() - self.install_logos_q = Queue() - self.install_logos_e = threading.Event() self.terminal_margin = 0 self.resizing = False @@ -87,8 +69,8 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.options_per_page = 0 # Window and Screen Management - self.tui_screens = [] - self.menu_options = [] + self.tui_screens: list[tui_screen.Screen] = [] + self.menu_options: list[Any] = [] self.window_height = self.window_width = self.console = self.menu_screen = ( self.active_screen ) = None @@ -432,8 +414,6 @@ def choice_processor(self, stdscr, screen_id, choice): 13: self.waiting_finish, 14: self.waiting_resize, 15: self.password_prompt, - 16: self.install_dependencies_confirm, - 17: self.manual_install_confirm, 18: self.utilities_menu_select, 19: self.renderer_select, 20: self.win_ver_logos_select, @@ -675,23 +655,6 @@ def password_prompt(self, choice): self.password_q.put(choice) self.password_e.set() - def install_dependencies_confirm(self, choice): - if choice: - if choice == "No": - self.go_to_main_menu() - else: - self.menu_screen.choice = "Processing" - self.confirm_e.set() - self.screen_q.put( - self.stack_text( - 13, - self.todo_q, - self.todo_e, - "Installing dependencies…\n", - wait=True, - ) - ) # noqa: E501 - def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: self.reset_screen() @@ -716,21 +679,6 @@ def win_ver_index_select(self, choice): self.status(f"Changed Windows version for Indexer to {choice}.", 100) self.go_to_main_menu() - def manual_install_confirm(self, choice): - if choice: - if choice == "Continue": - self.menu_screen.choice = "Processing" - self.manualinstall_e.set() - self.screen_q.put( - self.stack_text( - 13, - self.todo_q, - self.todo_e, - "Installing dependencies…\n", - wait=True, - ) - ) # noqa: E501 - def switch_screen(self): if ( self.active_screen is not None @@ -850,8 +798,10 @@ def report_waiting(self, text): # self.screen_q.put(self.stack_text(10, self.status_q, self.status_e, text, wait=True, dialog=dialog)) #noqa: E501 self.console_log.append(text) - def which_dialog_options(self, labels): - options = [] + def which_dialog_options(self, labels: list[str]) -> list[Any]: #noqa: E501 + # curses - list[str] + # dialog - list[tuple[str, str]] + options: list[Any] = [] option_number = 1 for label in labels: if self.use_python_dialog: diff --git a/ou_dedetai/tui_dialog.py b/ou_dedetai/tui_dialog.py index 5c80a636..8cd95469 100644 --- a/ou_dedetai/tui_dialog.py +++ b/ou_dedetai/tui_dialog.py @@ -1,7 +1,7 @@ import curses import logging try: - from dialog import Dialog + from dialog import Dialog #type: ignore[import-untyped] except ImportError: pass diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index 8cb0880c..bf9f474d 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -5,11 +5,9 @@ from ou_dedetai.app import App -from . import config from . import installer from . import system from . import tui_curses -from . import utils if system.have_dep("dialog"): from . import tui_dialog diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 9baa20c5..adeaa94f 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -6,6 +6,7 @@ import json import logging import os +import queue import psutil import re import shutil @@ -14,22 +15,15 @@ import subprocess import sys import tarfile -import threading import time from ou_dedetai.app import App from packaging import version from pathlib import Path from typing import List, Optional, Tuple -from . import config from . import constants -from . import msg from . import network from . import system -if system.have_dep("dialog"): - from . import tui_dialog as tui -else: - from . import tui_curses as tui from . import wine # TODO: Move config commands to config.py @@ -155,7 +149,7 @@ def install_dependencies(app: App): target_version=9 ) else: - logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") + logging.error(f"Unknown Target version, expecting 9 or 10 but got: {app.conf.faithlife_product_version}.") #noqa: E501 app.status("Installed dependencies.", 100) @@ -378,7 +372,7 @@ def get_path_size(file_path): return path_size -def get_folder_group_size(src_dirs, q): +def get_folder_group_size(src_dirs: list[Path], q: queue.Queue[int]): src_size = 0 for d in src_dirs: if not d.is_dir(): @@ -442,11 +436,10 @@ def compare_logos_linux_installer_version(app: App) -> Optional[VersionCompariso def compare_recommended_appimage_version(app: App): status = None message = None - wine_release = [] wine_exe_path = app.conf.wine_binary wine_release, error_message = wine.get_wine_release(wine_exe_path) if wine_release is not None and wine_release is not False: - current_version = '.'.join([str(n) for n in wine_release[:2]]) + current_version = f"{wine_release.major}.{wine_release.minor}" logging.debug(f"Current wine release: {current_version}") recommended_version = app.conf.wine_appimage_recommended_version @@ -537,7 +530,7 @@ def check_appimage(filestr): def find_appimage_files(app: App): - release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version + release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version #noqa: E501 appimages = [] directories = [ os.path.expanduser("~") + "/bin", @@ -586,8 +579,6 @@ def find_wine_binary_files(app: App, release_version): # Temporarily modify PATH for additional WINE64 binaries. for p in wine_binary_path_list: - if p is None: - continue if p not in os.environ['PATH'] and os.path.isdir(p): os.environ['PATH'] = os.environ['PATH'] + os.pathsep + p diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 1a7c2662..e7376a5b 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import logging import os import re @@ -39,12 +40,20 @@ def wineserver_wait(app: App): system.wait_pid(process) +@dataclass +class WineRelease: + major: int + minor: int + release: Optional[str] + + # FIXME: consider raising exceptions on error -def get_wine_release(binary): +def get_wine_release(binary: str) -> tuple[Optional[WineRelease], str]: cmd = [binary, "--version"] try: version_string = subprocess.check_output(cmd, encoding='utf-8').strip() logging.debug(f"Version string: {str(version_string)}") + release: Optional[str] try: version, release = version_string.split() except ValueError: @@ -55,32 +64,45 @@ def get_wine_release(binary): logging.debug(f"Wine branch of {binary}: {release}") if release is not None: - ver_major = version.split('.')[0].lstrip('wine-') # remove 'wine-' - ver_minor = version.split('.')[1] + ver_major = int(version.split('.')[0].lstrip('wine-')) # remove 'wine-' + ver_minor = int(version.split('.')[1]) release = release.lstrip('(').rstrip(')').lower() # remove parens else: ver_major = 0 ver_minor = 0 - wine_release = [int(ver_major), int(ver_minor), release] + wine_release = WineRelease(ver_major, ver_minor, release) logging.debug(f"Wine release of {binary}: {str(wine_release)}") if ver_major == 0: - return False, "Couldn't determine wine version." + return None, "Couldn't determine wine version." else: return wine_release, "yes" except subprocess.CalledProcessError as e: - return False, f"Error running command: {e}" + return None, f"Error running command: {e}" except ValueError as e: - return False, f"Error parsing version: {e}" + return None, f"Error parsing version: {e}" except Exception as e: - return False, f"Error: {e}" + return None, f"Error: {e}" + +@dataclass +class WineRule: + major: int + proton: bool + minor_bad: list[int] + allowed_releases: list[str] + devel_allowed: Optional[int] = None -def check_wine_rules(wine_release, release_version, faithlife_product_version: str): + +def check_wine_rules( + wine_release: Optional[WineRelease], + release_version, + faithlife_product_version: str +): # Does not check for Staging. Will not implement: expecting merging of # commits in time. logging.debug(f"Checking {wine_release} for {release_version}.") @@ -94,37 +116,26 @@ def check_wine_rules(wine_release, release_version, faithlife_product_version: s else: raise ValueError(f"Invalid target version, expecting 9 or 10 but got: {faithlife_product_version} ({type(faithlife_product_version)})") # noqa: E501 - rules = [ - { - "major": 7, - "proton": True, # Proton release tend to use the x.0 release, but can include changes found in devel/staging # noqa: E501 - "minor_bad": [], # exceptions to minimum - "allowed_releases": ["staging"] - }, - { - "major": 8, - "proton": False, - "minor_bad": [0], - "allowed_releases": ["staging"], - "devel_allowed": 16, # devel permissible at this point - }, - { - "major": 9, - "proton": False, - "minor_bad": [], - "allowed_releases": ["devel", "staging"], - }, + rules: list[WineRule] = [ + # Proton release tend to use the x.0 release, but can include changes found in devel/staging # noqa: E501 + # exceptions to minimum + WineRule(major=7, proton=True, minor_bad=[], allowed_releases=["staging"]), + # devel permissible at this point + WineRule(major=8, proton=False, minor_bad=[0], allowed_releases=["staging"], devel_allowed=16), #noqa: E501 + WineRule(major=9, proton=False, minor_bad=[], allowed_releases=["devel", "staging"]) #noqa: E501 ] major_min, minor_min = required_wine_minimum if wine_release: - major, minor, release_type = wine_release + major = wine_release.major + minor = wine_release.minor + release_type = wine_release.release result = True, "None" # Whether the release is allowed; error message for rule in rules: - if major == rule["major"]: + if major == rule.major: # Verify release is allowed - if release_type not in rule["allowed_releases"]: - if minor >= rule.get("devel_allowed", float('inf')): + if release_type not in rule.allowed_releases: + if minor >= (rule.devel_allowed or float('inf')): if release_type not in ["staging", "devel"]: result = ( False, @@ -138,13 +149,13 @@ def check_wine_rules(wine_release, release_version, faithlife_product_version: s result = ( False, ( - f"Wine release needs to be {rule['allowed_releases']}. " # noqa: E501 + f"Wine release needs to be {rule.allowed_releases}. " # noqa: E501 f"Current release: {release_type}." ) ) break # Verify version is allowed - if minor in rule.get("minor_bad", []): + if minor in rule.minor_bad: result = False, f"Wine version {major}.{minor} will not work." break if major < major_min: @@ -156,7 +167,7 @@ def check_wine_rules(wine_release, release_version, faithlife_product_version: s ) break elif major == major_min and minor < minor_min: - if not rule["proton"]: + if not rule.proton: result = ( False, ( @@ -466,7 +477,7 @@ def get_registry_value(reg_path, name, app: App): if 'non-zero exit status' in str(e): logging.warning(err_msg) return None - if result.stdout is not None: + if result is not None and result.stdout is not None: for line in result.stdout.splitlines(): if line.strip().startswith(name): value = line.split()[-1].strip() @@ -477,7 +488,7 @@ def get_registry_value(reg_path, name, app: App): return value -def get_mscoree_winebranch(mscoree_file): +def get_mscoree_winebranch(mscoree_file: Path) -> Optional[str]: try: with mscoree_file.open('rb') as f: for line in f: @@ -486,9 +497,10 @@ def get_mscoree_winebranch(mscoree_file): return m[0].decode().lstrip('wine-') except FileNotFoundError as e: logging.error(e) + return None -def get_wine_branch(binary): +def get_wine_branch(binary: str) -> Optional[str]: logging.info(f"Determining wine branch of '{binary}'") binary_obj = Path(binary).expanduser().resolve() if utils.check_appimage(binary_obj): @@ -500,7 +512,7 @@ def get_wine_branch(binary): encoding='UTF8' ) branch = None - while p.returncode is None: + while p.returncode is None and p.stdout is not None: for line in p.stdout: if line.startswith('/tmp'): tmp_dir = Path(line.rstrip()) From 3f4c87351ae7b3dba8a0cc382d677f83339dc671 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:00:02 -0800 Subject: [PATCH 086/137] chore: remove out of date comments --- ou_dedetai/config.py | 1 - ou_dedetai/main.py | 1 - ou_dedetai/system.py | 1 - ou_dedetai/tui_app.py | 2 -- ou_dedetai/utils.py | 24 ------------------------ ou_dedetai/wine.py | 2 +- pyproject.toml | 3 +-- 7 files changed, 2 insertions(+), 32 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 69ac08bb..f337a058 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -382,7 +382,6 @@ def write_config(self) -> None: for k, v in output.items(): if k == "install_dir": continue - # XXX: test this if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 output[k] = utils.get_relative_path(v, str(self.install_dir)) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index c4323107..43851c48 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -200,7 +200,6 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera else: ephemeral_config = EphemeralConfiguration.load() - # XXX: move the following options into the ephemeral_config if args.verbose: msg.update_log_level(logging.INFO) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 378d0897..598885e8 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -722,7 +722,6 @@ def postinstall_dependencies(superuser_command: str): return command -# XXX: move this to control, prompts additional values from app def install_dependencies(app: App, target_version=10): # noqa: E501 if app.conf.skip_install_system_dependencies: return diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index bc3e0230..95a4a1cf 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -733,12 +733,10 @@ def handle_ask_response(self, choice: Optional[str]): self.switch_screen() def handle_ask_file_response(self, choice: Optional[str]): - # XXX: can there be some sort of feedback if this file path isn't valid? if choice is not None and Path(choice).exists() and Path(choice).is_file(): self.handle_ask_response(choice) def handle_ask_directory_response(self, choice: Optional[str]): - # XXX: can there be some sort of feedback if this directory path isn't valid? if choice is not None and Path(choice).exists() and Path(choice).is_dir(): self.handle_ask_response(choice) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index adeaa94f..56799a50 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -26,8 +26,6 @@ from . import system from . import wine -# TODO: Move config commands to config.py - def get_calling_function_name(): if 'inspect' in sys.modules: @@ -46,28 +44,6 @@ def append_unique(list, item): logging.debug(f"{item} already in {list}.") -# XXX: remove, no need. -def write_config(config_file_path): - pass - - -# XXX: refactor -def update_config_file(config_file_path, key, value): - config_file_path = Path(config_file_path) - with config_file_path.open(mode='r') as f: - config_data = json.load(f) - - if config_data.get(key) != value: - logging.info(f"Updating {str(config_file_path)} with: {key} = {value}") - config_data[key] = value - try: - with config_file_path.open(mode='w') as f: - json.dump(config_data, f, indent=4, sort_keys=True) - f.write('\n') - except IOError as e: - raise (f"Error writing to config file {config_file_path}: {e}") from e # noqa: E501 - - def die_if_running(app: App): def remove_pid_file(): diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index e7376a5b..07c1432c 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -414,7 +414,7 @@ def set_win_version(app: App, exe: str, windows_version: str): system.wait_pid(process) -# FIXME: consider when to run this (in the update case) +# XXX: consider when to run this (in the update case) def enforce_icu_data_files(app: App): app.status("Downloading ICU files...") # XXX: consider moving the version and url information into config (and cached) diff --git a/pyproject.toml b/pyproject.toml index 2de388c0..048a81bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,8 +49,7 @@ no_implicit_reexport = true extra_checks = true [[tool.mypy.overrides]] -# XXX: change this to config after refactor is complete -module = "ou_dedetai.new_config" +module = "ou_dedetai.config" disallow_untyped_calls = true check_untyped_defs = true From a003b0ddb9e8b18d63b7fed046013a9e49e08130 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:34:00 -0800 Subject: [PATCH 087/137] fix: cli progress output and misc re-write the current line if multiple status are sent with the same text --- ou_dedetai/app.py | 7 ++++++- ou_dedetai/cli.py | 13 +++++++++++-- ou_dedetai/config.py | 26 +++++++++++++++++++------- ou_dedetai/installer.py | 5 +++-- ou_dedetai/logos.py | 2 +- ou_dedetai/wine.py | 6 +++--- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 69ccd232..2b564fb8 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -23,6 +23,8 @@ class App(abc.ABC): Non-daemon threads will be joined before shutdown """ + _last_status: Optional[str] = None + """The last status we had""" def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise these would be circular imports @@ -169,10 +171,13 @@ def status(self, message: str, percent: Optional[int | float] = None): else: # Otherwise just print status using the progress given self._status(message, percent) + self._last_status = message def _status(self, message: str, percent: Optional[int] = None): """Implementation for updating status pre-front end""" - print(f"{message}") + # De-dup + if message != self._last_status: + print(f"{message}") @property def superuser_command(self) -> str: diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 691f5d17..d5d4cab5 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -125,13 +125,22 @@ def exit(self, reason: str, intended: bool = False): def _status(self, message: str, percent: Optional[int] = None): """Implementation for updating status pre-front end""" + prefix = "" + end = "\n" + if message == self._last_status: + # Go back to the beginning of the line to re-write the current line + # Rather than sending a new one. This allows the current line to update + prefix += "\r" + end = "\r" if percent: + # XXX: it's possible for the progress to seem to go backwards if anyone + # status is sent during the same install step with percent 0 percent_per_char = 5 chars_of_progress = round(percent / percent_per_char) chars_remaining = round((100 - percent) / percent_per_char) progress_str = "[" + "-" * chars_of_progress + " " * chars_remaining + "] " - print(progress_str, end="") - print(f"{message}") + prefix += progress_str + print(f"{prefix}{message}", end=end) @property def superuser_command(self) -> str: diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index f337a058..70a61e39 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -488,6 +488,20 @@ def _write(self) -> None: self._raw.write_config() self.app._config_updated_hook() + def _absolute_from_install_dir(self, path: Path | str) -> str: + """Takes in a possibly relative path under install dir and turns it into an + absolute path + + Args: + path - can be absolute or relative to install dir + + Returns: + path - absolute + """ + if not Path(path).is_absolute(): + return str(Path(self.install_dir) / path) + return str(path) + # XXX: Add a reload command to resolve #168 (at least plumb the backend) @property @@ -592,10 +606,8 @@ def winetricks_binary(self) -> str: question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 options = utils.get_winetricks_options() output = self._ask_if_not_found("winetricks_binary", question, options) - if (Path(self.install_dir) / output).exists(): - return str(Path(self.install_dir) / output) - return output - + return self._absolute_from_install_dir(output) + @winetricks_binary.setter def winetricks_binary(self, value: Optional[str | Path]): if value is not None: @@ -690,15 +702,15 @@ def wineserver_binary(self) -> str: # FIXME: seems like the logic around wine appimages can be simplified # Should this be folded into wine_binary? @property - def wine_appimage_path(self) -> Optional[str]: + def wine_appimage_path(self) -> Optional[Path]: """Path to the wine appimage Returns: Path if wine is set to use an appimage, otherwise returns None""" if self._overrides.wine_appimage_path is not None: - return self._overrides.wine_appimage_path + return Path(self._absolute_from_install_dir(self._overrides.wine_appimage_path)) #noqa: E501 if self.wine_binary.lower().endswith("appimage"): - return self.wine_binary + return Path(self._absolute_from_install_dir(self.wine_binary)) return None @wine_appimage_path.setter diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 176f8f91..d82cc9db 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -374,6 +374,7 @@ def ensure_product_installed(app: App): app.status(f"Ensuring {app.conf.faithlife_product} is installed…") if not app.is_installed(): + # XXX: should we try to cleanup on a failed msi? process = wine.install_msi(app) system.wait_pid(process) @@ -412,7 +413,7 @@ def ensure_launcher_executable(app: App): logging.debug(f"> File exists?: {launcher_exe}: {launcher_exe.is_file()}") # noqa: E501 else: app.status( - "Running from source. Skipping launcher creation." + "Running from source. Skipping launcher copy." ) @@ -457,7 +458,7 @@ def create_wine_appimage_symlinks(app: App): if downloaded_file is None: logging.critical("Failed to get a valid wine appimage") return - if Path(downloaded_file).parent != appdir_bindir: + if not appimage_file.exists(): app.status(f"Copying: {downloaded_file} into: {appdir_bindir}") shutil.copy(downloaded_file, appdir_bindir) os.chmod(appimage_file, 0o755) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 71639d0a..2311b79d 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -84,7 +84,7 @@ def monitor(self): def start(self): self.logos_state = State.STARTING - wine_release, _ = wine.get_wine_release() + wine_release, _ = wine.get_wine_release(self.app.conf.wine_binary) def run_logos(): process = wine.run_wine_proc( diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 07c1432c..e64c4363 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -204,7 +204,7 @@ def check_wine_version_and_branch(release_version, test_binary, if not result: return result, message - if wine_release[0] > 9: + if wine_release.major > 9: pass return True, "None" @@ -257,7 +257,7 @@ def disable_winemenubuilder(app: App, wine64_binary: str): "winemenubuilder.exe"="" ''') wine_reg_install(app, reg_file, wine64_binary) - os.remove(workdir) + shutil.rmtree(workdir) def install_msi(app: App): @@ -377,7 +377,7 @@ def install_fonts(app: App): fonts = ['corefonts', 'tahoma'] if not app.conf.skip_install_fonts: for i, f in enumerate(fonts): - app.status("Configuring fonts, this step may take several minutes…", i / len(fonts)) # noqa: E501 + app.status(f"Configuring font: {f}…", i / len(fonts)) # noqa: E501 args = [f] run_winetricks_cmd(app, *args) From c7ff5523347e9f555fc68b3234ed44feb4cf9ce3 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:45:27 -0800 Subject: [PATCH 088/137] refactor: another way to call CLI lazily makes a generic function rather than enumerating each possibility --- ou_dedetai/cli.py | 89 ---------------------------------------------- ou_dedetai/main.py | 61 ++++++++++++++++++------------- 2 files changed, 36 insertions(+), 114 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index d5d4cab5..5a33efb6 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -176,92 +176,3 @@ def user_input_processor(self, evt=None) -> None: if choice is not None: self.choice_q.put(choice) self.choice_event.set() - - -# NOTE: These subcommands are outside the CLI class so that the class can be -# instantiated at the moment the subcommand is run. This lets any CLI-specific -# code get executed along with the subcommand. - -# NOTE: we should be able to achieve the same effect without re-declaring these -def backup(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).backup() - - -def create_shortcuts(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).create_shortcuts() - - -def edit_config(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).edit_config() - - -def get_winetricks(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).get_winetricks() - - -def install_app(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).install_app() - - -def install_d3d_compiler(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).install_d3d_compiler() - - -def install_dependencies(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).install_dependencies() - - -def install_fonts(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).install_fonts() - - -def install_icu(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).install_icu() - - -def remove_index_files(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).remove_index_files() - - -def remove_install_dir(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).remove_install_dir() - - -def remove_library_catalog(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).remove_library_catalog() - - -def restore(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).restore() - - -def run_indexing(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).run_indexing() - - -def run_installed_app(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).run_installed_app() - - -def run_winetricks(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).run_winetricks() - - -def set_appimage(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).set_appimage() - - -def toggle_app_logging(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).toggle_app_logging() - - -def update_latest_appimage(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).update_latest_appimage() - - -def update_self(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).update_self() - - -def winetricks(ephemeral_config: EphemeralConfiguration): - CLI(ephemeral_config).winetricks() diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 43851c48..c54534a8 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -246,33 +246,44 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera if args.passive: ephemeral_config.faithlife_install_passive = True + + def cli_operation(action: str) -> Callable[[EphemeralConfiguration], None]: + """Wrapper for a function pointer to a given function under CLI + + Lazilay instantiates CLI at call-time""" + def _run(config: EphemeralConfiguration): + getattr(cli.CLI(config), action)() + output = _run + output.__name__ = action + return output + # Set action return function. - actions = { - 'backup': cli.backup, - 'create_shortcuts': cli.create_shortcuts, - 'edit_config': cli.edit_config, - 'get_winetricks': cli.get_winetricks, - 'install_app': cli.install_app, - 'install_d3d_compiler': cli.install_d3d_compiler, - 'install_dependencies': cli.install_dependencies, - 'install_fonts': cli.install_fonts, - 'install_icu': cli.install_icu, - 'remove_index_files': cli.remove_index_files, - 'remove_install_dir': cli.remove_install_dir, - 'remove_library_catalog': cli.remove_library_catalog, - 'restore': cli.restore, - 'run_indexing': cli.run_indexing, - 'run_installed_app': cli.run_installed_app, - 'run_winetricks': cli.run_winetricks, - 'set_appimage': cli.set_appimage, - 'toggle_app_logging': cli.toggle_app_logging, - 'update_self': cli.update_self, - 'update_latest_appimage': cli.update_latest_appimage, - 'winetricks': cli.winetricks, - } + actions = [ + 'backup', + 'create_shortcuts', + 'edit_config', + 'get_winetricks', + 'install_app', + 'install_d3d_compiler', + 'install_dependencies', + 'install_fonts', + 'install_icu', + 'remove_index_files', + 'remove_install_dir', + 'remove_library_catalog', + 'restore', + 'run_indexing', + 'run_installed_app', + 'run_winetricks', + 'set_appimage', + 'toggle_app_logging', + 'update_self', + 'update_latest_appimage', + 'winetricks', + ] run_action = None - for arg, action in actions.items(): + for arg in actions: if getattr(args, arg): if arg == "set_appimage": ephemeral_config.wine_appimage_path = getattr(args, arg)[0] @@ -284,7 +295,7 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera raise argparse.ArgumentTypeError(e) if arg == 'winetricks': ephemeral_config.winetricks_args = getattr(args, 'winetricks') - run_action = action + run_action = cli_operation(arg) break if run_action is None: run_action = run_control_panel From 276b3668c71be239f32f6fe3563c1d7e70c46268 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:35:23 -0800 Subject: [PATCH 089/137] fix: bug where selection didn't reset going back to main menu probably introduced by defaulting selected away from config --- ou_dedetai/tui_app.py | 3 +++ ou_dedetai/tui_curses.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 95a4a1cf..9bd8da5c 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -440,6 +440,9 @@ def choice_processor(self, stdscr, screen_id, choice): def reset_screen(self): self.active_screen.running = 0 self.active_screen.choice = "Processing" + self.current_option = 0 + self.current_page = 0 + self.total_pages = 0 def go_to_main_menu(self): self.menu_screen.choice = "Processing" diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 37c1960f..94cda453 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -200,6 +200,8 @@ def draw(self): self.stdscr.erase() self.app.active_screen.set_options(self.options) self.total_pages = (len(self.options) - 1) // self.app.options_per_page + 1 + # Default menu_bottom to 0, it should get set to something larger + menu_bottom = 0 self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) #noqa: E501 # Display the options, centered From 7e8b2d59b32ecd131f436addae8ee35d409c857e Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:55:38 -0800 Subject: [PATCH 090/137] fix: additional mypy lints from untyped functions --- ou_dedetai/cli.py | 4 +- ou_dedetai/config.py | 9 ++- ou_dedetai/tui_app.py | 151 ++++++++++++++++++++++++++------------- ou_dedetai/tui_curses.py | 78 ++++++++------------ ou_dedetai/tui_dialog.py | 7 +- ou_dedetai/tui_screen.py | 34 ++++++--- ou_dedetai/utils.py | 13 ++-- ou_dedetai/wine.py | 2 +- pyproject.toml | 2 +- 9 files changed, 178 insertions(+), 122 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 5a33efb6..c3a1896b 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -58,7 +58,7 @@ def install_fonts(self): wine.install_fonts(self) def install_icu(self): - wine.enforce_icu_data_files() + wine.enforce_icu_data_files(self) def remove_index_files(self): control.remove_all_index_files(self) @@ -94,7 +94,7 @@ def update_self(self): utils.update_to_latest_lli_release(self) def winetricks(self): - wine.run_winetricks_cmd(self, *self.conf._overrides.winetricks_args) + wine.run_winetricks_cmd(self, *(self.conf._overrides.winetricks_args or [])) _exit_option: str = "Exit" diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 70a61e39..b6d64043 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -204,7 +204,7 @@ class EphemeralConfiguration: """Path this config was loaded from""" - winetricks_args: Optional[str] = None + winetricks_args: Optional[list[str]] = None """Arguments to winetricks if the action is running winetricks""" terminal_app_prefer_dialog: Optional[bool] = None @@ -714,7 +714,9 @@ def wine_appimage_path(self) -> Optional[Path]: return None @wine_appimage_path.setter - def wine_appimage_path(self, value: Optional[str]): + def wine_appimage_path(self, value: Optional[str | Path]) -> None: + if isinstance(value, Path): + value = str(value) if self._overrides.wine_appimage_path != value: self._overrides.wine_appimage_path = value # Reset dependents @@ -805,6 +807,9 @@ def toggle_installer_release_channel(self): else: new_channel = "stable" self._raw.app_release_channel = new_channel + # Reset dependents + self._raw.app_latest_version = None + self._raw.app_latest_version_url = None self._write() @property diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 9bd8da5c..951ca043 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -18,7 +18,6 @@ from . import installer from . import logos from . import msg -from . import network from . import system from . import tui_curses from . import tui_screen @@ -71,16 +70,21 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati # Window and Screen Management self.tui_screens: list[tui_screen.Screen] = [] self.menu_options: list[Any] = [] - self.window_height = self.window_width = self.console = self.menu_screen = ( - self.active_screen - ) = None - self.main_window_ratio = self.main_window_ratio = self.menu_window_ratio = ( - self.main_window_min - ) = None - self.menu_window_min = self.main_window_height = self.menu_window_height = ( - self.main_window - ) = None - self.menu_window = self.resize_window = None + + # Default height and width to something reasonable so these values are always + # ints, on each loop these values will be updated to their real values + self.window_height = self.window_width = 80 + self.main_window_height = self.menu_window_height = 80 + # Default to a value to allow for int type + self.main_window_min: int = 0 + self.menu_window_min: int = 0 + + self.menu_window_ratio: Optional[float] = None + self.main_window_ratio: Optional[float] = None + self.main_window_ratio = None + self.main_window: Optional[curses.window] = None + self.menu_window: Optional[curses.window] = None + self.resize_window: Optional[curses.window] = None # For menu dialogs. # a new MenuDialog is created every loop, so we can't store it there. @@ -88,6 +92,13 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.current_page: int = 0 self.total_pages: int = 0 + # Start internal property variables, shouldn't be accessed directly, see their + # corresponding @property functions + self._menu_screen: Optional[tui_screen.MenuScreen] = None + self._active_screen: Optional[tui_screen.Screen] = None + self._console: Optional[tui_screen.ConsoleScreen] = None + # End internal property values + # Lines for the on-screen console log self.console_log: list[str] = [] @@ -119,6 +130,39 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() + @property + def active_screen(self) -> tui_screen.Screen: + if self._active_screen is None: + self._active_screen = self.menu_screen + if self._active_screen is None: + raise ValueError("Curses hasn't been initialized yet") + return self._active_screen + + @active_screen.setter + def active_screen(self, value: tui_screen.Screen): + self._active_screen = value + + @property + def menu_screen(self) -> tui_screen.MenuScreen: + if self._menu_screen is None: + self._menu_screen = tui_screen.MenuScreen( + self, + 0, + self.status_q, + self.status_e, + "Main Menu", + self.set_tui_menu_options(), + ) # noqa: E501 + return self._menu_screen + + @property + def console(self) -> tui_screen.ConsoleScreen: + if self._console is None: + self._console = tui_screen.ConsoleScreen( + self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0 + ) # noqa: E501 + return self._console + @property def recent_console_log(self) -> list[str]: """Outputs console log trimmed by the maximum length""" @@ -174,34 +218,48 @@ def set_curses_style(): def set_curses_colors(self): if self.conf.curses_colors == "Logos": self.stdscr.bkgd(" ", curses.color_pair(3)) - self.main_window.bkgd(" ", curses.color_pair(3)) - self.menu_window.bkgd(" ", curses.color_pair(3)) + if self.main_window: + self.main_window.bkgd(" ", curses.color_pair(3)) + if self.menu_window: + self.menu_window.bkgd(" ", curses.color_pair(3)) elif self.conf.curses_colors == "Light": self.stdscr.bkgd(" ", curses.color_pair(6)) - self.main_window.bkgd(" ", curses.color_pair(6)) - self.menu_window.bkgd(" ", curses.color_pair(6)) + if self.main_window: + self.main_window.bkgd(" ", curses.color_pair(6)) + if self.menu_window: + self.menu_window.bkgd(" ", curses.color_pair(6)) elif self.conf.curses_colors == "Dark": self.stdscr.bkgd(" ", curses.color_pair(7)) - self.main_window.bkgd(" ", curses.color_pair(7)) - self.menu_window.bkgd(" ", curses.color_pair(7)) + if self.main_window: + self.main_window.bkgd(" ", curses.color_pair(7)) + if self.menu_window: + self.menu_window.bkgd(" ", curses.color_pair(7)) def update_windows(self): if isinstance(self.active_screen, tui_screen.CursesScreen): - self.main_window.erase() - self.menu_window.erase() + if self.main_window: + self.main_window.erase() + if self.menu_window: + self.menu_window.erase() self.stdscr.timeout(100) self.console.display() def clear(self): self.stdscr.clear() - self.main_window.clear() - self.menu_window.clear() - self.resize_window.clear() + if self.main_window: + self.main_window.clear() + if self.menu_window: + self.menu_window.clear() + if self.resize_window: + self.resize_window.clear() def refresh(self): - self.main_window.noutrefresh() - self.menu_window.noutrefresh() - self.resize_window.noutrefresh() + if self.main_window: + self.main_window.noutrefresh() + if self.menu_window: + self.menu_window.noutrefresh() + if self.resize_window: + self.resize_window.noutrefresh() curses.doupdate() def init_curses(self): @@ -215,10 +273,9 @@ def init_curses(self): curses.cbreak() self.stdscr.keypad(True) - self.console = tui_screen.ConsoleScreen( - self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0 - ) # noqa: E501 - self.menu_screen = tui_screen.MenuScreen( + # Reset console/menu_screen. They'll be initialized next access + self._console = None + self._menu_screen = tui_screen.MenuScreen( self, 0, self.status_q, @@ -263,9 +320,8 @@ def update_main_window_contents(self): self.clear() self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 - self.console = tui_screen.ConsoleScreen( - self, 0, self.status_q, self.status_e, self.title, self.subtitle, 0 - ) # noqa: E501 + # Reset internal variable, it'll be reset next access + self._console = None self.menu_screen.set_options(self.set_tui_menu_options()) # self.menu_screen.set_options(self.set_tui_menu_options(dialog=True)) self.switch_q.put(1) @@ -433,7 +489,7 @@ def choice_processor(self, stdscr, screen_id, choice): else: action = screen_actions.get(screen_id) if action: - action(choice) + action(choice) #type: ignore[operator] else: pass @@ -585,7 +641,6 @@ def utilities_menu_select(self, choice): elif choice == f"Change {constants.APP_NAME} Release Channel": self.reset_screen() self.conf.toggle_installer_release_channel() - network.set_logoslinuxinstaller_latest_release_config() self.update_main_window_contents() self.go_to_main_menu() elif choice == "Install Dependencies": @@ -605,10 +660,9 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files() - appimage_choices = [ - ["AppImage", filename, "AppImage of Wine64"] for filename in appimages - ] # noqa: E501 + appimages = utils.find_appimage_files(self) + # NOTE to reviewer: is this logic correct? + appimage_choices = appimages appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) self.menu_options = appimage_choices question = "Which AppImage should be used?" @@ -619,25 +673,25 @@ def utilities_menu_select(self, choice): ) # noqa: E501 elif choice == "Install ICU": self.reset_screen() - wine.enforce_icu_data_files() + wine.enforce_icu_data_files(self) self.go_to_main_menu() elif choice.endswith("Logging"): self.reset_screen() - wine.switch_logging() + self.logos.switch_logging() self.go_to_main_menu() - def custom_appimage_select(self, choice): + def custom_appimage_select(self, choice: str): # FIXME if choice == "Input Custom AppImage": - appimage_filename = tui_curses.get_user_input( - self, "Enter AppImage filename: ", "" - ) # noqa: E501 + appimage_filename = self.ask("Enter AppImage filename: ", [PROMPT_OPTION_FILE]) #noqa: E501 else: appimage_filename = choice - self.conf.wine_appimage_path = appimage_filename + self.conf.wine_appimage_path = Path(appimage_filename) utils.set_appimage_symlink(self) + if not self.menu_window: + raise ValueError("Curses hasn't been initialized") self.menu_screen.choice = "Processing" - self.appimage_q.put(self.conf.wine_appimage_path) + self.appimage_q.put(str(self.conf.wine_appimage_path)) self.appimage_e.set() def waiting(self, choice): @@ -972,7 +1026,7 @@ def stack_menu( ), ) # noqa: E501 - def stack_input(self, screen_id, queue, event, question, default): + def stack_input(self, screen_id, queue, event, question: str, default): if self.use_python_dialog: utils.append_unique( self.tui_screens, @@ -1135,9 +1189,6 @@ def stack_checklist( def update_tty_dimensions(self): self.window_height, self.window_width = self.stdscr.getmaxyx() - def get_main_window(self): - return self.main_window - def get_menu_window(self): return self.menu_window diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 94cda453..08c31912 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -2,20 +2,25 @@ import signal import textwrap +from ou_dedetai import tui_screen +from ou_dedetai.tui_app import TUI -def wrap_text(app, text): + +# NOTE to reviewer: does this convay the original meaning? +# The usages of the function seemed to have expected a list besides text_centered below +# Which handled the string case. Is it faithful to remove the string case? +def wrap_text(app: TUI, text: str) -> list[str]: # Turn text into wrapped text, line by line, centered if "\n" in text: lines = text.splitlines() wrapped_lines = [textwrap.fill(line, app.window_width - (app.terminal_margin * 2)) for line in lines] #noqa: E501 - lines = '\n'.join(wrapped_lines) + return wrapped_lines else: wrapped_text = textwrap.fill(text, app.window_width - (app.terminal_margin * 2)) - lines = wrapped_text.split('\n') - return lines + return wrapped_text.splitlines() -def write_line(app, stdscr, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): #noqa: E501 +def write_line(app: TUI, stdscr: curses.window, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): #noqa: E501 try: stdscr.addnstr(start_y, start_x, text, char_limit, attributes) except curses.error: @@ -35,12 +40,9 @@ def title(app, title_text, title_start_y_adj): return last_index -def text_centered(app, text, start_y=0): +def text_centered(app: TUI, text: str, start_y=0) -> tuple[int, list[str]]: stdscr = app.get_menu_window() - if "\n" in text: - text_lines = wrap_text(app, text).splitlines() - else: - text_lines = wrap_text(app, text) + text_lines = wrap_text(app, text) text_start_y = start_y text_width = max(len(line) for line in text_lines) for i, line in enumerate(text_lines): @@ -99,14 +101,15 @@ def run(self): class UserInputDialog(CursesDialog): - def __init__(self, app, question_text, default_text): + def __init__(self, app, question_text: str, default_text: str): super().__init__(app) self.question_text = question_text self.default_text = default_text self.user_input = "" self.submit = False - self.question_start_y = None - self.question_lines = None + + self.question_start_y, self.question_lines = text_centered(self.app, self.question_text) #noqa: E501 + def __str__(self): return "UserInput Curses Dialog" @@ -121,9 +124,14 @@ def draw(self): curses.noecho() self.stdscr.refresh() + @property + def show_text(self) -> str: + """Text to show to the user. Normally their input""" + return self.user_input + def input(self): - write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.user_input, self.app.window_width) #noqa: E501 - key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.user_input)) #noqa: E501 + write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.show_text, self.app.window_width) #noqa: E501 + key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.show_text)) #noqa: E501 try: if key == -1: # If key not found, keep processing. @@ -149,38 +157,10 @@ def run(self): class PasswordDialog(UserInputDialog): - def __init__(self, app, question_text, default_text): - super().__init__(app, question_text, default_text) - - self.obfuscation = "" - - def run(self): - if not self.submit: - self.draw() - return "Processing" - else: - if self.user_input is None or self.user_input == "": - self.user_input = self.default_text - return self.user_input - - def input(self): - write_line(self.app, self.stdscr, self.question_start_y + len(self.question_lines) + 2, 10, self.obfuscation, self.app.window_width) #noqa: E501 - key = self.stdscr.getch(self.question_start_y + len(self.question_lines) + 2, 10 + len(self.obfuscation)) #noqa: E501 - - try: - if key == -1: # If key not found, keep processing. - pass - elif key == ord('\n'): # Enter key - self.submit = True - elif key == curses.KEY_BACKSPACE or key == 127: - if len(self.user_input) > 0: - self.user_input = self.user_input[:-1] - self.obfuscation = '*' * len(self.user_input[:-1]) - else: - self.user_input += chr(key) - self.obfuscation = '*' * (len(self.obfuscation) + 1) - except KeyboardInterrupt: - signal.signal(signal.SIGINT, self.app.end) + @property + def show_text(self) -> str: + """Obfuscate the user's input""" + return "*" * len(self.user_input) class MenuDialog(CursesDialog): @@ -198,7 +178,9 @@ def __str__(self): def draw(self): self.stdscr.erase() - self.app.active_screen.set_options(self.options) + # We should be on a menu screen at this point + if isinstance(self.app.active_screen, tui_screen.MenuScreen): + self.app.active_screen.set_options(self.options) self.total_pages = (len(self.options) - 1) // self.app.options_per_page + 1 # Default menu_bottom to 0, it should get set to something larger menu_bottom = 0 diff --git a/ou_dedetai/tui_dialog.py b/ou_dedetai/tui_dialog.py index 8cd95469..afc9ef8f 100644 --- a/ou_dedetai/tui_dialog.py +++ b/ou_dedetai/tui_dialog.py @@ -1,5 +1,6 @@ import curses import logging +from typing import Optional try: from dialog import Dialog #type: ignore[import-untyped] except ImportError: @@ -122,9 +123,10 @@ def confirm(screen, question_text, yes_label="Yes", no_label="No", return check # Returns "ok" or "cancel" -def directory_picker(screen, path_dir, height=None, width=None, title=None, backtitle=None, colors=True): # noqa: E501 +def directory_picker(screen, path_dir, height=None, width=None, title=None, backtitle=None, colors=True) -> Optional[str]: # noqa: E501 str_dir = str(path_dir) + path = None try: dialog = Dialog() dialog.autowidgetsize = True @@ -138,7 +140,8 @@ def directory_picker(screen, path_dir, height=None, width=None, title=None, back if backtitle is not None: options['backtitle'] = backtitle curses.curs_set(1) - _, path = dialog.dselect(str_dir, **options) + _, raw_path = dialog.dselect(str_dir, **options) + path = str(raw_path) curses.curs_set(0) except Exception as e: logging.error("An error occurred:", e) diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index bf9f474d..e177ab01 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -1,7 +1,7 @@ import curses import logging import time -from pathlib import Path +from typing import Optional from ou_dedetai.app import App @@ -18,7 +18,7 @@ def __init__(self, app: App, screen_id, queue, event): if not isinstance(app, TUI): raise ValueError("Cannot start TUI screen with non-TUI app") self.app: TUI = app - self.stdscr = "" + self.stdscr: Optional[curses.window] = None self.screen_id = screen_id self.choice = "Processing" self.queue = queue @@ -39,7 +39,7 @@ def __str__(self): def display(self): pass - def get_stdscr(self): + def get_stdscr(self) -> curses.window: return self.app.stdscr def get_screen_id(self): @@ -72,7 +72,7 @@ def submit_choice_to_queue(self): class ConsoleScreen(CursesScreen): def __init__(self, app, screen_id, queue, event, title, subtitle, title_start_y): super().__init__(app, screen_id, queue, event) - self.stdscr = self.app.get_main_window() + self.stdscr: Optional[curses.window] = self.app.main_window self.title = title self.subtitle = subtitle self.title_start_y = title_start_y @@ -81,6 +81,9 @@ def __str__(self): return "Curses Console Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() subtitle_start = tui_curses.title(self.app, self.title, self.title_start_y) tui_curses.title(self.app, self.subtitle, subtitle_start + 1) @@ -114,6 +117,9 @@ def __str__(self): return "Curses Menu Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() self.choice = tui_curses.MenuDialog( self.app, @@ -144,6 +150,9 @@ def __str__(self): return "Curses Confirm Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() self.choice = tui_curses.MenuDialog( self.app, @@ -159,7 +168,7 @@ def display(self): class InputScreen(CursesScreen): - def __init__(self, app, screen_id, queue, event, question, default): + def __init__(self, app, screen_id, queue, event, question: str, default): super().__init__(app, screen_id, queue, event) self.stdscr = self.app.get_menu_window() self.question = question @@ -174,6 +183,9 @@ def __str__(self): return "Curses Input Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() self.choice = self.dialog.run() if not self.choice == "Processing": @@ -204,6 +216,9 @@ def __str__(self): return "Curses Password Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() self.choice = self.dialog.run() if not self.choice == "Processing": @@ -225,6 +240,9 @@ def __str__(self): return "Curses Text Screen" def display(self): + if self.stdscr is None: + raise Exception("stdscr should be set at this point in the console screen." + "Please report this incident to the developers") self.stdscr.erase() text_start_y, text_lines = tui_curses.text_centered(self.app, self.text) if self.wait: @@ -278,9 +296,9 @@ def __str__(self): def display(self): if self.running == 0: self.running = 1 - self.choice = tui_dialog.directory_picker(self.app, self.default) - if self.choice: - self.choice = Path(self.choice) + choice = tui_dialog.directory_picker(self.app, self.default) + if choice: + self.choice = choice self.submit_choice_to_queue() def get_question(self): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 56799a50..f431276d 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -278,14 +278,11 @@ def check_winetricks_version(winetricks_path: str) -> bool: return str(local_winetricks_version) == constants.WINETRICKS_VERSION #noqa: E501 -def get_procs_using_file(file_path, mode=None): +def get_procs_using_file(file_path): procs = set() for proc in psutil.process_iter(['pid', 'open_files', 'name']): try: - if mode is not None: - paths = [f.path for f in proc.open_files() if f.mode == mode] - else: - paths = [f.path for f in proc.open_files()] + paths = [f.path for f in proc.open_files()] if len(paths) > 0 and file_path in paths: procs.add(proc.pid) except psutil.AccessDenied: @@ -505,7 +502,7 @@ def check_appimage(filestr): return False -def find_appimage_files(app: App): +def find_appimage_files(app: App) -> list[str]: release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version #noqa: E501 appimages = [] directories = [ @@ -624,7 +621,7 @@ def set_appimage_symlink(app: App): delete_symlink(appimage_symlink_path) os.symlink(selected_appimage_file_path, appimage_symlink_path) - app.conf.wine_appimage_path = f"{selected_appimage_file_path.name}" # noqa: E501 + app.conf.wine_appimage_path = selected_appimage_file_path # noqa: E501 def update_to_latest_lli_release(app: App): @@ -645,7 +642,7 @@ def update_to_latest_recommended_appimage(app: App): if app.conf.wine_binary_code not in ["AppImage", "Recommended"]: logging.debug("AppImage commands disabled since we're not using an appimage") # noqa: E501 return - app.conf.wine_appimage_path = app.conf.wine_appimage_recommended_file_name # noqa: E501 + app.conf.wine_appimage_path = Path(app.conf.wine_appimage_recommended_file_name) # noqa: E501 status, _ = compare_recommended_appimage_version(app) if status == 0: set_appimage_symlink(app) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index e64c4363..fa3eee35 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -193,7 +193,7 @@ def check_wine_version_and_branch(release_version, test_binary, wine_release, error_message = get_wine_release(test_binary) - if wine_release is False and error_message is not None: + if wine_release is None: return False, error_message result, message = check_wine_rules( diff --git a/pyproject.toml b/pyproject.toml index 048a81bd..d86aabd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,11 +47,11 @@ warn_unused_ignores = true warn_return_any = true no_implicit_reexport = true extra_checks = true +check_untyped_defs = true [[tool.mypy.overrides]] module = "ou_dedetai.config" disallow_untyped_calls = true -check_untyped_defs = true disallow_any_generic = false strict_equality = true From 7750e13ef12ee21d711d0dbf024a05d41ab779e4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:15:41 -0800 Subject: [PATCH 091/137] fix: implement TUI status --- ou_dedetai/tui_app.py | 27 +++++++++------------------ ou_dedetai/tui_curses.py | 33 +++++++++++++++++++++++++-------- ou_dedetai/utils.py | 9 --------- ou_dedetai/wine.py | 1 + 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 951ca043..533217ab 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -454,7 +454,8 @@ def run(self): signal.signal(signal.SIGINT, self.end) def installing_pw_waiting(self): - self.start_thread(self.get_waiting, screen_id=15) + # self.start_thread(self.get_waiting, screen_id=15) + pass def choice_processor(self, stdscr, screen_id, choice): screen_actions = { @@ -798,29 +799,19 @@ def handle_ask_directory_response(self, choice: Optional[str]): self.handle_ask_response(choice) def _status(self, message: str, percent: int | None = None): - # XXX: update some screen? Something like get_waiting? - pass - - def _install_started_hook(self): - self.get_waiting(self) - - def get_waiting(self, screen_id=8): - text = ["Install is running…\n"] - processed_text = utils.str_array_to_string(text) - - percent = installer.get_progress_pct( - self.installer_step, self.installer_step_count - ) # noqa: E501 self.screen_q.put( self.stack_text( - screen_id, + 8, self.status_q, self.status_e, - processed_text, + message, wait=True, - percent=percent, + percent=percent or 0, ) - ) # noqa: E501 + ) + + def _install_started_hook(self): + self._status("Install is running…") # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 08c31912..4e8d3817 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -3,13 +3,16 @@ import textwrap from ou_dedetai import tui_screen -from ou_dedetai.tui_app import TUI +from ou_dedetai.app import App # NOTE to reviewer: does this convay the original meaning? # The usages of the function seemed to have expected a list besides text_centered below # Which handled the string case. Is it faithful to remove the string case? -def wrap_text(app: TUI, text: str) -> list[str]: +def wrap_text(app: App, text: str) -> list[str]: + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("curses MUST be used with the TUI") # Turn text into wrapped text, line by line, centered if "\n" in text: lines = text.splitlines() @@ -20,15 +23,23 @@ def wrap_text(app: TUI, text: str) -> list[str]: return wrapped_text.splitlines() -def write_line(app: TUI, stdscr: curses.window, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): #noqa: E501 +def write_line(app: App, stdscr: curses.window, start_y, start_x, text, char_limit, attributes=curses.A_NORMAL): #noqa: E501 + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("curses MUST be used with the TUI") try: stdscr.addnstr(start_y, start_x, text, char_limit, attributes) except curses.error: signal.signal(signal.SIGWINCH, app.signal_resize) -def title(app, title_text, title_start_y_adj): - stdscr = app.get_main_window() +def title(app: App, title_text, title_start_y_adj): + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("curses MUST be used with the TUI") + stdscr = app.main_window + if not stdscr: + raise Exception("Expected main window to be initialized, but it wasn't") title_lines = wrap_text(app, title_text) # title_start_y = max(0, app.window_height // 2 - len(title_lines) // 2) last_index = 0 @@ -40,7 +51,10 @@ def title(app, title_text, title_start_y_adj): return last_index -def text_centered(app: TUI, text: str, start_y=0) -> tuple[int, list[str]]: +def text_centered(app: App, text: str, start_y=0) -> tuple[int, list[str]]: + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("curses MUST be used with the TUI") stdscr = app.get_menu_window() text_lines = wrap_text(app, text) text_start_y = start_y @@ -53,7 +67,7 @@ def text_centered(app: TUI, text: str, start_y=0) -> tuple[int, list[str]]: return text_start_y, text_lines -def spinner(app, index, start_y=0): +def spinner(app: App, index: int, start_y: int = 0): spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"] i = index text_centered(app, spinner_chars[i], start_y) @@ -62,7 +76,10 @@ def spinner(app, index, start_y=0): #FIXME: Display flickers. -def confirm(app, question_text, height=None, width=None): +def confirm(app: App, question_text: str, height=None, width=None): + from ou_dedetai.tui_app import TUI + if not isinstance(app, TUI): + raise ValueError("curses MUST be used with the TUI") stdscr = app.get_menu_window() question_text = question_text + " [Y/n]: " question_start_y, question_lines = text_centered(app, question_text) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index f431276d..e33046fa 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -681,15 +681,6 @@ def grep(regexp, filepath): return found - -def str_array_to_string(text, delimeter="\n"): - try: - processed_text = delimeter.join(text) - return processed_text - except TypeError: - return text - - def untar_file(file_path, output_dir): if not os.path.exists(output_dir): os.makedirs(output_dir) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index fa3eee35..0d399899 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -394,6 +394,7 @@ def set_renderer(app: App, renderer: str): def set_win_version(app: App, exe: str, windows_version: str): if exe == "logos": + # XXX: This never exits run_winetricks_cmd(app, '-q', 'settings', f'{windows_version}') elif exe == "indexer": From be4166a2fb1c1bf6a297d5b5db4fa36bf2cee1f9 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:51:30 -0800 Subject: [PATCH 092/137] fix: type hints in GUI --- ou_dedetai/gui.py | 9 ++-- ou_dedetai/gui_app.py | 116 ++++++++++++++++-------------------------- ou_dedetai/utils.py | 3 +- ou_dedetai/wine.py | 4 +- 4 files changed, 55 insertions(+), 77 deletions(-) diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index fa4e76a5..dd97362e 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -357,8 +357,9 @@ def show_tooltip(self, event=None): def hide_tooltip(self, event=None): if self.tooltip_visible: - self.tooltip_window.destroy() self.tooltip_visible = False + if self.tooltip_window: + self.tooltip_window.destroy() class PromptGui(Frame): @@ -369,12 +370,14 @@ def __init__(self, root, title="", prompt="", **kwargs): self.options['title'] = title if prompt is not None: self.options['prompt'] = prompt + self.root = root def draw_prompt(self): + text = "Store Password" store_button = Button( self.root, - text="Store Password", - command=lambda: input_prompt(self.root, self.options) + text=text, + command=lambda: input_prompt(self.root, text, self.options) ) store_button.pack(pady=20) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 9128b78a..41e96515 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -14,7 +14,7 @@ from tkinter import Toplevel from tkinter import filedialog as fd from tkinter.ttk import Style -from typing import Optional +from typing import Callable, Optional from ou_dedetai.app import App from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -39,16 +39,16 @@ def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwa self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: - answer_q: Queue[str] = Queue() + answer_q: Queue[Optional[str]] = Queue() answer_event = Event() - def spawn_dialog(): + def spawn_dialog(options: list[str]): # Create a new popup (with it's own event loop) pop_up = ChoicePopUp(question, options, answer_q, answer_event) # Run the mainloop in this thread pop_up.mainloop() if isinstance(options, list): - self.start_thread(spawn_dialog) + self.start_thread(spawn_dialog, options) answer_event.wait() answer: Optional[str] = answer_q.get() @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): class ChoicePopUp(Tk): """Creates a pop-up with a choice""" - def __init__(self, question: str, options: list[str], answer_q: Queue[str], answer_event: Event, **kwargs): #noqa: E501 + def __init__(self, question: str, options: list[str], answer_q: Queue[Optional[str]], answer_event: Event, **kwargs): #noqa: E501 # Set root parameters. super().__init__() self.title(f"Quesiton: {question.strip().strip(':')}") @@ -197,7 +197,6 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): # Initialize variables. self.config_thread = None - self.appimages = None # Set widget callbacks and event bindings. self.gui.product_dropdown.bind( @@ -244,9 +243,10 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): '<>', self.start_indeterminate_progress ) + self.release_evt = "<>" self.root.bind( - "<>", - self.update_wine_check_progress + self.release_evt, + self.update_release_check_progress ) self.releases_q: Queue[list[str]] = Queue() self.wine_q: Queue[str] = Queue() @@ -265,7 +265,16 @@ def _config_updated_hook(self): self.gui.versionvar.set(self.conf._raw.faithlife_product_version or self.gui.version_dropdown['values'][-1]) #noqa: E501 self.gui.releasevar.set(self.conf._raw.faithlife_product_release or self.gui.release_dropdown['values'][0]) #noqa: E501 # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' #noqa: E501 - self.gui.winevar.set(self.conf._raw.wine_binary or next(iter(self.gui.wine_dropdown['values']), '')) #noqa: E501 + wine_binary: Optional[str] = self.conf._raw.wine_binary + if wine_binary is None: + wine_binary = next(iter(self.gui.wine_dropdown['values'])) or '' + self.gui.winevar.set(wine_binary) + # In case the product changes + self.root.icon = Path(self.conf.faithlife_product_icon_path) + # XXX: this function has a lot of logic in it, + # not sure if we want to run it every save. + # Ideally the path traversals this uses would be cached in Config + # self.update_wine_check_progress() def start_ensure_config(self): # Ensure progress counter is reset. @@ -277,7 +286,9 @@ def start_ensure_config(self): ) def get_winetricks_options(self): - self.conf.winetricks_binary = None # override config file b/c "Download" accounts for that # noqa: E501 + # override config file b/c "Download" accounts for that + # Type hinting ignored due to https://github.com/python/mypy/issues/3004 + self.conf.winetricks_binary = None # type: ignore[assignment] self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + ['Return to Main Menu'] #noqa: E501 self.gui.tricksvar.set(self.gui.tricks_dropdown['values'][0]) @@ -340,12 +351,6 @@ def start_releases_check(self): self.gui.release_check_button.state(['disabled']) # self.gui.releasevar.set('') self.gui.release_dropdown['values'] = [] - # Setup queue, signal, thread. - self.release_evt = "<>" - self.root.bind( - self.release_evt, - self.update_release_check_progress - ) # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() @@ -366,51 +371,24 @@ def set_release(self, evt=None): self.start_ensure_config() def start_find_appimage_files(self, release_version): - # Setup queue, signal, thread. - self.appimage_q = Queue() - self.appimage_evt = "<>" - self.root.bind( - self.appimage_evt, - self.update_find_appimage_progress - ) # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Finding available wine AppImages…") # Start thread. self.start_thread( - utils.find_appimage_files, - release_version=release_version, - app=self, + self.start_wine_versions_check, ) - def start_wine_versions_check(self, release_version): - if self.appimages is None: - self.appimages = [] - # self.start_find_appimage_files(release_version) - # return - # Setup queue, signal, thread. - self.wines_q = Queue() - self.wine_evt = "<>" - self.root.bind( - self.wine_evt, - self.update_wine_check_progress - ) + def start_wine_versions_check(self): # Start progress. self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set("Finding available wine binaries…") - def get_wine_options(app: InstallerWindow, app_images, binaries): - app.wines_q.put(utils.get_wine_options(app, app_images, binaries)) - app.root.event_generate(app.wine_evt) - # Start thread. self.start_thread( - get_wine_options, - self, - self.appimages, - utils.find_wine_binary_files(self, release_version), + self.update_wine_check_progress, ) def set_wine(self, evt=None): @@ -432,7 +410,8 @@ def set_winetricks(self, evt=None): self.conf.winetricks_binary = self.gui.tricksvar.get() self.gui.tricks_dropdown.selection_clear() if evt: # manual override - self.conf.winetricks_binary = None + # Type ignored due to https://github.com/python/mypy/issues/3004 + self.conf.winetricks_binary = None # type: ignore[assignment] self.start_ensure_config() def on_release_check_released(self, evt=None): @@ -440,19 +419,18 @@ def on_release_check_released(self, evt=None): def on_wine_check_released(self, evt=None): self.gui.wine_check_button.state(['disabled']) - self.start_wine_versions_check(self.conf.faithlife_product_release) + self.start_wine_versions_check() def set_skip_fonts(self, evt=None): - self.conf.skip_install_fonts = 1 - self.gui.fontsvar.get() # invert True/False + self.conf.skip_install_fonts = not self.gui.fontsvar.get() # invert True/False logging.debug(f"> config.SKIP_FONTS={self.conf.skip_install_fonts}") def set_skip_dependencies(self, evt=None): - self.conf.skip_install_system_dependencies = 1 - self.gui.skipdepsvar.get() # invert True/False # noqa: E501 + self.conf.skip_install_system_dependencies = self.gui.skipdepsvar.get() # invert True/False # noqa: E501 logging.debug(f"> config.SKIP_DEPENDENCIES={self.conf.skip_install_system_dependencies}") #noqa: E501 def on_okay_released(self, evt=None): # Update desktop panel icon. - self.root.icon = self.conf.faithlife_product_icon_path self.start_install_thread() def on_cancel_released(self, evt=None): @@ -501,16 +479,12 @@ def update_release_check_progress(self, evt=None): else: self.gui.statusvar.set("Failed to get release list. Check connection and try again.") # noqa: E501 - def update_find_appimage_progress(self, evt=None): - self.stop_indeterminate_progress() - if not self.appimage_q.empty(): - self.appimages = self.appimage_q.get() - self.start_wine_versions_check(self.conf.faithlife_product_release) - - def update_wine_check_progress(self, evt=None): - if evt and self.wines_q.empty(): - return - self.gui.wine_dropdown['values'] = self.wines_q.get() + def update_wine_check_progress(self): + release_version = self.conf.faithlife_product_release + binaries = utils.find_wine_binary_files(self, release_version) + app_images = utils.find_appimage_files(self) + wine_choices = utils.get_wine_options(self, app_images, binaries) + self.gui.wine_dropdown['values'] = wine_choices if not self.gui.winevar.get(): # If no value selected, default to 1st item in list. self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) @@ -541,7 +515,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.root.title(f"{constants.APP_NAME} Control Panel") self.root.resizable(False, False) self.gui = gui.ControlGui(self.root, app=self) - self.actioncmd = None + self.actioncmd: Optional[Callable[[], None]] = None text = self.gui.update_lli_label.cget('text') ver = constants.LLI_CURRENT_VERSION @@ -608,7 +582,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar ) self.root.bind('<>', self.update_app_button) - self.installer_window = None + self.installer_window: Optional[InstallerWindow] = None self.update_logging_button() @@ -628,13 +602,13 @@ def run_installer(self, evt=None): classname = constants.BINARY_NAME installer_window_top = Toplevel() self.installer_window = InstallerWindow(installer_window_top, self.root, app=self, class_=classname) #noqa: E501 - self.root.icon = self.conf.faithlife_product_icon_path def run_logos(self, evt=None): self.start_thread(self.logos.start) def run_action_cmd(self, evt=None): - self.actioncmd() + if self.actioncmd: + self.actioncmd() def on_action_radio_clicked(self, evt=None): logging.debug("gui_app.ControlPanel.on_action_radio_clicked START") @@ -649,17 +623,17 @@ def on_action_radio_clicked(self, evt=None): elif self.gui.actionsvar.get() == 'install-icu': self.actioncmd = self.install_icu - def run_indexing(self, evt=None): + def run_indexing(self): self.start_thread(self.logos.index) - def remove_library_catalog(self, evt=None): + def remove_library_catalog(self): control.remove_library_catalog(self) - def remove_indexes(self, evt=None): + def remove_indexes(self): self.gui.statusvar.set("Removing indexes…") self.start_thread(control.remove_all_index_files, app=self) - def install_icu(self, evt=None): + def install_icu(self): self.gui.statusvar.set("Installing ICU files…") self.start_thread(wine.enforce_icu_data_files, app=self) @@ -700,7 +674,7 @@ def set_appimage_symlink(self): self.update_latest_appimage_button() def update_to_latest_appimage(self, evt=None): - self.conf.wine_appimage_path = self.conf.wine_appimage_recommended_file_name # noqa: E501 + self.conf.wine_appimage_path = Path(self.conf.wine_appimage_recommended_file_name) # noqa: E501 self.start_indeterminate_progress() self.gui.statusvar.set("Updating to latest AppImage…") self.start_thread(self.set_appimage_symlink) @@ -751,7 +725,7 @@ def _config_updated_hook(self) -> None: if self.installer_window is not None: # XXX: for some reason mypy thinks this is unreachable. # consider the relationship between these too classes anyway - self.installer_window._config_updated_hook() #type: ignore[unreachable] + self.installer_window._config_updated_hook() return super()._config_updated_hook() # XXX: should this live here or in installerWindow? diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index e33046fa..3c2204bb 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -503,6 +503,7 @@ def check_appimage(filestr): def find_appimage_files(app: App) -> list[str]: + app.status("Finding available wine AppImages…") release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version #noqa: E501 appimages = [] directories = [ @@ -539,7 +540,7 @@ def find_appimage_files(app: App) -> list[str]: return appimages -def find_wine_binary_files(app: App, release_version): +def find_wine_binary_files(app: App, release_version: str) -> list[str]: wine_binary_path_list = [ "/usr/local/bin", os.path.expanduser("~") + "/bin", diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 0d399899..4dce6d60 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -100,7 +100,7 @@ class WineRule: def check_wine_rules( wine_release: Optional[WineRelease], - release_version, + release_version: str, faithlife_product_version: str ): # Does not check for Staging. Will not implement: expecting merging of @@ -181,7 +181,7 @@ def check_wine_rules( return True, "Default to trusting user override" -def check_wine_version_and_branch(release_version, test_binary, +def check_wine_version_and_branch(release_version: str, test_binary, faithlife_product_version): if not os.path.exists(test_binary): reason = "Binary does not exist." From ef63188ae12db474842c6797f8a5b6ce2993d357 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:02:04 -0800 Subject: [PATCH 093/137] fix: more type hints --- ou_dedetai/gui_app.py | 3 +-- ou_dedetai/logos.py | 47 +++++++++++++++++++++++++++++-------------- ou_dedetai/wine.py | 4 ++-- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 41e96515..188c2415 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -514,6 +514,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.root = root self.root.title(f"{constants.APP_NAME} Control Panel") self.root.resizable(False, False) + self.installer_window: Optional[InstallerWindow] = None self.gui = gui.ControlGui(self.root, app=self) self.actioncmd: Optional[Callable[[], None]] = None @@ -582,8 +583,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar ) self.root.bind('<>', self.update_app_button) - self.installer_window: Optional[InstallerWindow] = None - self.update_logging_button() def edit_config(self): diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 2311b79d..5fe0c196 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -33,16 +33,22 @@ def __init__(self, app: App): def monitor_indexing(self): if self.app.conf.logos_indexer_exe in self.existing_processes: - indexer = self.processes.get(self.app.conf.logos_indexer_exe) + indexer = self.existing_processes.get(self.app.conf.logos_indexer_exe) if indexer and isinstance(indexer[0], psutil.Process) and indexer[0].is_running(): # noqa: E501 self.indexing_state = State.RUNNING else: self.indexing_state = State.STOPPED def monitor_logos(self): - splash = self.existing_processes.get(self.app.conf.logos_exe, []) - login = self.existing_processes.get(self.app.conf.logos_login_exe, []) - cef = self.existing_processes.get(self.app.conf.logos_cef_exe, []) + splash = [] + login = [] + 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, []) splash_running = splash[0].is_running() if splash else False login_running = login[0].is_running() if login else False @@ -68,9 +74,12 @@ def monitor_logos(self): def get_logos_pids(self): app = self.app - self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) # noqa: E501 - self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) # noqa: E501 - self.existing_processes[app.conf.logos_cef_exe] = system.get_pids(app.conf.logos_cef_exe) # noqa: E501 + if app.conf.logos_exe: + self.existing_processes[app.conf.logos_exe] = system.get_pids(app.conf.logos_exe) # noqa: E501 + if app.conf.logos_indexer_exe: + self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) # noqa: E501 + if app.conf.logos_cef_exe: + self.existing_processes[app.conf.logos_cef_exe] = system.get_pids(app.conf.logos_cef_exe) # noqa: E501 def monitor(self): if self.app.is_installed(): @@ -87,6 +96,8 @@ def start(self): wine_release, _ = wine.get_wine_release(self.app.conf.wine_binary) def run_logos(): + if not self.app.conf.logos_exe: + raise ValueError("Could not find installed Logos EXE to run") process = wine.run_wine_proc( self.app.conf.wine_binary, self.app, @@ -137,9 +148,11 @@ def stop(self): self.app.conf.logos_login_exe, self.app.conf.logos_cef_exe ]: - process_list = self.processes.get(process_name) - if process_list: - pids.extend([str(process.pid) for process in process_list]) + if process_name is None: + continue + process = self.processes.get(process_name) + if process: + pids.append(str(process.pid)) else: logging.debug(f"No Logos processes found for {process_name}.") # noqa: E501 @@ -171,6 +184,8 @@ def index(self): index_finished = threading.Event() def run_indexing(): + if not self.app.conf.logos_indexer_exe: + raise ValueError("Cannot find installed indexer") process = wine.run_wine_proc( self.app.conf.wine_binary, app=self.app, @@ -218,9 +233,11 @@ def stop_indexing(self): if self.app: pids = [] for process_name in [self.app.conf.logos_indexer_exe]: - process_list = self.processes.get(process_name) - if process_list: - pids.extend([str(process.pid) for process in process_list]) + if process_name is None: + continue + process = self.processes.get(process_name) + if process: + pids.append(str(process.pid)) else: logging.debug(f"No LogosIndexer processes found for {process_name}.") # noqa: E501 @@ -284,5 +301,5 @@ def switch_logging(self, action=None): exe_args=exe_args ) system.wait_pid(process) - wine.wineserver_wait(self.app.conf.wineserver_binary) - self.app.conf.faithlife_product_logging = state + wine.wineserver_wait(self.app) + self.app.conf.faithlife_product_logging = state == state_enabled diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 4dce6d60..08ef66ae 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -100,14 +100,14 @@ class WineRule: def check_wine_rules( wine_release: Optional[WineRelease], - release_version: str, + release_version: Optional[str], faithlife_product_version: str ): # Does not check for Staging. Will not implement: expecting merging of # commits in time. logging.debug(f"Checking {wine_release} for {release_version}.") if faithlife_product_version == "10": - if utils.check_logos_release_version(release_version, 30, 1): + if release_version is not None and utils.check_logos_release_version(release_version, 30, 1): #noqa: E501 required_wine_minimum = [7, 18] else: required_wine_minimum = [9, 10] From 5ccea3bef708eafc2ce587534c1447407c0c622d Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:09:44 -0800 Subject: [PATCH 094/137] refactor: condense redundant steps --- ou_dedetai/installer.py | 133 ++++++++-------------------------------- 1 file changed, 24 insertions(+), 109 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index d82cc9db..99e7d279 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -13,125 +13,40 @@ from . import wine -# XXX: ideally this function wouldn't be needed, would happen automatically by nature of config accesses #noqa: E501 -def ensure_product_choice(app: App): +# This step doesn't do anything per-say, but "collects" all the choices in one step +# The app would continue to work without this function +def ensure_choices(app: App): app.installer_step_count += 1 - app.status("Choose product…") - logging.debug('- config.FLPRODUCT') - logging.debug(f"> config.FLPRODUCT={app.conf.faithlife_product}") - - -# XXX: we don't need this install step anymore -def ensure_version_choice(app: App): - app.installer_step_count += 1 - ensure_product_choice(app=app) - app.installer_step += 1 - app.status("Choose version…") - logging.debug('- config.TARGETVERSION') - # Accessing this ensures it's set - logging.debug(f"> config.TARGETVERSION={app.conf.faithlife_product_version=}") - - -# XXX: no longer needed -def ensure_release_choice(app: App): - app.installer_step_count += 1 - ensure_version_choice(app=app) - app.installer_step += 1 - app.status("Choose product release…") - logging.debug('- config.TARGET_RELEASE_VERSION') - logging.debug(f"> config.TARGET_RELEASE_VERSION={app.conf.faithlife_product_release}") #noqa: E501 - - -def ensure_install_dir_choice(app: App): - app.installer_step_count += 1 - ensure_release_choice(app=app) - app.installer_step += 1 - app.status("Choose installation folder…") - logging.debug('- config.INSTALLDIR') - # Accessing this sets install_dir and bin_dir - app.conf.install_dir - logging.debug(f"> config.INSTALLDIR={app.conf.install_dir=}") - logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_dir}") - - -def ensure_wine_choice(app: App): - app.installer_step_count += 1 - ensure_install_dir_choice(app=app) - app.installer_step += 1 - app.status("Choose wine binary…") - logging.debug('- config.SELECTED_APPIMAGE_FILENAME') - logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_URL') - logging.debug('- config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME') - logging.debug('- config.WINE_EXE') - logging.debug('- config.WINEBIN_CODE') - - m = f"Preparing to process WINE_EXE. Currently set to: {app.conf.wine_binary}." # noqa: E501 - logging.debug(m) - - logging.debug(f"> config.SELECTED_APPIMAGE_FILENAME={app.conf.wine_appimage_path}") - logging.debug(f"> config.RECOMMENDED_WINE64_APPIMAGE_URL={app.conf.wine_appimage_recommended_url}") #noqa: E501 - logging.debug(f"> config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME={app.conf.wine_appimage_recommended_file_name}") # noqa: E501 - logging.debug(f"> config.WINEBIN_CODE={app.conf.wine_binary_code}") + app.status("Asking questions if needed…") + + # Prompts (by nature of access and debug prints a number of choices the user has + logging.debug(f"> {app.conf.faithlife_product=}") + logging.debug(f"> {app.conf.faithlife_product_version=}") + logging.debug(f"> {app.conf.faithlife_product_release=}") + logging.debug(f"> {app.conf.install_dir=}") + logging.debug(f"> {app.conf.installer_binary_dir=}") + logging.debug(f"> {app.conf.wine_appimage_path=}") + logging.debug(f"> {app.conf.wine_appimage_recommended_url=}") + logging.debug(f"> {app.conf.wine_appimage_recommended_file_name=}") + logging.debug(f"> {app.conf.wine_binary_code=}") logging.debug(f"> {app.conf.wine_binary=}") - - -# XXX: this isn't needed anymore -def ensure_winetricks_choice(app: App): - app.installer_step_count += 1 - ensure_wine_choice(app=app) - app.installer_step += 1 - app.status("Choose winetricks binary…") - logging.debug('- config.WINETRICKSBIN') - # Accessing the winetricks_binary variable will do this. - logging.debug(f"> config.WINETRICKSBIN={app.conf.winetricks_binary}") - - -# XXX: huh? What does this do? -def ensure_install_fonts_choice(app: App): - app.installer_step_count += 1 - ensure_winetricks_choice(app=app) - app.installer_step += 1 - app.status("Ensuring install fonts choice…") - logging.debug('- config.SKIP_FONTS') - - logging.debug(f"> config.SKIP_FONTS={app.conf.skip_install_fonts}") - - -# XXX: huh? What does this do? -def ensure_check_sys_deps_choice(app: App): - app.installer_step_count += 1 - ensure_install_fonts_choice(app=app) - app.installer_step += 1 - app.status( - "Ensuring check system dependencies choice…" - ) - logging.debug('- config.SKIP_DEPENDENCIES') - - logging.debug(f"> config.SKIP_DEPENDENCIES={app.conf._overrides.winetricks_skip}") - - -# XXX: should this be it's own step? faithlife_product_version is asked -def ensure_installation_config(app: App): - app.installer_step_count += 1 - ensure_check_sys_deps_choice(app=app) - app.installer_step += 1 - app.status("Ensuring installation config is set…") - logging.debug('- config.LOGOS_ICON_URL') - logging.debug('- config.LOGOS_VERSION') - logging.debug('- config.LOGOS64_URL') - - logging.debug(f"> config.LOGOS_ICON_URL={app.conf.faithlife_product_icon_path}") - logging.debug(f"> config.LOGOS_VERSION={app.conf.faithlife_product_version}") - logging.debug(f"> config.LOGOS64_URL={app.conf.faithlife_installer_download_url}") + logging.debug(f"> {app.conf.winetricks_binary=}") + logging.debug(f"> {app.conf.skip_install_fonts=}") + logging.debug(f"> {app.conf._overrides.winetricks_skip=}") + logging.debug(f"> {app.conf.faithlife_product_icon_path}") + logging.debug(f"> {app.conf.faithlife_installer_download_url}") + # Debug print the entire config + logging.debug(f"> Config={app.conf.__dict__}") app._install_started_hook() app.status("Install is running…") + def ensure_install_dirs(app: App): app.installer_step_count += 1 - ensure_installation_config(app=app) + ensure_choices(app=app) app.installer_step += 1 app.status("Ensuring installation directories…") logging.debug('- config.INSTALLDIR') From 830fa10753c664b707a97c591b6e8f6c4cfa36d4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:14:00 -0800 Subject: [PATCH 095/137] refactor: cache all network requests --- ou_dedetai/config.py | 72 ++---- ou_dedetai/constants.py | 1 + ou_dedetai/gui_app.py | 8 +- ou_dedetai/installer.py | 3 +- ou_dedetai/network.py | 526 +++++++++++++++++++++++++--------------- ou_dedetai/utils.py | 3 +- ou_dedetai/wine.py | 13 +- 7 files changed, 362 insertions(+), 264 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index b6d64043..8d768616 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -4,7 +4,6 @@ import json import logging from pathlib import Path -import time from ou_dedetai import network, utils, constants, wine @@ -308,18 +307,6 @@ class PersistentConfiguration: # The Installer's release channel. Either "stable" or "beta" app_release_channel: str = "stable" - # Start Cache - # Some of these values are cached to avoid github api rate-limits - faithlife_product_releases: Optional[list[str]] = None - # FIXME: pull from legacy RECOMMENDED_WINE64_APPIMAGE_URL? - # in legacy refresh wasn't handled properly - wine_appimage_url: Optional[str] = None - app_latest_version_url: Optional[str] = None - app_latest_version: Optional[str] = None - - last_updated: Optional[float] = None - # End Cache - @classmethod def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": # XXX: handle legacy migration @@ -421,9 +408,7 @@ class Config: # Overriding programmatically generated values from ENV _overrides: EphemeralConfiguration - # XXX: Move this to it's own class/file. - # And check cache for all operations in network - # (similar to this struct but in network) + _network: network.NetworkRequests # Start Cache of values unlikely to change during operation. # i.e. filesystem traversals @@ -447,21 +432,7 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: self._raw = PersistentConfiguration.load_from_path(ephemeral_config.config_path) self._overrides = ephemeral_config - # Now check to see if the persistent cache is still valid - if ( - ephemeral_config.check_updates_now - or self._raw.last_updated is None - or self._raw.last_updated + constants.CACHE_LIFETIME_HOURS * 60 * 60 <= time.time() #noqa: E501 - ): - logging.debug("Cleaning out old cache.") - self._raw.faithlife_product_releases = None - self._raw.app_latest_version = None - self._raw.app_latest_version_url = None - self._raw.wine_appimage_url = None - self._raw.last_updated = time.time() - self._write() - else: - logging.debug("Cache is valid.") + self._network = network.NetworkRequests(ephemeral_config.check_updates_now) logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): @@ -546,13 +517,18 @@ def faithlife_product_version(self, value: Optional[str]): self._write() + @property + def faithlife_product_releases(self) -> list[str]: + return self._network.faithlife_product_releases( + product=self.faithlife_product, + version=self.faithlife_product_version, + channel=self.faithlife_product_release_channel + ) + @property def faithlife_product_release(self) -> str: question = f"Which version of {self.faithlife_product} {self.faithlife_product_version} do you want to install?: " # noqa: E501 - if self._raw.faithlife_product_releases is None: - self._raw.faithlife_product_releases = network.get_logos_releases(self.app) # noqa: E501 - self._write() - options = self._raw.faithlife_product_releases + options = self.faithlife_product_releases return self._ask_if_not_found("faithlife_product_release", question, options) @faithlife_product_release.setter @@ -734,10 +710,7 @@ def wine_appimage_recommended_url(self) -> str: """URL to recommended appimage. Talks to the network if required""" - if self._raw.wine_appimage_url is None: - self._raw.wine_appimage_url = network.get_recommended_appimage_url() - self._write() - return self._raw.wine_appimage_url + return self._network.wine_appimage_recommended_url() @property def wine_appimage_recommended_file_name(self) -> str: @@ -807,9 +780,6 @@ def toggle_installer_release_channel(self): else: new_channel = "stable" self._raw.app_release_channel = new_channel - # Reset dependents - self._raw.app_latest_version = None - self._raw.app_latest_version_url = None self._write() @property @@ -914,14 +884,16 @@ def installed_faithlife_product_release(self) -> Optional[str]: @property def app_latest_version_url(self) -> str: - if self._raw.app_latest_version_url is None: - self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - self._write() - return self._raw.app_latest_version_url + return self._network.app_latest_version(self.app_release_channel).download_url @property def app_latest_version(self) -> str: - if self._raw.app_latest_version is None: - self._raw.app_latest_version_url, self._raw.app_latest_version = network.get_oudedetai_latest_release_config(self.app_release_channel) #noqa: E501 - self._write() - return self._raw.app_latest_version + return self._network.app_latest_version(self.app_release_channel).version + + @property + def icu_latest_version(self) -> str: + return self._network.icu_latest_version().version + + @property + def icu_latest_version_url(self) -> str: + return self._network.icu_latest_version().download_url \ No newline at end of file diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 3d89937f..b4e11dbe 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -22,6 +22,7 @@ DEFAULT_CONFIG_PATH = os.path.expanduser(f"~/.config/FaithLife-Community/{BINARY_NAME}.json") # noqa: E501 DEFAULT_APP_WINE_LOG_PATH= os.path.expanduser("~/.local/state/FaithLife-Community/wine.log") # noqa: E501 DEFAULT_APP_LOG_PATH= os.path.expanduser(f"~/.local/state/FaithLife-Community/{BINARY_NAME}.log") # noqa: E501 +NETWORK_CACHE_PATH = os.path.expanduser("~/.cache/FaithLife-Community/network.json") # noqa: E501 DEFAULT_WINEDEBUG = "fixme-all,err-all" LEGACY_CONFIG_FILES = [ os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 188c2415..d6860634 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -24,7 +24,6 @@ from . import control from . import gui from . import installer -from . import network from . import system from . import utils from . import wine @@ -281,7 +280,7 @@ def start_ensure_config(self): self.installer_step = 0 self.installer_step_count = 0 self.config_thread = self.start_thread( - installer.ensure_installation_config, + installer.ensure_choices, app=self, ) @@ -342,8 +341,7 @@ def set_version(self, evt=None): self.start_ensure_config() def get_logos_releases(self): - filtered_releases = network.get_logos_releases(self) - self.releases_q.put(filtered_releases) + self.releases_q.put(self.conf.faithlife_product_releases) self.root.event_generate(self.release_evt) def start_releases_check(self): @@ -722,8 +720,6 @@ def switch_logging(self, evt=None): def _config_updated_hook(self) -> None: self.update_logging_button() if self.installer_window is not None: - # XXX: for some reason mypy thinks this is unreachable. - # consider the relationship between these too classes anyway self.installer_window._config_updated_hook() return super()._config_updated_hook() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 99e7d279..02dfd543 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -289,7 +289,8 @@ def ensure_product_installed(app: App): app.status(f"Ensuring {app.conf.faithlife_product} is installed…") if not app.is_installed(): - # XXX: should we try to cleanup on a failed msi? + # FIXME: Should we try to cleanup on a failed msi? + # Like terminating msiexec if already running for Logos process = wine.install_msi(app) system.wait_pid(process) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 34d808d0..c740dad9 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -1,7 +1,10 @@ +import abc +from dataclasses import dataclass, field import hashlib import json import logging import os +import time from typing import Optional import requests import shutil @@ -11,94 +14,104 @@ from urllib.parse import urlparse from xml.etree import ElementTree as ET +import requests.structures + from ou_dedetai.app import App from . import constants from . import utils +class Props(abc.ABC): + def __init__(self) -> None: + self._md5: Optional[str] = None + self._size: Optional[int] = None + + @property + def size(self) -> Optional[int]: + if self._size is None: + self._size = self._get_size() + return self._size + + @property + def md5(self) -> Optional[str]: + if self._md5 is None: + self._md5 = self._get_md5() + return self._md5 + + @abc.abstractmethod + def _get_size(self) -> Optional[int]: + """Get the size""" + + @abc.abstractmethod + def _get_md5(self) -> Optional[str]: + """Calculate the md5 sum""" -class Props(): - def __init__(self, uri=None): +class FileProps(Props): + def __init__(self, path: str | Path | None): + super(FileProps, self).__init__() self.path = None - self.size = None - self.md5 = None - if uri is not None: - self.path = uri + if path is not None: + self.path = Path(path) - -class FileProps(Props): - def __init__(self, f=None): - super().__init__(f) - if f is not None: - self.path = Path(self.path) - if self.path.is_file(): - self.get_size() - # self.get_md5() - - def get_size(self): + def _get_size(self): if self.path is None: return - self.size = self.path.stat().st_size - return self.size + if Path(self.path).is_file(): + return self.path.stat().st_size - def get_md5(self): + def _get_md5(self) -> Optional[str]: if self.path is None: - return + return None md5 = hashlib.md5() with self.path.open('rb') as f: for chunk in iter(lambda: f.read(524288), b''): md5.update(chunk) - self.md5 = b64encode(md5.digest()).decode('utf-8') - logging.debug(f"{str(self.path)} MD5: {self.md5}") - return self.md5 + return b64encode(md5.digest()).decode('utf-8') + +@dataclass +class SoftwareReleaseInfo: + version: str + download_url: str class UrlProps(Props): - def __init__(self, url=None): - super().__init__(url) - self.headers = None - if url is not None: - self.get_headers() - self.get_size() - self.get_md5() - - def get_headers(self): - if self.path is None: - self.headers = None + def __init__(self, url: str): + super(UrlProps, self).__init__() + self.path = url + self._headers: Optional[requests.structures.CaseInsensitiveDict] = None + + @property + def headers(self) -> requests.structures.CaseInsensitiveDict: + if self._headers is None: + self._headers = self._get_headers() + return self._headers + + def _get_headers(self) -> requests.structures.CaseInsensitiveDict: logging.debug(f"Getting headers from {self.path}.") try: h = {'Accept-Encoding': 'identity'} # force non-compressed txfr r = requests.head(self.path, allow_redirects=True, headers=h) except requests.exceptions.ConnectionError: logging.critical("Failed to connect to the server.") - return None + raise except Exception as e: logging.error(e) - return None + raise # XXX: should we have a more generic catch for KeyboardInterrupt rather than deep in this function? #noqa: E501 # except KeyboardInterrupt: - self.headers = r.headers - return self.headers - - def get_size(self): - if self.headers is None: - r = self.get_headers() - if r is None: - return + return r.headers + + def _get_size(self): content_length = self.headers.get('Content-Length') content_encoding = self.headers.get('Content-Encoding') if content_encoding is not None: logging.critical(f"The server requires receiving the file compressed as '{content_encoding}'.") # noqa: E501 logging.debug(f"{content_length=}") if content_length is not None: - self.size = int(content_length) - return self.size - - def get_md5(self): - if self.headers is None: - r = self.get_headers() - if r is None: - return + self._size = int(content_length) + return self._size + + def _get_md5(self): if self.headers.get('server') == 'AmazonS3': content_md5 = self.headers.get('etag') if content_md5 is not None: @@ -111,14 +124,170 @@ def get_md5(self): content_md5 = content_md5.strip('"').strip("'") logging.debug(f"{content_md5=}") if content_md5 is not None: - self.md5 = content_md5 - return self.md5 + self._md5 = content_md5 + return self._md5 + + +@dataclass +class CachedRequests: + """This struct all network requests and saves to a cache""" + # Some of these values are cached to avoid github api rate-limits + + faithlife_product_releases: dict[str, dict[str, dict[str, list[str]]]] = field(default_factory=dict) # noqa: E501 + """Cache of faithlife releases. + + Since this depends on the user's selection we need to scope the cache based on that + The cache key is the product, version, and release channel + """ + repository_latest_version: dict[str, str] = field(default_factory=dict) + """Cache of the latest versions keyed by repository slug + + Keyed by repository slug Owner/Repo + """ + repository_latest_url: dict[str, str] = field(default_factory=dict) + """Cache of the latest download url keyed by repository slug + + Keyed by repository slug Owner/Repo + """ + + + url_size_and_hash: dict[str, tuple[Optional[int], Optional[str]]] = field(default_factory=dict) # noqa: E501 + + last_updated: Optional[float] = None + + @classmethod + def load(cls) -> "CachedRequests": + """Load the cache from file if exists""" + path = Path(constants.NETWORK_CACHE_PATH) + if path.exists(): + with open(path, "r") as f: + try: + output: dict = json.load(f) + # Drop any unknown keys + known_keys = CachedRequests().__dict__.keys() + cache_keys = list(output.keys()) + for k in cache_keys: + if k not in known_keys: + del output[k] + return CachedRequests(**output) + except json.JSONDecodeError: + logging.warning("Failed to read cache JSON. Clearing...") + return CachedRequests( + last_updated=time.time() + ) + + def _write(self) -> None: + """Writes the cache to disk. Done internally when there are changes""" + path = Path(constants.NETWORK_CACHE_PATH) + path.parent.mkdir(exist_ok=True) + with open(path, "w") as f: + json.dump(self.__dict__, f, indent=4, sort_keys=True, default=vars) + f.write("\n") + + + def _is_fresh(self) -> bool: + """Returns whether or not this cache is valid""" + if self.last_updated is None: + return False + valid_until = self.last_updated + constants.CACHE_LIFETIME_HOURS * 60 * 60 + if valid_until <= time.time(): + return False + return True + + def clean_if_stale(self, force: bool = False): + if force or not self._is_fresh(): + logging.debug("Cleaning out cache...") + self = CachedRequests(last_updated=time.time()) + self._write() + else: + logging.debug("Cache is valid") + + +class NetworkRequests: + """Uses the cache if found, otherwise retrieves the value from the network.""" + + # This struct uses functions to call due to some of the values requiring parameters + + def __init__(self, force_clean: Optional[bool] = None) -> None: + self._cache = CachedRequests.load() + self._cache.clean_if_stale(force=force_clean or False) + + def faithlife_product_releases( + self, + product: str, + version: str, + channel: str + ) -> list[str]: + releases = self._cache.faithlife_product_releases + if product not in releases: + releases[product] = {} + if version not in releases[product]: + releases[product][version] = {} + if ( + channel + not in releases[product][version] + ): + releases[product][version][channel] = _get_faithlife_product_releases( + faithlife_product=product, + faithlife_product_version=version, + faithlife_product_release_channel=channel + ) + self._cache._write() + return releases[product][version][channel] + + def wine_appimage_recommended_url(self) -> str: + repo = "FaithLife-Community/wine-appimages" + return self._repo_latest_version(repo).download_url + + def _url_size_and_hash(self, url: str) -> tuple[Optional[int], Optional[str]]: + """Attempts to get the size and hash from a URL. + Uses cache if it exists + + Returns: + bytes - from the Content-Length leader + md5_hash - from the Content-MD5 header or S3's etag + """ + if url not in self._cache.url_size_and_hash: + props = UrlProps(url) + self._cache.url_size_and_hash[url] = props.size, props.md5 + self._cache._write() + return self._cache.url_size_and_hash[url] + + def url_size(self, url: str) -> Optional[int]: + return self._url_size_and_hash(url)[0] + + def url_md5(self, url: str) -> Optional[str]: + return self._url_size_and_hash(url)[1] + + def _repo_latest_version(self, repository: str) -> SoftwareReleaseInfo: + if ( + repository not in self._cache.repository_latest_version + or repository not in self._cache.repository_latest_url + ): + result = _get_latest_release_data(repository) + self._cache.repository_latest_version[repository] = result.version + self._cache.repository_latest_url[repository] = result.download_url + self._cache._write() + return SoftwareReleaseInfo( + version=self._cache.repository_latest_version[repository], + download_url=self._cache.repository_latest_url[repository] + ) + + def app_latest_version(self, channel: str) -> SoftwareReleaseInfo: + if channel == "stable": + repo = "FaithLife-Community/LogosLinuxInstaller" + else: + repo = "FaithLife-Community/test-builds" + return self._repo_latest_version(repo) + + def icu_latest_version(self) -> SoftwareReleaseInfo: + return self._repo_latest_version("FaithLife-Community/icu") def logos_reuse_download( - sourceurl, - file, - targetdir, + sourceurl: str, + file: str, + targetdir: str, app: App, ): dirs = [ @@ -133,7 +302,7 @@ def logos_reuse_download( file_path = Path(i) / file if os.path.isfile(file_path): logging.info(f"{file} exists in {i}. Verifying properties.") - if verify_downloaded_file( + if _verify_downloaded_file( sourceurl, file_path, app=app, @@ -149,14 +318,14 @@ def logos_reuse_download( else: logging.info(f"Incomplete file: {file_path}.") if found == 1: - file_path = os.path.join(app.conf.download_dir, file) + file_path = Path(os.path.join(app.conf.download_dir, file)) # Start download. - net_get( + _net_get( sourceurl, target=file_path, app=app, ) - if verify_downloaded_file( + if _verify_downloaded_file( sourceurl, file_path, app=app, @@ -171,24 +340,21 @@ def logos_reuse_download( # FIXME: refactor to raise rather than return None -def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): +def _net_get(url: str, target: Optional[Path]=None, app: Optional[App] = None): # TODO: # - Check available disk space before starting download logging.debug(f"Download source: {url}") logging.debug(f"Download destination: {target}") - target = FileProps(target) # sets path and size attribs - if app and target.path: - app.status(f"Downloading {target.path.name}…") + target_props = FileProps(target) # sets path and size attribs + if app and target_props.path: + app.status(f"Downloading {target_props.path.name}…") parsed_url = urlparse(url) domain = parsed_url.netloc # Gets the requested domain - url = UrlProps(url) # uses requests to set headers, size, md5 attribs - if url.headers is None: - logging.critical("Could not get headers.") - return None + url_props = UrlProps(url) # uses requests to set headers, size, md5 attribs # Initialize variables. local_size = 0 - total_size = url.size # None or int + total_size = url_props.size # None or int logging.debug(f"File size on server: {total_size}") percent = None chunk_size = 100 * 1024 # 100 KB default @@ -200,14 +366,14 @@ def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): file_mode = 'wb' # If file exists and URL is resumable, set download Range. - if target.path is not None and target.path.is_file(): - logging.debug(f"File exists: {str(target.path)}") - local_size = target.get_size() + if target_props.size: + logging.debug(f"File exists: {str(target_props.path)}") + local_size = target_props.size logging.info(f"Current downloaded size in bytes: {local_size}") - if url.headers.get('Accept-Ranges') == 'bytes': + if url_props.headers.get('Accept-Ranges') == 'bytes': logging.debug("Server accepts byte range; attempting to resume download.") # noqa: E501 file_mode = 'ab' - if type(url.size) is int: + if type(url_props.size) is int: headers['Range'] = f'bytes={local_size}-{total_size}' else: headers['Range'] = f'bytes={local_size}-' @@ -216,15 +382,18 @@ def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): # Log download type. if 'Range' in headers.keys(): - message = f"Continuing download for {url.path}." + message = f"Continuing download for {url_props.path}." else: - message = f"Starting new download for {url.path}." + message = f"Starting new download for {url_props.path}." logging.info(message) # Initiate download request. try: - if target.path is None: # return url content as text - with requests.get(url.path, headers=headers) as r: + # FIXME: consider splitting this into two functions with a common base. + # One that writes into a file, and one that returns a str, + # that share most of the internal logic + if target_props.path is None: # return url content as text + with requests.get(url_props.path, headers=headers) as r: if callable(r): logging.error("Failed to retrieve data from the URL.") return None @@ -244,142 +413,96 @@ def net_get(url, target=None, app: Optional[App] = None, evt=None, q=None): return r._content # raw bytes else: # download url to target.path - with requests.get(url.path, stream=True, headers=headers) as r: - with target.path.open(mode=file_mode) as f: + with requests.get(url_props.path, stream=True, headers=headers) as r: + with target_props.path.open(mode=file_mode) as f: if file_mode == 'wb': mode_text = 'Writing' else: mode_text = 'Appending' - logging.debug(f"{mode_text} data to file {target.path}.") + logging.debug(f"{mode_text} data to file {target_props.path}.") for chunk in r.iter_content(chunk_size=chunk_size): f.write(chunk) - local_size = target.get_size() + local_size = os.fstat(f.fileno()).st_size if type(total_size) is int: percent = round(local_size / total_size * 100) # if None not in [app, evt]: if app: # Send progress value to App app.status("Downloading...", percent=percent) - elif q is not None: - # Send progress value to queue param. - q.put(percent) except requests.exceptions.RequestException as e: logging.error(f"Error occurred during HTTP request: {e}") return None # Return None values to indicate an error condition -def verify_downloaded_file(url, file_path, app: Optional[App]=None): +def _verify_downloaded_file(url: str, file_path: Path | str, app: App): if app: app.status(f"Verifying {file_path}…", 0) - res = False - txt = f"{file_path} is the wrong size." - right_size = same_size(url, file_path) - if right_size: - txt = f"{file_path} has the wrong MD5 sum." - right_md5 = same_md5(url, file_path) - if right_md5: - txt = f"{file_path} is verified." - res = True - logging.info(txt) - return res - - -def same_md5(url, file_path): - logging.debug(f"Comparing MD5 of {url} and {file_path}.") - url_md5 = UrlProps(url).get_md5() - logging.debug(f"{url_md5=}") - if url_md5 is None: # skip MD5 check if not provided with URL - res = True - else: - file_md5 = FileProps(file_path).get_md5() - logging.debug(f"{file_md5=}") - res = url_md5 == file_md5 - return res - - -def same_size(url, file_path): - logging.debug(f"Comparing size of {url} and {file_path}.") - url_size = UrlProps(url).size - if not url_size: - return True - file_size = FileProps(file_path).size - logging.debug(f"{url_size=} B; {file_size=} B") - res = url_size == file_size - return res - - -def get_latest_release_data(repository): - release_url = f"https://api.github.com/repos/{repository}/releases/latest" - data = net_get(release_url) - if data: - try: - json_data = json.loads(data.decode()) - except json.JSONDecodeError as e: - logging.error(f"Error decoding JSON response: {e}") - return None - - return json_data - else: - logging.critical("Could not get latest release URL.") - return None - - -def get_first_asset_url(json_data) -> Optional[str]: - release_url = None - if json_data: - # FIXME: Portential KeyError - release_url = json_data.get('assets')[0].get('browser_download_url') - logging.info(f"Release URL: {release_url}") - return release_url - - -def get_tag_name(json_data) -> Optional[str]: + file_props = FileProps(file_path) + url_size = app.conf._network.url_size(url) + if url_size is not None and file_props.size != url_size: + logging.warning(f"{file_path} is the wrong size.") + return False + url_md5 = app.conf._network.url_md5(url) + if url_md5 is not None and file_props.md5 != url_md5: + logging.warning(f"{file_path} has the wrong MD5 sum.") + return False + logging.debug(f"{file_path} is verified.") + return True + + +def _get_first_asset_url(json_data: dict) -> str: + """Parses the github api response to find the first asset's download url + """ + assets = json_data.get('assets') or [] + if len(assets) == 0: + raise Exception("Failed to find the first asset in the repository data: " + f"{json_data}") + first_asset = assets[0] + download_url: Optional[str] = first_asset.get('browser_download_url') + if download_url is None: + raise Exception("Failed to find the download URL in the repository data: " + f"{json_data}") + return download_url + + +def _get_version_name(json_data: dict) -> str: """Gets tag name from json data, strips leading v if exists""" - tag_name: Optional[str] = None - if json_data: - tag_name = json_data.get('tag_name') - logging.info(f"Release URL Tag Name: {tag_name}") - if tag_name is not None: - tag_name = tag_name.lstrip("v") + tag_name: Optional[str] = json_data.get('tag_name') + if tag_name is None: + raise Exception("Failed to find the tag_name in the repository data: " + f"{json_data}") + # Trim a leading v to normalize the version + tag_name = tag_name.lstrip("v") return tag_name -def get_oudedetai_latest_release_config(channel: str = "stable") -> tuple[str, str]: - """Get latest release information +def _get_latest_release_data(repository) -> SoftwareReleaseInfo: + """Gets latest release information + Raises: + Exception - on failure to make network operation or parse github API + Returns: - url - version + SoftwareReleaseInfo """ - if channel == "stable": - repo = "FaithLife-Community/LogosLinuxInstaller" - else: - repo = "FaithLife-Community/test-builds" - json_data = get_latest_release_data(repo) - oudedetai_url = get_first_asset_url(json_data) - if oudedetai_url is None: - logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 - raise ValueError("Failed to find latest installer version") - latest_version = get_tag_name(json_data) - if latest_version is None: - logging.critical(f"Unable to set {constants.APP_NAME} release without the tag.") # noqa: E501 - raise ValueError("Failed to find latest installer version") - logging.info(f"config.LLI_LATEST_VERSION={latest_version}") - - return oudedetai_url, latest_version - - -def get_recommended_appimage_url() -> str: - repo = "FaithLife-Community/wine-appimages" - json_data = get_latest_release_data(repo) - appimage_url = get_first_asset_url(json_data) - if appimage_url is None: - # FIXME: changed this to raise an exception as we can't continue. - raise ValueError("Unable to set recommended appimage config without URL.") # noqa: E501 - return appimage_url - - -def get_recommended_appimage(app: App): + release_url = f"https://api.github.com/repos/{repository}/releases/latest" + data = _net_get(release_url) + if data is None: + raise Exception("Could not get latest release URL.") + try: + json_data: dict = json.loads(data.decode()) + except json.JSONDecodeError as e: + logging.error(f"Error decoding Github's JSON response: {e}") + raise + + download_url = _get_first_asset_url(json_data) + version = _get_version_name(json_data) + return SoftwareReleaseInfo( + version=version, + download_url=download_url + ) + +def dwonload_recommended_appimage(app: App): wine64_appimage_full_filename = Path(app.conf.wine_appimage_recommended_file_name) # noqa: E501 dest_path = Path(app.conf.installer_binary_dir) / wine64_appimage_full_filename if dest_path.is_file(): @@ -392,16 +515,19 @@ def get_recommended_appimage(app: App): app=app ) -def get_logos_releases(app: App) -> list[str]: - # TODO: Use already-downloaded list if requested again. - logging.debug(f"Downloading release list for {app.conf.faithlife_product} {app.conf.faithlife_product_version}…") # noqa: E501 +def _get_faithlife_product_releases( + faithlife_product: str, + faithlife_product_version: str, + faithlife_product_release_channel: str +) -> list[str]: + logging.debug(f"Downloading release list for {faithlife_product} {faithlife_product_version}…") # noqa: E501 # NOTE: This assumes that Verbum release numbers continue to mirror Logos. - if app.conf.faithlife_product_release_channel == "beta": + if faithlife_product_release_channel == "beta": url = "https://clientservices.logos.com/update/v1/feed/logos10/beta.xml" # noqa: E501 else: - url = f"https://clientservices.logos.com/update/v1/feed/logos{app.conf.faithlife_product_version}/stable.xml" # noqa: E501 + url = f"https://clientservices.logos.com/update/v1/feed/logos{faithlife_product_version}/stable.xml" # noqa: E501 - response_xml_bytes = net_get(url) + response_xml_bytes = _net_get(url) if response_xml_bytes is None: raise Exception("Failed to get logos releases") @@ -432,6 +558,14 @@ def get_logos_releases(app: App) -> list[str]: return filtered_releases +# XXX: remove this when it's no longer used +def get_logos_releases(app: App) -> list[str]: + return _get_faithlife_product_releases( + faithlife_product=app.conf.faithlife_product, + faithlife_product_version=app.conf.faithlife_product_version, + faithlife_product_release_channel=app.conf.faithlife_product_release_channel + ) + def update_lli_binary(app: App): lli_file_path = os.path.realpath(sys.argv[0]) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 3c2204bb..4628bd65 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -595,7 +595,8 @@ def set_appimage_symlink(app: App): appimage_symlink_path = appdir_bindir / app.conf.wine_appimage_link_file_name if appimage_file_path.name == app.conf.wine_appimage_recommended_file_name: # noqa: E501 # Default case. - network.get_recommended_appimage(app) + # FIXME: consider other locations to enforce this, perhaps config? + network.dwonload_recommended_appimage(app) selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_dir # noqa: E501 if not bindir_appimage.exists(): diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 08ef66ae..d7b0c091 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -11,7 +11,6 @@ from ou_dedetai.app import App -from . import constants from . import network from . import system from . import utils @@ -418,15 +417,9 @@ def set_win_version(app: App, exe: str, windows_version: str): # XXX: consider when to run this (in the update case) def enforce_icu_data_files(app: App): app.status("Downloading ICU files...") - # XXX: consider moving the version and url information into config (and cached) - repo = "FaithLife-Community/icu" - json_data = network.get_latest_release_data(repo) - icu_url = network.get_first_asset_url(json_data) - icu_latest_version = network.get_tag_name(json_data) - - if icu_url is None: - logging.critical(f"Unable to set {constants.APP_NAME} release without URL.") # noqa: E501 - return + icu_url = app.conf.icu_latest_version_url + icu_latest_version = app.conf.icu_latest_version + icu_filename = os.path.basename(icu_url).removesuffix(".tar.gz") # Append the version to the file name so it doesn't collide with previous versions icu_filename = f"{icu_filename}-{icu_latest_version}.tar.gz" From 50cfc2308405223765e4501e99885e41d8d5c90e Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:33:51 -0800 Subject: [PATCH 096/137] refactor: cache more file traversals --- ou_dedetai/config.py | 61 +++++++++++++++++++++++++++++-------------- ou_dedetai/gui_app.py | 12 +++------ ou_dedetai/system.py | 1 - ou_dedetai/tui_app.py | 2 +- ou_dedetai/utils.py | 4 ++- 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 8d768616..bebe0b49 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -286,9 +286,6 @@ class PersistentConfiguration: # See naming conventions in Config - # XXX: store a version in this config? - # Just in case we need to do conditional logic reading old version's configurations - faithlife_product: Optional[str] = None faithlife_product_version: Optional[str] = None faithlife_product_release: Optional[str] = None @@ -416,6 +413,8 @@ class Config: _download_dir: Optional[str] = None _wine_output_encoding: Optional[str] = None _installed_faithlife_product_release: Optional[str] = None + _wine_binary_files: Optional[list[str]] = None + _wine_appimage_files: Optional[list[str]] = None # Start constants _curses_colors_valid_values = ["Light", "Dark", "Logos"] @@ -439,7 +438,6 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: logging.debug(f"{k}: {v}") def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 - # XXX: should this also update the feedback? if not getattr(self._raw, parameter): if dependent_parameters is not None: for dependent_config_key in dependent_parameters: @@ -459,10 +457,22 @@ def _write(self) -> None: self._raw.write_config() self.app._config_updated_hook() + def _relative_from_install_dir(self, path: Path | str) -> str: + """Takes in a possibly absolute path under install dir and turns it into an + relative path if it is + + Args: + path - can be absolute or relative to install dir + + Returns: + path - absolute + """ + return str(Path(path).absolute()).lstrip(self.install_dir) + def _absolute_from_install_dir(self, path: Path | str) -> str: """Takes in a possibly relative path under install dir and turns it into an absolute path - + Args: path - can be absolute or relative to install dir @@ -535,6 +545,8 @@ def faithlife_product_release(self) -> str: def faithlife_product_release(self, value: str): if self._raw.faithlife_product_release != value: self._raw.faithlife_product_release = value + # Reset dependents + self._wine_binary_files = None self._write() @property @@ -592,7 +604,10 @@ def winetricks_binary(self, value: Optional[str | Path]): if not Path(value).exists(): raise ValueError("Winetricks binary must exist") if self._raw.winetricks_binary != value: - self._raw.winetricks_binary = value + if value is not None: + self._raw.winetricks_binary = self._relative_from_install_dir(value) + else: + self._raw.winetricks_binary = None self._write() @property @@ -623,11 +638,7 @@ def wine_binary(self) -> str: output = self._raw.wine_binary if output is None: question = f"Which Wine AppImage or binary should the script use to install {self.faithlife_product} v{self.faithlife_product_version} in {self.install_dir}?: " # noqa: E501 - options = utils.get_wine_options( - self.app, - utils.find_appimage_files(self.app), - utils.find_wine_binary_files(self.app, self.faithlife_product_release) - ) + options = utils.get_wine_options(self.app) choice = self.app.ask(question, options) @@ -641,21 +652,33 @@ def wine_binary(self) -> str: @wine_binary.setter def wine_binary(self, value: str): """Takes in a path to the wine binary and stores it as relative for storage""" - # XXX: change the logic to make ^ true - if (Path(self.install_dir) / value).exists(): - value = str((Path(self.install_dir) / Path(value)).absolute()) - if not Path(value).is_file(): + # Make the path absolute for comparison + aboslute = self._absolute_from_install_dir(value) + relative = self._relative_from_install_dir(value) + if not Path(aboslute).is_file(): raise ValueError("Wine Binary path must be a valid file") - if self._raw.wine_binary != value: - if value is not None: - value = str(Path(value).absolute()) - self._raw.wine_binary = value + if self._raw.wine_binary != relative: + self._raw.wine_binary = relative # Reset dependents self._raw.wine_binary_code = None self._overrides.wine_appimage_path = None self._write() + @property + def wine_binary_files(self) -> list[str]: + if self._wine_binary_files is None: + self._wine_binary_files = utils.find_wine_binary_files( + self.app, self.faithlife_product_release + ) + return self._wine_binary_files + + @property + def wine_app_image_files(self) -> list[str]: + if self._wine_appimage_files is None: + self._wine_appimage_files = utils.find_appimage_files(self.app) + return self._wine_appimage_files + @property def wine_binary_code(self) -> str: """Wine binary code. diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index d6860634..d96bea4b 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -270,10 +270,7 @@ def _config_updated_hook(self): self.gui.winevar.set(wine_binary) # In case the product changes self.root.icon = Path(self.conf.faithlife_product_icon_path) - # XXX: this function has a lot of logic in it, - # not sure if we want to run it every save. - # Ideally the path traversals this uses would be cached in Config - # self.update_wine_check_progress() + self.update_wine_check_progress() def start_ensure_config(self): # Ensure progress counter is reset. @@ -478,10 +475,7 @@ def update_release_check_progress(self, evt=None): self.gui.statusvar.set("Failed to get release list. Check connection and try again.") # noqa: E501 def update_wine_check_progress(self): - release_version = self.conf.faithlife_product_release - binaries = utils.find_wine_binary_files(self, release_version) - app_images = utils.find_appimage_files(self) - wine_choices = utils.get_wine_options(self, app_images, binaries) + wine_choices = utils.get_wine_options(self) self.gui.wine_dropdown['values'] = wine_choices if not self.gui.winevar.get(): # If no value selected, default to 1st item in list. @@ -746,9 +740,9 @@ def update_logging_button(self, evt=None): self.gui.loggingstatevar.set(state[:-1].title()) self.gui.logging_button.state(['!disabled']) + # XXX: also call this when config changes. Maybe only then? def update_app_button(self, evt=None): self.gui.app_button.state(['!disabled']) - # XXX: we may need another hook here to update the product version should it change #noqa: E501 self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.configure_app_button() self.update_run_winetricks_button() diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 598885e8..bef0c306 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -770,7 +770,6 @@ def install_dependencies(app: App, target_version=10): # noqa: E501 ) if os_name in bad_os: - # XXX: move the handling up here, possibly simplify? m = "Your distro requires manual dependency installation." logging.error(m) return diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 533217ab..f6f0d77c 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -661,7 +661,7 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() elif choice == "Set AppImage": # TODO: Allow specifying the AppImage File - appimages = utils.find_appimage_files(self) + appimages = self.conf.wine_app_image_files # NOTE to reviewer: is this logic correct? appimage_choices = appimages appimage_choices.extend(["Input Custom AppImage", "Return to Main Menu"]) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 4628bd65..a8979983 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -238,7 +238,9 @@ def get_winebin_code_and_desc(app: App, binary) -> Tuple[str, str | None]: return code, desc -def get_wine_options(app: App, appimages, binaries) -> List[str]: # noqa: E501 +def get_wine_options(app: App) -> List[str]: # noqa: E501 + appimages = app.conf.wine_app_image_files + binaries = app.conf.wine_binary_files logging.debug(f"{appimages=}") logging.debug(f"{binaries=}") wine_binary_options = [] From d0c4ba5c9e460e3d461e5a2434b64a27d66aec50 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 03:28:40 -0800 Subject: [PATCH 097/137] refactor: allow for multiple config update hooks starting attempting to handle this for the gui, and it got complicated. Is there a better way? --- ou_dedetai/app.py | 19 ++--- ou_dedetai/config.py | 11 ++- ou_dedetai/gui_app.py | 150 +++++++++++++++++++++++----------------- ou_dedetai/installer.py | 3 - ou_dedetai/tui_app.py | 17 ++--- 5 files changed, 107 insertions(+), 93 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 2b564fb8..370121b4 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -5,7 +5,7 @@ from pathlib import Path import sys import threading -from typing import NoReturn, Optional +from typing import Callable, NoReturn, Optional from ou_dedetai import constants from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE @@ -25,6 +25,7 @@ class App(abc.ABC): """ _last_status: Optional[str] = None """The last status we had""" + _config_updated_hooks: list[Callable[[], None]] = [] def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise these would be circular imports @@ -153,8 +154,8 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: def is_installed(self) -> bool: """Returns whether the install was successful by checking if the installed exe exists and is executable""" - if self.conf.logos_exe is not None: - return os.access(self.conf.logos_exe, os.X_OK) + if self.conf._logos_exe is not None: + return os.access(self.conf._logos_exe, os.X_OK) return False def status(self, message: str, percent: Optional[int | float] = None): @@ -189,16 +190,10 @@ def superuser_command(self) -> str: May be sudo or pkexec for example""" from ou_dedetai.system import get_superuser_command return get_superuser_command() - - # Start hooks - def _config_updated_hook(self) -> None: - """Function run when the config changes""" - - def _install_complete_hook(self): - """Function run when installation is complete.""" - def _install_started_hook(self): - """Function run when installation first begins.""" + def register_config_update_hook(self, func: Callable[[], None]) -> None: + """Register a function to be called when config is updated""" + self._config_updated_hooks += [func] def start_thread(self, task, *args, daemon_bool: bool = True, **kwargs): """Starts a new thread diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index bebe0b49..a19170e1 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -455,7 +455,12 @@ def _ask_if_not_found(self, parameter: str, question: str, options: list[str], d def _write(self) -> None: """Writes configuration to file and lets the app know something changed""" self._raw.write_config() - self.app._config_updated_hook() + def update_config_hooks(): + for hook in self.app._config_updated_hooks: + hook() + # Spin up a new thread to update the config just in case. + # We don't want a deadlock. Spinning up a new thread may be excessive + self.app.start_thread(update_config_hooks) def _relative_from_install_dir(self, path: Path | str) -> str: """Takes in a possibly absolute path under install dir and turns it into an @@ -655,8 +660,8 @@ def wine_binary(self, value: str): # Make the path absolute for comparison aboslute = self._absolute_from_install_dir(value) relative = self._relative_from_install_dir(value) - if not Path(aboslute).is_file(): - raise ValueError("Wine Binary path must be a valid file") + # if not Path(aboslute).is_file(): + # raise ValueError("Wine Binary path must be a valid file") if self._raw.wine_binary != relative: self._raw.wine_binary = relative diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index d96bea4b..24637cc5 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -38,18 +38,27 @@ def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwa self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: + error_q: Queue[Exception] = Queue() answer_q: Queue[Optional[str]] = Queue() answer_event = Event() def spawn_dialog(options: list[str]): # Create a new popup (with it's own event loop) - pop_up = ChoicePopUp(question, options, answer_q, answer_event) - - # Run the mainloop in this thread - pop_up.mainloop() + try: + pop_up = ChoicePopUp(question, options, answer_q, answer_event) + + # Run the mainloop in this thread + pop_up.mainloop() + except RuntimeError as e: + # Let the other thread know we failed. + error_q.put(e) + answer_event.set() + # raise e if isinstance(options, list): self.start_thread(spawn_dialog, options) answer_event.wait() + if not error_q.empty(): + raise error_q.get() answer: Optional[str] = answer_q.get() elif isinstance(options, str): answer = options @@ -184,15 +193,17 @@ def on_cancel_released(self, evt=None): self.destroy() -class InstallerWindow(GuiApp): +class InstallerWindow: def __init__(self, new_win, root: Root, app: App, **kwargs): - super().__init__(root, app.conf._overrides) # Set root parameters. self.win = new_win self.root = root self.win.title(f"{constants.APP_NAME} Installer") self.win.resizable(False, False) self.gui = gui.InstallerGui(self.win, app) + self.app = app + self.conf = app.conf + self.start_thread = app.start_thread # Initialize variables. self.config_thread = None @@ -253,33 +264,57 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): # Run commands. self.get_winetricks_options() + self.app.register_config_update_hook(self._config_updated_hook) + + # XXX: this got quite complicated + # don't like the leakage of cross-variable dependecy out of config def _config_updated_hook(self): """Update the GUI to reflect changes in the configuration if they were prompted separately""" #noqa: E501 # The configuration enforces dependencies, if product is unset, so will it's # dependents (version and release) - # XXX: test this hook. Interesting thing is, this may never be called in - # production, as it's only called (presently) when the separate prompt returns - # Returns either from config or the dropdown - self.gui.productvar.set(self.conf._raw.faithlife_product or self.gui.product_dropdown['values'][0]) #noqa: E501 - self.gui.versionvar.set(self.conf._raw.faithlife_product_version or self.gui.version_dropdown['values'][-1]) #noqa: E501 - self.gui.releasevar.set(self.conf._raw.faithlife_product_release or self.gui.release_dropdown['values'][0]) #noqa: E501 + product_canidate = self.conf._raw.faithlife_product + if product_canidate is None and len(self.gui.product_dropdown['values']) > 0: + product_canidate = self.gui.product_dropdown['values'][0] + if product_canidate is not None: + self.gui.productvar.set(product_canidate) + + version_canidate = self.conf._raw.faithlife_product_version + if version_canidate is None and len(self.gui.version_dropdown['values']) > 0: + version_canidate = self.gui.version_dropdown['values'][-1] + if version_canidate: + self.gui.versionvar.set(version_canidate) + + release_canidate = self.conf._raw.faithlife_product_release + if release_canidate is None and len(self.gui.release_dropdown['values']) > 0: + release_canidate = self.gui.release_dropdown['values'][0] + if release_canidate: + self.gui.releasevar.set(release_canidate) + + if self.conf._raw.faithlife_product and self.conf._raw.faithlife_product_version: #noqa: E501 + # We have all the prompts we need to download the releases. + # XXX: Don't love how we're downloading in a hook, but that's what the separate thread is for #noqa: E501 + self.gui.release_dropdown['values'] = self.conf.faithlife_product_releases + self.gui.releasevar.set(self.gui.release_dropdown['values'][0]) + # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' #noqa: E501 wine_binary: Optional[str] = self.conf._raw.wine_binary if wine_binary is None: - wine_binary = next(iter(self.gui.wine_dropdown['values'])) or '' + if len(self.gui.wine_dropdown['values']) > 0: + wine_binary = self.gui.wine_dropdown['values'][0] + else: + wine_binary = '' + self.gui.winevar.set(wine_binary) # In case the product changes self.root.icon = Path(self.conf.faithlife_product_icon_path) - self.update_wine_check_progress() + # Make sure the release is set before asking + if self.conf._raw.faithlife_product_release: + self.update_wine_check_progress() def start_ensure_config(self): # Ensure progress counter is reset. self.installer_step = 0 self.installer_step_count = 0 - self.config_thread = self.start_thread( - installer.ensure_choices, - app=self, - ) def get_winetricks_options(self): # override config file b/c "Download" accounts for that @@ -308,11 +343,6 @@ def set_input_widgets_state(self, state, widgets='all'): for w in widgets: w.state(state) - def _install_started_hook(self): - self.gui.statusvar.set('Ready to install!') - self.gui.progressvar.set(0) - self.set_input_widgets_state('disabled') - def set_product(self, evt=None): if self.gui.productvar.get().startswith('C'): # ignore default text return @@ -433,23 +463,30 @@ def on_cancel_released(self, evt=None): return 1 def start_install_thread(self, evt=None): - self.gui.progress.config(mode='determinate') - self.start_thread(installer.install, app=self) - - # XXX: where should this live? here or ControlWindow? - def _status(self, message: str, percent: int | None = None): - if percent: + def _install(): + """Function to handle the install""" + installer.install(self.app) + # Install complete, cleaning up... self.gui.progress.stop() - self.gui.progress.state(['disabled']) self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(percent) - else: - self.gui.progress.state(['!disabled']) self.gui.progressvar.set(0) - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() - self.gui.statusvar.set(message) - super()._status(message, percent) + self.gui.statusvar.set('') + self.gui.okay_button.config( + text="Exit", + command=self.on_cancel_released, + ) + self.gui.okay_button.state(['!disabled']) + self.root.event_generate('<>') + self.win.destroy() + return 0 + + # Setup for the install + self.gui.progress.config(mode='determinate') + self.gui.statusvar.set('Ready to install!') + self.gui.progressvar.set(0) + self.set_input_widgets_state('disabled') + + self.start_thread(_install) def start_indeterminate_progress(self, evt=None): self.gui.progress.state(['!disabled']) @@ -475,7 +512,7 @@ def update_release_check_progress(self, evt=None): self.gui.statusvar.set("Failed to get release list. Check connection and try again.") # noqa: E501 def update_wine_check_progress(self): - wine_choices = utils.get_wine_options(self) + wine_choices = utils.get_wine_options(self.app) self.gui.wine_dropdown['values'] = wine_choices if not self.gui.winevar.get(): # If no value selected, default to 1st item in list. @@ -484,20 +521,6 @@ def update_wine_check_progress(self): self.stop_indeterminate_progress() self.gui.wine_check_button.state(['!disabled']) - def _install_complete_hook(self): - self.gui.progress.stop() - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - self.gui.statusvar.set('') - self.gui.okay_button.config( - text="Exit", - command=self.on_cancel_released, - ) - self.gui.okay_button.state(['!disabled']) - self.root.event_generate('<>') - self.win.destroy() - return 0 - class ControlWindow(GuiApp): def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwargs): @@ -506,7 +529,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.root = root self.root.title(f"{constants.APP_NAME} Control Panel") self.root.resizable(False, False) - self.installer_window: Optional[InstallerWindow] = None self.gui = gui.ControlGui(self.root, app=self) self.actioncmd: Optional[Callable[[], None]] = None @@ -546,7 +568,7 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.gui.latest_appimage_button.config( command=self.update_to_latest_appimage ) - if self.conf.wine_binary_code != "AppImage" and self.conf.wine_binary_code != "Recommended": # noqa: E501 + if self.conf._raw.wine_binary_code not in ["Recommended", "AppImage", None]: # noqa: E501 self.gui.latest_appimage_button.state(['disabled']) gui.ToolTip( self.gui.latest_appimage_button, @@ -558,11 +580,13 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 ) self.update_latest_lli_release_button() - self.update_latest_appimage_button() self.gui.set_appimage_button.config(command=self.set_appimage) self.gui.get_winetricks_button.config(command=self.get_winetricks) self.gui.run_winetricks_button.config(command=self.launch_winetricks) - self.update_run_winetricks_button() + + # XXX: These can be called before installation started, causing them to fail + # self.update_run_winetricks_button() + # self.update_latest_appimage_button() self.root.bind('<>', self.clear_status_text) self.root.bind( @@ -575,7 +599,9 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar ) self.root.bind('<>', self.update_app_button) + # These can be expanded to change the UI based on config changes. self.update_logging_button() + self.register_config_update_hook(self.update_logging_button) def edit_config(self): control.edit_file(self.conf.config_file_path) @@ -592,7 +618,7 @@ def configure_app_button(self, evt=None): def run_installer(self, evt=None): classname = constants.BINARY_NAME installer_window_top = Toplevel() - self.installer_window = InstallerWindow(installer_window_top, self.root, app=self, class_=classname) #noqa: E501 + InstallerWindow(installer_window_top, self.root, app=self, class_=classname) #noqa: E501 def run_logos(self, evt=None): self.start_thread(self.logos.start) @@ -710,14 +736,7 @@ def switch_logging(self, evt=None): self.logos.switch_logging, action=desired_state.lower() ) - - def _config_updated_hook(self) -> None: - self.update_logging_button() - if self.installer_window is not None: - self.installer_window._config_updated_hook() - return super()._config_updated_hook() - # XXX: should this live here or in installerWindow? def _status(self, message: str, percent: int | None = None): if percent: self.gui.progress.stop() @@ -779,6 +798,9 @@ def update_latest_appimage_button(self, evt=None): elif status == 2: state = 'disabled' msg = "This button is disabled. The AppImage version is newer than the latest recommended." # noqa: E501 + else: + # Failed to check + state = '!disabled' if msg: gui.ToolTip(self.gui.latest_appimage_button, msg) self.clear_status_text() diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 02dfd543..410d716c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -39,7 +39,6 @@ def ensure_choices(app: App): # Debug print the entire config logging.debug(f"> Config={app.conf.__dict__}") - app._install_started_hook() app.status("Install is running…") @@ -308,8 +307,6 @@ def ensure_config_file(app: App): app.status("Install has finished.", 100) - app._install_complete_hook() - def ensure_launcher_executable(app: App): app.installer_step_count += 1 diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index f6f0d77c..8949259e 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -130,6 +130,8 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() + self.register_config_update_hook(self.set_curses_colors) + @property def active_screen(self) -> tui_screen.Screen: if self._active_screen is None: @@ -292,9 +294,6 @@ def init_curses(self): logging.error(f"An error occurred in init_curses(): {e}") raise - def _config_updated_hook(self): - self.set_curses_colors() - def end_curses(self): try: self.stdscr.keypad(False) @@ -312,10 +311,6 @@ def end(self, signal, frame): self.llirunning = False curses.endwin() - def _install_complete_hook(self): - # Update the contents going back to the start - self.update_main_window_contents() - def update_main_window_contents(self): self.clear() self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 @@ -506,6 +501,9 @@ def go_to_main_menu(self): self.choice_q.put("Return to Main Menu") def main_menu_select(self, choice): + def _install(): + installer.install(app=self) + self.update_main_window_contents() if choice is None or choice == "Exit": logging.info("Exiting installation.") self.tui_screens = [] @@ -515,7 +513,7 @@ def main_menu_select(self, choice): self.installer_step = 0 self.installer_step_count = 0 self.start_thread( - installer.install, + _install, daemon_bool=True, app=self, ) @@ -810,9 +808,6 @@ def _status(self, message: str, percent: int | None = None): ) ) - def _install_started_hook(self): - self._status("Install is running…") - # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " # f"Please provide your password to provide escalation privileges.") From 773f8c48805e3d35136dd14bad7bef3bda53063d Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:37:14 -0800 Subject: [PATCH 098/137] refactor: load all GUI state from config --- ou_dedetai/app.py | 20 ++- ou_dedetai/config.py | 70 +++++--- ou_dedetai/gui.py | 31 +--- ou_dedetai/gui_app.py | 395 ++++++++++++++---------------------------- ou_dedetai/tui_app.py | 2 +- ou_dedetai/utils.py | 1 - ou_dedetai/wine.py | 3 + 7 files changed, 197 insertions(+), 325 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 370121b4..43fd06ac 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -25,7 +25,8 @@ class App(abc.ABC): """ _last_status: Optional[str] = None """The last status we had""" - _config_updated_hooks: list[Callable[[], None]] = [] + config_updated_hooks: list[Callable[[], None]] = [] + _config_updated_event: threading.Event = threading.Event() def __init__(self, config, **kwargs) -> None: # This lazy load is required otherwise these would be circular imports @@ -39,6 +40,15 @@ def __init__(self, config, **kwargs) -> None: # Ensure everything is good to start check_incompatibilities(self) + def _config_updated_hook_runner(): + while True: + self._config_updated_event.wait() + self._config_updated_event.clear() + for hook in self.config_updated_hooks: + hook() + _config_updated_hook_runner.__name__ = "Config Update Hook" + self.start_thread(_config_updated_hook_runner, daemon_bool=True) + def ask(self, question: str, options: list[str]) -> str: """Asks the user a question with a list of supplied options @@ -154,8 +164,8 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: def is_installed(self) -> bool: """Returns whether the install was successful by checking if the installed exe exists and is executable""" - if self.conf._logos_exe is not None: - return os.access(self.conf._logos_exe, os.X_OK) + if self.conf.logos_exe is not None: + return os.access(self.conf.logos_exe, os.X_OK) return False def status(self, message: str, percent: Optional[int | float] = None): @@ -191,10 +201,6 @@ def superuser_command(self) -> str: from ou_dedetai.system import get_superuser_command return get_superuser_command() - def register_config_update_hook(self, func: Callable[[], None]) -> None: - """Register a function to be called when config is updated""" - self._config_updated_hooks += [func] - def start_thread(self, task, *args, daemon_bool: bool = True, **kwargs): """Starts a new thread diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index a19170e1..34d9b5da 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -1,3 +1,4 @@ +import copy import os from typing import Optional from dataclasses import dataclass @@ -290,12 +291,12 @@ class PersistentConfiguration: faithlife_product_version: Optional[str] = None faithlife_product_release: Optional[str] = None faithlife_product_logging: Optional[bool] = None - install_dir: Optional[Path] = None + install_dir: Optional[str] = None winetricks_binary: Optional[str] = None wine_binary: Optional[str] = None # This is where to search for wine wine_binary_code: Optional[str] = None - backup_dir: Optional[Path] = None + backup_dir: Optional[str] = None # Color to use in curses. Either "Logos", "Light", or "Dark" curses_colors: str = "Logos" @@ -329,23 +330,17 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": @classmethod def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": - backup_dir = None - if legacy.BACKUPDIR is not None: - backup_dir = Path(legacy.BACKUPDIR) - install_dir = None - if legacy.INSTALLDIR is not None: - install_dir = Path(legacy.INSTALLDIR) faithlife_product_logging = None if legacy.LOGS is not None: faithlife_product_logging = utils.parse_bool(legacy.LOGS) return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, - backup_dir=backup_dir, + backup_dir=legacy.BACKUPDIR, curses_colors=legacy.curses_colors or 'Logos', faithlife_product_release=legacy.TARGET_RELEASE_VERSION, faithlife_product_release_channel=legacy.logos_release_channel or 'stable', faithlife_product_version=legacy.TARGETVERSION, - install_dir=install_dir, + install_dir=legacy.INSTALLDIR, app_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, @@ -356,7 +351,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": def write_config(self) -> None: config_file_path = LegacyConfiguration.config_file_path() # XXX: we may need to merge this dict with the legacy configuration's extended config (as we don't store that persistently anymore) #noqa: E501 - output = self.__dict__ + output = copy.deepcopy(self.__dict__) logging.info(f"Writing config to {config_file_path}") os.makedirs(os.path.dirname(config_file_path), exist_ok=True) @@ -365,12 +360,15 @@ def write_config(self) -> None: # Ensure all paths stored are relative to install_dir for k, v in output.items(): if k == "install_dir": + if v is not None: + output[k] = str(v) continue - if isinstance(v, Path) or (isinstance(v, str) and v.startswith(str(self.install_dir))): #noqa: E501 - output[k] = utils.get_relative_path(v, str(self.install_dir)) + if (isinstance(v, str) and v.startswith(self.install_dir)): #noqa: E501 + output[k] = utils.get_relative_path(v, self.install_dir) try: with open(config_file_path, 'w') as config_file: + # XXX: would it be possible to avoid writing if this would fail? json.dump(output, config_file, indent=4, sort_keys=True) config_file.write('\n') except IOError as e: @@ -455,12 +453,7 @@ def _ask_if_not_found(self, parameter: str, question: str, options: list[str], d def _write(self) -> None: """Writes configuration to file and lets the app know something changed""" self._raw.write_config() - def update_config_hooks(): - for hook in self.app._config_updated_hooks: - hook() - # Spin up a new thread to update the config just in case. - # We don't want a deadlock. Spinning up a new thread may be excessive - self.app.start_thread(update_config_hooks) + self.app._config_updated_event.set() def _relative_from_install_dir(self, path: Path | str) -> str: """Takes in a possibly absolute path under install dir and turns it into an @@ -594,11 +587,15 @@ def app_release_channel(self) -> str: @property def winetricks_binary(self) -> str: + # FIXME: consider implications of having "Download" placeholder + # Should we return None instead? """This may be a path to the winetricks binary or it may be "Download" """ question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 options = utils.get_winetricks_options() output = self._ask_if_not_found("winetricks_binary", question, options) + if output == "Download": + return output return self._absolute_from_install_dir(output) @winetricks_binary.setter @@ -609,19 +606,34 @@ def winetricks_binary(self, value: Optional[str | Path]): if not Path(value).exists(): raise ValueError("Winetricks binary must exist") if self._raw.winetricks_binary != value: - if value is not None: + if value == "Download": + self._raw.winetricks_binary = value + elif value is not None: self._raw.winetricks_binary = self._relative_from_install_dir(value) else: self._raw.winetricks_binary = None self._write() + @property + def install_dir_default(self) -> str: + return f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 + @property def install_dir(self) -> str: - default = f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 + default = self.install_dir_default question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 options = [default, PROMPT_OPTION_DIRECTORY] output = self._ask_if_not_found("install_dir", question, options) return output + + @install_dir.setter + def install_dir(self, value: str | Path): + value = str(value) + if self._raw.install_dir != value: + self._raw.install_dir = value + # Reset cache that depends on install_dir + self._wine_appimage_files = None + self._write() @property # This used to be called APPDIR_BINDIR @@ -658,8 +670,10 @@ def wine_binary(self) -> str: def wine_binary(self, value: str): """Takes in a path to the wine binary and stores it as relative for storage""" # Make the path absolute for comparison - aboslute = self._absolute_from_install_dir(value) relative = self._relative_from_install_dir(value) + # XXX: consider this, it doesn't work at present as the wine_binary may be an + # appimage that hasn't been downloaded yet + # aboslute = self._absolute_from_install_dir(value) # if not Path(aboslute).is_file(): # raise ValueError("Wine Binary path must be a valid file") @@ -841,8 +855,16 @@ def cycle_curses_color_scheme(self): @property def logos_exe(self) -> Optional[str]: # Cache a successful result - if self._logos_exe is None: - self._logos_exe = utils.find_installed_product(self.faithlife_product, self.wine_prefix) # noqa: E501 + if ( + # Ensure we have all the context we need before attempting + self._logos_exe is None + and self._raw.faithlife_product is not None + and self._raw.install_dir is not None + ): + self._logos_exe = utils.find_installed_product( + self._raw.faithlife_product, + self.wine_prefix + ) return self._logos_exe @property diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index dd97362e..29b2e83a 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -67,8 +67,8 @@ def __init__(self, root, app: App, **kwargs): self.product_dropdown = Combobox(self, textvariable=self.productvar) self.product_dropdown.state(['readonly']) self.product_dropdown['values'] = ('Logos', 'Verbum') - if app.conf.faithlife_product in self.product_dropdown['values']: - self.product_dropdown.set(app.conf.faithlife_product) + if app.conf._raw.faithlife_product in self.product_dropdown['values']: + self.product_dropdown.set(app.conf._raw.faithlife_product) # version drop-down menu self.versionvar = StringVar() self.version_dropdown = Combobox( @@ -79,8 +79,8 @@ def __init__(self, root, app: App, **kwargs): self.version_dropdown.state(['readonly']) self.version_dropdown['values'] = ('9', '10') self.versionvar.set(self.version_dropdown['values'][1]) - if app.conf.faithlife_product_version in self.version_dropdown['values']: - self.version_dropdown.set(app.conf.faithlife_product_version) + if app.conf._raw.faithlife_product_version in self.version_dropdown['values']: + self.version_dropdown.set(app.conf._raw.faithlife_product_version) # Release row. self.release_label = Label(self, text="Release: ") @@ -93,10 +93,6 @@ def __init__(self, root, app: App, **kwargs): self.release_dropdown['values'] = [app.conf._raw.faithlife_product_release] self.releasevar.set(app.conf._raw.faithlife_product_release) - # release check button - self.release_check_button = Button(self, text="Get Release List") - self.release_check_button.state(['disabled']) - # Wine row. self.wine_label = Label(self, text="Wine exe: ") self.winevar = StringVar() @@ -107,8 +103,6 @@ def __init__(self, root, app: App, **kwargs): if self.app.conf._raw.wine_binary: self.wine_dropdown['values'] = [self.app.conf.wine_binary] self.winevar.set(self.app.conf.wine_binary) - self.wine_check_button = Button(self, text="Get EXE List") - self.wine_check_button.state(['disabled']) # Winetricks row. self.tricks_label = Label(self, text="Winetricks: ") @@ -128,7 +122,7 @@ def __init__(self, root, app: App, **kwargs): # Skip Dependencies row. self.skipdeps_label = Label(self, text="Install Dependencies: ") - self.skipdepsvar = BooleanVar(value=self.app.conf.skip_install_system_dependencies) #noqa: E501 + self.skipdepsvar = BooleanVar(value=not self.app.conf.skip_install_system_dependencies) #noqa: E501 self.skipdeps_checkbox = Checkbutton(self, variable=self.skipdepsvar) # Cancel/Okay buttons row. @@ -136,13 +130,6 @@ def __init__(self, root, app: App, **kwargs): self.okay_button = Button(self, text="Install") self.okay_button.state(['disabled']) - # Status area. - s1 = Separator(self, orient='horizontal') - self.statusvar = StringVar() - self.status_label = Label(self, textvariable=self.statusvar) - self.progressvar = IntVar() - self.progress = Progressbar(self, variable=self.progressvar) - # Place widgets. row = 0 self.product_label.grid(column=0, row=row, sticky='nws', pady=2) @@ -151,11 +138,9 @@ def __init__(self, root, app: App, **kwargs): row += 1 self.release_label.grid(column=0, row=row, sticky='w', pady=2) self.release_dropdown.grid(column=1, row=row, sticky='w', pady=2) - self.release_check_button.grid(column=2, row=row, sticky='w', pady=2) row += 1 self.wine_label.grid(column=0, row=row, sticky='w', pady=2) self.wine_dropdown.grid(column=1, row=row, columnspan=3, sticky='we', pady=2) # noqa: E501 - self.wine_check_button.grid(column=4, row=row, sticky='e', pady=2) row += 1 self.tricks_label.grid(column=0, row=row, sticky='w', pady=2) self.tricks_dropdown.grid(column=1, row=row, sticky='we', pady=2) @@ -168,12 +153,6 @@ def __init__(self, root, app: App, **kwargs): self.cancel_button.grid(column=3, row=row, sticky='e', pady=2) self.okay_button.grid(column=4, row=row, sticky='e', pady=2) row += 1 - # Status area - s1.grid(column=0, row=row, columnspan=5, sticky='we') - row += 1 - self.status_label.grid(column=0, row=row, columnspan=5, sticky='w', pady=2) # noqa: E501 - row += 1 - self.progress.grid(column=0, row=row, columnspan=5, sticky='we', pady=2) # noqa: E501 class ControlGui(Frame): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 24637cc5..3f75323e 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -38,6 +38,8 @@ def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwa self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: + # XXX: would it be possible to use root.bind to call this from the main loop? + # So we don't need to make our own root? error_q: Queue[Exception] = Queue() answer_q: Queue[Optional[str]] = Queue() answer_event = Event() @@ -194,7 +196,7 @@ def on_cancel_released(self, evt=None): class InstallerWindow: - def __init__(self, new_win, root: Root, app: App, **kwargs): + def __init__(self, new_win, root: Root, app: "ControlWindow", **kwargs): # Set root parameters. self.win = new_win self.root = root @@ -221,16 +223,10 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): '<>', self.set_release ) - self.gui.release_check_button.config( - command=self.on_release_check_released - ) self.gui.wine_dropdown.bind( '<>', self.set_wine ) - self.gui.wine_check_button.config( - command=self.on_wine_check_released - ) self.gui.tricks_dropdown.bind( '<>', self.set_winetricks @@ -249,78 +245,81 @@ def __init__(self, new_win, root: Root, app: App, **kwargs): "", self.on_cancel_released ) - self.root.bind( - '<>', - self.start_indeterminate_progress - ) - self.release_evt = "<>" - self.root.bind( - self.release_evt, - self.update_release_check_progress - ) - self.releases_q: Queue[list[str]] = Queue() - self.wine_q: Queue[str] = Queue() # Run commands. self.get_winetricks_options() - self.app.register_config_update_hook(self._config_updated_hook) + self.app.config_updated_hooks += [self._config_updated_hook] + # Start out enforcing this + self._config_updated_hook() - # XXX: this got quite complicated - # don't like the leakage of cross-variable dependecy out of config def _config_updated_hook(self): - """Update the GUI to reflect changes in the configuration if they were prompted separately""" #noqa: E501 - # The configuration enforces dependencies, if product is unset, so will it's - # dependents (version and release) - product_canidate = self.conf._raw.faithlife_product - if product_canidate is None and len(self.gui.product_dropdown['values']) > 0: - product_canidate = self.gui.product_dropdown['values'][0] - if product_canidate is not None: - self.gui.productvar.set(product_canidate) - - version_canidate = self.conf._raw.faithlife_product_version - if version_canidate is None and len(self.gui.version_dropdown['values']) > 0: - version_canidate = self.gui.version_dropdown['values'][-1] - if version_canidate: - self.gui.versionvar.set(version_canidate) - - release_canidate = self.conf._raw.faithlife_product_release - if release_canidate is None and len(self.gui.release_dropdown['values']) > 0: - release_canidate = self.gui.release_dropdown['values'][0] - if release_canidate: - self.gui.releasevar.set(release_canidate) - - if self.conf._raw.faithlife_product and self.conf._raw.faithlife_product_version: #noqa: E501 - # We have all the prompts we need to download the releases. - # XXX: Don't love how we're downloading in a hook, but that's what the separate thread is for #noqa: E501 - self.gui.release_dropdown['values'] = self.conf.faithlife_product_releases - self.gui.releasevar.set(self.gui.release_dropdown['values'][0]) + """Update the GUI to reflect changes in the configuration/network""" #noqa: E501 + + # The product/version dropdown values are static, they will always be populated + + # Tor the GUI, use defaults until user says otherwise. + if self.conf._raw.faithlife_product is None: + self.conf.faithlife_product = self.gui.product_dropdown['values'][0] + self.gui.productvar.set(self.conf.faithlife_product) + if self.conf._raw.faithlife_product_version is None: + self.conf.faithlife_product_version = self.gui.version_dropdown['values'][-1] #noqa :E501 + self.gui.versionvar.set(self.conf.faithlife_product_version) # noqa: E501 + + # Now that we know product and version are set we can download the releases + # And use the first one + # FIXME: consider what to do if network is slow, we may want to do this on a + # Separate thread to not hang the UI + self.gui.release_dropdown['values'] = self.conf.faithlife_product_releases + if self.conf._raw.faithlife_product_release is None: + self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 + self.gui.releasevar.set(self.conf.faithlife_product_release) + + # Set the install_dir to default, no option in the GUI to change it + if self.conf._raw.install_dir is None: + self.conf.install_dir = self.conf.install_dir_default + + self.gui.skipdepsvar.set(not self.conf.skip_install_system_dependencies) + self.gui.fontsvar.set(not self.conf.skip_install_fonts) # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' #noqa: E501 wine_binary: Optional[str] = self.conf._raw.wine_binary if wine_binary is None: if len(self.gui.wine_dropdown['values']) > 0: wine_binary = self.gui.wine_dropdown['values'][0] + self.app.conf.wine_binary = wine_binary else: wine_binary = '' self.gui.winevar.set(wine_binary) # In case the product changes self.root.icon = Path(self.conf.faithlife_product_icon_path) - # Make sure the release is set before asking - if self.conf._raw.faithlife_product_release: - self.update_wine_check_progress() - def start_ensure_config(self): + if self.conf._raw.winetricks_binary is None: + self.conf.winetricks_binary = self.gui.tricksvar.get() + + wine_choices = utils.get_wine_options(self.app) + self.gui.wine_dropdown['values'] = wine_choices + if not self.gui.winevar.get(): + # If no value selected, default to 1st item in list. + self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) + + # At this point all variables are populated, we're ready to install! + self.set_input_widgets_state('enabled', [self.gui.okay_button]) + + def _post_dropdown_change(self): + """Steps to preform after a dropdown has been updated""" # Ensure progress counter is reset. self.installer_step = 0 self.installer_step_count = 0 + # Reset install_dir to default based on possible new value + self.conf.install_dir = self.conf.install_dir_default def get_winetricks_options(self): # override config file b/c "Download" accounts for that # Type hinting ignored due to https://github.com/python/mypy/issues/3004 self.conf.winetricks_binary = None # type: ignore[assignment] - self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() + ['Return to Main Menu'] #noqa: E501 + self.gui.tricks_dropdown['values'] = utils.get_winetricks_options() #noqa: E501 self.gui.tricksvar.set(self.gui.tricks_dropdown['values'][0]) def set_input_widgets_state(self, state, widgets='all'): @@ -332,9 +331,7 @@ def set_input_widgets_state(self, state, widgets='all'): self.gui.product_dropdown, self.gui.version_dropdown, self.gui.release_dropdown, - self.gui.release_check_button, self.gui.wine_dropdown, - self.gui.wine_check_button, self.gui.tricks_dropdown, self.gui.okay_button, ] @@ -344,107 +341,29 @@ def set_input_widgets_state(self, state, widgets='all'): w.state(state) def set_product(self, evt=None): - if self.gui.productvar.get().startswith('C'): # ignore default text - return self.conf.faithlife_product = self.gui.productvar.get() self.gui.product_dropdown.selection_clear() - if evt: # manual override; reset dependent variables - logging.debug(f"User changed faithlife_product to '{self.conf.faithlife_product}'") #noqa: E501 - self.gui.versionvar.set('') - self.gui.releasevar.set('') - self.gui.winevar.set('') - - self.start_ensure_config() + self._post_dropdown_change() def set_version(self, evt=None): self.conf.faithlife_product_version = self.gui.versionvar.get() self.gui.version_dropdown.selection_clear() - if evt: # manual override; reset dependent variables - logging.debug(f"User changed Target Version to '{self.conf.faithlife_product_version}'") # noqa: E501 - self.gui.releasevar.set('') - - self.gui.winevar.set('') - - self.start_ensure_config() - - def get_logos_releases(self): - self.releases_q.put(self.conf.faithlife_product_releases) - self.root.event_generate(self.release_evt) - - def start_releases_check(self): - # Disable button; clear list. - self.gui.release_check_button.state(['disabled']) - # self.gui.releasevar.set('') - self.gui.release_dropdown['values'] = [] - # Start progress. - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() - self.gui.statusvar.set("Downloading Release list…") - # Start thread. - self.start_thread(self.get_logos_releases) + self._post_dropdown_change() def set_release(self, evt=None): - if self.gui.releasevar.get()[0] == 'C': # ignore default text - return self.conf.faithlife_product_release = self.gui.releasevar.get() self.gui.release_dropdown.selection_clear() - if evt: # manual override - logging.debug(f"User changed release version to '{self.conf.faithlife_product_release}'") # noqa: E501 - - self.gui.winevar.set('') - - self.start_ensure_config() - - def start_find_appimage_files(self, release_version): - # Start progress. - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() - self.gui.statusvar.set("Finding available wine AppImages…") - # Start thread. - self.start_thread( - self.start_wine_versions_check, - ) - - def start_wine_versions_check(self): - # Start progress. - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() - self.gui.statusvar.set("Finding available wine binaries…") - - # Start thread. - self.start_thread( - self.update_wine_check_progress, - ) + self._post_dropdown_change() def set_wine(self, evt=None): self.conf.wine_binary = self.gui.winevar.get() self.gui.wine_dropdown.selection_clear() - if evt: # manual override - logging.debug(f"User changed wine binary to '{self.conf.wine_binary}'") - - self.start_ensure_config() - else: - self.wine_q.put( - str(utils.get_relative_path( - utils.get_config_var(self.conf.wine_binary), - self.conf.install_dir - )) - ) + self._post_dropdown_change() def set_winetricks(self, evt=None): self.conf.winetricks_binary = self.gui.tricksvar.get() self.gui.tricks_dropdown.selection_clear() - if evt: # manual override - # Type ignored due to https://github.com/python/mypy/issues/3004 - self.conf.winetricks_binary = None # type: ignore[assignment] - self.start_ensure_config() - - def on_release_check_released(self, evt=None): - self.start_releases_check() - - def on_wine_check_released(self, evt=None): - self.gui.wine_check_button.state(['disabled']) - self.start_wine_versions_check() + self._post_dropdown_change() def set_skip_fonts(self, evt=None): self.conf.skip_install_fonts = not self.gui.fontsvar.get() # invert True/False @@ -459,6 +378,9 @@ def on_okay_released(self, evt=None): self.start_install_thread() def on_cancel_released(self, evt=None): + self.app.config_updated_hooks.remove(self._config_updated_hook) + # Reset status + self.app.clear_status() self.win.destroy() return 1 @@ -467,60 +389,21 @@ def _install(): """Function to handle the install""" installer.install(self.app) # Install complete, cleaning up... - self.gui.progress.stop() - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - self.gui.statusvar.set('') + self.app._status("", 0) self.gui.okay_button.config( text="Exit", command=self.on_cancel_released, ) self.gui.okay_button.state(['!disabled']) - self.root.event_generate('<>') self.win.destroy() return 0 # Setup for the install - self.gui.progress.config(mode='determinate') - self.gui.statusvar.set('Ready to install!') - self.gui.progressvar.set(0) + self.app.status('Ready to install!', 0) self.set_input_widgets_state('disabled') self.start_thread(_install) - def start_indeterminate_progress(self, evt=None): - self.gui.progress.state(['!disabled']) - self.gui.progressvar.set(0) - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() - - def stop_indeterminate_progress(self, evt=None): - self.gui.progress.stop() - self.gui.progress.state(['disabled']) - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - self.gui.statusvar.set('') - - def update_release_check_progress(self, evt=None): - self.stop_indeterminate_progress() - self.gui.release_check_button.state(['!disabled']) - if not self.releases_q.empty(): - self.gui.release_dropdown['values'] = self.releases_q.get() - self.gui.releasevar.set(self.gui.release_dropdown['values'][0]) - self.set_release() - else: - self.gui.statusvar.set("Failed to get release list. Check connection and try again.") # noqa: E501 - - def update_wine_check_progress(self): - wine_choices = utils.get_wine_options(self.app) - self.gui.wine_dropdown['values'] = wine_choices - if not self.gui.winevar.get(): - # If no value selected, default to 1st item in list. - self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) - self.set_wine() - self.stop_indeterminate_progress() - self.gui.wine_check_button.state(['!disabled']) - class ControlWindow(GuiApp): def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwargs): @@ -536,7 +419,6 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar ver = constants.LLI_CURRENT_VERSION text = f"{text}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" self.gui.update_lli_label.config(text=text) - self.configure_app_button() self.gui.run_indexing_radio.config( command=self.on_action_radio_clicked ) @@ -562,59 +444,25 @@ def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwar self.gui.deps_button.config(command=self.install_deps) self.gui.backup_button.config(command=self.run_backup) self.gui.restore_button.config(command=self.run_restore) + # XXX: do these need to be manual? self.gui.update_lli_button.config( command=self.update_to_latest_lli_release ) self.gui.latest_appimage_button.config( command=self.update_to_latest_appimage ) - if self.conf._raw.wine_binary_code not in ["Recommended", "AppImage", None]: # noqa: E501 - self.gui.latest_appimage_button.state(['disabled']) - gui.ToolTip( - self.gui.latest_appimage_button, - "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 - ) - self.gui.set_appimage_button.state(['disabled']) - gui.ToolTip( - self.gui.set_appimage_button, - "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 - ) self.update_latest_lli_release_button() self.gui.set_appimage_button.config(command=self.set_appimage) self.gui.get_winetricks_button.config(command=self.get_winetricks) self.gui.run_winetricks_button.config(command=self.launch_winetricks) - # XXX: These can be called before installation started, causing them to fail - # self.update_run_winetricks_button() - # self.update_latest_appimage_button() - - self.root.bind('<>', self.clear_status_text) - self.root.bind( - '<>', - self.start_indeterminate_progress - ) - self.root.bind( - "<>", - self.update_latest_appimage_button - ) - self.root.bind('<>', self.update_app_button) - + self._config_update_hook() # These can be expanded to change the UI based on config changes. - self.update_logging_button() - self.register_config_update_hook(self.update_logging_button) + self.config_updated_hooks += [self._config_update_hook] def edit_config(self): control.edit_file(self.conf.config_file_path) - def configure_app_button(self, evt=None): - if self.is_installed(): - # wine.set_logos_paths() - self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") - self.gui.app_button.config(command=self.run_logos) - self.gui.get_winetricks_button.state(['!disabled']) - else: - self.gui.app_button.config(command=self.run_installer) - def run_installer(self, evt=None): classname = constants.BINARY_NAME installer_window_top = Toplevel() @@ -668,7 +516,7 @@ def run_restore(self, evt=None): self.start_thread(control.restore, app=self) def install_deps(self, evt=None): - self.start_indeterminate_progress() + self.status("Installing dependencies...") self.start_thread(utils.install_dependencies, self) def open_file_dialog(self, filetype_name, filetype_extension): @@ -682,8 +530,7 @@ def open_file_dialog(self, filetype_name, filetype_extension): return file_path def update_to_latest_lli_release(self, evt=None): - self.start_indeterminate_progress() - self.gui.statusvar.set(f"Updating to latest {constants.APP_NAME} version…") # noqa: E501 + self.status(f"Updating to latest {constants.APP_NAME} version…") # noqa: E501 self.start_thread(utils.update_to_latest_lli_release, app=self) def set_appimage_symlink(self): @@ -692,8 +539,7 @@ def set_appimage_symlink(self): def update_to_latest_appimage(self, evt=None): self.conf.wine_appimage_path = Path(self.conf.wine_appimage_recommended_file_name) # noqa: E501 - self.start_indeterminate_progress() - self.gui.statusvar.set("Updating to latest AppImage…") + self.status("Updating to latest AppImage…") self.start_thread(self.set_appimage_symlink) def set_appimage(self, evt=None): @@ -712,14 +558,13 @@ def get_winetricks(self, evt=None): self.conf.installer_binary_dir, app=self ) - self.update_run_winetricks_button() def launch_winetricks(self, evt=None): self.gui.statusvar.set("Launching Winetricks…") # Start winetricks in thread. self.start_thread(self.run_winetricks) # Start thread to clear status after delay. - args = [12000, self.root.event_generate, '<>'] + args = [12000, self.clear_status] self.start_thread(self.root.after, *args) def run_winetricks(self): @@ -727,10 +572,7 @@ def run_winetricks(self): def switch_logging(self, evt=None): desired_state = self.gui.loggingstatevar.get() - self.gui.statusvar.set(f"Switching app logging to '{desired_state}d'…") - self.start_indeterminate_progress() - self.gui.progress.state(['!disabled']) - self.gui.progress.start() + self._status(f"Switching app logging to '{desired_state}d'…") self.gui.logging_button.state(['disabled']) self.start_thread( self.logos.switch_logging, @@ -738,7 +580,7 @@ def switch_logging(self, evt=None): ) def _status(self, message: str, percent: int | None = None): - if percent: + if percent is not None: self.gui.progress.stop() self.gui.progress.state(['disabled']) self.gui.progress.config(mode='determinate') @@ -749,23 +591,22 @@ def _status(self, message: str, percent: int | None = None): self.gui.progress.config(mode='indeterminate') self.gui.progress.start() self.gui.statusvar.set(message) - super()._status(message, percent) + if message: + super()._status(message, percent) def update_logging_button(self, evt=None): - self.gui.statusvar.set('') - self.gui.progress.stop() - self.gui.progress.state(['disabled']) state = self.reverse_logging_state_value(self.current_logging_state_value()) self.gui.loggingstatevar.set(state[:-1].title()) self.gui.logging_button.state(['!disabled']) - # XXX: also call this when config changes. Maybe only then? def update_app_button(self, evt=None): - self.gui.app_button.state(['!disabled']) - self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") - self.configure_app_button() - self.update_run_winetricks_button() - self.gui.logging_button.state(['!disabled']) + if self.is_installed(): + self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") + self.gui.app_button.config(command=self.run_logos) + self.gui.get_winetricks_button.state(['!disabled']) + self.gui.logging_button.state(['!disabled']) + else: + self.gui.app_button.config(command=self.run_installer) def update_latest_lli_release_button(self, evt=None): msg = None @@ -783,43 +624,70 @@ def update_latest_lli_release_button(self, evt=None): msg = f"This button is disabled. {constants.APP_NAME} is newer than the latest release." # noqa: E501 if msg: gui.ToolTip(self.gui.update_lli_button, msg) - self.clear_status_text() - self.stop_indeterminate_progress() + self.clear_status() self.gui.update_lli_button.state([state]) def update_latest_appimage_button(self, evt=None): - status, reason = utils.compare_recommended_appimage_version(self) + state = None msg = None - if status == 0: - state = '!disabled' - elif status == 1: + if not self.is_installed(): + state = "disabled" + msg = "Please install first" + elif self.conf._raw.wine_binary_code not in ["Recommended", "AppImage", None]: # noqa: E501 state = 'disabled' - msg = "This button is disabled. The AppImage is already set to the latest recommended." # noqa: E501 - elif status == 2: - state = 'disabled' - msg = "This button is disabled. The AppImage version is newer than the latest recommended." # noqa: E501 + msg = "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 + self.gui.set_appimage_button.state(['disabled']) + gui.ToolTip( + self.gui.set_appimage_button, + "This button is disabled. The configured install was not created using an AppImage." # noqa: E501 + ) + elif self.conf._raw.wine_binary is not None: + status, _ = utils.compare_recommended_appimage_version(self) + if status == 0: + state = '!disabled' + elif status == 1: + state = 'disabled' + msg = "This button is disabled. The AppImage is already set to the latest recommended." # noqa: E501 + elif status == 2: + state = 'disabled' + msg = "This button is disabled. The AppImage version is newer than the latest recommended." # noqa: E501 + else: + # Failed to check + state = '!disabled' else: - # Failed to check + # Not enough context to figure out if this should be enabled or not state = '!disabled' if msg: gui.ToolTip(self.gui.latest_appimage_button, msg) - self.clear_status_text() - self.stop_indeterminate_progress() + self.clear_status() self.gui.latest_appimage_button.state([state]) - def stop_indeterminate_progress(self, evt=None): - self.gui.progress.stop() - self.gui.progress.state(['disabled']) - self.gui.progress.config(mode='determinate') - self.gui.progressvar.set(0) - def update_run_winetricks_button(self, evt=None): - if utils.file_exists(self.conf.winetricks_binary): - state = '!disabled' + if self.conf._raw.winetricks_binary is not None: + # Path may be stored as relative + if Path(self.conf._raw.winetricks_binary).is_absolute(): + winetricks_binary = self.conf._raw.winetricks_binary + elif self.conf._raw.install_dir is not None: + winetricks_binary = str(Path(self.conf._raw.install_dir) / self.conf._raw.winetricks_binary) #noqa: E501 + else: + winetricks_binary = None + + if winetricks_binary is not None and utils.file_exists(winetricks_binary): + state = '!disabled' + else: + state = 'disabled' else: state = 'disabled' self.gui.run_winetricks_button.state([state]) + def _config_update_hook(self, evt=None): + self.update_logging_button() + self.update_app_button() + self.update_latest_lli_release_button() + self.update_latest_appimage_button() + self.update_run_winetricks_button() + + def current_logging_state_value(self) -> str: if self.conf.faithlife_product_logging: return 'ENABLED' @@ -832,14 +700,9 @@ def reverse_logging_state_value(self, state) ->str: else: return 'DISABLED' - def clear_status_text(self, evt=None): - self.gui.statusvar.set('') + def clear_status(self): + self._status('', 0) - def start_indeterminate_progress(self, evt=None): - self.gui.progress.state(['!disabled']) - self.gui.progressvar.set(0) - self.gui.progress.config(mode='indeterminate') - self.gui.progress.start() diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 8949259e..4519a849 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -130,7 +130,7 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() - self.register_config_update_hook(self.set_curses_colors) + self.config_updated_hooks += [self.set_curses_colors] @property def active_screen(self) -> tui_screen.Screen: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index a8979983..09fa559f 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -505,7 +505,6 @@ def check_appimage(filestr): def find_appimage_files(app: App) -> list[str]: - app.status("Finding available wine AppImages…") release_version = app.conf.installed_faithlife_product_release or app.conf.faithlife_product_version #noqa: E501 appimages = [] directories = [ diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index d7b0c091..daa5911e 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -16,6 +16,9 @@ from . import utils def check_wineserver(app: App): + # FIXME: if the wine version changes, we may need to restart the wineserver + # (or at least kill it). Gotten into several states in dev where this happend + # Normally when an msi install failed try: # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of # WINESERVER_EXE changed it to use wineserver_binary, this change may alter the From 81b205d1df95d66183d11058445821801392f18a Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:26:54 -0800 Subject: [PATCH 099/137] fix: increase robustness of spawning dialogs on tk --- ou_dedetai/gui.py | 4 +-- ou_dedetai/gui_app.py | 74 +++++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index 29b2e83a..4acebde1 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -156,13 +156,11 @@ def __init__(self, root, app: App, **kwargs): class ControlGui(Frame): - def __init__(self, root, app: App, *args, **kwargs): + def __init__(self, root, *args, **kwargs): super(ControlGui, self).__init__(root, **kwargs) self.config(padding=5) self.grid(row=0, column=0, sticky='nwes') - self.app = app - # Run/install app button self.app_buttonvar = StringVar() self.app_buttonvar.set("Install") diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 3f75323e..a06d7fc1 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -9,6 +9,7 @@ import shutil from threading import Event +import threading from tkinter import PhotoImage, messagebox from tkinter import Tk from tkinter import Toplevel @@ -38,29 +39,15 @@ def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwa self.root = root def _ask(self, question: str, options: list[str] | str) -> Optional[str]: - # XXX: would it be possible to use root.bind to call this from the main loop? - # So we don't need to make our own root? - error_q: Queue[Exception] = Queue() - answer_q: Queue[Optional[str]] = Queue() - answer_event = Event() - def spawn_dialog(options: list[str]): - # Create a new popup (with it's own event loop) - try: - pop_up = ChoicePopUp(question, options, answer_q, answer_event) - - # Run the mainloop in this thread - pop_up.mainloop() - except RuntimeError as e: - # Let the other thread know we failed. - error_q.put(e) - answer_event.set() - # raise e + # This cannot be run from the main thread as the dialog will never appear + # since the tinker mainloop hasn't started and we block on a response + if isinstance(options, list): - self.start_thread(spawn_dialog, options) + answer_q: Queue[Optional[str]] = Queue() + answer_event = Event() + ChoicePopUp(question, options, answer_q, answer_event) answer_event.wait() - if not error_q.empty(): - raise error_q.get() answer: Optional[str] = answer_q.get() elif isinstance(options, str): answer = options @@ -159,20 +146,20 @@ def __init__(self, *args, **kwargs): self.iconphoto(False, self.pi) -class ChoicePopUp(Tk): +class ChoicePopUp: """Creates a pop-up with a choice""" def __init__(self, question: str, options: list[str], answer_q: Queue[Optional[str]], answer_event: Event, **kwargs): #noqa: E501 + self.root = Toplevel() # Set root parameters. - super().__init__() - self.title(f"Quesiton: {question.strip().strip(':')}") - self.resizable(False, False) - self.gui = gui.ChoiceGui(self, question, options) + self.gui = gui.ChoiceGui(self.root, question, options) + self.root.title(f"Quesiton: {question.strip().strip(':')}") + self.root.resizable(False, False) # Set root widget event bindings. - self.bind( + self.root.bind( "", self.on_confirm_choice ) - self.bind( + self.root.bind( "", self.on_cancel_released ) @@ -187,12 +174,12 @@ def on_confirm_choice(self, evt=None): answer = self.gui.answer_dropdown.get() self.answer_q.put(answer) self.answer_event.set() - self.destroy() + self.root.destroy() def on_cancel_released(self, evt=None): self.answer_q.put(None) self.answer_event.set() - self.destroy() + self.root.destroy() class InstallerWindow: @@ -406,13 +393,13 @@ def _install(): class ControlWindow(GuiApp): - def __init__(self, root, ephemeral_config: EphemeralConfiguration, *args, **kwargs): + def __init__(self, root, control_gui: gui.ControlGui, + ephemeral_config: EphemeralConfiguration, *args, **kwargs): super().__init__(root, ephemeral_config) + # Set root parameters. self.root = root - self.root.title(f"{constants.APP_NAME} Control Panel") - self.root.resizable(False, False) - self.gui = gui.ControlGui(self.root, app=self) + self.gui = control_gui self.actioncmd: Optional[Callable[[], None]] = None text = self.gui.update_lli_label.cget('text') @@ -709,5 +696,24 @@ def clear_status(self): def control_panel_app(ephemeral_config: EphemeralConfiguration): classname = constants.BINARY_NAME root = Root(className=classname) - ControlWindow(root, ephemeral_config, class_=classname) + + # Need to title/resize and create the initial gui + # BEFORE mainloop is started to get sizing correct + # other things in the ControlWindow constructor are run after mainloop is running + # To allow them to ask questions while the mainloop is running + root.title(f"{constants.APP_NAME} Control Panel") + root.resizable(False, False) + control_gui = gui.ControlGui(root) + + def _start_control_panel(): + ControlWindow(root, control_gui, ephemeral_config, class_=classname) + + # Start the control panel on a new thread so it can open dialogs + # as a part of it's constructor + threading.Thread( + name=f"{constants.APP_NAME} GUI main loop", + target=_start_control_panel, + daemon=True + ).start() + root.mainloop() From dcd84c52ba6efbb56fad72ccd192de257317c11b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:32:36 -0800 Subject: [PATCH 100/137] refactor: resolve some comments --- ou_dedetai/cli.py | 2 +- ou_dedetai/gui_app.py | 5 ++--- ou_dedetai/installer.py | 24 ++++++++---------------- ou_dedetai/network.py | 2 -- ou_dedetai/system.py | 2 +- ou_dedetai/utils.py | 4 ++-- ou_dedetai/wine.py | 18 ++++++------------ 7 files changed, 20 insertions(+), 37 deletions(-) diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index c3a1896b..35d75545 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -94,7 +94,7 @@ def update_self(self): utils.update_to_latest_lli_release(self) def winetricks(self): - wine.run_winetricks_cmd(self, *(self.conf._overrides.winetricks_args or [])) + wine.run_winetricks(self, *(self.conf._overrides.winetricks_args or [])) _exit_option: str = "Exit" diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index a06d7fc1..063e1b52 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -354,11 +354,11 @@ def set_winetricks(self, evt=None): def set_skip_fonts(self, evt=None): self.conf.skip_install_fonts = not self.gui.fontsvar.get() # invert True/False - logging.debug(f"> config.SKIP_FONTS={self.conf.skip_install_fonts}") + logging.debug(f"> {self.conf.skip_install_fonts=}") def set_skip_dependencies(self, evt=None): self.conf.skip_install_system_dependencies = self.gui.skipdepsvar.get() # invert True/False # noqa: E501 - logging.debug(f"> config.SKIP_DEPENDENCIES={self.conf.skip_install_system_dependencies}") #noqa: E501 + logging.debug(f"> {self.conf.skip_install_system_dependencies=}") #noqa: E501 def on_okay_released(self, evt=None): # Update desktop panel icon. @@ -431,7 +431,6 @@ def __init__(self, root, control_gui: gui.ControlGui, self.gui.deps_button.config(command=self.install_deps) self.gui.backup_button.config(command=self.run_backup) self.gui.restore_button.config(command=self.run_restore) - # XXX: do these need to be manual? self.gui.update_lli_button.config( command=self.update_to_latest_lli_release ) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 410d716c..7276b866 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -48,24 +48,20 @@ def ensure_install_dirs(app: App): ensure_choices(app=app) app.installer_step += 1 app.status("Ensuring installation directories…") - logging.debug('- config.INSTALLDIR') - logging.debug('- config.WINEPREFIX') - logging.debug('- data/bin') - logging.debug('- data/wine64_bottle') wine_dir = Path("") bin_dir = Path(app.conf.installer_binary_dir) bin_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {bin_dir} exists?: {bin_dir.is_dir()}") - logging.debug(f"> config.INSTALLDIR={app.conf.installer_binary_dir}") - logging.debug(f"> config.APPDIR_BINDIR={app.conf.installer_binary_dir}") + logging.debug(f"> {app.conf.install_dir=}") + logging.debug(f"> {app.conf.installer_binary_dir=}") wine_dir = Path(f"{app.conf.wine_prefix}") wine_dir.mkdir(parents=True, exist_ok=True) logging.debug(f"> {wine_dir} exists: {wine_dir.is_dir()}") - logging.debug(f"> config.WINEPREFIX={app.conf.wine_prefix}") + logging.debug(f"> {app.conf.wine_prefix=}") def ensure_sys_deps(app: App): @@ -110,20 +106,16 @@ def ensure_wine_executables(app: App): ensure_appimage_download(app=app) app.installer_step += 1 app.status("Ensuring wine executables are available…") - logging.debug('- config.WINESERVER_EXE') - logging.debug('- wine') - logging.debug('- wine64') - logging.debug('- wineserver') create_wine_appimage_symlinks(app=app) # PATH is modified if wine appimage isn't found, but it's not modified # during a restarted installation, so shutil.which doesn't find the # executables in that case. - logging.debug(f"> wine path: {app.conf.wine_binary}") - logging.debug(f"> wine64 path: {app.conf.wine64_binary}") - logging.debug(f"> wineserver path: {app.conf.wineserver_binary}") - logging.debug(f"> winetricks path: {app.conf.winetricks_binary}") + logging.debug(f"> {app.conf.wine_binary=}") + logging.debug(f"> {app.conf.wine64_binary=}") + logging.debug(f"> {app.conf.wineserver_binary=}") + logging.debug(f"> {app.conf.winetricks_binary=}") def ensure_winetricks_executable(app: App): @@ -296,7 +288,7 @@ def ensure_product_installed(app: App): # Clean up temp files, etc. utils.clean_all() - logging.debug(f"> Product path: config.LOGOS_EXE={app.conf.logos_exe}") + logging.debug(f"> {app.conf.logos_exe=}") def ensure_config_file(app: App): diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index c740dad9..b4c02ac8 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -97,8 +97,6 @@ def _get_headers(self) -> requests.structures.CaseInsensitiveDict: except Exception as e: logging.error(e) raise - # XXX: should we have a more generic catch for KeyboardInterrupt rather than deep in this function? #noqa: E501 - # except KeyboardInterrupt: return r.headers def _get_size(self): diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index bef0c306..c9f32ef1 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -227,7 +227,7 @@ def get_dialog() -> str: sys.exit(1) dialog = os.getenv('DIALOG') - # Set config.DIALOG. + # find dialog if dialog is not None: dialog = dialog.lower() if dialog not in ['cli', 'curses', 'tk']: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 09fa559f..1c7824a8 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -589,8 +589,8 @@ def set_appimage_symlink(app: App): logging.debug("No need to set appimage symlink, as it wasn't set") return - logging.debug(f"config.APPIMAGE_FILE_PATH={app.conf.wine_appimage_path}") - logging.debug(f"config.RECOMMENDED_WINE64_APPIMAGE_FULL_FILENAME={app.conf.wine_appimage_recommended_file_name}") + logging.debug(f"{app.conf.wine_appimage_path=}") + logging.debug(f"{app.conf.wine_appimage_recommended_file_name=}") appimage_file_path = Path(app.conf.wine_appimage_path) appdir_bindir = Path(app.conf.installer_binary_dir) appimage_symlink_path = appdir_bindir / app.conf.wine_appimage_link_file_name diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index daa5911e..3ea9e80a 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -347,13 +347,7 @@ def run_wine_proc( return process -def run_winetricks(app: App, cmd=None): - process = run_wine_proc(app.conf.winetricks_binary, app=app, exe=cmd) - system.wait_pid(process) - wineserver_wait(app) - -# XXX: this function looks similar to the one above. duplicate? -def run_winetricks_cmd(app: App, *args): +def run_winetricks(app: App, *args): cmd = [*args] # FIXME: test this to ensure it behaves as expected if "-q" not in args and app.conf.winetricks_binary: @@ -372,7 +366,7 @@ def run_winetricks_cmd(app: App, *args): def install_d3d_compiler(app: App): cmd = ['d3dcompiler_47'] - run_winetricks_cmd(app, *cmd) + run_winetricks(app, *cmd) def install_fonts(app: App): @@ -381,23 +375,23 @@ def install_fonts(app: App): for i, f in enumerate(fonts): app.status(f"Configuring font: {f}…", i / len(fonts)) # noqa: E501 args = [f] - run_winetricks_cmd(app, *args) + run_winetricks(app, *args) def install_font_smoothing(app: App): logging.info("Setting font smoothing…") args = ['settings', 'fontsmooth=rgb'] - run_winetricks_cmd(app, *args) + run_winetricks(app, *args) def set_renderer(app: App, renderer: str): - run_winetricks_cmd(app, "-q", "settings", f"renderer={renderer}") + run_winetricks(app, "-q", "settings", f"renderer={renderer}") def set_win_version(app: App, exe: str, windows_version: str): if exe == "logos": # XXX: This never exits - run_winetricks_cmd(app, '-q', 'settings', f'{windows_version}') + run_winetricks(app, '-q', 'settings', f'{windows_version}') elif exe == "indexer": reg = f"HKCU\\Software\\Wine\\AppDefaults\\{app.conf.faithlife_product}Indexer.exe" # noqa: E501 From de3992cb0547e089a25ea2447d6cb76e41f52223 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:15:01 -0800 Subject: [PATCH 101/137] fix: spawn tui actions on a new thread as well as other comment resolutions --- ou_dedetai/app.py | 6 +++++- ou_dedetai/network.py | 8 ------- ou_dedetai/system.py | 7 +++--- ou_dedetai/tui_app.py | 46 ++++++++++++++++++++++------------------ ou_dedetai/tui_curses.py | 4 +++- ou_dedetai/utils.py | 4 ++-- ou_dedetai/wine.py | 17 ++++++++++++++- 7 files changed, 54 insertions(+), 38 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 43fd06ac..02930338 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -132,7 +132,11 @@ def exit(self, reason: str, intended: bool = False) -> NoReturn: for thread in self._threads: # Only wait on non-daemon threads. if not thread.daemon: - thread.join() + try: + thread.join() + except RuntimeError: + # Will happen if we try to join the current thread + pass # Remove pid file if exists try: os.remove(constants.PID_FILE) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index b4c02ac8..3adaf434 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -556,14 +556,6 @@ def _get_faithlife_product_releases( return filtered_releases -# XXX: remove this when it's no longer used -def get_logos_releases(app: App) -> list[str]: - return _get_faithlife_product_releases( - faithlife_product=app.conf.faithlife_product, - faithlife_product_version=app.conf.faithlife_product_version, - faithlife_product_release_channel=app.conf.faithlife_product_release_channel - ) - def update_lli_binary(app: App): lli_file_path = os.path.realpath(sys.argv[0]) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index c9f32ef1..30dd354f 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -109,7 +109,7 @@ def run_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.Co return None -def popen_command(command, retries=1, delay=0, **kwargs): +def popen_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess.Popen[bytes]]: #noqa: E501 shell = kwargs.get("shell", False) env = kwargs.get("env", None) cwd = kwargs.get("cwd", None) @@ -134,7 +134,6 @@ def popen_command(command, retries=1, delay=0, **kwargs): process_group = kwargs.get("process_group", None) encoding = kwargs.get("encoding", None) errors = kwargs.get("errors", None) - text = kwargs.get("text", None) if retries < 1: retries = 1 @@ -170,7 +169,7 @@ def popen_command(command, retries=1, delay=0, **kwargs): process_group=process_group, encoding=encoding, errors=errors, - text=text + text=False ) return process @@ -902,7 +901,7 @@ def install_winetricks( app.conf.winetricks_binary = f"{installdir}/winetricks" logging.debug("Winetricks installed.") -def wait_pid(process): +def wait_pid(process: subprocess.Popen): os.waitpid(-process.pid, 0) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 4519a849..69e9fa74 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -33,7 +33,11 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati super().__init__(ephemeral_config) self.stdscr = stdscr self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 - self.subtitle = f"Logos Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 + product_name = self.conf._raw.faithlife_product or "Logos" + if self.is_installed(): + self.subtitle = f"{product_name} Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 + else: + self.subtitle = f"{product_name} not installed" # else: # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" @@ -457,8 +461,6 @@ def choice_processor(self, stdscr, screen_id, choice): 0: self.main_menu_select, 1: self.custom_appimage_select, 2: self.handle_ask_response, - 3: self.handle_ask_file_response, - 4: self.handle_ask_directory_response, 8: self.waiting, 10: self.waiting_releases, 11: self.winetricks_menu_select, @@ -485,7 +487,12 @@ def choice_processor(self, stdscr, screen_id, choice): else: action = screen_actions.get(screen_id) if action: - action(choice) #type: ignore[operator] + # Start the action in a new thread to not interrupt the input thread + self.start_thread( + action, + choice, + daemon_bool=False, + ) else: pass @@ -515,7 +522,6 @@ def _install(): self.start_thread( _install, daemon_bool=True, - app=self, ) elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release(self) @@ -751,6 +757,7 @@ def switch_screen(self): _exit_option = "Return to Main Menu" def _ask(self, question: str, options: list[str] | str) -> Optional[str]: + self.ask_answer_event.clear() if isinstance(options, str): answer = options elif isinstance(options, list): @@ -765,11 +772,11 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: self.ask_answer_event.wait() answer = self.ask_answer_queue.get() + self.ask_answer_event.clear() if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: - stack_index = 3 if answer == PROMPT_OPTION_FILE else 4 self.screen_q.put( self.stack_input( - stack_index, + 2, Queue(), threading.Event(), question, @@ -778,23 +785,20 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: ) # noqa: E501 # Now wait for it to complete self.ask_answer_event.wait() - answer = self.ask_answer_queue.get() - - return answer + new_answer = self.ask_answer_queue.get() + if answer == PROMPT_OPTION_DIRECTORY: + # Make the directory if it doesn't exit. + # form a terminal UI, it's not easy for the user to manually + os.makedirs(new_answer, exist_ok=True) - def handle_ask_response(self, choice: Optional[str]): - if choice is not None: - self.ask_answer_queue.put(choice) - self.ask_answer_event.set() - self.switch_screen() + answer = new_answer - def handle_ask_file_response(self, choice: Optional[str]): - if choice is not None and Path(choice).exists() and Path(choice).is_file(): - self.handle_ask_response(choice) + return answer - def handle_ask_directory_response(self, choice: Optional[str]): - if choice is not None and Path(choice).exists() and Path(choice).is_dir(): - self.handle_ask_response(choice) + def handle_ask_response(self, choice: str): + self.ask_answer_queue.put(choice) + self.ask_answer_event.set() + self.switch_screen() def _status(self, message: str, percent: int | None = None): self.screen_q.put( diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 4e8d3817..739da951 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -30,7 +30,9 @@ def write_line(app: App, stdscr: curses.window, start_y, start_x, text, char_lim try: stdscr.addnstr(start_y, start_x, text, char_limit, attributes) except curses.error: - signal.signal(signal.SIGWINCH, app.signal_resize) + # FIXME: what do we want to do to handle this error? + # Before we were registering a signal handler + pass def title(app: App, title_text, title_start_y_adj): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 1c7824a8..437d5eea 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -109,7 +109,7 @@ def delete_symlink(symlink_path): logging.error(f"Error removing symlink: {e}") -# XXX: seems like it should be in control +# FIXME: should this be in control? def install_dependencies(app: App): if app.conf.faithlife_product_version: targetversion = int(app.conf.faithlife_product_version) @@ -640,7 +640,7 @@ def update_to_latest_lli_release(app: App): logging.debug(f"{constants.APP_NAME} is at a newer version than the latest.") # noqa: 501 -# XXX: seems like this should be in control +# FIXME: consider moving this to control def update_to_latest_recommended_appimage(app: App): if app.conf.wine_binary_code not in ["AppImage", "Recommended"]: logging.debug("AppImage commands disabled since we're not using an appimage") # noqa: E501 diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 3ea9e80a..2c0c501d 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -24,6 +24,9 @@ def check_wineserver(app: App): # WINESERVER_EXE changed it to use wineserver_binary, this change may alter the # behavior, to match what the code intended process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-p"]) + if not process: + logging.debug("Failed to spawn wineserver to check it") + return False system.wait_pid(process) return process.returncode == 0 except Exception: @@ -33,12 +36,18 @@ def check_wineserver(app: App): def wineserver_kill(app: App): if check_wineserver(app): process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-k"]) + if not process: + logging.debug("Failed to spawn wineserver to kill it") + return False system.wait_pid(process) def wineserver_wait(app: App): if check_wineserver(app): process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-w"]) + if not process: + logging.debug("Failed to spawn wineserver to wait for it") + return False system.wait_pid(process) @@ -240,6 +249,8 @@ def wine_reg_install(app: App, reg_file, wine64_binary): ) # NOTE: For some reason system.wait_pid results in the reg install failing. # system.wait_pid(process) + if process is None: + app.exit("Failed to spawn command to install reg file") process.wait() if process is None or process.returncode != 0: failed = "Failed to install reg file" @@ -298,7 +309,7 @@ def run_wine_proc( exe_args=list(), init=False, additional_wine_dll_overrides: Optional[str] = None -): +) -> Optional[subprocess.Popen[bytes]]: logging.debug("Getting wine environment.") env = get_wine_env(app, additional_wine_dll_overrides) if isinstance(winecmd, Path): @@ -354,6 +365,8 @@ def run_winetricks(app: App, *args): cmd.insert(0, "-q") logging.info(f"running \"winetricks {' '.join(cmd)}\"") process = run_wine_proc(app.conf.winetricks_binary, app, exe_args=cmd) + if process is None: + app.exit("Failed to spawn winetricks") system.wait_pid(process) logging.info(f"\"winetricks {' '.join(cmd)}\" DONE!") wineserver_wait(app) @@ -408,6 +421,8 @@ def set_win_version(app: App, exe: str, windows_version: str): exe='reg', exe_args=exe_args ) + if process is None: + app.exit("Failed to spawn command to set windows version for indexer") system.wait_pid(process) From d29eed34c8ed6d2e18b035efb9f970e2772917c1 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:14:15 -0800 Subject: [PATCH 102/137] fix: winetricks binary storage Store as None if the user wishes to download from the internet Also skip the prompt if winetricks wasn't found in PATH. The legacy WINETRICKSBIN will still function --- ou_dedetai/config.py | 82 ++++++++++++++++++++++++++++++++--------- ou_dedetai/control.py | 13 +++---- ou_dedetai/gui_app.py | 3 -- ou_dedetai/installer.py | 9 +++-- ou_dedetai/network.py | 7 +++- ou_dedetai/system.py | 13 +++++-- ou_dedetai/tui_app.py | 1 + ou_dedetai/wine.py | 1 - 8 files changed, 90 insertions(+), 39 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 34d9b5da..0fd761b8 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -333,6 +333,12 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": faithlife_product_logging = None if legacy.LOGS is not None: faithlife_product_logging = utils.parse_bool(legacy.LOGS) + winetricks_binary = None + if ( + legacy.WINETRICKSBIN is not None + and legacy.WINETRICKSBIN != constants.DOWNLOAD + ): + winetricks_binary = legacy.WINETRICKSBIN return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, backup_dir=legacy.BACKUPDIR, @@ -344,7 +350,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": app_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, - winetricks_binary=legacy.WINETRICKSBIN, + winetricks_binary=winetricks_binary, faithlife_product_logging=faithlife_product_logging ) @@ -465,7 +471,10 @@ def _relative_from_install_dir(self, path: Path | str) -> str: Returns: path - absolute """ - return str(Path(path).absolute()).lstrip(self.install_dir) + output = str(path) + if Path(path).is_absolute() and output.startswith(self.install_dir): + output = output[len(self.install_dir):].lstrip("/") + return output def _absolute_from_install_dir(self, path: Path | str) -> str: """Takes in a possibly relative path under install dir and turns it into an @@ -587,33 +596,70 @@ def app_release_channel(self) -> str: @property def winetricks_binary(self) -> str: - # FIXME: consider implications of having "Download" placeholder - # Should we return None instead? - """This may be a path to the winetricks binary or it may be "Download" - """ - question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 - options = utils.get_winetricks_options() - output = self._ask_if_not_found("winetricks_binary", question, options) - if output == "Download": - return output - return self._absolute_from_install_dir(output) + """Download winetricks if it doesn't exist""" + from ou_dedetai import system + if ( + self._raw.winetricks_binary is not None and + not Path(self._absolute_from_install_dir(self._raw.winetricks_binary)).exists() #noqa: E501 + ): + logging.warning("Given winetricks doesn't exist. Downloading from internet") + self._raw.winetricks_binary = None + + if ( + self._winetricks_binary is None + # Informs mypy of the type without relying on implementation of + # self._winetricks_binary + or self._raw.winetricks_binary is None + ): + self._raw.winetricks_binary = system.install_winetricks( + self.installer_binary_dir, + app=self.app, + status_messages=False + ) + return self._absolute_from_install_dir(self._raw.winetricks_binary) @winetricks_binary.setter def winetricks_binary(self, value: Optional[str | Path]): if value is not None: - value = str(value) - if value is not None and value != "Download": - if not Path(value).exists(): + # Legacy had this value be "Download" when the user wanted the default + # Now we encode that in None + if value == constants.DOWNLOAD: + value = None + else: + value = self._relative_from_install_dir(value) + if value is not None: + if not Path(self._absolute_from_install_dir(value)).exists(): raise ValueError("Winetricks binary must exist") if self._raw.winetricks_binary != value: - if value == "Download": - self._raw.winetricks_binary = value - elif value is not None: + if value is not None: self._raw.winetricks_binary = self._relative_from_install_dir(value) else: self._raw.winetricks_binary = None self._write() + @property + def _winetricks_binary(self) -> Optional[str]: + """Get the path to winetricks + + Prompt if the user has any choices besides download + """ + question = f"Should the script use the system's local winetricks or download the latest winetricks from the Internet? The script needs to set some Wine options that {self.faithlife_product} requires on Linux." # noqa: E501 + options = utils.get_winetricks_options() + # Only prompt if the user has other options besides Downloading. + # the legacy WINETRICKSBIN config key is still supported. + if len(options) == 1: + # Use whatever we have stored + output = self._raw.winetricks_binary + else: + if self._raw.winetricks_binary is None: + self.winetricks_binary = self.app.ask(question, options) + output = self._raw.winetricks_binary + + if output is not None: + return self._absolute_from_install_dir(output) + else: + return None + @property def install_dir_default(self) -> str: return f"{str(Path.home())}/{self.faithlife_product}Bible{self.faithlife_product_version}" # noqa: E501 diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 55a88e32..6dcd10a4 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -13,7 +13,6 @@ from ou_dedetai.app import App -from . import constants from . import system from . import utils @@ -202,25 +201,25 @@ def remove_library_catalog(app: App): def set_winetricks(app: App): app.status("Preparing winetricks…") - if app.conf.winetricks_binary != constants.DOWNLOAD: + if app.conf._winetricks_binary is not None: valid = True # Double check it's a valid winetricks - if not Path(app.conf.winetricks_binary).exists(): + if not Path(app.conf._winetricks_binary).exists(): logging.warning("Winetricks path does not exist, downloading instead...") valid = False - if not os.access(app.conf.winetricks_binary, os.X_OK): + if not os.access(app.conf._winetricks_binary, os.X_OK): logging.warning("Winetricks path given is not executable, downloading instead...") #noqa: E501 valid = False - if not utils.check_winetricks_version(app.conf.winetricks_binary): + if not utils.check_winetricks_version(app.conf._winetricks_binary): logging.warning("Winetricks version mismatch, downloading instead...") valid = False if valid: - logging.info(f"Found valid winetricks: {app.conf.winetricks_binary}") + logging.info(f"Found valid winetricks: {app.conf._winetricks_binary}") return 0 # Continue executing the download if it wasn't valid system.install_winetricks(app.conf.installer_binary_dir, app) - app.conf.wine_binary = os.path.join( + app.conf.winetricks_binary = os.path.join( app.conf.installer_binary_dir, "winetricks" ) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 063e1b52..39ebefbf 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -282,9 +282,6 @@ def _config_updated_hook(self): # In case the product changes self.root.icon = Path(self.conf.faithlife_product_icon_path) - if self.conf._raw.winetricks_binary is None: - self.conf.winetricks_binary = self.gui.tricksvar.get() - wine_choices = utils.get_wine_options(self.app) self.gui.wine_dropdown['values'] = wine_choices if not self.gui.winevar.get(): diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 7276b866..786cdf13 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -31,7 +31,7 @@ def ensure_choices(app: App): logging.debug(f"> {app.conf.wine_appimage_recommended_file_name=}") logging.debug(f"> {app.conf.wine_binary_code=}") logging.debug(f"> {app.conf.wine_binary=}") - logging.debug(f"> {app.conf.winetricks_binary=}") + logging.debug(f"> {app.conf._raw.winetricks_binary=}") logging.debug(f"> {app.conf.skip_install_fonts=}") logging.debug(f"> {app.conf._overrides.winetricks_skip=}") logging.debug(f"> {app.conf.faithlife_product_icon_path}") @@ -115,7 +115,7 @@ def ensure_wine_executables(app: App): logging.debug(f"> {app.conf.wine_binary=}") logging.debug(f"> {app.conf.wine64_binary=}") logging.debug(f"> {app.conf.wineserver_binary=}") - logging.debug(f"> {app.conf.winetricks_binary=}") + logging.debug(f"> {app.conf._raw.winetricks_binary=}") def ensure_winetricks_executable(app: App): @@ -124,8 +124,9 @@ def ensure_winetricks_executable(app: App): app.installer_step += 1 app.status("Ensuring winetricks executable is available…") - app.status("Downloading winetricks from the Internet…") - system.install_winetricks(app.conf.installer_binary_dir, app=app) + if app.conf._winetricks_binary is None: + app.status("Downloading winetricks from the Internet…") + system.install_winetricks(app.conf.installer_binary_dir, app=app) logging.debug(f"> {app.conf.winetricks_binary} is executable?: {os.access(app.conf.winetricks_binary, os.X_OK)}") # noqa: E501 return 0 diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 3adaf434..080f91ef 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -287,6 +287,7 @@ def logos_reuse_download( file: str, targetdir: str, app: App, + status_messages: bool = True ): dirs = [ app.conf.install_dir, @@ -304,6 +305,7 @@ def logos_reuse_download( sourceurl, file_path, app=app, + status_messages=status_messages ): logging.info(f"{file} properties match. Using it…") logging.debug(f"Copying {file} into {targetdir}") @@ -327,6 +329,7 @@ def logos_reuse_download( sourceurl, file_path, app=app, + status_messages=status_messages ): logging.debug(f"Copying: {file} into: {targetdir}") try: @@ -432,8 +435,8 @@ def _net_get(url: str, target: Optional[Path]=None, app: Optional[App] = None): return None # Return None values to indicate an error condition -def _verify_downloaded_file(url: str, file_path: Path | str, app: App): - if app: +def _verify_downloaded_file(url: str, file_path: Path | str, app: App, status_messages: bool = True): #noqa: E501 + if status_messages: app.status(f"Verifying {file_path}…", 0) file_props = FileProps(file_path) url_size = app.conf._network.url_size(url) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index 30dd354f..aa0d1f39 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -877,8 +877,11 @@ def install_winetricks( installdir, app: App, version=constants.WINETRICKS_VERSION, -): - app.status(f"Installing winetricks v{version}…") + status_messages: bool = True +) -> str: + winetricks_path = f"{installdir}/winetricks" + if status_messages: + app.status(f"Installing winetricks v{version}…") base_url = "https://codeload.github.com/Winetricks/winetricks/zip/refs/tags" # noqa: E501 zip_name = f"{version}.zip" network.logos_reuse_download( @@ -886,6 +889,7 @@ def install_winetricks( zip_name, app.conf.download_dir, app=app, + status_messages=status_messages ) wtzip = f"{app.conf.download_dir}/{zip_name}" logging.debug(f"Extracting winetricks script into {installdir}…") @@ -897,9 +901,10 @@ def install_winetricks( if zi.filename == 'winetricks': z.extract(zi, path=installdir) break - os.chmod(f"{installdir}/winetricks", 0o755) - app.conf.winetricks_binary = f"{installdir}/winetricks" + os.chmod(winetricks_path, 0o755) + app.conf.winetricks_binary = winetricks_path logging.debug("Winetricks installed.") + return winetricks_path def wait_pid(process: subprocess.Popen): os.waitpid(-process.pid, 0) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 69e9fa74..aec960df 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -579,6 +579,7 @@ def winetricks_menu_select(self, choice): self.go_to_main_menu() elif choice == "Run Winetricks": self.reset_screen() + self.status("Running winetricks...") wine.run_winetricks(self) self.go_to_main_menu() elif choice == "Install d3dcompiler": diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 2c0c501d..a389a7ed 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -360,7 +360,6 @@ def run_wine_proc( def run_winetricks(app: App, *args): cmd = [*args] - # FIXME: test this to ensure it behaves as expected if "-q" not in args and app.conf.winetricks_binary: cmd.insert(0, "-q") logging.info(f"running \"winetricks {' '.join(cmd)}\"") From 8b2e2d3a5bd87d677842c8969fecc22846229a3d Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:21:09 -0800 Subject: [PATCH 103/137] fix: no need to display this so prominently app.status takes over the view of the TUI --- ou_dedetai/logos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 5fe0c196..64162b19 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -160,7 +160,7 @@ def stop(self): try: system.run_command(['kill', '-9'] + pids) self.logos_state = State.STOPPED - self.app.status(f"Stopped Logos processes at PIDs {', '.join(pids)}.") # noqa: E501 + logging.debug(f"Stopped Logos processes at PIDs {', '.join(pids)}.") # noqa: E501 except Exception as e: logging.debug(f"Error while stopping Logos processes: {e}.") # noqa: E501 else: From d7f9b4510816a60e0e155e123e1314a3a807c0bc Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:22:10 -0800 Subject: [PATCH 104/137] fix: logs state manager mypy lints --- ou_dedetai/logos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 64162b19..621c34c2 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -103,7 +103,7 @@ def run_logos(): self.app, exe=self.app.conf.logos_exe ) - if isinstance(process, subprocess.Popen): + if process is not None: self.processes[self.app.conf.logos_exe] = process # Ensure wine version is compatible with Logos release version. @@ -191,7 +191,7 @@ def run_indexing(): app=self.app, exe=self.app.conf.logos_indexer_exe ) - if isinstance(process, subprocess.Popen): + if process is not None: self.processes[self.app.conf.logos_indexer_exe] = process def check_if_indexing(process): @@ -300,6 +300,7 @@ def switch_logging(self, action=None): exe='reg', exe_args=exe_args ) - system.wait_pid(process) + if process: + system.wait_pid(process) wine.wineserver_wait(self.app) self.app.conf.faithlife_product_logging = state == state_enabled From 9811e05e783fc685c237c061420247798919377b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:22:56 -0800 Subject: [PATCH 105/137] fix: display recent messages in tui log Also fixed percent going backwards with the \r notation --- ou_dedetai/app.py | 24 +++++++++++++++++++++--- ou_dedetai/cli.py | 5 +++-- ou_dedetai/gui_app.py | 3 ++- ou_dedetai/network.py | 9 ++++++--- ou_dedetai/tui_app.py | 2 ++ ou_dedetai/wine.py | 1 - 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 02930338..ed750fd9 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -173,7 +173,14 @@ def is_installed(self) -> bool: return False def status(self, message: str, percent: Optional[int | float] = None): - """A status update""" + """A status update + + Args: + message: str - if it ends with a \r that signifies that this message is + intended to be overrighten next time + percent: Optional[int] - percent of the way through the current install step + (if installing) + """ if isinstance(percent, float): percent = round(percent * 100) # If we're installing @@ -189,10 +196,21 @@ def status(self, message: str, percent: Optional[int | float] = None): self._last_status = message def _status(self, message: str, percent: Optional[int] = None): - """Implementation for updating status pre-front end""" + """Implementation for updating status pre-front end + + Args: + message: str - if it ends with a \r that signifies that this message is + intended to be overrighten next time + percent: Optional[int] - percent complete of the current overall operation + if None that signifies we can't track the progress. + Feel free to implement a spinner + """ # De-dup if message != self._last_status: - print(f"{message}") + if message.endswith("\r"): + print(f"{message}", end="\r") + else: + print(f"{message}") @property def superuser_command(self) -> str: diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index 35d75545..aa7bc5ae 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -127,14 +127,15 @@ def _status(self, message: str, percent: Optional[int] = None): """Implementation for updating status pre-front end""" prefix = "" end = "\n" + # Signifies we want to overwrite the last line + if message.endswith("\r"): + end = "\r" if message == self._last_status: # Go back to the beginning of the line to re-write the current line # Rather than sending a new one. This allows the current line to update prefix += "\r" end = "\r" if percent: - # XXX: it's possible for the progress to seem to go backwards if anyone - # status is sent during the same install step with percent 0 percent_per_char = 5 chars_of_progress = round(percent / percent_per_char) chars_remaining = round((100 - percent) / percent_per_char) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 39ebefbf..af2b2710 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -543,7 +543,7 @@ def get_winetricks(self, evt=None): ) def launch_winetricks(self, evt=None): - self.gui.statusvar.set("Launching Winetricks…") + self.status("Launching Winetricks…") # Start winetricks in thread. self.start_thread(self.run_winetricks) # Start thread to clear status after delay. @@ -563,6 +563,7 @@ def switch_logging(self, evt=None): ) def _status(self, message: str, percent: int | None = None): + message = message.lstrip("\r") if percent is not None: self.gui.progress.stop() self.gui.progress.state(['disabled']) diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 080f91ef..dc71a84d 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -425,11 +425,14 @@ def _net_get(url: str, target: Optional[Path]=None, app: Optional[App] = None): f.write(chunk) local_size = os.fstat(f.fileno()).st_size if type(total_size) is int: - percent = round(local_size / total_size * 100) + percent = round(local_size / total_size * 10) # if None not in [app, evt]: if app: - # Send progress value to App - app.status("Downloading...", percent=percent) + # Show dots corresponding to show download progress + # While we could use percent, it's likely to interfere + # With whatever install step we are on + message = "Downloading" + "." * percent + "\r" + app.status(message) except requests.exceptions.RequestException as e: logging.error(f"Error occurred during HTTP request: {e}") return None # Return None values to indicate an error condition diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index aec960df..c860039d 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -802,6 +802,8 @@ def handle_ask_response(self, choice: str): self.switch_screen() def _status(self, message: str, percent: int | None = None): + message = message.lstrip("\r") + self.console_log.append(message) self.screen_q.put( self.stack_text( 8, diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index a389a7ed..47da55a8 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -402,7 +402,6 @@ def set_renderer(app: App, renderer: str): def set_win_version(app: App, exe: str, windows_version: str): if exe == "logos": - # XXX: This never exits run_winetricks(app, '-q', 'settings', f'{windows_version}') elif exe == "indexer": From d435dc29ba24a6015c39146c7aca99e990e483b3 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:53:30 -0800 Subject: [PATCH 106/137] fix: migrate legacy config and remove unused functions Tested migrating config from commit 0526030 and back, works as expected --- ou_dedetai/config.py | 72 ++++++++++++++++++++++++++++------------ ou_dedetai/constants.py | 5 +-- ou_dedetai/main.py | 16 --------- ou_dedetai/system.py | 2 +- ou_dedetai/tui_app.py | 4 +-- ou_dedetai/tui_screen.py | 12 ------- ou_dedetai/utils.py | 55 ------------------------------ 7 files changed, 57 insertions(+), 109 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 0fd761b8..3938fa36 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -19,7 +19,7 @@ class LegacyConfiguration: FLPRODUCT: Optional[str] = None TARGETVERSION: Optional[str] = None TARGET_RELEASE_VERSION: Optional[str] = None - current_logos_version: Optional[str] = None + current_logos_version: Optional[str] = None # Unused in new code curses_colors: Optional[str] = None INSTALLDIR: Optional[str] = None WINETRICKSBIN: Optional[str] = None @@ -28,9 +28,9 @@ class LegacyConfiguration: WINECMD_ENCODING: Optional[str] = None LOGS: Optional[str] = None BACKUPDIR: Optional[str] = None - LAST_UPDATED: Optional[str] = None - RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None - LLI_LATEST_VERSION: Optional[str] = None + LAST_UPDATED: Optional[str] = None # Unused in new code + RECOMMENDED_WINE64_APPIMAGE_URL: Optional[str] = None # Unused in new code + LLI_LATEST_VERSION: Optional[str] = None # Unused in new code logos_release_channel: Optional[str] = None lli_release_channel: Optional[str] = None @@ -42,10 +42,11 @@ class LegacyConfiguration: CUSTOMBINPATH: Optional[str] = None DEBUG: Optional[bool] = None DELETE_LOG: Optional[str] = None - DIALOG: Optional[str] = None + # XXX: Do we need to load this? For dialog detection? + DIALOG: Optional[str] = None # Unused in new code LOGOS_LOG: Optional[str] = None wine_log: Optional[str] = None - LOGOS_EXE: Optional[str] = None + LOGOS_EXE: Optional[str] = None # Unused in new code # This is the logos installer executable name (NOT path) LOGOS_EXECUTABLE: Optional[str] = None LOGOS_VERSION: Optional[str] = None @@ -67,7 +68,6 @@ class LegacyConfiguration: @classmethod def config_file_path(cls) -> str: - # XXX: consider legacy config files return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH @classmethod @@ -75,14 +75,19 @@ def load(cls) -> "LegacyConfiguration": """Find the relevant config file and load it""" # Update config from CONFIG_FILE. config_file_path = LegacyConfiguration.config_file_path() - if not utils.file_exists(config_file_path): # noqa: E501 + # This moves the first legacy config to the new location + if not utils.file_exists(config_file_path): for legacy_config in constants.LEGACY_CONFIG_FILES: if utils.file_exists(legacy_config): - return LegacyConfiguration.load_from_path(legacy_config) - else: + os.rename(legacy_config, config_file_path) + break + # This may be a config that used to be in the legacy location + # Now it's all in the same location + if utils.file_exists(config_file_path): return LegacyConfiguration.load_from_path(config_file_path) - logging.debug("Couldn't find config file, loading defaults...") - return LegacyConfiguration() + else: + logging.debug("Couldn't find config file, loading defaults...") + return LegacyConfiguration() @classmethod def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": @@ -305,17 +310,27 @@ class PersistentConfiguration: # The Installer's release channel. Either "stable" or "beta" app_release_channel: str = "stable" + _legacy: Optional[LegacyConfiguration] = None + """A Copy of the legacy configuration. + + Merge this when writing. + Kept just in case the user wants to go back to an older installer version + """ + @classmethod def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": - # XXX: handle legacy migration - # First read in the legacy configuration - new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_path(config_file_path)) #noqa: E501 + legacy = LegacyConfiguration.load_from_path(config_file_path) + new_config: PersistentConfiguration = PersistentConfiguration.from_legacy(legacy) #noqa: E501 new_keys = new_config.__dict__.keys() config_dict = new_config.__dict__ + # Check to see if this config is actually "legacy" + if len([k for k, v in legacy.__dict__.items() if v is not None]) > 1: + config_dict["_legacy"] = legacy + if config_file_path.endswith('.json') and Path(config_file_path).exists(): with open(config_file_path, 'r') as config_file: cfg = json.load(config_file) @@ -351,13 +366,26 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, winetricks_binary=winetricks_binary, - faithlife_product_logging=faithlife_product_logging + faithlife_product_logging=faithlife_product_logging, + _legacy=legacy ) def write_config(self) -> None: config_file_path = LegacyConfiguration.config_file_path() - # XXX: we may need to merge this dict with the legacy configuration's extended config (as we don't store that persistently anymore) #noqa: E501 + # Copy the values into a flat structure for easy json dumping output = copy.deepcopy(self.__dict__) + # Merge the legacy dictionary if present + if self._legacy is not None: + output |= self._legacy.__dict__ + + # Remove all keys starting with _ (to remove legacy from the saved blob) + for k in list(output.keys()): + if ( + k.startswith("_") + or output[k] is None + or k == "CONFIG_FILE" + ): + del output[k] logging.info(f"Writing config to {config_file_path}") os.makedirs(os.path.dirname(config_file_path), exist_ok=True) @@ -365,7 +393,7 @@ def write_config(self) -> None: if self.install_dir is not None: # Ensure all paths stored are relative to install_dir for k, v in output.items(): - if k == "install_dir": + if k in ["install_dir", "INSTALLDIR", "WINETRICKSBIN"]: if v is not None: output[k] = str(v) continue @@ -374,8 +402,10 @@ def write_config(self) -> None: try: with open(config_file_path, 'w') as config_file: - # XXX: would it be possible to avoid writing if this would fail? - json.dump(output, config_file, indent=4, sort_keys=True) + # Write this into a string first to avoid partial writes + # if encoding fails (which it shouldn't) + json_str = json.dumps(output, indent=4, sort_keys=True) + config_file.write(json_str) config_file.write('\n') except IOError as e: logging.error(f"Error writing to config file {config_file_path}: {e}") # noqa: E501 @@ -602,7 +632,7 @@ def winetricks_binary(self) -> str: self._raw.winetricks_binary is not None and not Path(self._absolute_from_install_dir(self._raw.winetricks_binary)).exists() #noqa: E501 ): - logging.warning("Given winetricks doesn't exist. Downloading from internet") + logging.info("Given winetricks doesn't exist. Downloading from internet") self._raw.winetricks_binary = None if ( diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index b4e11dbe..73e59c06 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -25,8 +25,9 @@ NETWORK_CACHE_PATH = os.path.expanduser("~/.cache/FaithLife-Community/network.json") # noqa: E501 DEFAULT_WINEDEBUG = "fixme-all,err-all" LEGACY_CONFIG_FILES = [ - os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), # noqa: E501 - os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") # noqa: E501 + os.path.expanduser("~/.config/FaithLife-Community/Logos_on_Linux.json"), + os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.json"), + os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") ] LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" LLI_CURRENT_VERSION = "4.0.0-beta.4" diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index c54534a8..3bc23ac1 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -339,20 +339,6 @@ def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfigura # Initialize logging. msg.initialize_logging(log_level, app_log_path) - # XXX: do this in the new scheme (read then write the config). - # We also want to remove the old file, (stored in CONFIG_FILE?) - - # # Update config from CONFIG_FILE. - # if not utils.file_exists(config.CONFIG_FILE): # noqa: E501 - # for legacy_config in constants.LEGACY_CONFIG_FILES: - # if utils.file_exists(legacy_config): - # config.set_config_env(legacy_config) - # utils.write_config(config.CONFIG_FILE) - # os.remove(legacy_config) - # break - # else: - # config.set_config_env(config.CONFIG_FILE) - # Parse CLI args and update affected config vars. return parse_args(cli_args, parser) @@ -412,8 +398,6 @@ def main(): ephemeral_config, action = setup_config() system.check_architecture() - # XXX: consider configuration migration from legacy to new - # NOTE: DELETE_LOG is an outlier here. It's an action, but it's one that # can be run in conjunction with other actions, so it gets special # treatment here once config is set. diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index aa0d1f39..a355daf0 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -141,7 +141,7 @@ def popen_command(command, retries=1, delay=0, **kwargs) -> Optional[subprocess. if isinstance(command, str) and not shell: command = command.split() - for attempt in range(retries): + for _ in range(retries): try: process = subprocess.Popen( command, diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index c860039d..ca5bb52c 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -355,7 +355,7 @@ def signal_resize(self, signum, frame): ): self.choice_q.put("Return to Main Menu") else: - if self.active_screen.get_screen_id == 14: + if self.active_screen.screen_id == 14: self.update_tty_dimensions() if self.window_height > 9: self.switch_q.put(1) @@ -408,7 +408,7 @@ def display(self): if self.choice_q.qsize() > 0: self.choice_processor( self.menu_window, - self.active_screen.get_screen_id(), + self.active_screen.screen_id, self.choice_q.get(), ) diff --git a/ou_dedetai/tui_screen.py b/ou_dedetai/tui_screen.py index e177ab01..1201b40a 100644 --- a/ou_dedetai/tui_screen.py +++ b/ou_dedetai/tui_screen.py @@ -42,12 +42,6 @@ def display(self): def get_stdscr(self) -> curses.window: return self.app.stdscr - def get_screen_id(self): - return self.screen_id - - def get_choice(self): - return self.choice - def wait_event(self): self.event.wait() @@ -454,9 +448,6 @@ def set_elements(self, elements): self.elements = elements self.updated = True - def get_text(self): - return self.text - class BuildListDialog(DialogScreen): def __init__(self, app, screen_id, queue, event, question, options, list_height=None, height=None, width=None): #noqa: E501 @@ -507,8 +498,5 @@ def display(self): self.width, self.list_height) self.running = 2 - def get_question(self): - return self.question - def set_options(self, new_options): self.options = new_options diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 437d5eea..87e020d3 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -161,26 +161,6 @@ def get_current_logos_version(install_dir: str) -> Optional[str]: return None -def convert_logos_release(logos_release): - if logos_release is not None: - ver_major = logos_release.split('.')[0] - ver_minor = logos_release.split('.')[1] - release = logos_release.split('.')[2] - point = logos_release.split('.')[3] - else: - ver_major = 0 - ver_minor = 0 - release = 0 - point = 0 - - logos_release_arr = [ - int(ver_major), - int(ver_minor), - int(release), - int(point), - ] - return logos_release_arr - def check_logos_release_version(version, threshold, check_version_part): if version is not None: version_parts = list(map(int, version.split('.'))) @@ -188,11 +168,6 @@ def check_logos_release_version(version, threshold, check_version_part): else: return False - -def filter_versions(versions, threshold, check_version_part): - return [version for version in versions if check_logos_release_version(version, threshold, check_version_part)] # noqa: E501 - - # FIXME: consider where we want this def get_winebin_code_and_desc(app: App, binary) -> Tuple[str, str | None]: """Gets the type of wine in use and it's description @@ -356,15 +331,6 @@ def get_folder_group_size(src_dirs: list[Path], q: queue.Queue[int]): q.put(src_size) -def get_copy_progress(dest_path, txfr_size, dest_size_init=0): - dest_size_now = get_path_size(dest_path) - if dest_size_now is None: - dest_size_now = 0 - size_diff = dest_size_now - dest_size_init - progress = round(size_diff / txfr_size * 100) - return progress - - def get_latest_folder(folder_path): folders = [f for f in Path(folder_path).glob('*')] if not folders: @@ -715,27 +681,6 @@ def get_relative_path(path: Path | str, base_path: str) -> str | Path: return path -def create_dynamic_path(path, base_path): - if is_relative_path(path): - if isinstance(path, str): - path = Path(path) - if isinstance(base_path, str): - base_path = Path(base_path) - logging.debug(f"dynamic_path: {base_path / path}") - return base_path / path - else: - logging.debug(f"dynamic_path: {Path(path)}") - return Path(path) - - -def get_config_var(var): - if var is not None: - if callable(var): - return var() - return var - else: - return None - def stopwatch(start_time=None, interval=10.0): if start_time is None: start_time = time.time() From 03ea2308ddeef4776206c76e087afeeae19d11fc Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:22:41 -0800 Subject: [PATCH 107/137] fix: load DIALOG from config --- ou_dedetai/config.py | 11 ++++++++--- ou_dedetai/main.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 3938fa36..1f63c60e 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -42,7 +42,6 @@ class LegacyConfiguration: CUSTOMBINPATH: Optional[str] = None DEBUG: Optional[bool] = None DELETE_LOG: Optional[str] = None - # XXX: Do we need to load this? For dialog detection? DIALOG: Optional[str] = None # Unused in new code LOGOS_LOG: Optional[str] = None wine_log: Optional[str] = None @@ -208,6 +207,10 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" + dialog: Optional[str] = None + """Override if we want to select a specific type of front-end + + Accepted values: tk (GUI), curses (TUI), cli (CLI)""" winetricks_args: Optional[list[str]] = None """Arguments to winetricks if the action is running winetricks""" @@ -264,7 +267,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": wine_appimage_link_file_name=legacy.APPIMAGE_LINK_SELECTION_NAME, wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, wine_output_encoding=legacy.WINECMD_ENCODING, - terminal_app_prefer_dialog=terminal_app_prefer_dialog + terminal_app_prefer_dialog=terminal_app_prefer_dialog, + dialog=legacy.DIALOG ) @classmethod @@ -815,7 +819,8 @@ def wine_appimage_path(self, value: Optional[str | Path]) -> None: self._overrides.wine_appimage_path = value # Reset dependents self._raw.wine_binary_code = None - # XXX: Should we save? There should be something here we should store + # NOTE: we don't save this persistently, it's assumed + # it'll be saved under wine_binary if it's used @property def wine_appimage_link_file_name(self) -> str: diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 3bc23ac1..a257ba28 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -304,7 +304,7 @@ def _run(config: EphemeralConfiguration): def run_control_panel(ephemeral_config: EphemeralConfiguration): - dialog = system.get_dialog() + dialog = ephemeral_config.dialog or system.get_dialog() logging.info(f"Using DIALOG: {dialog}") if dialog == 'tk': gui_app.control_panel_app(ephemeral_config) From a7c763483a53264e720582309206c6df37a55a0a Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:05:31 -0800 Subject: [PATCH 108/137] feat: add -y and -q flags Allows for non-interactive silent installs Useful in automation and reducing noise --- ou_dedetai/app.py | 15 +++++++++++++-- ou_dedetai/config.py | 8 ++++++++ ou_dedetai/main.py | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index ed750fd9..e0d7849c 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -79,7 +79,13 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: # Not valid return None - + + # Check to see if we're supposed to prompt the user + if self.conf._overrides.assume_yes: + # Get the first non-dynamic option + for option in options: + if option not in [PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE]: + return option passed_options: list[str] | str = options if len(passed_options) == 1 and ( @@ -181,6 +187,10 @@ def status(self, message: str, percent: Optional[int | float] = None): percent: Optional[int] - percent of the way through the current install step (if installing) """ + # Check to see if we want to suppress all output + if self.conf._overrides.quiet: + return + if isinstance(percent, float): percent = round(percent * 100) # If we're installing @@ -188,10 +198,11 @@ def status(self, message: str, percent: Optional[int | float] = None): current_step_percent = percent or 0 # We're further than the start of our current step, percent more installer_percent = round((self.installer_step * 100 + current_step_percent) / self.installer_step_count) # noqa: E501 - logging.debug(f"Install step {self.installer_step} of {self.installer_step_count}") # noqa: E501 + logging.debug(f"Install {installer_percent}: {message}") self._status(message, percent=installer_percent) else: # Otherwise just print status using the progress given + logging.debug(f"{message}: {percent}") self._status(message, percent) self._last_status = message diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 1f63c60e..b2f546bf 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -207,6 +207,14 @@ class EphemeralConfiguration: config_path: str """Path this config was loaded from""" + assume_yes: bool = False + """Whether to assume yes to all prompts or ask the user + + Useful for non-interactive installs""" + + quiet: bool = False + """Whether or not to output any non-error messages""" + dialog: Optional[str] = None """Override if we want to select a specific type of front-end diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index a257ba28..186ae345 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -84,6 +84,15 @@ def get_parser(): '-P', '--passive', action='store_true', help='run product installer non-interactively', ) + cfg.add_argument( + '-y', '--assume-yes', action='store_true', + help='Assumes yes (or default) to all prompts. ' + 'Useful for entirely non-interactive installs', + ) + cfg.add_argument( + '-q', '--quiet', action='store_true', + help='Suppress all non-error output', + ) # Define runtime actions (mutually exclusive). grp = parser.add_argument_group( @@ -200,6 +209,10 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera else: ephemeral_config = EphemeralConfiguration.load() + if args.quiet: + msg.update_log_level(logging.WARNING) + ephemeral_config.quiet = True + if args.verbose: msg.update_log_level(logging.INFO) @@ -243,7 +256,10 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera message = f"Custom binary path does not exist: \"{args.custom_binary_path}\"\n" # noqa: E501 parser.exit(status=1, message=message) - if args.passive: + if args.assume_yes: + ephemeral_config.assume_yes = True + + if args.passive or args.assume_yes: ephemeral_config.faithlife_install_passive = True From 6a0030a6d8bd88432be2a62cad1f5194f5c162d5 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:22:47 -0800 Subject: [PATCH 109/137] feat: added a reload function only really makes sense in the TUI, on the cli the process isn't up long enough to need this, and on the GUI we want to keep it as simple as possible I'm not sure how maintainable this will be, isn't it easy enough to restart the application? --- ou_dedetai/config.py | 10 +++++++++- ou_dedetai/main.py | 2 ++ ou_dedetai/tui_app.py | 28 ++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index b2f546bf..cc6925b8 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -532,7 +532,15 @@ def _absolute_from_install_dir(self, path: Path | str) -> str: return str(Path(self.install_dir) / path) return str(path) - # XXX: Add a reload command to resolve #168 (at least plumb the backend) + def reload(self): + """Re-loads the configuration file on disk""" + self._raw = PersistentConfiguration.load_from_path(self._overrides.config_path) + # Also clear out our cached values + self._logos_exe = self._download_dir = self._wine_output_encoding = None + self._installed_faithlife_product_release = self._wine_binary_files = None + self._wine_appimage_files = None + + self.app._config_updated_event.set() @property def config_file_path(self) -> str: diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 186ae345..7cd57f2d 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -84,6 +84,8 @@ def get_parser(): '-P', '--passive', action='store_true', help='run product installer non-interactively', ) + # XXX: consider if we want to keep --assume-yes and --quiet + # Don't want to support more than we'd use cfg.add_argument( '-y', '--assume-yes', action='store_true', help='Assumes yes (or default) to all prompts. ' diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index ca5bb52c..4eaf78f6 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -32,12 +32,7 @@ class TUI(App): def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfiguration): super().__init__(ephemeral_config) self.stdscr = stdscr - self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 - product_name = self.conf._raw.faithlife_product or "Logos" - if self.is_installed(): - self.subtitle = f"{product_name} Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 - else: - self.subtitle = f"{product_name} not installed" + self.set_title() # else: # self.title = f"Welcome to {constants.APP_NAME} ({constants.LLI_CURRENT_VERSION})" # noqa: E501 self.console_message = "Starting TUI…" @@ -134,7 +129,17 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() - self.config_updated_hooks += [self.set_curses_colors] + self.config_updated_hooks += [self._config_update_hook] + + def set_title(self): + self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 + product_name = self.conf._raw.faithlife_product or "Logos" + if self.is_installed(): + self.subtitle = f"{product_name} Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 + else: + self.subtitle = f"{product_name} not installed" + # Reset the console to force a re-draw + self._console = None @property def active_screen(self) -> tui_screen.Screen: @@ -639,6 +644,9 @@ def utilities_menu_select(self, choice): self.reset_screen() control.edit_file(self.conf.config_file_path) self.go_to_main_menu() + elif choice == "Reload Config": + self.conf.reload() + self.go_to_main_menu() elif choice == "Change Logos Release Channel": self.reset_screen() self.conf.toggle_faithlife_product_release_channel() @@ -815,6 +823,10 @@ def _status(self, message: str, percent: int | None = None): ) ) + def _config_update_hook(self): + self.set_curses_colors() + self.set_title() + # def get_password(self, dialog): # question = (f"Logos Linux Installer needs to run a command as root. " # f"Please provide your password to provide escalation privileges.") @@ -952,7 +964,7 @@ def set_utilities_menu_options(self): ] labels.extend(labels_catalog) - labels_utilities = ["Install Dependencies", "Edit Config"] + labels_utilities = ["Install Dependencies", "Edit Config", "Reload Config"] labels.extend(labels_utilities) if self.is_installed(): From 9f675c4623a7dda7636692a9590c54970fedc2b4 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:29:52 -0800 Subject: [PATCH 110/137] fix: resolve remaining todos --- ou_dedetai/config.py | 2 +- ou_dedetai/tui_app.py | 6 +++--- ou_dedetai/wine.py | 10 +++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index cc6925b8..c6f46359 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -767,7 +767,7 @@ def wine_binary(self, value: str): """Takes in a path to the wine binary and stores it as relative for storage""" # Make the path absolute for comparison relative = self._relative_from_install_dir(value) - # XXX: consider this, it doesn't work at present as the wine_binary may be an + # FIXME: consider this, it doesn't work at present as the wine_binary may be an # appimage that hasn't been downloaded yet # aboslute = self._absolute_from_install_dir(value) # if not Path(aboslute).is_file(): diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 4eaf78f6..833328cd 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -107,9 +107,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati # Before some function calls didn't pass use_python_dialog falling back to False # now it all respects use_python_dialog # some menus may open in dialog that didn't before. - # - # XXX: consider hard coding this to false for the time being - # Is there value in supportting both curses and dialog? self.use_python_dialog: bool = False if "dialog" in sys.modules and ephemeral_config.terminal_app_prefer_dialog is not False: #noqa: E501 result = system.test_dialog_version() @@ -125,6 +122,9 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati logging.error( "Dialog version is outdated. The program will fall back to Curses." ) # noqa: E501 + # FIXME: remove this hard-coding after considering whether we want to continue + # to support both + self.use_python_dialog = False logging.debug(f"Use Python Dialog?: {self.use_python_dialog}") self.set_window_dimensions() diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 47da55a8..4506c364 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -424,7 +424,15 @@ def set_win_version(app: App, exe: str, windows_version: str): system.wait_pid(process) -# XXX: consider when to run this (in the update case) +# FIXME: Consider when to re-run this if it changes. +# Perhaps we should have a "apply installation updates" +# or similar mechanism to ensure all of our latest methods are installed +# including but not limited to: system packages, winetricks options, +# icu files, fonts, registry edits, etc. +# +# Seems like we want to have a more holistic mechanism for ensuring +# all users use the latest and greatest. +# Sort of like an update, but for wine and all of the bits underneath "Logos" itself def enforce_icu_data_files(app: App): app.status("Downloading ICU files...") icu_url = app.conf.icu_latest_version_url From c0febe45d47803f8a9332f401bb8914df967db4f Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:36:16 -0800 Subject: [PATCH 111/137] fix: resolve a couple fixmes --- ou_dedetai/control.py | 9 ++++++--- ou_dedetai/installer.py | 2 +- ou_dedetai/tui_app.py | 1 - ou_dedetai/utils.py | 8 +------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 6dcd10a4..7f8a0d82 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -81,13 +81,16 @@ def backup_and_restore(mode: str, app: App): # Get source transfer size. q: queue.Queue[int] = queue.Queue() - app.status("Calculating backup size…") + message = "Calculating backup size…" + app.status(message) + i = 0 t = app.start_thread(utils.get_folder_group_size, src_dirs, q) try: while t.is_alive(): - # FIXME: consider showing a sign of life to the app + i += 1 + i = i % 20 + app.status(f"{message}{"." * i}\r") time.sleep(0.5) - print() except KeyboardInterrupt: print() app.exit("Cancelled with Ctrl+C.") diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 786cdf13..bb78595f 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -333,7 +333,7 @@ def ensure_launcher_shortcuts(app: App): app.status("Creating launcher shortcuts…") create_launcher_shortcuts(app) else: - # FIXME: Is this because it's hard to find the python binary? + # Speculation: Is this because it's hard to find the python binary? app.status( "Running from source. Skipping launcher creation.", ) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 833328cd..bef85ed8 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -695,7 +695,6 @@ def utilities_menu_select(self, choice): self.go_to_main_menu() def custom_appimage_select(self, choice: str): - # FIXME if choice == "Input Custom AppImage": appimage_filename = self.ask("Enter AppImage filename: ", [PROMPT_OPTION_FILE]) #noqa: E501 else: diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index 87e020d3..e2411e45 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -168,7 +168,7 @@ def check_logos_release_version(version, threshold, check_version_part): else: return False -# FIXME: consider where we want this + def get_winebin_code_and_desc(app: App, binary) -> Tuple[str, str | None]: """Gets the type of wine in use and it's description @@ -499,11 +499,6 @@ def find_appimage_files(app: App) -> list[str]: else: logging.info(f"AppImage file {p} not added: {output2}") - # FIXME: consider if this messaging is needed - # if app: - # app.appimage_q.put(appimages) - # app.root.event_generate(app.appimage_evt) - return appimages @@ -562,7 +557,6 @@ def set_appimage_symlink(app: App): appimage_symlink_path = appdir_bindir / app.conf.wine_appimage_link_file_name if appimage_file_path.name == app.conf.wine_appimage_recommended_file_name: # noqa: E501 # Default case. - # FIXME: consider other locations to enforce this, perhaps config? network.dwonload_recommended_appimage(app) selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_dir # noqa: E501 From e45f571aadaa6de5a52e6401b66d1d36c60e0de5 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:52:20 -0800 Subject: [PATCH 112/137] feat: add integration test eventually we'll want to migrate this into a larger framework, works for now Also fixed passing INSTALLDIR via ENV and closing the logos login window too and added the --stop-installed-app cli command Also fixed build-binary so it rebuilds properly --- ou_dedetai/app.py | 1 + ou_dedetai/cli.py | 20 ++-- ou_dedetai/config.py | 32 ++--- ou_dedetai/control.py | 2 +- ou_dedetai/logos.py | 53 ++++----- ou_dedetai/main.py | 9 +- ou_dedetai/tui_app.py | 2 + scripts/build-binary.sh | 4 + scripts/ensure-venv.sh | 6 +- tests/integration.py | 253 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 324 insertions(+), 58 deletions(-) create mode 100644 tests/integration.py diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index e0d7849c..e47e437a 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -206,6 +206,7 @@ def status(self, message: str, percent: Optional[int | float] = None): self._status(message, percent) self._last_status = message + @abc.abstractmethod def _status(self, message: str, percent: Optional[int] = None): """Implementation for updating status pre-front end diff --git a/ou_dedetai/cli.py b/ou_dedetai/cli.py index aa7bc5ae..dd0fbfd8 100644 --- a/ou_dedetai/cli.py +++ b/ou_dedetai/cli.py @@ -3,7 +3,6 @@ import threading from typing import Optional, Tuple -from ou_dedetai import constants from ou_dedetai.app import App from ou_dedetai.config import EphemeralConfiguration from ou_dedetai.system import SuperuserCommandNotFound @@ -22,6 +21,7 @@ def __init__(self, ephemeral_config: EphemeralConfiguration): self.input_q: queue.Queue[Tuple[str, list[str]] | None] = queue.Queue() self.input_event = threading.Event() self.choice_event = threading.Event() + self.start_thread(self.user_input_processor) def backup(self): control.backup(app=self) @@ -36,17 +36,8 @@ def get_winetricks(self): control.set_winetricks(self) def install_app(self): - def install(app: CLI): - installer.install(app) - app.exit("Install has finished", intended=True) - self.thread = threading.Thread( - name=f"{constants.APP_NAME} install", - target=install, - daemon=False, - args=[self] - ) - self.thread.start() - self.user_input_processor() + installer.install(self) + self.exit("Install has finished", intended=True) def install_d3d_compiler(self): wine.install_d3d_compiler(self) @@ -78,6 +69,9 @@ def run_indexing(self): def run_installed_app(self): self.logos.start() + def stop_installed_app(self): + self.logos.stop() + def run_winetricks(self): wine.run_winetricks(self) @@ -135,7 +129,7 @@ def _status(self, message: str, percent: Optional[int] = None): # Rather than sending a new one. This allows the current line to update prefix += "\r" end = "\r" - if percent: + if percent is not None: percent_per_char = 5 chars_of_progress = round(percent / percent_per_char) chars_remaining = round((100 - percent) / percent_per_char) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index c6f46359..28867330 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -67,7 +67,7 @@ class LegacyConfiguration: @classmethod def config_file_path(cls) -> str: - return os.getenv("CONFIG_PATH") or constants.DEFAULT_CONFIG_PATH + return os.getenv("CONFIG_FILE") or constants.DEFAULT_CONFIG_PATH @classmethod def load(cls) -> "LegacyConfiguration": @@ -82,20 +82,15 @@ def load(cls) -> "LegacyConfiguration": break # This may be a config that used to be in the legacy location # Now it's all in the same location - if utils.file_exists(config_file_path): - return LegacyConfiguration.load_from_path(config_file_path) - else: - logging.debug("Couldn't find config file, loading defaults...") - return LegacyConfiguration() + return LegacyConfiguration.load_from_path(config_file_path) @classmethod def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": config_dict = {} if not Path(config_file_path).exists(): - return LegacyConfiguration(CONFIG_FILE=config_file_path) - - if config_file_path.endswith('.json'): + pass + elif config_file_path.endswith('.json'): try: with open(config_file_path, 'r') as config_file: cfg = json.load(config_file) @@ -158,6 +153,7 @@ class EphemeralConfiguration: # Start user overridable via env or cli arg installer_binary_dir: Optional[str] + install_dir: Optional[str] wineserver_binary: Optional[str] faithlife_product_version: Optional[str] faithlife_installer_name: Optional[str] @@ -252,6 +248,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": terminal_app_prefer_dialog = None if legacy.use_python_dialog is not None: terminal_app_prefer_dialog = utils.parse_bool(legacy.use_python_dialog) + install_dir = None + if legacy.INSTALLDIR is not None: + install_dir = str(Path(os.path.expanduser(legacy.INSTALLDIR)).absolute()) return EphemeralConfiguration( installer_binary_dir=legacy.APPDIR_BINDIR, wineserver_binary=legacy.WINESERVER_EXE, @@ -276,7 +275,8 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "EphemeralConfiguration": wine_appimage_path=legacy.SELECTED_APPIMAGE_FILENAME, wine_output_encoding=legacy.WINECMD_ENCODING, terminal_app_prefer_dialog=terminal_app_prefer_dialog, - dialog=legacy.DIALOG + dialog=legacy.DIALOG, + install_dir=install_dir ) @classmethod @@ -366,6 +366,9 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": and legacy.WINETRICKSBIN != constants.DOWNLOAD ): winetricks_binary = legacy.WINETRICKSBIN + install_dir = None + if legacy.INSTALLDIR is not None: + install_dir = str(Path(os.path.expanduser(legacy.INSTALLDIR)).absolute()) return PersistentConfiguration( faithlife_product=legacy.FLPRODUCT, backup_dir=legacy.BACKUPDIR, @@ -373,7 +376,7 @@ def from_legacy(cls, legacy: LegacyConfiguration) -> "PersistentConfiguration": faithlife_product_release=legacy.TARGET_RELEASE_VERSION, faithlife_product_release_channel=legacy.logos_release_channel or 'stable', faithlife_product_version=legacy.TARGETVERSION, - install_dir=legacy.INSTALLDIR, + install_dir=install_dir, app_release_channel=legacy.lli_release_channel or 'stable', wine_binary=legacy.WINE_EXE, wine_binary_code=legacy.WINEBIN_CODE, @@ -576,7 +579,8 @@ def faithlife_product_version(self, value: Optional[str]): # Set dependents self._raw.faithlife_product_release = None # Install Dir has the name of the product and it's version. Reset it too - self._raw.install_dir = None + if self._overrides.install_dir is None: + self._raw.install_dir = None # Wine is dependent on the product/version selected self._raw.wine_binary = None self._raw.wine_binary_code = None @@ -716,6 +720,8 @@ def install_dir_default(self) -> str: @property def install_dir(self) -> str: + if self._overrides.install_dir: + return self._overrides.install_dir default = self.install_dir_default question = f"Where should {self.faithlife_product} files be installed to?: " # noqa: E501 options = [default, PROMPT_OPTION_DIRECTORY] @@ -724,7 +730,7 @@ def install_dir(self) -> str: @install_dir.setter def install_dir(self, value: str | Path): - value = str(value) + value = str(Path(value).absolute()) if self._raw.install_dir != value: self._raw.install_dir = value # Reset cache that depends on install_dir diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index 7f8a0d82..cd942c85 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -162,7 +162,7 @@ def remove_install_dir(app: App): return if app.approve(question): shutil.rmtree(folder) - logging.warning(f"Deleted folder and all its contents: {folder}") + logging.info(f"Deleted folder and all its contents: {folder}") def remove_all_index_files(app: App): diff --git a/ou_dedetai/logos.py b/ou_dedetai/logos.py index 621c34c2..4803ce28 100644 --- a/ou_dedetai/logos.py +++ b/ou_dedetai/logos.py @@ -74,8 +74,14 @@ def monitor_logos(self): 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) # noqa: E501 + 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\\Logos\\System\\Logos.exe" #noqa: E501 + self.existing_processes[logos_system_exe] = system.get_pids(logos_system_exe) # noqa: E501 if app.conf.logos_indexer_exe: self.existing_processes[app.conf.logos_indexer_exe] = system.get_pids(app.conf.logos_indexer_exe) # noqa: E501 if app.conf.logos_cef_exe: @@ -141,31 +147,26 @@ def run_logos(): def stop(self): logging.debug("Stopping LogosManager.") self.logos_state = State.STOPPING - if self.app: - pids = [] - for process_name in [ - self.app.conf.logos_exe, - self.app.conf.logos_login_exe, - self.app.conf.logos_cef_exe - ]: - if process_name is None: - continue - process = self.processes.get(process_name) - if process: - pids.append(str(process.pid)) - else: - logging.debug(f"No Logos processes found for {process_name}.") # noqa: E501 + if len(self.existing_processes) == 0: + self.get_logos_pids() - if pids: - try: - system.run_command(['kill', '-9'] + pids) - self.logos_state = State.STOPPED - logging.debug(f"Stopped Logos processes at PIDs {', '.join(pids)}.") # noqa: E501 - except Exception as e: - logging.debug(f"Error while stopping Logos processes: {e}.") # noqa: E501 - else: - logging.debug("No Logos processes to stop.") + pids: list[str] = [] + for processes in self.processes.values(): + pids.append(str(processes.pid)) + + for existing_processes in self.existing_processes.values(): + pids.extend(str(proc.pid) for proc in existing_processes) + + if pids: + try: + system.run_command(['kill', '-9'] + pids) self.logos_state = State.STOPPED + logging.debug(f"Stopped Logos processes at PIDs {', '.join(pids)}.") # noqa: E501 + except Exception as e: + logging.debug(f"Error while stopping Logos processes: {e}.") # noqa: E501 + else: + logging.debug("No Logos processes to stop.") + self.logos_state = State.STOPPED wine.wineserver_wait(self.app) def end_processes(self): @@ -194,11 +195,11 @@ def run_indexing(): if process is not None: self.processes[self.app.conf.logos_indexer_exe] = process - def check_if_indexing(process): + def check_if_indexing(process: threading.Thread): start_time = time.time() last_time = start_time update_send = 0 - while process.poll() is None: + while process.is_alive(): update, last_time = utils.stopwatch(last_time, 3) if update: update_send = update_send + 1 @@ -218,7 +219,7 @@ def wait_on_indexing(): wine.wineserver_wait(app=self.app) wine.wineserver_kill(self.app) - self.app.status("Indexing has begun…") + self.app.status("Indexing has begun…", 0) index_thread = self.app.start_thread(run_indexing, daemon_bool=False) self.indexing_state = State.RUNNING self.app.start_thread( diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 7cd57f2d..3fc94add 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -84,8 +84,6 @@ def get_parser(): '-P', '--passive', action='store_true', help='run product installer non-interactively', ) - # XXX: consider if we want to keep --assume-yes and --quiet - # Don't want to support more than we'd use cfg.add_argument( '-y', '--assume-yes', action='store_true', help='Assumes yes (or default) to all prompts. ' @@ -113,6 +111,11 @@ def get_parser(): '--run-installed-app', '-C', action='store_true', help='run installed FaithLife app', ) + # NOTE to reviewers: this function was added mostly for tests + cmd.add_argument( + '--stop-installed-app', action='store_true', + help='stop the installed FaithLife app if running', + ) cmd.add_argument( '--run-indexing', action='store_true', help='perform indexing', @@ -292,6 +295,7 @@ def _run(config: EphemeralConfiguration): 'restore', 'run_indexing', 'run_installed_app', + 'stop_installed_app', 'run_winetricks', 'set_appimage', 'toggle_app_logging', @@ -394,6 +398,7 @@ def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralCon 'restore', 'run_indexing', 'run_installed_app', + 'stop_installed_app', 'run_winetricks', 'set_appimage', 'toggle_app_logging', diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index bef85ed8..d837faf6 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -589,7 +589,9 @@ def winetricks_menu_select(self, choice): self.go_to_main_menu() elif choice == "Install d3dcompiler": self.reset_screen() + self.status("Installing d3dcompiler...") wine.install_d3d_compiler(self) + self.go_to_main_menu() elif choice == "Install Fonts": self.reset_screen() wine.install_fonts(self) diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh index 217cc15f..e7891a4c 100755 --- a/scripts/build-binary.sh +++ b/scripts/build-binary.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -e start_dir="$PWD" script_dir="$(dirname "$0")" repo_root="$(dirname "$script_dir")" @@ -7,5 +8,8 @@ if ! which pyinstaller >/dev/null 2>&1 || ! which oudedetai >/dev/null; then # Install build deps. python3 -m pip install .[build] fi +# Ensure the source in our python venv is up to date +python3 -m pip install . +# Build the installer binary pyinstaller --clean --log-level DEBUG ou_dedetai.spec cd "$start_dir" \ No newline at end of file diff --git a/scripts/ensure-venv.sh b/scripts/ensure-venv.sh index ec41e306..638d161f 100755 --- a/scripts/ensure-venv.sh +++ b/scripts/ensure-venv.sh @@ -38,7 +38,7 @@ echo "Virtual env setup as '${venv}/'. Activate with:" echo "source ${venv}/bin/activate" echo echo "Install runtime dependencies with:" -echo "pip install -r requirements.txt" +echo "pip install ." echo -echo "To build locally install pyinstaller with:" -echo "pip install pyinstaller" +echo "To build locally install:" +echo "pip install .[build]" diff --git a/tests/integration.py b/tests/integration.py new file mode 100644 index 00000000..2bef1a43 --- /dev/null +++ b/tests/integration.py @@ -0,0 +1,253 @@ +"""Basic implementations of some rudimentary tests + +Should be migrated into unittests once that branch is merged +""" +# FIXME: refactor into unittests + +from dataclasses import dataclass +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import time +from typing import Callable, Optional + +REPOSITORY_ROOT_PATH = Path(__file__).parent.parent + +@dataclass +class CommandFailedError(Exception): + """Command Failed to execute""" + command: list[str] + stdout: str + stderr: str + +class TestFailed(Exception): + pass + +def run_cmd(*args, **kwargs) -> subprocess.CompletedProcess[str]: + """Wrapper around subprocess.run that: + - captures stdin/stderr + - sets text mode + - checks returncode before returning + + All other args are passed through to subprocess.run + """ + if "stdout" not in kwargs: + kwargs["stdout"] = subprocess.PIPE + if "stderr" not in kwargs: + kwargs["stderr"] = subprocess.PIPE + kwargs["text"] = True + output = subprocess.run(*args, **kwargs) + try: + output.check_returncode() + except subprocess.CalledProcessError as e: + raise CommandFailedError( + command=args[0], + stderr=output.stderr, + stdout=output.stdout + ) from e + return output + +class OuDedetai: + _binary: Optional[str] = None + _temp_dir: Optional[str] = None + config: Optional[Path] = None + install_dir: Optional[Path] = None + log_level: str + """Log level. One of: + - quiet - warn+ - status + - normal - warn+ + - verbose - info+ + - debug - debug + """ + + + def __init__(self, isolate: bool = True, log_level: str = "quiet"): + if isolate: + self.isolate_files() + self.log_level = log_level + + def isolate_files(self): + if self._temp_dir is not None: + shutil.rmtree(self._temp_dir) + self._temp_dir = tempfile.mkdtemp() + self.config = Path(self._temp_dir) / "config.json" + self.install_dir = Path(self._temp_dir) / "install_dir" + + @classmethod + def _source_last_update(cls) -> float: + """Last updated time of any source code in seconds since epoch""" + path = REPOSITORY_ROOT_PATH / "ou_dedetai" + output: float = 0 + for root, _, files in os.walk(path): + for file in files: + file_m = os.stat(Path(root) / file).st_mtime + if file_m > output: + output = file_m + return output + + @classmethod + def _oudedetai_binary(cls) -> str: + """Return the path to the binary""" + output = REPOSITORY_ROOT_PATH / "dist" / "oudedetai" + # First check to see if we need to build. + # If either the file doesn't exist, or it was last modified earlier than + # the source code, rebuild. + if ( + not output.exists() + or cls._source_last_update() > os.stat(str(output)).st_mtime + ): + print("Building binary...") + if output.exists(): + os.remove(str(output)) + run_cmd(f"{REPOSITORY_ROOT_PATH / "scripts" / "build-binary.sh"}") + + if not output.exists(): + raise Exception("Build process failed to yield binary") + print("Built binary.") + + return str(output) + + def run(self, *args, **kwargs): + if self._binary is None: + self._binary = self._oudedetai_binary() + if "env" not in kwargs: + kwargs["env"] = {} + env: dict[str, str] = {} + if self.config: + env["CONFIG_FILE"] = str(self.config) + if self.install_dir: + env["INSTALLDIR"] = str(self.install_dir) + env["PATH"] = os.environ.get("PATH", "") + env["HOME"] = os.environ.get("HOME", "") + env["DISPLAY"] = os.environ.get("DISPLAY", "") + kwargs["env"] = env + log_level = "" + if self.log_level == "debug": + log_level = "--debug" + elif self.log_level == "verbose": + log_level = "--verbose" + elif self.log_level == "quiet": + log_level = "--quiet" + args = ([self._binary, log_level] + args[0], *args[1:]) + # FIXME: Output to both stdout and PIPE (for debugging these tests) + output = run_cmd(*args, **kwargs) + + # Output from the app indicates error/warning. Raise. + if output.stderr: + raise CommandFailedError( + args[0], + stdout=output.stdout, + stderr=output.stderr + ) + return output + + def clean(self): + if self.install_dir and self.install_dir.exists(): + shutil.rmtree(self.install_dir) + if self.config: + os.remove(self.config) + if self._temp_dir: + shutil.rmtree(self._temp_dir) + + +def wait_for_true(callable: Callable[[], Optional[bool]], timeout: int = 10) -> bool: + exception = None + start_time = time.time() + while time.time() - start_time < timeout: + try: + if callable(): + return True + except Exception as e: + exception = e + time.sleep(.1) + if exception: + raise exception + return False + + +def wait_for_window(window_name: str, timeout: int = 10): + """Waits for an Xorg window to open, raises exception if it doesn't""" + def _window_open(): + output = run_cmd(["xwininfo", "-tree", "-root"]) + if output.stderr: + raise Exception(f"xwininfo failed: {output.stdout}\n{output.stderr}") + if window_name not in output.stdout: + raise Exception(f"Could not find {window_name} in {output.stdout}") + return True + wait_for_true(_window_open, timeout=timeout) + + +def check_logos_open() -> None: + """Raises an exception if Logos isn't open""" + # Check with Xorg to see if there is a window running with the string logos.exe + wait_for_window("logos.exe") + + + +def test_run(ou_dedetai: OuDedetai): + ou_dedetai.run(["--stop-installed-app"]) + + # First launch Run the app. This assumes that logos is spawned before this completes + ou_dedetai.run(["--run-installed-app"]) + + wait_for_true(check_logos_open) + + ou_dedetai.run(["--stop-installed-app"]) + + +def test_install() -> OuDedetai: + ou_dedetai = OuDedetai(log_level="debug") + ou_dedetai.run(["--install-app", "--assume-yes"]) + + # To actually test the install we need to run it + test_run(ou_dedetai) + return ou_dedetai + + +def test_remove_install_dir(ou_dedetai: OuDedetai): + if ou_dedetai.install_dir is None: + raise ValueError("Can only test removing install dir on isolated install") + ou_dedetai.run(["--remove-install-dir", "--assume-yes"]) + if ou_dedetai.install_dir.exists(): + raise TestFailed("Installation directory exists after --remove-install-dir") + ou_dedetai.install_dir = None + + +def main(): + # FIXME: consider loop to run all of these in their supported distroboxes (https://distrobox.it/) + ou_dedetai = test_install() + test_remove_install_dir(ou_dedetai) + + ou_dedetai.clean() + + + # Untested: + # - run_indexing - Need to be logged in + # - edit-config - would need to modify EDITOR for this, not a lot of value + # --install-dependencies - would be easy enough to run this, but not a real test + # considering the machine the tests are running on probably already has it + # installed and it's already run in install-all + # --update-self - we might be able to fake it into thinking we're an older version + # --update-latest-appimage - we're already at latest as a result of install-app + # --install-* - already effectively tested as a result of install-app, may be + # difficult to confirm independently + # --set-appimage - just needs to be implemented + # --get-winetricks - no need to test independently, covered in install_app + # --run-winetricks - needs a way to cleanup after this spawns + # --toggle-app-logging - difficult to confirm + # --create-shortcuts - easy enough, unsure the use of this, shouldn't this already + # be done? Nothing in here should change? The user can always re-run the entire + # process if they want to do this + # --winetricks - unsure how'd we confirm it work + + # Final message + print("Tests passed.") + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + pass From b188f90bd8f8856c1d8837baef87589cf3627b30 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:42:33 -0800 Subject: [PATCH 113/137] fix: change color scheme --- ou_dedetai/tui_app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index d837faf6..15cd3945 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -509,6 +509,7 @@ def reset_screen(self): self.total_pages = 0 def go_to_main_menu(self): + self.reset_screen() self.menu_screen.choice = "Processing" self.choice_q.put("Return to Main Menu") @@ -530,12 +531,12 @@ def _install(): ) elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release(self) - elif choice == f"Run {self.conf.faithlife_product}": + elif self.conf._raw.faithlife_product and choice == f"Run {self.conf._raw.faithlife_product}": #noqa: E501 self.reset_screen() self.logos.start() self.menu_screen.set_options(self.set_tui_menu_options()) self.switch_q.put(1) - elif choice == f"Stop {self.conf.faithlife_product}": + elif self.conf._raw.faithlife_product and choice == f"Stop {self.conf.faithlife_product}": #noqa: E501 self.reset_screen() self.logos.stop() self.menu_screen.set_options(self.set_tui_menu_options()) @@ -575,7 +576,7 @@ def _install(): elif choice == "Change Color Scheme": self.status("Changing color scheme") self.conf.cycle_curses_color_scheme() - self.reset_screen() + self.go_to_main_menu() def winetricks_menu_select(self, choice): if choice == "Download or Update Winetricks": From 6e2e5dd035759ac00a4acf04640a9d1539bda31c Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:46:03 -0800 Subject: [PATCH 114/137] fix: console log de-dup --- ou_dedetai/tui_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 15cd3945..ffb0c1a2 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -813,6 +813,8 @@ def handle_ask_response(self, choice: str): def _status(self, message: str, percent: int | None = None): message = message.lstrip("\r") + if self.console_log[-1] == message: + return self.console_log.append(message) self.screen_q.put( self.stack_text( From fd44ae36d26b7d787c5bf4a00636040339267ad3 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:07:13 -0800 Subject: [PATCH 115/137] fix: de-dup wine binary options --- ou_dedetai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index e2411e45..e650763f 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -233,7 +233,7 @@ def get_wine_options(app: App) -> List[str]: # noqa: E501 # Create wine binary option array wine_binary_options.append(wine_binary_path) logging.debug(f"{wine_binary_options=}") - return wine_binary_options + return list(set(wine_binary_options)) def get_winetricks_options() -> list[str]: From 631f00f4078d394d5905d870c9903ea6a02c9be2 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:17:00 -0800 Subject: [PATCH 116/137] fix: handle case where install is started after going back to the menu Fixes the following bug: - Remove your config - Start an install - On one of the asks go back to main menu - Select install again - Choose an option - Then choose go to main menu again - Then choose install again - Then attempt to select an option Somewhere in this process before you would have seen "Your response is invalid" and if you were using a debugger you would have seen multiple install threads all stopped at different points (with different _asks) --- ou_dedetai/app.py | 18 ++++++++++++++---- ou_dedetai/constants.py | 2 ++ ou_dedetai/tui_app.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index e47e437a..efcd5713 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -8,7 +8,11 @@ from typing import Callable, NoReturn, Optional from ou_dedetai import constants -from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE +from ou_dedetai.constants import ( + PROMPT_ANSWER_COME_AGAIN, + PROMPT_OPTION_DIRECTORY, + PROMPT_OPTION_FILE +) class App(abc.ABC): @@ -63,6 +67,9 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: # This MUST have the same indexes as above simple_options_lower = [opt.lower() for opt in simple_options] + # Easiest case first + if answer == PROMPT_ANSWER_COME_AGAIN: + return None # Case sensitive check first if answer in simple_options: return answer @@ -99,9 +106,12 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: answer = self._ask(question, passed_options) while answer is None or validate_result(answer, options) is None: - invalid_response = "That response is not valid, please try again." - new_question = f"{invalid_response}\n{question}" - answer = self._ask(new_question, passed_options) + if answer == PROMPT_ANSWER_COME_AGAIN: + answer = self._ask(question, passed_options) + else: + invalid_response = "That response is not valid, please try again." + new_question = f"{invalid_response}\n{question}" + answer = self._ask(new_question, passed_options) if answer is not None: answer = validate_result(answer, options) diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 73e59c06..352e510a 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -45,6 +45,8 @@ # Strings for choosing a follow up file or directory PROMPT_OPTION_DIRECTORY = "Choose Directory" PROMPT_OPTION_FILE = "Choose File" +# Useful for signaling the app to ask the same question again. +PROMPT_ANSWER_COME_AGAIN = "Come Again? I didn't quite get that." # String for when a binary is meant to be downloaded later DOWNLOAD = "Download" diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index ffb0c1a2..823e1ea0 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -10,7 +10,11 @@ from typing import Any, Optional from ou_dedetai.app import App -from ou_dedetai.constants import PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE +from ou_dedetai.constants import ( + PROMPT_ANSWER_COME_AGAIN, + PROMPT_OPTION_DIRECTORY, + PROMPT_OPTION_FILE +) from ou_dedetai.config import EphemeralConfiguration from . import control @@ -70,6 +74,8 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.tui_screens: list[tui_screen.Screen] = [] self.menu_options: list[Any] = [] + self._installer_thread: Optional[threading.Thread] = None + # Default height and width to something reasonable so these values are always # ints, on each loop these values will be updated to their real values self.window_height = self.window_width = 80 @@ -483,6 +489,8 @@ def choice_processor(self, stdscr, screen_id, choice): # Capture menu exiting before processing in the rest of the handler if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): + if choice == "Return to Main Menu": + self.tui_screens = [] self.reset_screen() self.switch_q.put(1) # FIXME: There is some kind of graphical glitch that activates on returning @@ -515,8 +523,10 @@ def go_to_main_menu(self): def main_menu_select(self, choice): def _install(): + self.status("Installing...") installer.install(app=self) self.update_main_window_contents() + self.go_to_main_menu() if choice is None or choice == "Exit": logging.info("Exiting installation.") self.tui_screens = [] @@ -525,10 +535,17 @@ def _install(): self.reset_screen() self.installer_step = 0 self.installer_step_count = 0 - self.start_thread( - _install, - daemon_bool=True, - ) + if self._installer_thread is None: + self._installer_thread = self.start_thread( + _install, + daemon_bool=True, + ) + else: + # Looks like we returned to the main menu mid-install + # Tell the app to ask us that question again. + self.ask_answer_queue.put(PROMPT_ANSWER_COME_AGAIN) + self.ask_answer_event.set() + elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release(self) elif self.conf._raw.faithlife_product and choice == f"Run {self.conf._raw.faithlife_product}": #noqa: E501 @@ -779,12 +796,12 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: ) ) # noqa: E501 - # Now wait for it to complete + # Now wait for it to complete. self.ask_answer_event.wait() answer = self.ask_answer_queue.get() self.ask_answer_event.clear() - if answer == PROMPT_OPTION_DIRECTORY or answer == PROMPT_OPTION_FILE: + if answer in [PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE]: self.screen_q.put( self.stack_input( 2, @@ -809,7 +826,6 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: def handle_ask_response(self, choice: str): self.ask_answer_queue.put(choice) self.ask_answer_event.set() - self.switch_screen() def _status(self, message: str, percent: int | None = None): message = message.lstrip("\r") From 33bb556db237d634167b9fbc1aa837cd6cda55ea Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 5 Dec 2024 05:02:05 +0000 Subject: [PATCH 117/137] Update ou_dedetai/utils.py --- ou_dedetai/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index e650763f..c628aa8d 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -478,7 +478,6 @@ def find_appimage_files(app: App) -> list[str]: app.conf.installer_binary_dir, app.conf.download_dir ] - # FIXME: consider what we should do with this, promote to top level config? if app.conf._overrides.custom_binary_path is not None: directories.append(app.conf._overrides.custom_binary_path) From db4f1756dbdd8977781205b439ff566013a1af23 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:27:33 -0800 Subject: [PATCH 118/137] feat: handle tab completion if the user input is path like For now we can only complete as much as we know without ambiguity eventually maybe the other options should be rendered below --- ou_dedetai/config.py | 2 +- ou_dedetai/tui_app.py | 1 + ou_dedetai/tui_curses.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 28867330..2858165c 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -560,7 +560,7 @@ def faithlife_product(self, value: Optional[str]): if self._raw.faithlife_product != value: self._raw.faithlife_product = value # Reset dependent variables - self._raw.faithlife_product_release = None + self.faithlife_product_version = None # type: ignore[assignment] self._write() diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 823e1ea0..6f42a95e 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -541,6 +541,7 @@ def _install(): daemon_bool=True, ) else: + # XXX: consider how to reset choices # Looks like we returned to the main menu mid-install # Tell the app to ask us that question again. self.ask_answer_queue.put(PROMPT_ANSWER_COME_AGAIN) diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 739da951..5f634461 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -1,4 +1,6 @@ import curses +import os +from pathlib import Path import signal import textwrap @@ -160,6 +162,35 @@ def input(self): elif key == curses.KEY_BACKSPACE or key == 127: if len(self.user_input) > 0: self.user_input = self.user_input[:-1] + elif key == 9: # Tab + # Handle tab complete if the input is path life + if self.user_input.startswith("~"): + self.user_input = os.path.expanduser(self.user_input) + if self.user_input.startswith(os.path.sep): + path = Path(self.user_input) + dir_path = path.parent + if self.user_input.endswith(os.path.sep): + path_name = "" + dir_path = path + elif path.parent.exists(): + path_name = path.name + if dir_path.exists(): + options = os.listdir(dir_path) + options = [option for option in options if option.startswith(path_name)] #noqa: E501 + # Displaying all these options may be complicated, for now for + # now only display if there is only one option + if len(options) == 1: + self.user_input = options[0] + if Path(self.user_input).is_dir(): + self.user_input += os.path.sep + # Or see if all the options have the same prefix + common_chars = "" + for i in range(min([len(option) for option in options])): + # If all of the options are the same + if len(set([option[i] for option in options])) == 1: + common_chars += options[0][i] + if common_chars: + self.user_input = str(dir_path / common_chars) else: self.user_input += chr(key) except KeyboardInterrupt: From e8fed2ceca6dd3a330de0edc730e2b756337122e Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:44:21 -0800 Subject: [PATCH 119/137] fix: resolve review comments --- CHANGELOG.md | 3 +++ ou_dedetai/constants.py | 4 ++-- ou_dedetai/control.py | 8 ++++---- ou_dedetai/gui_app.py | 2 +- ou_dedetai/main.py | 2 +- ou_dedetai/network.py | 4 ++-- ou_dedetai/tui_app.py | 8 ++++---- ou_dedetai/tui_curses.py | 11 ++--------- ou_dedetai/wine.py | 7 ++----- scripts/ensure-python.sh | 4 ++-- tests/integration.py | 2 +- 11 files changed, 24 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d302bf29..d41d52a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +- 4.0.0-beta.5 + + - ??? - 4.0.0-beta.4 - Fix #220 [N. Shaaban] - 4.0.0-beta.3 diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 352e510a..eb2f970f 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -29,8 +29,8 @@ os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.json"), os.path.expanduser("~/.config/Logos_on_Linux/Logos_on_Linux.conf") ] -LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti" -LLI_CURRENT_VERSION = "4.0.0-beta.4" +LLI_AUTHOR = "Ferion11, John Goodman, T. H. Wright, N. Marti, N. Shaaban" +LLI_CURRENT_VERSION = "4.0.0-beta.5" DEFAULT_LOG_LEVEL = logging.WARNING LOGOS_BLUE = '#0082FF' LOGOS_GRAY = '#E7E7E7' diff --git a/ou_dedetai/control.py b/ou_dedetai/control.py index cd942c85..c476e09a 100644 --- a/ou_dedetai/control.py +++ b/ou_dedetai/control.py @@ -33,7 +33,7 @@ def restore(app: App): # for a more detailed progress bar # FIXME: consider moving this into it's own file/module. def backup_and_restore(mode: str, app: App): - app.status(f"Starting {mode}...") + app.status(f"Starting {mode}…") data_dirs = ['Data', 'Documents', 'Users'] backup_dir = Path(app.conf.backup_dir).expanduser().resolve() @@ -208,13 +208,13 @@ def set_winetricks(app: App): valid = True # Double check it's a valid winetricks if not Path(app.conf._winetricks_binary).exists(): - logging.warning("Winetricks path does not exist, downloading instead...") + logging.warning("Winetricks path does not exist, downloading instead…") valid = False if not os.access(app.conf._winetricks_binary, os.X_OK): - logging.warning("Winetricks path given is not executable, downloading instead...") #noqa: E501 + logging.warning("Winetricks path given is not executable, downloading instead…") #noqa: E501 valid = False if not utils.check_winetricks_version(app.conf._winetricks_binary): - logging.warning("Winetricks version mismatch, downloading instead...") + logging.warning("Winetricks version mismatch, downloading instead…") valid = False if valid: logging.info(f"Found valid winetricks: {app.conf._winetricks_binary}") diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index af2b2710..a8b066e5 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -499,7 +499,7 @@ def run_restore(self, evt=None): self.start_thread(control.restore, app=self) def install_deps(self, evt=None): - self.status("Installing dependencies...") + self.status("Installing dependencies…") self.start_thread(utils.install_dependencies, self) def open_file_dialog(self, filetype_name, filetype_extension): diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 3fc94add..0846e3f1 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -336,7 +336,7 @@ def run_control_panel(ephemeral_config: EphemeralConfiguration): except KeyboardInterrupt: raise except SystemExit: - logging.info("Caught SystemExit, exiting gracefully...") + logging.info("Caught SystemExit, exiting gracefully…") raise except curses.error as e: logging.error(f"Curses error in run_control_panel(): {e}") diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index dc71a84d..29d9612a 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -169,7 +169,7 @@ def load(cls) -> "CachedRequests": del output[k] return CachedRequests(**output) except json.JSONDecodeError: - logging.warning("Failed to read cache JSON. Clearing...") + logging.warning("Failed to read cache JSON. Clearing…") return CachedRequests( last_updated=time.time() ) @@ -194,7 +194,7 @@ def _is_fresh(self) -> bool: def clean_if_stale(self, force: bool = False): if force or not self._is_fresh(): - logging.debug("Cleaning out cache...") + logging.debug("Cleaning out cache…") self = CachedRequests(last_updated=time.time()) self._write() else: diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 6f42a95e..bcb7666d 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -523,7 +523,7 @@ def go_to_main_menu(self): def main_menu_select(self, choice): def _install(): - self.status("Installing...") + self.status("Installing…") installer.install(app=self) self.update_main_window_contents() self.go_to_main_menu() @@ -603,12 +603,12 @@ def winetricks_menu_select(self, choice): self.go_to_main_menu() elif choice == "Run Winetricks": self.reset_screen() - self.status("Running winetricks...") + self.status("Running winetricks…") wine.run_winetricks(self) self.go_to_main_menu() elif choice == "Install d3dcompiler": self.reset_screen() - self.status("Installing d3dcompiler...") + self.status("Installing d3dcompiler…") wine.install_d3d_compiler(self) self.go_to_main_menu() elif choice == "Install Fonts": @@ -749,7 +749,7 @@ def password_prompt(self, choice): def renderer_select(self, choice): if choice in ["gdi", "gl", "vulkan"]: self.reset_screen() - self.status(f"Changing renderer to {choice}.") + self.status(f"Changing renderer to {choice}.", 0) wine.set_renderer(self, choice) self.status(f"Changed renderer to {choice}.", 100) self.go_to_main_menu() diff --git a/ou_dedetai/tui_curses.py b/ou_dedetai/tui_curses.py index 5f634461..07f09132 100644 --- a/ou_dedetai/tui_curses.py +++ b/ou_dedetai/tui_curses.py @@ -8,9 +8,6 @@ from ou_dedetai.app import App -# NOTE to reviewer: does this convay the original meaning? -# The usages of the function seemed to have expected a list besides text_centered below -# Which handled the string case. Is it faithful to remove the string case? def wrap_text(app: App, text: str) -> list[str]: from ou_dedetai.tui_app import TUI if not isinstance(app, TUI): @@ -32,8 +29,8 @@ def write_line(app: App, stdscr: curses.window, start_y, start_x, text, char_lim try: stdscr.addnstr(start_y, start_x, text, char_limit, attributes) except curses.error: - # FIXME: what do we want to do to handle this error? - # Before we were registering a signal handler + # This may happen if we try to write beyond the screen limits + # May happen when the window is resized before we've handled it pass @@ -335,10 +332,6 @@ def input(self): self.user_input = self.options[self.app.current_option] elif key == ord('\x1b'): signal.signal(signal.SIGINT, self.app.end) - # FIXME: do we need to log this? - # else: - # logging.debug(f"Input unknown: {key}") - # pass except KeyboardInterrupt: signal.signal(signal.SIGINT, self.app.end) diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index 4506c364..e5c0c371 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -20,9 +20,6 @@ def check_wineserver(app: App): # (or at least kill it). Gotten into several states in dev where this happend # Normally when an msi install failed try: - # NOTE to reviewer: this used to be a non-existent key WINESERVER instead of - # WINESERVER_EXE changed it to use wineserver_binary, this change may alter the - # behavior, to match what the code intended process = run_wine_proc(app.conf.wineserver_binary, app, exe_args=["-p"]) if not process: logging.debug("Failed to spawn wineserver to check it") @@ -434,7 +431,7 @@ def set_win_version(app: App, exe: str, windows_version: str): # all users use the latest and greatest. # Sort of like an update, but for wine and all of the bits underneath "Logos" itself def enforce_icu_data_files(app: App): - app.status("Downloading ICU files...") + app.status("Downloading ICU files…") icu_url = app.conf.icu_latest_version_url icu_latest_version = app.conf.icu_latest_version @@ -448,7 +445,7 @@ def enforce_icu_data_files(app: App): app=app ) - app.status("Copying ICU files...") + app.status("Copying ICU files…") drive_c = f"{app.conf.wine_prefix}/drive_c" utils.untar_file(f"{app.conf.download_dir}/{icu_filename}", drive_c) diff --git a/scripts/ensure-python.sh b/scripts/ensure-python.sh index 367016d8..b702d49d 100755 --- a/scripts/ensure-python.sh +++ b/scripts/ensure-python.sh @@ -26,7 +26,7 @@ if [[ ${ans,,} != 'y' && $ans != '' ]]; then fi # Download and build python3.12 from source. -echo "Downloading $python_src..." +echo "Downloading $python_src…" wget "$python_src" if [[ -r "$tarxz" ]]; then tar xf "$tarxz" @@ -44,7 +44,7 @@ else fi # Install python. -echo "Installing..." +echo "Installing…" ./configure --enable-shared --prefix="$prefix" make sudo make install diff --git a/tests/integration.py b/tests/integration.py index 2bef1a43..2b9406fb 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -98,7 +98,7 @@ def _oudedetai_binary(cls) -> str: not output.exists() or cls._source_last_update() > os.stat(str(output)).st_mtime ): - print("Building binary...") + print("Building binary…") if output.exists(): os.remove(str(output)) run_cmd(f"{REPOSITORY_ROOT_PATH / "scripts" / "build-binary.sh"}") From 7f70380fa4776dc1c26d4e46f85276504874c37f Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:48:59 -0800 Subject: [PATCH 120/137] docs: update changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d41d52a8..621f2381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog - 4.0.0-beta.5 - - - ??? + - Implemented network cache [N. Shaaban] + - Refactored configuration handling (no user action required) [N. Shaaban] + - Fix #17 [T. H. Wright] + - Make dependency lists more precise [M. Marti, N. Shaaban] + - Fix #230 [N. Shaaban] - 4.0.0-beta.4 - Fix #220 [N. Shaaban] - 4.0.0-beta.3 From 0a02d4003d3fcd0cb6fc06a6a70fc2a7f96c1924 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:19:04 -0800 Subject: [PATCH 121/137] fix: reset user options when hitting install again for example in the case of: - install started - product selected - return to main menu - install started - before it would ask for the next question, now it resets --- ou_dedetai/app.py | 13 +++------- ou_dedetai/constants.py | 2 -- ou_dedetai/tui_app.py | 54 +++++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index efcd5713..3406a0dd 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -9,7 +9,6 @@ from ou_dedetai import constants from ou_dedetai.constants import ( - PROMPT_ANSWER_COME_AGAIN, PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE ) @@ -67,9 +66,6 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: # This MUST have the same indexes as above simple_options_lower = [opt.lower() for opt in simple_options] - # Easiest case first - if answer == PROMPT_ANSWER_COME_AGAIN: - return None # Case sensitive check first if answer in simple_options: return answer @@ -106,12 +102,9 @@ def validate_result(answer: str, options: list[str]) -> Optional[str]: answer = self._ask(question, passed_options) while answer is None or validate_result(answer, options) is None: - if answer == PROMPT_ANSWER_COME_AGAIN: - answer = self._ask(question, passed_options) - else: - invalid_response = "That response is not valid, please try again." - new_question = f"{invalid_response}\n{question}" - answer = self._ask(new_question, passed_options) + invalid_response = "That response is not valid, please try again." + new_question = f"{invalid_response}\n{question}" + answer = self._ask(new_question, passed_options) if answer is not None: answer = validate_result(answer, options) diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index eb2f970f..6dcefc2b 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -45,8 +45,6 @@ # Strings for choosing a follow up file or directory PROMPT_OPTION_DIRECTORY = "Choose Directory" PROMPT_OPTION_FILE = "Choose File" -# Useful for signaling the app to ask the same question again. -PROMPT_ANSWER_COME_AGAIN = "Come Again? I didn't quite get that." # String for when a binary is meant to be downloaded later DOWNLOAD = "Download" diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index bcb7666d..51ed17a4 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -11,7 +11,6 @@ from ou_dedetai.app import App from ou_dedetai.constants import ( - PROMPT_ANSWER_COME_AGAIN, PROMPT_OPTION_DIRECTORY, PROMPT_OPTION_FILE ) @@ -31,6 +30,13 @@ console_message = "" +class ReturningToMainMenu(Exception): + """Exception raised when user returns to the main menu + + effectively stopping execution on the executing thread where this exception + originated from""" + + # TODO: Fix hitting cancel in Dialog Screens; currently crashes program. class TUI(App): def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfiguration): @@ -63,6 +69,7 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.password_e = threading.Event() self.appimage_q: Queue[str] = Queue() self.appimage_e = threading.Event() + self._installer_thread: Optional[threading.Thread] = None self.terminal_margin = 0 self.resizing = False @@ -74,8 +81,6 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati self.tui_screens: list[tui_screen.Screen] = [] self.menu_options: list[Any] = [] - self._installer_thread: Optional[threading.Thread] = None - # Default height and width to something reasonable so these values are always # ints, on each loop these values will be updated to their real values self.window_height = self.window_width = 80 @@ -488,7 +493,7 @@ def choice_processor(self, stdscr, screen_id, choice): } # Capture menu exiting before processing in the rest of the handler - if screen_id != 0 and (choice == "Return to Main Menu" or choice == "Exit"): + if screen_id not in [0, 2] and (choice in ["Return to Main Menu", "Exit"]): if choice == "Return to Main Menu": self.tui_screens = [] self.reset_screen() @@ -523,10 +528,13 @@ def go_to_main_menu(self): def main_menu_select(self, choice): def _install(): - self.status("Installing…") - installer.install(app=self) - self.update_main_window_contents() - self.go_to_main_menu() + try: + self.status("Installing…") + installer.install(app=self) + self.update_main_window_contents() + self.go_to_main_menu() + except ReturningToMainMenu: + pass if choice is None or choice == "Exit": logging.info("Exiting installation.") self.tui_screens = [] @@ -535,17 +543,17 @@ def _install(): self.reset_screen() self.installer_step = 0 self.installer_step_count = 0 - if self._installer_thread is None: - self._installer_thread = self.start_thread( - _install, - daemon_bool=True, - ) - else: - # XXX: consider how to reset choices - # Looks like we returned to the main menu mid-install - # Tell the app to ask us that question again. - self.ask_answer_queue.put(PROMPT_ANSWER_COME_AGAIN) - self.ask_answer_event.set() + if self._installer_thread is not None: + # The install thread should have completed with ReturningToMainMenu + # Check just in case + if self._installer_thread.is_alive(): + raise Exception("Previous install is still running") + # Reset user choices and try again! + self.conf.faithlife_product = None # type: ignore[assignment] + self._installer_thread = self.start_thread( + _install, + daemon_bool=True, + ) elif choice.startswith(f"Update {constants.APP_NAME}"): utils.update_to_latest_lli_release(self) @@ -578,7 +586,6 @@ def _install(): self.set_winetricks_menu_options(), ) ) # noqa: E501 - self.choice_q.put("0") elif choice.startswith("Utilities"): self.reset_screen() self.screen_q.put( @@ -590,7 +597,6 @@ def _install(): self.set_utilities_menu_options(), ) ) # noqa: E501 - self.choice_q.put("0") elif choice == "Change Color Scheme": self.status("Changing color scheme") self.conf.cycle_curses_color_scheme() @@ -822,6 +828,12 @@ def _ask(self, question: str, options: list[str] | str) -> Optional[str]: answer = new_answer + if answer == self._exit_option: + self.tui_screens = [] + self.reset_screen() + self.switch_q.put(1) + raise ReturningToMainMenu + return answer def handle_ask_response(self, choice: str): From becb67e4213cc82eca17444568459c8edfc54bfb Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:22:00 -0800 Subject: [PATCH 122/137] fix: handle single appimage duplicate rather than reordering --- ou_dedetai/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index c628aa8d..c578e458 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -220,9 +220,12 @@ def get_wine_options(app: App) -> List[str]: # noqa: E501 logging.debug(f"{binaries=}") wine_binary_options = [] + reccomended_appimage = f"{app.conf.installer_binary_dir}/{app.conf.wine_appimage_recommended_file_name}" # noqa: E501 + # Add AppImages to list - wine_binary_options.append(f"{app.conf.installer_binary_dir}/{app.conf.wine_appimage_recommended_file_name}") # noqa: E501 wine_binary_options.extend(appimages) + if reccomended_appimage not in wine_binary_options: + wine_binary_options.append(reccomended_appimage) sorted_binaries = sorted(list(set(binaries))) logging.debug(f"{sorted_binaries=}") @@ -233,7 +236,7 @@ def get_wine_options(app: App) -> List[str]: # noqa: E501 # Create wine binary option array wine_binary_options.append(wine_binary_path) logging.debug(f"{wine_binary_options=}") - return list(set(wine_binary_options)) + return wine_binary_options def get_winetricks_options() -> list[str]: From d6bd94101648bc0401ec0ada753cbe3ca081374b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:23:05 -0800 Subject: [PATCH 123/137] docs: update comment as to why launcher shortcuts are skipped Co-authored-by: n8marti --- ou_dedetai/installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index bb78595f..450f71c3 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -333,9 +333,9 @@ def ensure_launcher_shortcuts(app: App): app.status("Creating launcher shortcuts…") create_launcher_shortcuts(app) else: - # Speculation: Is this because it's hard to find the python binary? + # This is done so devs can run this without it clobbering their install app.status( - "Running from source. Skipping launcher creation.", + "Running from source. Won't clobber your desktop shortcuts", ) def install(app: App): From 59ace4f2a41b6a9c798ddc86f342faf0c13709a1 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:39:42 -0800 Subject: [PATCH 124/137] fix: restore app logging want to initialize logging as early as possible on the offchance reading config fails --- ou_dedetai/main.py | 7 +++++-- ou_dedetai/msg.py | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 0846e3f1..8158a610 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -358,8 +358,10 @@ def setup_config() -> Tuple[EphemeralConfiguration, Callable[[EphemeralConfigura del temp # Set runtime config. - # Initialize logging. - msg.initialize_logging(log_level, app_log_path) + # Update log configuration. + msg.update_log_level(log_level) + msg.update_log_path(app_log_path) + # test = logging.getLogger().handlers # Parse CLI args and update affected config vars. return parse_args(cli_args, parser) @@ -418,6 +420,7 @@ def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralCon def main(): + msg.initialize_logging() ephemeral_config, action = setup_config() system.check_architecture() diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 30120ebe..8bdde79d 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -7,6 +7,8 @@ from pathlib import Path +from ou_dedetai import constants + class GzippedRotatingFileHandler(RotatingFileHandler): def doRollover(self): @@ -60,7 +62,7 @@ def get_log_level_name(level): return name -def initialize_logging(log_level: str | int, app_log_path: str): +def initialize_logging(): ''' Log levels: Level Value Description @@ -72,6 +74,7 @@ def initialize_logging(log_level: str | int, app_log_path: str): NOTSET 0 all events are handled ''' + app_log_path = constants.DEFAULT_APP_LOG_PATH # Ensure the application log's directory exists os.makedirs(os.path.dirname(app_log_path), exist_ok=True) @@ -80,8 +83,6 @@ def initialize_logging(log_level: str | int, app_log_path: str): if not log_parent.is_dir(): log_parent.mkdir(parents=True) - logging.debug(f"Installer log file: {app_log_path}") - # Define logging handlers. file_h = GzippedRotatingFileHandler( app_log_path, @@ -96,7 +97,7 @@ def initialize_logging(log_level: str | int, app_log_path: str): # stdout_h.setLevel(stdout_log_level) stderr_h = logging.StreamHandler(sys.stderr) stderr_h.name = "terminal" - stderr_h.setLevel(log_level) + stderr_h.setLevel(logging.WARN) stderr_h.addFilter(DeduplicateFilter()) handlers: list[logging.Handler] = [ file_h, @@ -111,6 +112,7 @@ def initialize_logging(log_level: str | int, app_log_path: str): datefmt='%Y-%m-%d %H:%M:%S', handlers=handlers, ) + logging.debug(f"Installer log file: {app_log_path}") def initialize_tui_logging(): @@ -121,10 +123,19 @@ def initialize_tui_logging(): break -def update_log_level(new_level): +def update_log_level(new_level: int | str): # Update logging level from config. for h in logging.getLogger().handlers: if type(h) is logging.StreamHandler: h.setLevel(new_level) logging.info(f"Terminal log level set to {get_log_level_name(new_level)}") + +def update_log_path(app_log_path: str | Path): + for h in logging.getLogger().handlers: + if type(h) is GzippedRotatingFileHandler and h.name == "logfile": + new_base_filename = os.path.abspath(os.fspath(app_log_path)) + if new_base_filename != h.baseFilename: + # One last message on the old log to let them know it moved + logging.debug(f"Installer log file changed to: {app_log_path}") + h.baseFilename = new_base_filename From 7fc7515a1e9ec55c42a203e77a8d2127a663275b Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:46 -0800 Subject: [PATCH 125/137] fix: logging --- ou_dedetai/main.py | 7 +------ ou_dedetai/msg.py | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 8158a610..a5e2da79 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -223,12 +223,6 @@ def parse_args(args, parser) -> Tuple[EphemeralConfiguration, Callable[[Ephemera if args.debug: msg.update_log_level(logging.DEBUG) - # Also add stdout for debugging purposes - stdout_h = logging.StreamHandler(sys.stdout) - stdout_h.name = "terminal" - stdout_h.setLevel(logging.DEBUG) - stdout_h.addFilter(msg.DeduplicateFilter()) - logging.root.addHandler(stdout_h) if args.delete_log: ephemeral_config.delete_log = True @@ -421,6 +415,7 @@ def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralCon def main(): msg.initialize_logging() + handlers = logging.getLogger().handlers ephemeral_config, action = setup_config() system.check_architecture() diff --git a/ou_dedetai/msg.py b/ou_dedetai/msg.py index 8bdde79d..3bd72d56 100644 --- a/ou_dedetai/msg.py +++ b/ou_dedetai/msg.py @@ -93,6 +93,8 @@ def initialize_logging(): file_h.name = "logfile" file_h.setLevel(logging.DEBUG) file_h.addFilter(DeduplicateFilter()) + # FIXME: Consider adding stdout that displays INFO/DEBUG (if configured) + # and edit stderr to only display WARN/ERROR/CRITICAL # stdout_h = logging.StreamHandler(sys.stdout) # stdout_h.setLevel(stdout_log_level) stderr_h = logging.StreamHandler(sys.stderr) From 671d56315800e2009428e13ee57ea312558ad418 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:28:02 -0800 Subject: [PATCH 126/137] chore: more descriptive message when file config not found --- ou_dedetai/config.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 2858165c..bb47568a 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -343,15 +343,18 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": if len([k for k, v in legacy.__dict__.items() if v is not None]) > 1: config_dict["_legacy"] = legacy - if config_file_path.endswith('.json') and Path(config_file_path).exists(): - with open(config_file_path, 'r') as config_file: - cfg = json.load(config_file) + if Path(config_file_path).exists(): + if config_file_path.endswith('.json'): + with open(config_file_path, 'r') as config_file: + cfg = json.load(config_file) - for key, value in cfg.items(): - if key in new_keys: - config_dict[key] = value + for key, value in cfg.items(): + if key in new_keys: + config_dict[key] = value + else: + logging.info("Not reading new values from non-json config") else: - logging.info("Not reading new values from non-json config") + logging.info("Not reading new values from non-existant config") return PersistentConfiguration(**config_dict) From 6dd127bd1e9f6817b922024ccf67d7ae333df367 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:31:42 -0800 Subject: [PATCH 127/137] chore: remove unused code --- ou_dedetai/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index a5e2da79..97015f9a 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -415,7 +415,6 @@ def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralCon def main(): msg.initialize_logging() - handlers = logging.getLogger().handlers ephemeral_config, action = setup_config() system.check_architecture() From 4819bb4d6ad91974a14017cef957673b7198f931 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:14:01 -0800 Subject: [PATCH 128/137] feat: add a install button that doesn't prompt in GUI and moved faithlife products/versions to a constants --- ou_dedetai/config.py | 4 +- ou_dedetai/constants.py | 3 ++ ou_dedetai/gui.py | 11 ++++- ou_dedetai/gui_app.py | 102 +++++++++++++++++++++++++--------------- ou_dedetai/installer.py | 2 + ou_dedetai/tui_app.py | 3 +- 6 files changed, 82 insertions(+), 43 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index bb47568a..8fdffbf7 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -555,7 +555,7 @@ def config_file_path(self) -> str: @property def faithlife_product(self) -> str: question = "Choose which FaithLife product the script should install: " # noqa: E501 - options = ["Logos", "Verbum"] + options = constants.FAITHLIFE_PRODUCTS return self._ask_if_not_found("faithlife_product", question, options, ["faithlife_product_version", "faithlife_product_release"]) # noqa: E501 @faithlife_product.setter @@ -572,7 +572,7 @@ def faithlife_product_version(self) -> str: if self._overrides.faithlife_product_version is not None: return self._overrides.faithlife_product_version question = f"Which version of {self.faithlife_product} should the script install?: " # noqa: E501 - options = ["10", "9"] + options = constants.FAITHLIFE_PRODUCT_VERSIONS return self._ask_if_not_found("faithlife_product_version", question, options, []) # noqa: E501 @faithlife_product_version.setter diff --git a/ou_dedetai/constants.py b/ou_dedetai/constants.py index 6dcefc2b..a36f5c88 100644 --- a/ou_dedetai/constants.py +++ b/ou_dedetai/constants.py @@ -40,6 +40,9 @@ PID_FILE = f'/tmp/{BINARY_NAME}.pid' WINETRICKS_VERSION = '20220411' +FAITHLIFE_PRODUCTS = ["Logos", "Verbum"] +FAITHLIFE_PRODUCT_VERSIONS = ["10", "9"] + SUPPORT_MESSAGE = f"If you need help, please consult:\n{WIKI_LINK}\nIf that doesn't answer your question, please send the following files {DEFAULT_CONFIG_PATH}, {DEFAULT_APP_WINE_LOG_PATH} and {DEFAULT_APP_LOG_PATH} to one of the following group chats:\nTelegram: {TELEGRAM_LINK}\nMatrix: {MATRIX_LINK}" # noqa: E501 # Strings for choosing a follow up file or directory diff --git a/ou_dedetai/gui.py b/ou_dedetai/gui.py index 4acebde1..e8957b3c 100644 --- a/ou_dedetai/gui.py +++ b/ou_dedetai/gui.py @@ -167,6 +167,10 @@ def __init__(self, root, *args, **kwargs): self.app_label = Label(self, text="FaithLife app") self.app_button = Button(self, textvariable=self.app_buttonvar) + self.app_install_advancedvar = StringVar() + self.app_install_advancedvar.set("Advanced Install") + self.app_install_advanced = Button(self, textvariable=self.app_install_advancedvar) #noqa: E501 + # Installed app actions # -> Run indexing, Remove library catalog, Remove all index files s1 = Separator(self, orient='horizontal') @@ -210,7 +214,9 @@ def __init__(self, root, *args, **kwargs): self.backups_label = Label(self, text="Backup/restore data") self.backup_button = Button(self, text="Backup") self.restore_button = Button(self, text="Restore") - self.update_lli_label = Label(self, text=f"Update {constants.APP_NAME}") # noqa: E501 + # The normal text has three lines. Make this the same + # in order for tkinker to know how large to draw it + self.update_lli_label = Label(self, text=f"Update {constants.APP_NAME}\n\n") # noqa: E501 self.update_lli_button = Button(self, text="Update") # AppImage buttons self.latest_appimage_label = Label( @@ -249,6 +255,7 @@ def __init__(self, root, *args, **kwargs): row = 0 self.app_label.grid(column=0, row=row, sticky='w', pady=2) self.app_button.grid(column=1, row=row, sticky='w', pady=2) + self.show_advanced_install_button() row += 1 s1.grid(column=0, row=1, columnspan=3, sticky='we', pady=2) row += 1 @@ -296,6 +303,8 @@ def __init__(self, root, *args, **kwargs): row += 1 self.progress.grid(column=0, row=row, columnspan=3, sticky='we', pady=2) # noqa: E501 + def show_advanced_install_button(self): + self.app_install_advanced.grid(column=2, row=0, sticky='w', pady=2) class ToolTip: def __init__(self, widget, text): diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index a8b066e5..d45b1391 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -88,6 +88,35 @@ def superuser_command(self) -> str: return "pkexec" else: raise system.SuperuserCommandNotFound("No superuser command found. Please install pkexec.") # noqa: E501 + + def populate_defaults(self) -> None: + """If any prompt is unset, set it to it's default value + + Useful for startign the UI at an installable state, + the user can change these choices later""" + + # For the GUI, use defaults until user says otherwise. + # XXX: move these to constants + if self.conf._raw.faithlife_product is None: + self.conf.faithlife_product = constants.FAITHLIFE_PRODUCTS[0] + if self.conf._raw.faithlife_product_version is None: + self.conf.faithlife_product_version = constants.FAITHLIFE_PRODUCT_VERSIONS[0] #noqa: E501 + + # Now that we know product and version are set we can download the releases + # And use the first one + # This will delay the UI from loading, but will be more robust if it's done in a + # blocking manner like it is + if self.conf._raw.faithlife_product_release is None: + self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 + + # Set the install_dir to default, no option in the GUI to change it + if self.conf._raw.install_dir is None: + self.conf.install_dir = self.conf.install_dir_default + + if self.conf._raw.wine_binary is None: + wine_choices = utils.get_wine_options(self) + if len(wine_choices) > 0: + self.conf.wine_binary = wine_choices[0] class Root(Tk): def __init__(self, *args, **kwargs): @@ -237,57 +266,37 @@ def __init__(self, new_win, root: Root, app: "ControlWindow", **kwargs): self.get_winetricks_options() self.app.config_updated_hooks += [self._config_updated_hook] + # Ensure default choices are set so user isn't App.ask'ed + self.app.populate_defaults() # Start out enforcing this self._config_updated_hook() def _config_updated_hook(self): """Update the GUI to reflect changes in the configuration/network""" #noqa: E501 - # The product/version dropdown values are static, they will always be populated + self.app.populate_defaults() - # Tor the GUI, use defaults until user says otherwise. - if self.conf._raw.faithlife_product is None: - self.conf.faithlife_product = self.gui.product_dropdown['values'][0] + # Fill in the UI elements from the config self.gui.productvar.set(self.conf.faithlife_product) - if self.conf._raw.faithlife_product_version is None: - self.conf.faithlife_product_version = self.gui.version_dropdown['values'][-1] #noqa :E501 self.gui.versionvar.set(self.conf.faithlife_product_version) # noqa: E501 # Now that we know product and version are set we can download the releases - # And use the first one - # FIXME: consider what to do if network is slow, we may want to do this on a - # Separate thread to not hang the UI self.gui.release_dropdown['values'] = self.conf.faithlife_product_releases - if self.conf._raw.faithlife_product_release is None: - self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 self.gui.releasevar.set(self.conf.faithlife_product_release) - # Set the install_dir to default, no option in the GUI to change it - if self.conf._raw.install_dir is None: - self.conf.install_dir = self.conf.install_dir_default - self.gui.skipdepsvar.set(not self.conf.skip_install_system_dependencies) self.gui.fontsvar.set(not self.conf.skip_install_fonts) - # Returns either wine_binary if set, or self.gui.wine_dropdown['values'] if it has a value, otherwise '' #noqa: E501 - wine_binary: Optional[str] = self.conf._raw.wine_binary - if wine_binary is None: - if len(self.gui.wine_dropdown['values']) > 0: - wine_binary = self.gui.wine_dropdown['values'][0] - self.app.conf.wine_binary = wine_binary - else: - wine_binary = '' - - self.gui.winevar.set(wine_binary) # In case the product changes self.root.icon = Path(self.conf.faithlife_product_icon_path) - wine_choices = utils.get_wine_options(self.app) - self.gui.wine_dropdown['values'] = wine_choices + self.gui.wine_dropdown['values'] = utils.get_wine_options(self.app) if not self.gui.winevar.get(): # If no value selected, default to 1st item in list. self.gui.winevar.set(self.gui.wine_dropdown['values'][0]) + self.gui.winevar.set(self.conf._raw.wine_binary or '') + # At this point all variables are populated, we're ready to install! self.set_input_widgets_state('enabled', [self.gui.okay_button]) @@ -361,25 +370,24 @@ def on_okay_released(self, evt=None): # Update desktop panel icon. self.start_install_thread() - def on_cancel_released(self, evt=None): + def close(self): self.app.config_updated_hooks.remove(self._config_updated_hook) # Reset status self.app.clear_status() self.win.destroy() + + def on_cancel_released(self, evt=None): + self.app.clear_status() + self.close() return 1 def start_install_thread(self, evt=None): def _install(): """Function to handle the install""" + # Close the options window and let the install run + self.close() installer.install(self.app) # Install complete, cleaning up... - self.app._status("", 0) - self.gui.okay_button.config( - text="Exit", - command=self.on_cancel_released, - ) - self.gui.okay_button.state(['!disabled']) - self.win.destroy() return 0 # Setup for the install @@ -399,9 +407,8 @@ def __init__(self, root, control_gui: gui.ControlGui, self.gui = control_gui self.actioncmd: Optional[Callable[[], None]] = None - text = self.gui.update_lli_label.cget('text') ver = constants.LLI_CURRENT_VERSION - text = f"{text}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" + text = f"Update {constants.APP_NAME}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" #noqa: E501 self.gui.update_lli_label.config(text=text) self.gui.run_indexing_radio.config( command=self.on_action_radio_clicked @@ -446,6 +453,22 @@ def __init__(self, root, control_gui: gui.ControlGui, def edit_config(self): control.edit_file(self.conf.config_file_path) + def run_install(self, evt=None): + """Directly install the product. + + Fallback to defaults if we don't know a response""" + def _install(): + self.populate_defaults() + installer.install(self) + # Enable the run button + self.gui.app_button.state(['!disabled']) + self.update_app_button() + # Disable the install buttons + self.gui.app_button.state(['disabled']) + self.gui.app_install_advanced.state(['disabled']) + # Start the install thread. + self.start_thread(_install) + def run_installer(self, evt=None): classname = constants.BINARY_NAME installer_window_top = Toplevel() @@ -589,8 +612,11 @@ def update_app_button(self, evt=None): self.gui.app_button.config(command=self.run_logos) self.gui.get_winetricks_button.state(['!disabled']) self.gui.logging_button.state(['!disabled']) + self.gui.app_install_advanced.grid_forget() else: - self.gui.app_button.config(command=self.run_installer) + self.gui.app_button.config(command=self.run_install) + self.gui.app_install_advanced.config(command=self.run_installer) + self.gui.show_advanced_install_button() def update_latest_lli_release_button(self, evt=None): msg = None diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 450f71c3..e96b118d 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -340,7 +340,9 @@ def ensure_launcher_shortcuts(app: App): def install(app: App): """Entrypoint for installing""" + app.status('Installing…') ensure_launcher_shortcuts(app) + app.status("Install Complete!", 100) def get_progress_pct(current, total): diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 51ed17a4..4f3b7966 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -144,7 +144,7 @@ def __init__(self, stdscr: curses.window, ephemeral_config: EphemeralConfigurati def set_title(self): self.title = f"Welcome to {constants.APP_NAME} {constants.LLI_CURRENT_VERSION} ({self.conf.app_release_channel})" # noqa: E501 - product_name = self.conf._raw.faithlife_product or "Logos" + product_name = self.conf._raw.faithlife_product or constants.FAITHLIFE_PRODUCTS[0] #noqa: E501 if self.is_installed(): self.subtitle = f"{product_name} Version: {self.conf.installed_faithlife_product_release} ({self.conf.faithlife_product_release_channel})" # noqa: E501 else: @@ -529,7 +529,6 @@ def go_to_main_menu(self): def main_menu_select(self, choice): def _install(): try: - self.status("Installing…") installer.install(app=self) self.update_main_window_contents() self.go_to_main_menu() From 8a9a52ed086f12384a6479b426f9e4206357a599 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:48:10 -0800 Subject: [PATCH 129/137] fix: support env overriding Also properly parse legacy ENVs as bools Also dump other configs --- ou_dedetai/config.py | 58 ++++++++++++++++++++++++++++++++++++++------ ou_dedetai/main.py | 2 +- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 8fdffbf7..92c03bb8 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -65,6 +65,18 @@ class LegacyConfiguration: WINESERVER_EXE: Optional[str] = None WINETRICKS_UNATTENDED: Optional[str] = None + @classmethod + def bool_keys(cls) -> list[str]: + """Returns a list of keys that are of type bool""" + return [ + "VERBOSE", + "SKIP_WINETRICKS", + "SKIP_FONTS", + "SKIP_DEPENDENCIES", + "DEBUG", + "CHECK_UPDATES" + ] + @classmethod def config_file_path(cls) -> str: return os.getenv("CONFIG_FILE") or constants.DEFAULT_CONFIG_PATH @@ -86,7 +98,7 @@ def load(cls) -> "LegacyConfiguration": @classmethod def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": - config_dict = {} + config_dict: dict[str, str] = {} if not Path(config_file_path).exists(): pass @@ -126,19 +138,36 @@ def load_from_path(cls, config_file_path: str) -> "LegacyConfiguration": config_dict[parts[0]] = value # Now restrict the key values pairs to just those found in LegacyConfiguration - output = {} + output: dict = {} + config_env = LegacyConfiguration.load_from_env().__dict__ # Now update from ENV - for var in LegacyConfiguration().__dict__.keys(): - if os.getenv(var) is not None: - config_dict[var] = os.getenv(var) - if var in config_dict: - output[var] = config_dict[var] + for var, env_var in config_env.items(): + if env_var is not None: + output[var] = env_var + elif var in config_dict: + if var in LegacyConfiguration.bool_keys(): + output[var] = utils.parse_bool(config_dict[var]) + else: + output[var] = config_dict[var] # Populate the path this config was loaded from output["CONFIG_FILE"] = config_file_path return LegacyConfiguration(**output) + @classmethod + def load_from_env(cls) -> "LegacyConfiguration": + output: dict = {} + # Now update from ENV + for var in LegacyConfiguration().__dict__.keys(): + env_var = os.getenv(var) + if env_var is not None: + if var in LegacyConfiguration.bool_keys(): + output[var] = utils.parse_bool(env_var) + else: + output[var] = env_var + return LegacyConfiguration(**output) + @dataclass class EphemeralConfiguration: @@ -354,7 +383,13 @@ def load_from_path(cls, config_file_path: str) -> "PersistentConfiguration": else: logging.info("Not reading new values from non-json config") else: - logging.info("Not reading new values from non-existant config") + logging.info("Not reading new values from non-existent config") + + # Now override with values from ENV + config_env = PersistentConfiguration.from_legacy(LegacyConfiguration.load_from_env()) #noqa: E501 + for k, v in config_env.__dict__.items(): + if v is not None: + config_dict[k] = v return PersistentConfiguration(**config_dict) @@ -488,6 +523,13 @@ def __init__(self, ephemeral_config: EphemeralConfiguration, app) -> None: logging.debug("Current persistent config:") for k, v in self._raw.__dict__.items(): logging.debug(f"{k}: {v}") + logging.debug("Current ephemeral config:") + for k, v in self._overrides.__dict__.items(): + logging.debug(f"{k}: {v}") + logging.debug("Current network cache:") + for k, v in self._network._cache.__dict__.items(): + logging.debug(f"{k}: {v}") + logging.debug("End config dump") def _ask_if_not_found(self, parameter: str, question: str, options: list[str], dependent_parameters: Optional[list[str]] = None) -> str: #noqa: E501 if not getattr(self._raw, parameter): diff --git a/ou_dedetai/main.py b/ou_dedetai/main.py index 97015f9a..427d3765 100755 --- a/ou_dedetai/main.py +++ b/ou_dedetai/main.py @@ -409,7 +409,7 @@ def run(ephemeral_config: EphemeralConfiguration, action: Callable[[EphemeralCon logging.info(f"Running function: {action.__name__}") # noqa: E501 action(ephemeral_config) else: # install_required, but app not installed - print("App not installed, but required for this operation. Consider installing first.", file=sys.stderr) #noqa: E501 + print("App is not installed, but required for this operation. Consider installing first.", file=sys.stderr) #noqa: E501 sys.exit(1) From 99d724361355a0f06aa164ad0192be9886b58f46 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:54:58 -0800 Subject: [PATCH 130/137] chore: fix spelling --- ou_dedetai/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index e96b118d..9f04c15c 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -238,7 +238,7 @@ def ensure_winetricks_applied(app: App): wine.set_renderer(app, "gdi") if not utils.grep(r'"FontSmoothingType"=dword:00000002', usr_reg): - app.status("Setting Font Smooting to RGB…") + app.status("Setting Font Smoothing to RGB…") wine.install_font_smoothing(app) if not app.conf.skip_install_fonts and not utils.grep(r'"Tahoma \(TrueType\)"="tahoma.ttf"', sys_reg): # noqa: E501 From 9d3b99a1a465163e8659a78302dbcac603e01f25 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:57:54 -0800 Subject: [PATCH 131/137] fix: optional pid --- ou_dedetai/system.py | 5 +++-- ou_dedetai/wine.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ou_dedetai/system.py b/ou_dedetai/system.py index a355daf0..2bcaa039 100644 --- a/ou_dedetai/system.py +++ b/ou_dedetai/system.py @@ -906,8 +906,9 @@ def install_winetricks( logging.debug("Winetricks installed.") return winetricks_path -def wait_pid(process: subprocess.Popen): - os.waitpid(-process.pid, 0) +def wait_pid(process: Optional[subprocess.Popen]): + if process is not None: + os.waitpid(-process.pid, 0) def check_incompatibilities(app: App): diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index e5c0c371..bd3aac0d 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -218,7 +218,7 @@ def check_wine_version_and_branch(release_version: str, test_binary, return True, "None" -def initializeWineBottle(wine64_binary: str, app: App): +def initializeWineBottle(wine64_binary: str, app: App) -> Optional[subprocess.Popen[bytes]]: #noqa: E501 app.status("Initializing wine bottle…") logging.debug(f"{wine64_binary=}") # Avoid wine-mono window From 632e98b76d3f20c3cd8101d8f8f7d37efd1f3a11 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:17:16 -0800 Subject: [PATCH 132/137] fix: ensure wine_appimage_path is in appbin dir --- ou_dedetai/gui_app.py | 1 - ou_dedetai/installer.py | 2 ++ ou_dedetai/utils.py | 33 +++++++++++---------------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index d45b1391..bb04386f 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -544,7 +544,6 @@ def set_appimage_symlink(self): self.update_latest_appimage_button() def update_to_latest_appimage(self, evt=None): - self.conf.wine_appimage_path = Path(self.conf.wine_appimage_recommended_file_name) # noqa: E501 self.status("Updating to latest AppImage…") self.start_thread(self.set_appimage_symlink) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index 9f04c15c..d4ab67d3 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -343,6 +343,8 @@ def install(app: App): app.status('Installing…') ensure_launcher_shortcuts(app) app.status("Install Complete!", 100) + # Trigger a config update event to refresh the UIs + app._config_updated_event.set() def get_progress_pct(current, total): diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index c578e458..c46cd3cc 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -557,36 +557,25 @@ def set_appimage_symlink(app: App): appimage_file_path = Path(app.conf.wine_appimage_path) appdir_bindir = Path(app.conf.installer_binary_dir) appimage_symlink_path = appdir_bindir / app.conf.wine_appimage_link_file_name + + destination_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 + if appimage_file_path.name == app.conf.wine_appimage_recommended_file_name: # noqa: E501 # Default case. + # This saves in the install binary dir network.dwonload_recommended_appimage(app) - selected_appimage_file_path = appdir_bindir / appimage_file_path.name # noqa: E501 - bindir_appimage = selected_appimage_file_path / app.conf.installer_binary_dir # noqa: E501 - if not bindir_appimage.exists(): - logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 - shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_dir}") else: - selected_appimage_file_path = appimage_file_path # Verify user-selected AppImage. - if not check_appimage(selected_appimage_file_path): - app.exit(f"Cannot use {selected_appimage_file_path}.") + if not check_appimage(appimage_file_path): + app.exit(f"Cannot use {appimage_file_path}.") - # Determine if user wants their AppImage in the app bin dir. - copy_question = ( - f"Should the program copy {selected_appimage_file_path} to the" - f" {app.conf.installer_binary_dir} directory?" - ) - # Copy AppImage if confirmed. - if app.approve(copy_question): - logging.info(f"Copying {selected_appimage_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 - dest = appdir_bindir / selected_appimage_file_path.name - if not dest.exists(): - shutil.copy(selected_appimage_file_path, f"{app.conf.installer_binary_dir}") # noqa: E501 - selected_appimage_file_path = dest + if destination_file_path != appimage_file_path: + logging.info(f"Copying {destination_file_path} to {app.conf.installer_binary_dir}.") # noqa: E501 + shutil.copy(appimage_file_path, destination_file_path) delete_symlink(appimage_symlink_path) - os.symlink(selected_appimage_file_path, appimage_symlink_path) - app.conf.wine_appimage_path = selected_appimage_file_path # noqa: E501 + os.symlink(destination_file_path, appimage_symlink_path) + app.conf.wine_appimage_path = destination_file_path # noqa: E501 def update_to_latest_lli_release(app: App): From da8516336f4f9e75a089a8a7b360c69d9f975899 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:18:16 -0800 Subject: [PATCH 133/137] chore: ignore .vscode files as well in the case .vscode is a symlink --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a76dd021..f5d3d1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ venv/ .venv/ .idea/ *.egg-info -.vscode/ \ No newline at end of file +.vscode \ No newline at end of file From baed48c37cf733796914131f1bc7d0487a313a19 Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:21:42 -0800 Subject: [PATCH 134/137] refactor: move post-install hooks recently added a full config hook in installer.install, these separate ones are no longer needed --- ou_dedetai/gui_app.py | 1 - ou_dedetai/tui_app.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index bb04386f..91208be5 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -462,7 +462,6 @@ def _install(): installer.install(self) # Enable the run button self.gui.app_button.state(['!disabled']) - self.update_app_button() # Disable the install buttons self.gui.app_button.state(['disabled']) self.gui.app_install_advanced.state(['disabled']) diff --git a/ou_dedetai/tui_app.py b/ou_dedetai/tui_app.py index 4f3b7966..0257ec4d 100644 --- a/ou_dedetai/tui_app.py +++ b/ou_dedetai/tui_app.py @@ -530,7 +530,6 @@ def main_menu_select(self, choice): def _install(): try: installer.install(app=self) - self.update_main_window_contents() self.go_to_main_menu() except ReturningToMainMenu: pass @@ -856,6 +855,7 @@ def _status(self, message: str, percent: int | None = None): ) def _config_update_hook(self): + self.update_main_window_contents() self.set_curses_colors() self.set_title() From eb11a91af21d668e8b657b081a370959eecff3bc Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:47:06 -0800 Subject: [PATCH 135/137] fix: handle slow network case gray out install buttons until faithlife product versions downloads (so we know which one to install) --- ou_dedetai/app.py | 5 ++++- ou_dedetai/gui_app.py | 50 ++++++++++++++++++++++++++++++++++--------- ou_dedetai/network.py | 37 ++++++++++++++++++++++---------- 3 files changed, 70 insertions(+), 22 deletions(-) diff --git a/ou_dedetai/app.py b/ou_dedetai/app.py index 3406a0dd..64064081 100644 --- a/ou_dedetai/app.py +++ b/ou_dedetai/app.py @@ -48,7 +48,10 @@ def _config_updated_hook_runner(): self._config_updated_event.wait() self._config_updated_event.clear() for hook in self.config_updated_hooks: - hook() + try: + hook() + except Exception: + logging.exception("Failed to run config update hook") _config_updated_hook_runner.__name__ = "Config Update Hook" self.start_thread(_config_updated_hook_runner, daemon_bool=True) diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index 91208be5..d804aedd 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -37,6 +37,12 @@ class GuiApp(App): def __init__(self, root: "Root", ephemeral_config: EphemeralConfiguration, **kwargs): #noqa: E501 super().__init__(ephemeral_config) self.root = root + # Now spawn a new thread to ensure choices are set to set to defaults so user + # isn't App.ask'ed + def _populate_initial_defaults(): + self.populate_defaults() + self.start_thread(_populate_initial_defaults) + def _ask(self, question: str, options: list[str] | str) -> Optional[str]: # This cannot be run from the main thread as the dialog will never appear @@ -104,10 +110,11 @@ def populate_defaults(self) -> None: # Now that we know product and version are set we can download the releases # And use the first one - # This will delay the UI from loading, but will be more robust if it's done in a - # blocking manner like it is if self.conf._raw.faithlife_product_release is None: - self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 + # Spawn a thread that does this, as the download takes a second + def _populate_product_release_default(): + self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 + self.start_thread(_populate_product_release_default) # Set the install_dir to default, no option in the GUI to change it if self.conf._raw.install_dir is None: @@ -266,8 +273,6 @@ def __init__(self, new_win, root: Root, app: "ControlWindow", **kwargs): self.get_winetricks_options() self.app.config_updated_hooks += [self._config_updated_hook] - # Ensure default choices are set so user isn't App.ask'ed - self.app.populate_defaults() # Start out enforcing this self._config_updated_hook() @@ -408,7 +413,14 @@ def __init__(self, root, control_gui: gui.ControlGui, self.actioncmd: Optional[Callable[[], None]] = None ver = constants.LLI_CURRENT_VERSION - text = f"Update {constants.APP_NAME}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" #noqa: E501 + text = f"Update {constants.APP_NAME}\ncurrent: v{ver}\nlatest: ..." #noqa: E501 + # Spawn a thread to update the label with the current version + def _update_lli_version(): + text = f"Update {constants.APP_NAME}\ncurrent: v{ver}\nlatest: v{self.conf.app_latest_version}" #noqa: E501 + self.gui.update_lli_label.config(text=text) + self.update_latest_lli_release_button() + self.gui.update_lli_button.state(['disabled']) + self.start_thread(_update_lli_version) self.gui.update_lli_label.config(text=text) self.gui.run_indexing_radio.config( command=self.on_action_radio_clicked @@ -441,7 +453,6 @@ def __init__(self, root, control_gui: gui.ControlGui, self.gui.latest_appimage_button.config( command=self.update_to_latest_appimage ) - self.update_latest_lli_release_button() self.gui.set_appimage_button.config(command=self.set_appimage) self.gui.get_winetricks_button.config(command=self.get_winetricks) self.gui.run_winetricks_button.config(command=self.launch_winetricks) @@ -458,7 +469,6 @@ def run_install(self, evt=None): Fallback to defaults if we don't know a response""" def _install(): - self.populate_defaults() installer.install(self) # Enable the run button self.gui.app_button.state(['!disabled']) @@ -605,6 +615,7 @@ def update_logging_button(self, evt=None): self.gui.logging_button.state(['!disabled']) def update_app_button(self, evt=None): + self.gui.app_button.state(['!disabled']) if self.is_installed(): self.gui.app_buttonvar.set(f"Run {self.conf.faithlife_product}") self.gui.app_button.config(command=self.run_logos) @@ -615,6 +626,19 @@ def update_app_button(self, evt=None): self.gui.app_button.config(command=self.run_install) self.gui.app_install_advanced.config(command=self.run_installer) self.gui.show_advanced_install_button() + # This function checks to make sure the product/version/channel is non-None + if self.conf._network._faithlife_product_releases( + self.conf._raw.faithlife_product, + self.conf._raw.faithlife_product_version, + self.conf._raw.faithlife_product_version + ): + # Everything is ready, we can install + self.gui.app_button.state(['!disabled']) + self.gui.app_install_advanced.state(['!disabled']) + else: + # Disable Both install buttons + self.gui.app_button.state(['disabled']) + self.gui.app_install_advanced.state(['disabled']) def update_latest_lli_release_button(self, evt=None): msg = None @@ -691,9 +715,15 @@ def update_run_winetricks_button(self, evt=None): def _config_update_hook(self, evt=None): self.update_logging_button() self.update_app_button() - self.update_latest_lli_release_button() - self.update_latest_appimage_button() self.update_run_winetricks_button() + try: + self.update_latest_lli_release_button() + except Exception: + logging.exception("Failed to update release button") + try: + self.update_latest_appimage_button() + except Exception: + logging.exception("Failed to update appimage button") def current_logging_state_value(self) -> str: diff --git a/ou_dedetai/network.py b/ou_dedetai/network.py index 29d9612a..b72c4188 100644 --- a/ou_dedetai/network.py +++ b/ou_dedetai/network.py @@ -210,12 +210,14 @@ def __init__(self, force_clean: Optional[bool] = None) -> None: self._cache = CachedRequests.load() self._cache.clean_if_stale(force=force_clean or False) - def faithlife_product_releases( + def _faithlife_product_releases( self, - product: str, - version: str, - channel: str - ) -> list[str]: + product: Optional[str], + version: Optional[str], + channel: Optional[str] + ) -> Optional[list[str]]: + if product is None or version is None or channel is None: + return None releases = self._cache.faithlife_product_releases if product not in releases: releases[product] = {} @@ -225,13 +227,26 @@ def faithlife_product_releases( channel not in releases[product][version] ): - releases[product][version][channel] = _get_faithlife_product_releases( - faithlife_product=product, - faithlife_product_version=version, - faithlife_product_release_channel=channel - ) - self._cache._write() + return None return releases[product][version][channel] + + def faithlife_product_releases( + self, + product: str, + version: str, + channel: str + ) -> list[str]: + output = self._faithlife_product_releases(product, version, channel) + if output is not None: + return output + output = _get_faithlife_product_releases( + faithlife_product=product, + faithlife_product_version=version, + faithlife_product_release_channel=channel + ) + self._cache.faithlife_product_releases[product][version][channel] = output + self._cache._write() + return output def wine_appimage_recommended_url(self) -> str: repo = "FaithLife-Community/wine-appimages" From 31571b8aac1cbd2df469813fd2dde3e65272a17c Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 13 Dec 2024 21:32:00 -0800 Subject: [PATCH 136/137] fix: enable install button after release download is complete - de-couple wine_binary_files from faithlife_product_version - populate product release immediately if possible --- ou_dedetai/config.py | 3 ++- ou_dedetai/gui_app.py | 15 +++++++++++---- ou_dedetai/utils.py | 2 +- ou_dedetai/wine.py | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ou_dedetai/config.py b/ou_dedetai/config.py index 92c03bb8..18478a43 100644 --- a/ou_dedetai/config.py +++ b/ou_dedetai/config.py @@ -835,7 +835,8 @@ def wine_binary(self, value: str): def wine_binary_files(self) -> list[str]: if self._wine_binary_files is None: self._wine_binary_files = utils.find_wine_binary_files( - self.app, self.faithlife_product_release + self.app, + self._raw.faithlife_product_release ) return self._wine_binary_files diff --git a/ou_dedetai/gui_app.py b/ou_dedetai/gui_app.py index d804aedd..9cdb9d1a 100644 --- a/ou_dedetai/gui_app.py +++ b/ou_dedetai/gui_app.py @@ -111,10 +111,17 @@ def populate_defaults(self) -> None: # Now that we know product and version are set we can download the releases # And use the first one if self.conf._raw.faithlife_product_release is None: - # Spawn a thread that does this, as the download takes a second - def _populate_product_release_default(): + if self.conf._network._faithlife_product_releases( + self.conf._raw.faithlife_product, + self.conf._raw.faithlife_product_version, + self.conf._raw.faithlife_product_release_channel, + ) is not None: self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 - self.start_thread(_populate_product_release_default) + else: + # Spawn a thread that does this, as the download takes a second + def _populate_product_release_default(): + self.conf.faithlife_product_release = self.conf.faithlife_product_releases[0] #noqa: E501 + self.start_thread(_populate_product_release_default) # Set the install_dir to default, no option in the GUI to change it if self.conf._raw.install_dir is None: @@ -630,7 +637,7 @@ def update_app_button(self, evt=None): if self.conf._network._faithlife_product_releases( self.conf._raw.faithlife_product, self.conf._raw.faithlife_product_version, - self.conf._raw.faithlife_product_version + self.conf._raw.faithlife_product_release_channel ): # Everything is ready, we can install self.gui.app_button.state(['!disabled']) diff --git a/ou_dedetai/utils.py b/ou_dedetai/utils.py index c46cd3cc..44641236 100644 --- a/ou_dedetai/utils.py +++ b/ou_dedetai/utils.py @@ -504,7 +504,7 @@ def find_appimage_files(app: App) -> list[str]: return appimages -def find_wine_binary_files(app: App, release_version: str) -> list[str]: +def find_wine_binary_files(app: App, release_version: Optional[str]) -> list[str]: wine_binary_path_list = [ "/usr/local/bin", os.path.expanduser("~") + "/bin", diff --git a/ou_dedetai/wine.py b/ou_dedetai/wine.py index bd3aac0d..c52adea3 100644 --- a/ou_dedetai/wine.py +++ b/ou_dedetai/wine.py @@ -189,7 +189,7 @@ def check_wine_rules( return True, "Default to trusting user override" -def check_wine_version_and_branch(release_version: str, test_binary, +def check_wine_version_and_branch(release_version: Optional[str], test_binary, faithlife_product_version): if not os.path.exists(test_binary): reason = "Binary does not exist." From 7ab46cd057d6d7abf9eddd4cec3b2b3c8adaf82a Mon Sep 17 00:00:00 2001 From: Nathan Shaaban <86252985+ctrlaltf24@users.noreply.github.com> Date: Fri, 13 Dec 2024 21:51:05 -0800 Subject: [PATCH 137/137] fix: normalize appimage path into install_dir --- ou_dedetai/installer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ou_dedetai/installer.py b/ou_dedetai/installer.py index d4ab67d3..59f5ef3d 100644 --- a/ou_dedetai/installer.py +++ b/ou_dedetai/installer.py @@ -361,7 +361,7 @@ def create_wine_appimage_symlinks(app: App): logging.debug("No need to symlink non-appimages") return - appimage_file = Path(app.conf.wine_appimage_path) + appimage_file = appdir_bindir / app.conf.wine_appimage_path.name appimage_filename = Path(app.conf.wine_appimage_path).name # Ensure appimage is copied to appdir_bindir. downloaded_file = utils.get_downloaded_file_path(app.conf.download_dir, appimage_filename) #noqa: E501 @@ -372,7 +372,8 @@ def create_wine_appimage_symlinks(app: App): app.status(f"Copying: {downloaded_file} into: {appdir_bindir}") shutil.copy(downloaded_file, appdir_bindir) os.chmod(appimage_file, 0o755) - appimage_filename = appimage_file.name + app.conf.wine_appimage_path = appimage_file + app.conf.wine_binary = str(appimage_file) appimage_link.unlink(missing_ok=True) # remove & replace appimage_link.symlink_to(f"./{appimage_filename}")