-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add abstract app module and refactor config #206
Open
ctrlaltf24
wants to merge
141
commits into
FaithLife-Community:main
Choose a base branch
from
ctrlaltf24:feat-platform-independent-prompts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
141 commits
Select commit
Hold shift + click to select a range
82d0c94
feat: generic prompts
ctrlaltf24 a3bfa78
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 60860f8
refactor: move constants to their own file
ctrlaltf24 a3244a7
refactor: move function from config to utils
ctrlaltf24 4676033
feat(wip): move the install prompts into the abstract class
ctrlaltf24 923cbbc
refactor: function never called with an argument
ctrlaltf24 c97b5f5
fix(wip): add prompting and started new storage implementation
ctrlaltf24 a2001e8
feat(wip): more progress
ctrlaltf24 e27d248
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 630bbcf
feat: load legacy envs into new framework
ctrlaltf24 c68e29f
fix: migrate CUSTOMBINPATH
ctrlaltf24 43d7644
fix: migrate LOGOS_VERSION
ctrlaltf24 49a3c8c
fix: migrate LOGOS_EXECUTABLE
ctrlaltf24 3246bc5
fix: remove unused LOGOS64_MSI
ctrlaltf24 d930d47
fix: migrate LOGOS64_URL
ctrlaltf24 197609c
fix: migrate WINEDLLOVERRIDES
ctrlaltf24 4b76466
fix: migrate SKIP_WINETRICKS
ctrlaltf24 65e5369
fix: remove unused REINSTALL_DEPENDENCIES
ctrlaltf24 ac5308b
fix: migrate WINEDEBUG, VERBOSE, DEBUG and LOG_LEVEL
ctrlaltf24 7c162e0
fix: migrate WINEPREFIX
ctrlaltf24 45067bf
fix: migrate wine_log and standardize config naming
ctrlaltf24 d53ba49
feat: migrate LOGOS_LOG
ctrlaltf24 b138e52
fix: migrate INSTALLDIR
ctrlaltf24 07df426
fix: migrate WINE_EXE
ctrlaltf24 29af5ad
refactor: passed config explicitly
ctrlaltf24 ac353bf
fix: migrate INSTALL_STEP
ctrlaltf24 6045412
fix: migrate WINETRICKS_UNATTENDED
ctrlaltf24 f87448b
fix: plum new configuration through to main
ctrlaltf24 443c6a8
fix: misc
ctrlaltf24 cdc8e09
fix: misc
ctrlaltf24 2c5a4fa
fix: migrate LOGOS_EXE
ctrlaltf24 ec6abe3
fix: migrate DELETE_LOG
ctrlaltf24 b06c38c
fix: migrate CHECK_UPDATES
ctrlaltf24 f3221eb
fix: migrate SKIP_DEPENDENCIES
ctrlaltf24 4a73f8e
fix: migrate SKIP_FONTS
ctrlaltf24 db38ed2
fix: remove unused LOGOS_ICON_FILENAME
ctrlaltf24 753af64
refactor: remove need for separate icon name
ctrlaltf24 fe6e061
fix: migrate LOGOS_ICON_URL
ctrlaltf24 2cb8323
fix: remove OS_NAME and OS_RELEASE
ctrlaltf24 89e8718
refactor: separate network cache
ctrlaltf24 d18544a
fix: typing/stype thanks to mypy/ruff
ctrlaltf24 d47eb53
fix: migrate MYDOWNLOADS
ctrlaltf24 4857014
fix: migrate LOGOS_FORCE_ROOT and PASSIVE
ctrlaltf24 6087e99
fix: migrate appimage vars
ctrlaltf24 ffe81f9
fix: migrate winecmd_encoding
ctrlaltf24 78e8479
fix: migrate package manager to new format
ctrlaltf24 a0253ef
refactor: move globally scoped tui variables into TUI
ctrlaltf24 965d197
fix: remove check if indexing
ctrlaltf24 77e4065
refactor: migrate current_logos_version
ctrlaltf24 e0aa905
refactor: migrate LOGS
ctrlaltf24 912fded
fix: installed_faithlife_product_release
ctrlaltf24 b4b33c4
refactor: migrate latest version
ctrlaltf24 13ed0d0
refactor: move network cache into persistent config
ctrlaltf24 b7147ad
refactor: limit temp workdir to scope
ctrlaltf24 40ad90d
fix: misc
ctrlaltf24 e903d57
refactor: move new_config into config.py
ctrlaltf24 c9cf534
refactor: migrate config.SUPERUSER_COMMAND
ctrlaltf24 dd4ef56
refactor: remove more DIALOG
ctrlaltf24 27f3a43
refactor: move DIALOG removing
ctrlaltf24 b4ca5fc
refactor: more DIALOG
ctrlaltf24 d18b907
fix: misc
ctrlaltf24 3a28e3f
refactor: migrate config.processes
ctrlaltf24 818b063
chore: remove unused
ctrlaltf24 92bf465
fix: misc
ctrlaltf24 e113149
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 d07c92f
chore: add comments for additional work
ctrlaltf24 be9f468
docs: add comment to architecture code
ctrlaltf24 d6c26a4
fix: migrate new code to config
ctrlaltf24 c566637
fix: validate responses from _ask
ctrlaltf24 ffe1362
refactor: migrate msg questions into app
ctrlaltf24 dbcfffa
refactor: msg.logos_error
ctrlaltf24 17f6ac9
refactor: replace logos_warning with app.exit
ctrlaltf24 755df3d
refactor: remove unused ui_message
ctrlaltf24 5e0526c
refactor: remove msg.progress
ctrlaltf24 5fd98d2
refactor: update_install_feedback to app.status
ctrlaltf24 9d95a0e
fix: include progress bar in cli output
ctrlaltf24 976ce27
refactor: msg.status
ctrlaltf24 0361d08
refactor: msg.logos_warn
ctrlaltf24 8fcfeaf
refactor: remove logos_progress
ctrlaltf24 d9a0685
refactor: msg.logos_msg
ctrlaltf24 5c46fd7
chore: remove unused functions
ctrlaltf24 923b531
fix: resolve misc warning and errors
ctrlaltf24 3b8273e
refactor: DIALOG and use_python_dialog
ctrlaltf24 4d732fc
refactor: migrate config.threads to the app
ctrlaltf24 703198d
refactor: console_log
ctrlaltf24 7edb53a
refactor: migrate ACTION
ctrlaltf24 6dc5a07
refactor: migrate CONFIG_FILE
ctrlaltf24 cdfd46e
fix: resolve remaining mypy/ruff lints
ctrlaltf24 3f4c873
chore: remove out of date comments
ctrlaltf24 a003b0d
fix: cli progress output and misc
ctrlaltf24 c7ff552
refactor: another way to call CLI lazily
ctrlaltf24 276b366
fix: bug where selection didn't reset going back to main menu
ctrlaltf24 7e8b2d5
fix: additional mypy lints from untyped functions
ctrlaltf24 7750e13
fix: implement TUI status
ctrlaltf24 be4166a
fix: type hints in GUI
ctrlaltf24 ef63188
fix: more type hints
ctrlaltf24 5ccea3b
refactor: condense redundant steps
ctrlaltf24 830fa10
refactor: cache all network requests
ctrlaltf24 50cfc23
refactor: cache more file traversals
ctrlaltf24 d0c4ba5
refactor: allow for multiple config update hooks
ctrlaltf24 773f8c4
refactor: load all GUI state from config
ctrlaltf24 81b205d
fix: increase robustness of spawning dialogs on tk
ctrlaltf24 dcd84c5
refactor: resolve some comments
ctrlaltf24 de3992c
fix: spawn tui actions on a new thread
ctrlaltf24 d29eed3
fix: winetricks binary storage
ctrlaltf24 8b2e2d3
fix: no need to display this so prominently
ctrlaltf24 d7f9b45
fix: logs state manager mypy lints
ctrlaltf24 9811e05
fix: display recent messages in tui log
ctrlaltf24 d435dc2
fix: migrate legacy config
ctrlaltf24 03ea230
fix: load DIALOG from config
ctrlaltf24 a7c7634
feat: add -y and -q flags
ctrlaltf24 6a0030a
feat: added a reload function
ctrlaltf24 9f675c4
fix: resolve remaining todos
ctrlaltf24 c0febe4
fix: resolve a couple fixmes
ctrlaltf24 e45f571
feat: add integration test
ctrlaltf24 b188f90
fix: change color scheme
ctrlaltf24 6e2e5dd
fix: console log de-dup
ctrlaltf24 fd44ae3
fix: de-dup wine binary options
ctrlaltf24 631f00f
fix: handle case where install is started after going back to the menu
ctrlaltf24 33bb556
Update ou_dedetai/utils.py
ctrlaltf24 db4f175
feat: handle tab completion if the user input is path like
ctrlaltf24 1e2072e
Merge remote-tracking branch 'refs/remotes/origin/feat-platform-indep…
ctrlaltf24 e8fed2c
fix: resolve review comments
ctrlaltf24 7f70380
docs: update changelog
ctrlaltf24 0a02d40
fix: reset user options when hitting install again
ctrlaltf24 becb67e
fix: handle single appimage duplicate rather than reordering
ctrlaltf24 d6bd941
docs: update comment as to why launcher shortcuts are skipped
ctrlaltf24 59ace4f
fix: restore app logging
ctrlaltf24 7fc7515
fix: logging
ctrlaltf24 671d563
chore: more descriptive message when file config not found
ctrlaltf24 6dd127b
chore: remove unused code
ctrlaltf24 4819bb4
feat: add a install button that doesn't prompt in GUI
ctrlaltf24 8a9a52e
fix: support env overriding
ctrlaltf24 99d7243
chore: fix spelling
ctrlaltf24 9d3b99a
fix: optional pid
ctrlaltf24 632e98b
fix: ensure wine_appimage_path is in appbin dir
ctrlaltf24 da85163
chore: ignore .vscode files as well
ctrlaltf24 baed48c
refactor: move post-install hooks
ctrlaltf24 eb11a91
fix: handle slow network case
ctrlaltf24 31571b8
fix: enable install button after release download is complete
ctrlaltf24 7ab46cd
fix: normalize appimage path into install_dir
ctrlaltf24 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,5 @@ env/ | |
venv/ | ||
.venv/ | ||
.idea/ | ||
*.egg-info | ||
*.egg-info | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
|
||
import abc | ||
import logging | ||
import os | ||
from pathlib import Path | ||
import sys | ||
import threading | ||
from typing import Callable, NoReturn, Optional | ||
|
||
from ou_dedetai import constants | ||
from ou_dedetai.constants import ( | ||
PROMPT_OPTION_DIRECTORY, | ||
PROMPT_OPTION_FILE | ||
) | ||
|
||
|
||
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 | ||
"""Step the installer is on. Starts at 0""" | ||
|
||
_threads: list[threading.Thread] | ||
"""List of threads | ||
|
||
Non-daemon threads will be joined before shutdown | ||
""" | ||
_last_status: Optional[str] = None | ||
"""The last status we had""" | ||
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 | ||
from ou_dedetai.config import Config | ||
from ou_dedetai.logos import LogosManager | ||
from ou_dedetai.system import check_incompatibilities | ||
|
||
self.conf = Config(config, self) | ||
self.logos = LogosManager(app=self) | ||
self._threads = [] | ||
# 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: | ||
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) | ||
|
||
def ask(self, question: str, options: list[str]) -> str: | ||
"""Asks 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 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 | ||
|
||
# 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 ( | ||
PROMPT_OPTION_DIRECTORY in passed_options | ||
or PROMPT_OPTION_FILE in passed_options | ||
): | ||
# Set the only option to be the follow up prompt | ||
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 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: | ||
self.exit("Failed to get a valid value from user") | ||
|
||
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.approve(question, context): | ||
self.exit(f"User refused the prompt: {question}") | ||
|
||
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"] | ||
return self.ask(question, options) == "Yes" | ||
|
||
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: | ||
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) | ||
except FileNotFoundError: # no pid file when testing functions | ||
pass | ||
# exit from the process | ||
if intended: | ||
sys.exit(0) | ||
else: | ||
logging.critical(f"Cannot continue because {reason}\n{constants.SUPPORT_MESSAGE}") #noqa: E501 | ||
sys.exit(1) | ||
|
||
_exit_option: Optional[str] = "Exit" | ||
|
||
@abc.abstractmethod | ||
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. | ||
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 | ||
""" | ||
raise NotImplementedError() | ||
|
||
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 | ||
|
||
def status(self, message: str, percent: Optional[int | float] = None): | ||
"""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) | ||
""" | ||
# 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 | ||
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 {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 | ||
|
||
@abc.abstractmethod | ||
def _status(self, message: str, percent: Optional[int] = None): | ||
"""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: | ||
if message.endswith("\r"): | ||
print(f"{message}", end="\r") | ||
else: | ||
print(f"{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() | ||
|
||
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a good improvement; alternatively, split up long steps into smaller steps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favor of the steps being as discreet as possible, as in, as small as possible. That way the installer can check for each one in turn and only run the step if needed. That being said, all the gathering of user responses could maybe be a single step? That would help the progress bar's output seem more intuitive, I think.