Files
kiauh/kiauh/core/menus/base_menu.py
dw-0 234cf2c751 chore(copyright): update year (#645)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-22 16:21:34 +01:00

240 lines
8.3 KiB
Python

# ======================================================================= #
# Copyright (C) 2020 - 2025 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 enum import Enum
from typing import Dict, Type
from core.logger import Logger
from core.menus import FooterType, Option
from core.services.message_service import MessageService
from core.spinner import Spinner
from core.types.color import Color
from utils.input_utils import get_selection_input
def clear() -> None:
subprocess.call("clear -x", shell=True)
def print_header() -> None:
line1 = " [ KIAUH ] "
line2 = "Klipper Installation And Update Helper"
line3 = ""
color = Color.CYAN
count = 62 - len(str(color)) - len(str(Color.RST))
header = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{Color.apply(f"{line1:~^{count}}", color)}
{Color.apply(f"{line2:^{count}}", color)}
{Color.apply(f"{line3:~^{count}}", color)}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(header, end="")
def print_quit_footer() -> None:
text = "Q) Quit"
color = Color.RED
count = 62 - len(str(color)) - len(str(Color.RST))
footer = textwrap.dedent(
f"""
{color}{text:^{count}}{Color.RST}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_back_footer() -> None:
text = "B) « Back"
color = Color.GREEN
count = 62 - len(str(color)) - len(str(Color.RST))
footer = textwrap.dedent(
f"""
{color}{text:^{count}}{Color.RST}
╚═══════════════════════════════════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_back_help_footer() -> None:
text1 = "B) « Back"
text2 = "H) Help [?]"
color1 = Color.GREEN
color2 = Color.YELLOW
count = 34 - len(str(color1)) - len(str(Color.RST))
footer = textwrap.dedent(
f"""
{color1}{text1:^{count}}{Color.RST}{color2}{text2:^{count}}{Color.RST}
╚═══════════════════════════╧═══════════════════════════╝
"""
)[1:]
print(footer, end="")
def print_blank_footer() -> None:
print("╚═══════════════════════════════════════════════════════╝")
class MenuTitleStyle(Enum):
PLAIN = "plain"
STYLED = "styled"
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
loading_msg: str = ""
spinner: Spinner | None = None
title: str = ""
title_style: MenuTitleStyle = MenuTitleStyle.STYLED
title_color: Color = Color.WHITE
previous_menu: Type[BaseMenu] | None = None
help_menu: Type[BaseMenu] | None = None
footer_type: FooterType = FooterType.BACK
message_service = MessageService()
def __init__(self, **kwargs) -> None:
if type(self) is BaseMenu:
raise NotImplementedError("BaseMenu cannot be instantiated directly.")
def __post_init__(self) -> None:
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)
if self.footer_type is FooterType.BACK:
self.options["b"] = Option(method=self.__go_back)
if self.footer_type is FooterType.BACK_HELP:
self.options["b"] = Option(method=self.__go_back)
self.options["h"] = Option(method=self.__go_to_help)
# 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) -> None:
if self.previous_menu is None:
return
self.previous_menu().run()
def __go_to_help(self, **kwargs) -> None:
if self.help_menu is None:
return
self.help_menu(previous_menu=self.__class__).run()
def __exit(self, **kwargs) -> None:
Logger.print_ok("###### Happy printing!", False)
sys.exit(0)
@abstractmethod
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
raise NotImplementedError
@abstractmethod
def set_options(self) -> None:
raise NotImplementedError
@abstractmethod
def print_menu(self) -> None:
raise NotImplementedError
def is_loading(self, state: bool) -> None:
if not self.spinner and state:
self.spinner = Spinner(self.loading_msg)
self.spinner.start()
else:
self.spinner.stop()
self.spinner = None
def __print_menu_title(self) -> None:
count = 62 - len(str(self.title_color)) - len(str(Color.RST))
menu_title = "╔═══════════════════════════════════════════════════════╗\n"
if self.title:
title = (
f" [ {self.title} ] "
if self.title_style == MenuTitleStyle.STYLED
else self.title
)
line = (
f"{title:~^{count}}"
if self.title_style == MenuTitleStyle.STYLED
else f"{title:^{count}}"
)
menu_title += f"{Color.apply(line, self.title_color)}\n"
print(menu_title, end="")
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:
self.message_service.display_message()
if self.header:
print_header()
self.__print_menu_title()
self.print_menu()
self.__print_footer()
def run(self) -> None:
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
try:
self.__display_menu()
option = get_selection_input(self.input_label_txt, self.options)
selected_option: Option = self.options.get(option)
selected_option.method(
opt_index=selected_option.opt_index,
opt_data=selected_option.opt_data,
)
self.run()
except Exception as e:
Logger.print_error(
f"An unexpected error occured:\n{e}\n{traceback.format_exc()}"
)