Files
kiauh/kiauh/core/menus/base_menu.py
2024-06-22 12:30:29 +02:00

217 lines
7.7 KiB
Python

# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
import subprocess
import sys
import textwrap
import traceback
from abc import abstractmethod
from typing import Dict, Optional, Type
from core.menus import FooterType, Option
from utils.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_RED,
COLOR_YELLOW,
RESET_FORMAT,
)
from utils.logger import Logger
def clear():
subprocess.call("clear", shell=True)
def print_header():
line1 = " [ KIAUH ] "
line2 = "Klipper Installation And Update Helper"
line3 = ""
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
header = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{line1:~^{count}}{RESET_FORMAT}
{color}{line2:^{count}}{RESET_FORMAT}
{color}{line3:~^{count}}{RESET_FORMAT}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(header, end="")
def print_quit_footer():
text = "Q) Quit"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
footer = textwrap.dedent(
f"""
{color}{text:^{count}}{RESET_FORMAT}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_back_footer():
text = "B) « Back"
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
footer = textwrap.dedent(
f"""
{color}{text:^{count}}{RESET_FORMAT}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_back_help_footer():
text1 = "B) « Back"
text2 = "H) Help [?]"
color1 = COLOR_GREEN
color2 = COLOR_YELLOW
count = 34 - len(color1) - len(RESET_FORMAT)
footer = textwrap.dedent(
f"""
{color1}{text1:^{count}}{RESET_FORMAT}{color2}{text2:^{count}}{RESET_FORMAT}
╚═══════════════════════════╧═══════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_blank_footer():
print("╚═══════════════════════════════════════════════════════╝")
class PostInitCaller(type):
def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs)
obj.__post_init__()
return obj
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class BaseMenu(metaclass=PostInitCaller):
options: Dict[str, Option] = {}
options_offset: int = 0
default_option: Option = None
input_label_txt: str = "Perform action"
header: bool = False
previous_menu: Type[BaseMenu] = None
help_menu: Type[BaseMenu] = None
footer_type: FooterType = FooterType.BACK
def __init__(self, **kwargs):
if type(self) is BaseMenu:
raise NotImplementedError("BaseMenu cannot be instantiated directly.")
def __post_init__(self):
self.set_previous_menu(self.previous_menu)
self.set_options()
# conditionally add options based on footer type
if self.footer_type is FooterType.QUIT:
self.options["q"] = Option(method=self.__exit, menu=False)
if self.footer_type is FooterType.BACK:
self.options["b"] = Option(method=self.__go_back, menu=False)
if self.footer_type is FooterType.BACK_HELP:
self.options["b"] = Option(method=self.__go_back, menu=False)
self.options["h"] = Option(method=self.__go_to_help, menu=False)
# if defined, add the default option to the options dict
if self.default_option is not None:
self.options[""] = self.default_option
def __go_back(self, **kwargs):
self.previous_menu().run()
def __go_to_help(self, **kwargs):
self.help_menu(previous_menu=self).run()
def __exit(self, **kwargs):
Logger.print_ok("###### Happy printing!", False)
sys.exit(0)
@abstractmethod
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
raise NotImplementedError
@abstractmethod
def set_options(self) -> None:
raise NotImplementedError
@abstractmethod
def print_menu(self) -> None:
raise NotImplementedError
def print_footer(self) -> None:
if self.footer_type is FooterType.QUIT:
print_quit_footer()
elif self.footer_type is FooterType.BACK:
print_back_footer()
elif self.footer_type is FooterType.BACK_HELP:
print_back_help_footer()
elif self.footer_type is FooterType.BLANK:
print_blank_footer()
else:
raise NotImplementedError("FooterType not correctly implemented!")
def display_menu(self) -> None:
if self.header:
print_header()
self.print_menu()
self.print_footer()
def validate_user_input(self, usr_input: str) -> Option:
"""
Validate the user input and either return an Option, a string or None
:param usr_input: The user input in form of a string
:return: Option, str or None
"""
usr_input = usr_input.lower()
option = self.options.get(usr_input, Option(None, False, "", None))
# if option/usr_input is None/empty string, we execute the menus default option if specified
if (option is None or usr_input == "") and self.default_option is not None:
self.default_option.opt_index = usr_input
return self.default_option
# user selected a regular option
option.opt_index = usr_input
return option
def handle_user_input(self) -> Option:
"""Handle the user input, return the validated input or print an error."""
while True:
print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="")
usr_input = input().lower()
validated_input = self.validate_user_input(usr_input)
if validated_input.method is not None:
return validated_input
else:
Logger.print_error("Invalid input!", False)
def run(self) -> None:
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
try:
self.display_menu()
option = self.handle_user_input()
option.method(opt_index=option.opt_index, opt_data=option.opt_data)
self.run()
except Exception as e:
Logger.print_error(
f"An unexpected error occured:\n{e}\n{traceback.format_exc()}"
)