Skip to content
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

Draft
wants to merge 64 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
82d0c94
feat: generic prompts
ctrlaltf24 Oct 24, 2024
a3bfa78
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 Nov 12, 2024
60860f8
refactor: move constants to their own file
ctrlaltf24 Nov 12, 2024
a3244a7
refactor: move function from config to utils
ctrlaltf24 Nov 12, 2024
4676033
feat(wip): move the install prompts into the abstract class
ctrlaltf24 Nov 13, 2024
923cbbc
refactor: function never called with an argument
ctrlaltf24 Nov 13, 2024
c97b5f5
fix(wip): add prompting and started new storage implementation
ctrlaltf24 Nov 13, 2024
a2001e8
feat(wip): more progress
ctrlaltf24 Nov 13, 2024
e27d248
Merge remote-tracking branch 'upstream/main' into feat-platform-indep…
ctrlaltf24 Nov 23, 2024
630bbcf
feat: load legacy envs into new framework
ctrlaltf24 Nov 23, 2024
c68e29f
fix: migrate CUSTOMBINPATH
ctrlaltf24 Nov 23, 2024
43d7644
fix: migrate LOGOS_VERSION
ctrlaltf24 Nov 23, 2024
49a3c8c
fix: migrate LOGOS_EXECUTABLE
ctrlaltf24 Nov 23, 2024
3246bc5
fix: remove unused LOGOS64_MSI
ctrlaltf24 Nov 23, 2024
d930d47
fix: migrate LOGOS64_URL
ctrlaltf24 Nov 23, 2024
197609c
fix: migrate WINEDLLOVERRIDES
ctrlaltf24 Nov 23, 2024
4b76466
fix: migrate SKIP_WINETRICKS
ctrlaltf24 Nov 23, 2024
65e5369
fix: remove unused REINSTALL_DEPENDENCIES
ctrlaltf24 Nov 23, 2024
ac5308b
fix: migrate WINEDEBUG, VERBOSE, DEBUG and LOG_LEVEL
ctrlaltf24 Nov 24, 2024
7c162e0
fix: migrate WINEPREFIX
ctrlaltf24 Nov 24, 2024
45067bf
fix: migrate wine_log and standardize config naming
ctrlaltf24 Nov 24, 2024
d53ba49
feat: migrate LOGOS_LOG
ctrlaltf24 Nov 24, 2024
b138e52
fix: migrate INSTALLDIR
ctrlaltf24 Nov 24, 2024
07df426
fix: migrate WINE_EXE
ctrlaltf24 Nov 24, 2024
29af5ad
refactor: passed config explicitly
ctrlaltf24 Nov 24, 2024
ac353bf
fix: migrate INSTALL_STEP
ctrlaltf24 Nov 24, 2024
6045412
fix: migrate WINETRICKS_UNATTENDED
ctrlaltf24 Nov 24, 2024
f87448b
fix: plum new configuration through to main
ctrlaltf24 Nov 24, 2024
443c6a8
fix: misc
ctrlaltf24 Nov 24, 2024
cdc8e09
fix: misc
ctrlaltf24 Nov 24, 2024
2c5a4fa
fix: migrate LOGOS_EXE
ctrlaltf24 Nov 24, 2024
ec6abe3
fix: migrate DELETE_LOG
ctrlaltf24 Nov 24, 2024
b06c38c
fix: migrate CHECK_UPDATES
ctrlaltf24 Nov 24, 2024
f3221eb
fix: migrate SKIP_DEPENDENCIES
ctrlaltf24 Nov 24, 2024
4a73f8e
fix: migrate SKIP_FONTS
ctrlaltf24 Nov 24, 2024
db38ed2
fix: remove unused LOGOS_ICON_FILENAME
ctrlaltf24 Nov 24, 2024
753af64
refactor: remove need for separate icon name
ctrlaltf24 Nov 24, 2024
fe6e061
fix: migrate LOGOS_ICON_URL
ctrlaltf24 Nov 24, 2024
2cb8323
fix: remove OS_NAME and OS_RELEASE
ctrlaltf24 Nov 29, 2024
89e8718
refactor: separate network cache
ctrlaltf24 Nov 29, 2024
d18544a
fix: typing/stype thanks to mypy/ruff
ctrlaltf24 Nov 29, 2024
d47eb53
fix: migrate MYDOWNLOADS
ctrlaltf24 Nov 29, 2024
4857014
fix: migrate LOGOS_FORCE_ROOT and PASSIVE
ctrlaltf24 Nov 29, 2024
6087e99
fix: migrate appimage vars
ctrlaltf24 Nov 30, 2024
ffe81f9
fix: migrate winecmd_encoding
ctrlaltf24 Nov 30, 2024
78e8479
fix: migrate package manager to new format
ctrlaltf24 Nov 30, 2024
a0253ef
refactor: move globally scoped tui variables into TUI
ctrlaltf24 Nov 30, 2024
965d197
fix: remove check if indexing
ctrlaltf24 Nov 30, 2024
77e4065
refactor: migrate current_logos_version
ctrlaltf24 Nov 30, 2024
e0aa905
refactor: migrate LOGS
ctrlaltf24 Nov 30, 2024
912fded
fix: installed_faithlife_product_release
ctrlaltf24 Nov 30, 2024
b4b33c4
refactor: migrate latest version
ctrlaltf24 Nov 30, 2024
13ed0d0
refactor: move network cache into persistent config
ctrlaltf24 Nov 30, 2024
b7147ad
refactor: limit temp workdir to scope
ctrlaltf24 Nov 30, 2024
40ad90d
fix: misc
ctrlaltf24 Nov 30, 2024
e903d57
refactor: move new_config into config.py
ctrlaltf24 Nov 30, 2024
c9cf534
refactor: migrate config.SUPERUSER_COMMAND
ctrlaltf24 Nov 30, 2024
dd4ef56
refactor: remove more DIALOG
ctrlaltf24 Nov 30, 2024
27f3a43
refactor: move DIALOG removing
ctrlaltf24 Nov 30, 2024
b4ca5fc
refactor: more DIALOG
ctrlaltf24 Nov 30, 2024
d18b907
fix: misc
ctrlaltf24 Nov 30, 2024
3a28e3f
refactor: migrate config.processes
ctrlaltf24 Nov 30, 2024
818b063
chore: remove unused
ctrlaltf24 Nov 30, 2024
92bf465
fix: misc
ctrlaltf24 Nov 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions ou_dedetai/app.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 20 additions & 2 deletions ou_dedetai/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import queue
import threading
from typing import Optional

from ou_dedetai.app import App

from . import control
from . import installer
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions ou_dedetai/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
117 changes: 97 additions & 20 deletions ou_dedetai/gui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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(
"<Return>",
self.on_confirm_choice
)
self.bind(
"<Escape>",
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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
36 changes: 10 additions & 26 deletions ou_dedetai/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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=}")

Expand Down
Loading