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: generic prompts #206

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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