From 106bf7675fe409e2d7c925930e69b7f3f83a967e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Fri, 25 Oct 2024 13:01:12 +0200 Subject: [PATCH 01/18] fix: port reconfiguration menu displays wrong port Signed-off-by: Dominik Willner --- kiauh/components/webui_client/base_data.py | 1 + kiauh/components/webui_client/client_utils.py | 70 +++++++++++++------ kiauh/components/webui_client/fluidd_data.py | 2 + .../components/webui_client/mainsail_data.py | 2 + .../webui_client/menus/client_install_menu.py | 13 +++- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/kiauh/components/webui_client/base_data.py b/kiauh/components/webui_client/base_data.py index f46798b..e3afa4e 100644 --- a/kiauh/components/webui_client/base_data.py +++ b/kiauh/components/webui_client/base_data.py @@ -37,6 +37,7 @@ class BaseWebClient(ABC): backup_dir: Path repo_path: str download_url: str + nginx_config: Path nginx_access_log: Path nginx_error_log: Path client_config: BaseWebClientConfig diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 0141160..56699d1 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -338,36 +338,62 @@ def create_nginx_cfg( raise +def get_nginx_config_list() -> List[Path]: + """ + Get a list of all NGINX config files in /etc/nginx/sites-enabled + :return: List of NGINX config files + """ + configs: List[Path] = [] + for config in NGINX_SITES_ENABLED.iterdir(): + if not config.is_file(): + continue + configs.append(config) + return configs + + +def get_nginx_listen_port(config: Path) -> int | None: + """ + Get the listen port from an NGINX config file + :param config: The NGINX config file to read the port from + :return: The listen port as int or None if not found/parsable + """ + + # noinspection HttpUrlsUsage + pattern = r"default_server|http://|https://|[;\[\]]" + port = "" + with open(config, "r") as cfg: + for line in cfg.readlines(): + line = re.sub(pattern, "", line.strip()) + if line.startswith("listen"): + if ":" not in line: + port = line.split()[-1] + else: + port = line.split(":")[-1] + try: + return int(port) + except ValueError: + Logger.print_error( + f"Unable to parse listen port {port} from {config.name}!" + ) + return None + + def read_ports_from_nginx_configs() -> List[int]: """ - Helper function to iterate over all NGINX configs and read all ports defined for listen + Helper function to iterate over all NGINX configs + and read all ports defined for listen :return: A sorted list of listen ports """ if not NGINX_SITES_ENABLED.exists(): return [] - port_list = [] - for config in NGINX_SITES_ENABLED.iterdir(): - if not config.is_file(): - continue + port_list: List[int] = [] + for config in get_nginx_config_list(): + port = get_nginx_listen_port(config) + if port is not None: + port_list.append(port) - with open(config, "r") as cfg: - lines = cfg.readlines() - - for line in lines: - line = re.sub( - r"default_server|http://|https://|[;\[\]]", - "", - line.strip(), - ) - if line.startswith("listen"): - if ":" not in line: - port_list.append(line.split()[-1]) - else: - port_list.append(line.split(":")[-1]) - - ports_to_ints_list = [int(port) for port in port_list] - return sorted(ports_to_ints_list, key=lambda x: int(x)) + return sorted(port_list, key=lambda x: int(x)) def get_client_port_selection( diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py index b499351..79c23b1 100644 --- a/kiauh/components/webui_client/fluidd_data.py +++ b/kiauh/components/webui_client/fluidd_data.py @@ -19,6 +19,7 @@ from components.webui_client.base_data import ( WebClientType, ) from core.backup_manager import BACKUP_ROOT_DIR +from core.constants import NGINX_SITES_AVAILABLE @dataclass() @@ -44,6 +45,7 @@ class FluiddData(BaseWebClient): config_file: Path = client_dir.joinpath("config.json") backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups") repo_path: str = "fluidd-core/fluidd" + nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log") nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log") client_config: BaseWebClientConfig = None diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py index 1d520a1..aa2030d 100644 --- a/kiauh/components/webui_client/mainsail_data.py +++ b/kiauh/components/webui_client/mainsail_data.py @@ -19,6 +19,7 @@ from components.webui_client.base_data import ( WebClientType, ) from core.backup_manager import BACKUP_ROOT_DIR +from core.constants import NGINX_SITES_AVAILABLE @dataclass() @@ -44,6 +45,7 @@ class MainsailData(BaseWebClient): config_file: Path = client_dir.joinpath("config.json") backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups") repo_path: str = "mainsail-crew/mainsail" + nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") nginx_access_log: Path = Path("/var/log/nginx/mainsail-access.log") nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log") client_config: BaseWebClientConfig = None diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py index a741888..ae3ad42 100644 --- a/kiauh/components/webui_client/menus/client_install_menu.py +++ b/kiauh/components/webui_client/menus/client_install_menu.py @@ -15,6 +15,7 @@ from components.webui_client.base_data import BaseWebClient from components.webui_client.client_setup import install_client from components.webui_client.client_utils import ( get_client_port_selection, + get_nginx_listen_port, set_listen_port, ) from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT @@ -53,7 +54,7 @@ class ClientInstallMenu(BaseMenu): header = f" [ Installation Menu > {client_name} ] " color = COLOR_GREEN count = 62 - len(color) - len(RESET_FORMAT) - port = f"(Current: {COLOR_CYAN}{int(self.client_settings.port)}{RESET_FORMAT})" + port = f"(Current: {COLOR_CYAN}{self._get_current_port()}{RESET_FORMAT})" menu = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -70,7 +71,7 @@ class ClientInstallMenu(BaseMenu): install_client(self.client, settings=self.settings, reinstall=True) def change_listen_port(self, **kwargs) -> None: - curr_port = int(self.client_settings.port) + curr_port = self._get_current_port() new_port = get_client_port_selection( self.client, self.settings, @@ -99,6 +100,14 @@ class ClientInstallMenu(BaseMenu): ], ) + def _get_current_port(self) -> int: + curr_port = get_nginx_listen_port(self.client.nginx_config) + if curr_port is None: + # if the port is not found in the config file we use + # the default port from the kiauh settings as fallback + return int(self.client_settings.port) + return curr_port + def _go_back(self, **kwargs) -> None: if self.previous_menu is not None: self.previous_menu().run() From e63d9d67ec9b21780fadcf3352b8e760351aa73e Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 26 Oct 2024 00:02:37 +0200 Subject: [PATCH 02/18] refactor: overhaul color mechanics Signed-off-by: Dominik Willner --- kiauh/components/crowsnest/crowsnest.py | 2 +- kiauh/components/klipper/klipper_dialogs.py | 25 ++-- kiauh/components/klipper/klipper_utils.py | 2 +- .../klipper/menus/klipper_remove_menu.py | 18 +-- .../menus/klipper_build_menu.py | 29 +++-- .../menus/klipper_flash_error_menu.py | 28 ++--- .../menus/klipper_flash_help_menu.py | 44 +++---- .../menus/klipper_flash_menu.py | 78 ++++++------ .../components/klipperscreen/klipperscreen.py | 2 +- .../log_uploads/menus/log_upload_menu.py | 11 +- .../moonraker/menus/moonraker_remove_menu.py | 18 +-- .../components/moonraker/moonraker_dialogs.py | 18 +-- kiauh/components/moonraker/moonraker_utils.py | 2 +- kiauh/components/webui_client/client_setup.py | 5 +- kiauh/components/webui_client/client_utils.py | 18 ++- .../webui_client/menus/client_install_menu.py | 16 +-- .../webui_client/menus/client_remove_menu.py | 14 +-- kiauh/core/constants.py | 9 -- kiauh/core/logger.py | 119 ++++++++---------- kiauh/core/menus/advanced_menu.py | 11 +- kiauh/core/menus/backup_menu.py | 13 +- kiauh/core/menus/base_menu.py | 87 +++++++++---- kiauh/core/menus/install_menu.py | 11 +- kiauh/core/menus/main_menu.py | 39 +++--- kiauh/core/menus/remove_menu.py | 11 +- kiauh/core/menus/settings_menu.py | 24 ++-- kiauh/core/menus/update_menu.py | 49 +++----- kiauh/core/spinner.py | 23 +--- kiauh/core/types/__init__.py | 0 kiauh/core/types/color.py | 29 +++++ .../{types.py => types/component_status.py} | 0 kiauh/extensions/extensions_menu.py | 20 ++- .../mainsail_theme_installer_extension.py | 13 +- kiauh/utils/common.py | 7 +- kiauh/utils/input_utils.py | 5 +- 35 files changed, 377 insertions(+), 423 deletions(-) create mode 100644 kiauh/core/types/__init__.py create mode 100644 kiauh/core/types/color.py rename kiauh/core/{types.py => types/component_status.py} (100%) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py index 085a9b2..a330973 100644 --- a/kiauh/components/crowsnest/crowsnest.py +++ b/kiauh/components/crowsnest/crowsnest.py @@ -30,7 +30,7 @@ from core.backup_manager.backup_manager import BackupManager from core.constants import CURRENT_USER from core.logger import DialogType, Logger from core.settings.kiauh_settings import KiauhSettings -from core.types import ComponentStatus +from core.types.component_status import ComponentStatus from utils.common import ( check_install_dependencies, get_install_status, diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py index 61895e7..c54abdb 100644 --- a/kiauh/components/klipper/klipper_dialogs.py +++ b/kiauh/components/klipper/klipper_dialogs.py @@ -11,13 +11,8 @@ import textwrap from enum import Enum, unique from typing import List -from core.constants import ( - COLOR_CYAN, - COLOR_GREEN, - COLOR_YELLOW, - RESET_FORMAT, -) from core.menus.base_menu import print_back_footer +from core.types.color import Color from utils.instance_type import InstanceType @@ -42,12 +37,12 @@ def print_instance_overview( if display_type is DisplayType.SERVICE_NAME else "printer directories" ) - headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}" + headline = Color.apply(f"The following {d_type} were found:", Color.GREEN) dialog += f"║{headline:^64}║\n" dialog += "╟───────────────────────────────────────────────────────╢\n" if show_select_all: - select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" + select_all = Color.apply("a) Select all", Color.YELLOW) dialog += f"║ {select_all:<63}║\n" dialog += "║ ║\n" @@ -56,7 +51,9 @@ def print_instance_overview( name = s.service_file_path.stem else: name = s.data_dir - line = f"{COLOR_CYAN}{f'{i + start_index})' if show_index else '●'} {name}{RESET_FORMAT}" + line = Color.apply( + f"{f'{i + start_index})' if show_index else '●'} {name}", Color.CYAN + ) dialog += f"║ {line:<63}║\n" dialog += "╟───────────────────────────────────────────────────────╢\n" @@ -65,8 +62,10 @@ def print_instance_overview( def print_select_instance_count_dialog() -> None: - line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}" + line1 = Color.apply("WARNING:", Color.YELLOW) + line2 = Color.apply( + "Setting up too many instances may crash your system.", Color.YELLOW + ) dialog = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -85,8 +84,8 @@ def print_select_instance_count_dialog() -> None: def print_select_custom_name_dialog() -> None: - line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" - line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}" + line1 = Color.apply("INFO:", Color.YELLOW) + line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW) dialog = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py index 1f161db..a85e531 100644 --- a/kiauh/components/klipper/klipper_utils.py +++ b/kiauh/components/klipper/klipper_utils.py @@ -36,7 +36,7 @@ from core.logger import DialogType, Logger from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, ) -from core.types import ComponentStatus +from core.types.component_status import ComponentStatus from utils.common import get_install_status from utils.input_utils import get_confirm, get_number_input, get_string_input from utils.instance_utils import get_instances diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index c488a8a..4785806 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -12,15 +12,17 @@ import textwrap from typing import Type from components.klipper import klipper_remove -from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu +from core.types.color import Color # noinspection PyUnusedLocal class KlipperRemoveMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Remove Klipper" + self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu self.footer_type = FooterType.BACK self.remove_klipper_service = False @@ -43,18 +45,13 @@ class KlipperRemoveMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Remove Klipper ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + checked = f"[{Color.apply('x', Color.CYAN)}]" unchecked = "[ ]" o1 = checked if self.remove_klipper_service else unchecked o2 = checked if self.remove_klipper_dir else unchecked o3 = checked if self.remove_klipper_env else unchecked menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ @@ -92,8 +89,11 @@ class KlipperRemoveMenu(BaseMenu): and not self.remove_klipper_dir and not self.remove_klipper_env ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) + print( + Color.apply( + "Nothing selected! Select options to remove first.", Color.RED + ) + ) return klipper_remove.run_klipper_removal( diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index 6a3ab5b..1237eed 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -17,10 +17,10 @@ from components.klipper_firmware.firmware_utils import ( run_make_clean, run_make_menuconfig, ) -from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT from core.logger import Logger from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color from utils.sys_utils import ( check_package_install, install_system_packages, @@ -33,6 +33,8 @@ from utils.sys_utils import ( class KlipperBuildFirmwareMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Build Firmware Menu" + self.title_color = Color.CYAN self.previous_menu: Type[BaseMenu] | None = previous_menu self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} self.missing_deps: List[str] = check_package_install(self.deps) @@ -53,13 +55,8 @@ class KlipperBuildFirmwareMenu(BaseMenu): self.default_option = Option(method=self.install_missing_deps) def print_menu(self) -> None: - header = " [ Build Firmware Menu ] " - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────────────────────────────────╢ ║ The following dependencies are required: ║ ║ ║ @@ -67,20 +64,22 @@ class KlipperBuildFirmwareMenu(BaseMenu): )[1:] for d in self.deps: - status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}" - status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}" + status_ok = Color.apply("*INSTALLED*", Color.GREEN) + status_missing = Color.apply("*MISSING*", Color.RED) status = status_missing if d in self.missing_deps else status_ok padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) - d = f" {COLOR_CYAN}● {d}{RESET_FORMAT}" + d = Color.apply(f"● {d}", Color.CYAN) menu += f"║ {d}{status:>{padding}} ║\n" menu += "║ ║\n" - if len(self.missing_deps) == 0: - line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}" - else: - line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}" + color = Color.GREEN if len(self.missing_deps) == 0 else Color.RED + txt = ( + "All dependencies are met!" + if len(self.missing_deps) == 0 + else "Dependencies are missing!" + ) - menu += f"║ {line:<62} ║\n" + menu += f"║ {Color.apply(txt, color):<62} ║\n" menu += "╟───────────────────────────────────────────────────────╢\n" print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py index 19ab94e..913e34e 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py @@ -12,9 +12,9 @@ import textwrap from typing import Type from components.klipper_firmware.flash_options import FlashMethod, FlashOptions -from core.constants import COLOR_RED, RESET_FORMAT from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, MenuTitleStyle +from core.types.color import Color # noinspection PyUnusedLocal @@ -22,6 +22,9 @@ from core.menus.base_menu import BaseMenu class KlipperNoFirmwareErrorMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "!!! NO FIRMWARE FILE FOUND !!!" + self.title_color = Color.RED + self.title_style = MenuTitleStyle.PLAIN self.previous_menu: Type[BaseMenu] | None = previous_menu self.flash_options = FlashOptions() @@ -35,16 +38,11 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): self.default_option = Option(method=self.go_back) def print_menu(self) -> None: - header = "!!! NO FIRMWARE FILE FOUND !!!" - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}" + line1 = "Unable to find a compiled firmware file!" menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ - ║ {line1:<62} ║ + ║ {Color.apply(line1, Color.RED):<62} ║ ║ ║ ║ Make sure, that: ║ ║ ● the folder '~/klipper/out' and its content exist ║ @@ -71,6 +69,9 @@ class KlipperNoFirmwareErrorMenu(BaseMenu): class KlipperNoBoardTypesErrorMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "!!! ERROR GETTING BOARD LIST !!!" + self.title_color = Color.RED + self.title_style = MenuTitleStyle.PLAIN self.previous_menu: Type[BaseMenu] | None = previous_menu self.footer_type = FooterType.BLANK self.input_label_txt = "Press ENTER to go back to [Main Menu]" @@ -82,16 +83,11 @@ class KlipperNoBoardTypesErrorMenu(BaseMenu): self.default_option = Option(method=self.go_back) def print_menu(self) -> None: - header = "!!! ERROR GETTING BOARD LIST !!!" - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}" + line1 = "Reading the list of supported boards failed!" menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ - ║ {line1:<62} ║ + ║ {Color.apply(line1, Color.RED):<62} ║ ║ ║ ║ Make sure, that: ║ ║ ● the folder '~/klipper' and all its content exist ║ diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py index f69ea21..ef6503a 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py @@ -9,16 +9,21 @@ from __future__ import annotations import textwrap -from typing import Type +from typing import Tuple, Type -from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, MenuTitleStyle +from core.types.color import Color + + +def __title_config__() -> Tuple[str, Color, MenuTitleStyle]: + return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN # noinspection DuplicatedCode class KlipperFlashMethodHelpMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title, self.title_color, self.title_style = __title_config__() self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -34,15 +39,10 @@ class KlipperFlashMethodHelpMenu(BaseMenu): pass def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}" + subheader1 = Color.apply("Regular flashing method:", Color.CYAN) + subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {subheader1:<62} ║ ║ The default method to flash controller boards which ║ @@ -77,6 +77,7 @@ class KlipperFlashMethodHelpMenu(BaseMenu): class KlipperFlashCommandHelpMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title, self.title_color, self.title_style = __title_config__() self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -92,15 +93,10 @@ class KlipperFlashCommandHelpMenu(BaseMenu): pass def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}" + subheader1 = Color.apply("make flash:", Color.CYAN) + subheader2 = Color.apply("make serialflash:", Color.CYAN) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {subheader1:<62} ║ ║ The default command to flash controller board, it ║ @@ -121,6 +117,7 @@ class KlipperFlashCommandHelpMenu(BaseMenu): class KlipperMcuConnectionHelpMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title, self.title_color, self.title_style = __title_config__() self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -138,17 +135,12 @@ class KlipperMcuConnectionHelpMenu(BaseMenu): pass def print_menu(self) -> None: - header = " < ? > Help: Flash MCU < ? > " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) - subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" - subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}" - subheader3 = f"{COLOR_CYAN}USB DFU:{RESET_FORMAT}" - subheader4 = f"{COLOR_CYAN}USB RP2040 Boot:{RESET_FORMAT}" + subheader1 = Color.apply("USB:", Color.CYAN) + subheader2 = Color.apply("UART:", Color.CYAN) + subheader3 = Color.apply("USB DFU:", Color.CYAN) + subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {subheader1:<62} ║ ║ Selecting USB as the connection method will scan the ║ diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index b1373f0..b7d741b 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -36,10 +36,10 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import ( KlipperFlashMethodHelpMenu, KlipperMcuConnectionHelpMenu, ) -from core.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT from core.logger import DialogType, Logger from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, MenuTitleStyle +from core.types.color import Color from utils.input_utils import get_number_input @@ -48,6 +48,8 @@ from utils.input_utils import get_number_input class KlipperFlashMethodMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "MCU Flash Menu" + self.title_color = Color.CYAN self.help_menu = KlipperFlashMethodHelpMenu self.input_label_txt = "Select flash method" self.footer_type = FooterType.BACK_HELP @@ -67,17 +69,13 @@ class KlipperFlashMethodMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ MCU Flash Menu ] " - subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}" - subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}" - subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}" - - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) + subheader = Color.apply("ATTENTION:", Color.YELLOW) + subline1 = Color.apply( + "Make sure to select the correct method for the MCU!", Color.YELLOW + ) + subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Select the flash method for flashing the MCU. ║ ║ ║ @@ -112,6 +110,9 @@ class KlipperFlashMethodMenu(BaseMenu): class KlipperFlashCommandMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Which flash command to use for flashing the MCU?" + self.title_style = MenuTitleStyle.PLAIN + self.title_color = Color.YELLOW self.help_menu = KlipperFlashCommandHelpMenu self.input_label_txt = "Select flash command" self.footer_type = FooterType.BACK_HELP @@ -132,8 +133,6 @@ class KlipperFlashCommandMenu(BaseMenu): def print_menu(self) -> None: menu = textwrap.dedent( """ - ╔═══════════════════════════════════════════════════════╗ - ║ Which flash command to use for flashing the MCU? ║ ╟───────────────────────────────────────────────────────╢ ║ 1) make flash (default) ║ ║ 2) make serialflash (stm32flash) ║ @@ -161,6 +160,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False ): super().__init__() + self.title = "Make sure that the controller board is connected now!" + self.title_style = MenuTitleStyle.PLAIN + self.title_color = Color.YELLOW self.previous_menu: Type[BaseMenu] | None = previous_menu self.__standalone = standalone self.help_menu = KlipperMcuConnectionHelpMenu @@ -182,13 +184,8 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): } def print_menu(self) -> None: - header = "Make sure that the controller board is connected now!" - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────────────────────────────────╢ ║ How is the controller board connected to the host? ║ ╟───────────────────────────────────────────────────────╢ @@ -230,7 +227,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): Logger.print_status("Identifying MCU connected via USB in DFU mode ...") self.flash_options.mcu_list = find_usb_dfu_device() elif conn_type is ConnectionType.USB_RP2040: - Logger.print_status("Identifying MCU connected via USB in RP2 Boot mode ...") + Logger.print_status( + "Identifying MCU connected via USB in RP2 Boot mode ..." + ) self.flash_options.mcu_list = find_usb_rp2_boot_device() if len(self.flash_options.mcu_list) < 1: @@ -241,7 +240,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): if self.__standalone and len(self.flash_options.mcu_list) > 0: Logger.print_ok("The following MCUs were found:", prefix=False) for i, mcu in enumerate(self.flash_options.mcu_list): - print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}") + print(f" ● MCU #{i}: {Color.CYAN}{mcu}{Color.RST}") time.sleep(3) return @@ -256,6 +255,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu): class KlipperSelectMcuIdMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "!!! ATTENTION !!!" + self.title_style = MenuTitleStyle.PLAIN + self.title_color = Color.RED self.flash_options = FlashOptions() self.mcu_list = self.flash_options.mcu_list self.input_label_txt = "Select MCU to flash" @@ -274,14 +276,9 @@ class KlipperSelectMcuIdMenu(BaseMenu): } def print_menu(self) -> None: - header = "!!! ATTENTION !!!" - header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]" - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) + header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]" menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Make sure, to select the correct MCU! ║ ║ ONLY flash a firmware created for the respective MCU! ║ @@ -293,7 +290,7 @@ class KlipperSelectMcuIdMenu(BaseMenu): for i, mcu in enumerate(self.mcu_list): mcu = mcu.split("/")[-1] - menu += f"║ {i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}║\n" + menu += f"║ {i}) {Color.apply(f'{mcu:<51}', Color.CYAN)}║\n" menu += textwrap.dedent( """ @@ -348,7 +345,6 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): else: menu = textwrap.dedent( """ - ╔═══════════════════════════════════════════════════════╗ ║ Please select the type of board that corresponds to ║ ║ the currently selected MCU ID you chose before. ║ ║ ║ @@ -400,6 +396,9 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu): class KlipperFlashOverviewMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "!!! ATTENTION !!!" + self.title_style = MenuTitleStyle.PLAIN + self.title_color = Color.RED self.flash_options = FlashOptions() self.input_label_txt = "Perform action (default=Y)" @@ -415,21 +414,16 @@ class KlipperFlashOverviewMenu(BaseMenu): self.default_option = Option(self.execute_flash) def print_menu(self) -> None: - header = "!!! ATTENTION !!!" - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - method = self.flash_options.flash_method.value command = self.flash_options.flash_command.value conn_type = self.flash_options.connection_type.value mcu = self.flash_options.selected_mcu.split("/")[-1] board = self.flash_options.selected_board baudrate = self.flash_options.selected_baudrate - subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" + color = Color.CYAN + subheader = f"[{Color.apply('Overview', color)}]" menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Before contuining the flashing process, please check ║ ║ if all parameters were set correctly! Once you made ║ @@ -443,18 +437,18 @@ class KlipperFlashOverviewMenu(BaseMenu): menu += textwrap.dedent( f""" - ║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT} ║ - ║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT} ║ - ║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT} ║ - ║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT} ║ + ║ MCU: {Color.apply(f"{mcu:<48}", color)} ║ + ║ Connection: {Color.apply(f"{conn_type:<41}", color)} ║ + ║ Flash method: {Color.apply(f"{method:<39}", color)} ║ + ║ Flash command: {Color.apply(f"{command:<38}", color)} ║ """ )[1:] if self.flash_options.flash_method is FlashMethod.SD_CARD: menu += textwrap.dedent( f""" - ║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT} ║ - ║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT} ║ + ║ Board type: {Color.apply(f"{board:<41}", color)} ║ + ║ Baudrate: {Color.apply(f"{baudrate:<43}", color)} ║ """ )[1:] diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py index 692b722..4834f07 100644 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ b/kiauh/components/klipperscreen/klipperscreen.py @@ -30,7 +30,7 @@ from core.constants import SYSTEMD from core.instance_manager.instance_manager import InstanceManager from core.logger import DialogType, Logger from core.settings.kiauh_settings import KiauhSettings -from core.types import ComponentStatus +from core.types.component_status import ComponentStatus from utils.common import ( check_install_dependencies, get_install_status, diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py index 5867e4e..5ef885a 100644 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ b/kiauh/components/log_uploads/menus/log_upload_menu.py @@ -12,16 +12,18 @@ import textwrap from typing import Type from components.log_uploads.log_upload_utils import get_logfile_list, upload_logfile -from core.constants import COLOR_YELLOW, RESET_FORMAT from core.logger import Logger from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color # noinspection PyMethodMayBeStatic class LogUploadMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Log Upload" + self.title_color = Color.YELLOW self.previous_menu: Type[BaseMenu] | None = previous_menu self.logfile_list = get_logfile_list() @@ -37,13 +39,8 @@ class LogUploadMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Log Upload ] " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────────────────────────────────╢ ║ You can select the following logfiles for uploading: ║ ║ ║ diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py index 2721675..39db4b5 100644 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ b/kiauh/components/moonraker/menus/moonraker_remove_menu.py @@ -12,15 +12,17 @@ import textwrap from typing import Type from components.moonraker import moonraker_remove -from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color # noinspection PyUnusedLocal class MoonrakerRemoveMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Remove Moonraker" + self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu self.remove_moonraker_service = False self.remove_moonraker_dir = False @@ -44,10 +46,7 @@ class MoonrakerRemoveMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Remove Moonraker ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + checked = f"[{Color.apply('x', Color.CYAN)}]" unchecked = "[ ]" o1 = checked if self.remove_moonraker_service else unchecked o2 = checked if self.remove_moonraker_dir else unchecked @@ -55,8 +54,6 @@ class MoonrakerRemoveMenu(BaseMenu): o4 = checked if self.remove_moonraker_polkit else unchecked menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ @@ -100,8 +97,11 @@ class MoonrakerRemoveMenu(BaseMenu): and not self.remove_moonraker_env and not self.remove_moonraker_polkit ): - error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" - print(error) + print( + Color.apply( + "Nothing selected! Select options to remove first.", Color.RED + ) + ) return moonraker_remove.run_moonraker_removal( diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py index 63e6789..a445149 100644 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ b/kiauh/components/moonraker/moonraker_dialogs.py @@ -12,8 +12,8 @@ from typing import List from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker -from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT from core.menus.base_menu import print_back_footer +from core.types.color import Color def print_moonraker_overview( @@ -22,7 +22,7 @@ def print_moonraker_overview( show_index=False, show_select_all=False, ): - headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}" + headline = Color.apply("The following instances were found:", Color.GREEN) dialog = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ @@ -32,7 +32,7 @@ def print_moonraker_overview( )[1:] if show_select_all: - select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" + select_all = Color.apply("a) Select all", Color.YELLOW) dialog += f"║ {select_all:<63}║\n" dialog += "║ ║\n" @@ -48,12 +48,16 @@ def print_moonraker_overview( for i, k in enumerate(instance_map): mr_name = instance_map.get(k) m = f"<-> {mr_name}" if mr_name != "" else "" - line = f"{COLOR_CYAN}{f'{i+1})' if show_index else '●'} {k} {m} {RESET_FORMAT}" + line = Color.apply(f"{f'{i+1})' if show_index else '●'} {k} {m}", Color.CYAN) dialog += f"║ {line:<63}║\n" - warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}" - warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}" - warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}" + warn_l1 = Color.apply("PLEASE NOTE:", Color.YELLOW) + warn_l2 = Color.apply( + "If you select an instance with an existing Moonraker", Color.YELLOW + ) + warn_l3 = Color.apply( + "instance, that Moonraker instance will be re-created!", Color.YELLOW + ) warning = textwrap.dedent( f""" ║ ║ diff --git a/kiauh/components/moonraker/moonraker_utils.py b/kiauh/components/moonraker/moonraker_utils.py index f23e34b..3241edb 100644 --- a/kiauh/components/moonraker/moonraker_utils.py +++ b/kiauh/components/moonraker/moonraker_utils.py @@ -25,7 +25,7 @@ from core.logger import Logger from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, ) -from core.types import ComponentStatus +from core.types.component_status import ComponentStatus from utils.common import get_install_status from utils.instance_utils import get_instances from utils.sys_utils import ( diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py index 89bcac8..6456469 100644 --- a/kiauh/components/webui_client/client_setup.py +++ b/kiauh/components/webui_client/client_setup.py @@ -36,8 +36,9 @@ from components.webui_client.client_utils import ( symlink_webui_nginx_log, ) from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogCustomColor, DialogType, Logger +from core.logger import DialogType, Logger from core.settings.kiauh_settings import KiauhSettings +from core.types.color import Color from utils.common import backup_printer_config_dir, check_install_dependencies from utils.config_utils import add_config_section from utils.fs_utils import unzip @@ -140,7 +141,7 @@ def install_client( Logger.print_dialog( DialogType.CUSTOM, custom_title=f"{client.display_name} installation complete!", - custom_color=DialogCustomColor.GREEN, + custom_color=Color.GREEN, center_content=True, content=[ f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}", diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py index 56699d1..52757d9 100644 --- a/kiauh/components/webui_client/client_utils.py +++ b/kiauh/components/webui_client/client_utils.py @@ -26,19 +26,17 @@ from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from core.backup_manager.backup_manager import BackupManager from core.constants import ( - COLOR_CYAN, - COLOR_YELLOW, NGINX_CONFD, NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED, - RESET_FORMAT, ) from core.logger import Logger from core.settings.kiauh_settings import KiauhSettings, WebUiSettings from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( SimpleConfigParser, ) -from core.types import ComponentStatus +from core.types.color import Color +from core.types.component_status import ComponentStatus from utils.common import get_install_status from utils.fs_utils import create_symlink, remove_file from utils.git_utils import ( @@ -79,10 +77,10 @@ def get_current_client_config() -> str: installed = [c for c in clients if c.client_config.config_dir.exists()] if not installed: - return f"{COLOR_CYAN}-{RESET_FORMAT}" + return Color.apply("-", Color.CYAN) elif len(installed) == 1: cfg = installed[0].client_config - return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}" + return Color.apply(cfg.display_name, Color.CYAN) # at this point, both client config folders exists, so we need to check # which are actually included in the printer.cfg of all klipper instances @@ -101,18 +99,18 @@ def get_current_client_config() -> str: # if both are included in the same file, we have a potential conflict if includes_mainsail and includes_fluidd: - return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}" + return Color.apply("Conflict", Color.YELLOW) if not mainsail_includes and not fluidd_includes: # there are no includes at all, even though the client config folders exist - return f"{COLOR_CYAN}-{RESET_FORMAT}" + return Color.apply("-", Color.CYAN) elif len(fluidd_includes) > len(mainsail_includes): # there are more instances that include fluidd than mainsail - return f"{COLOR_CYAN}{fluidd.client_config.display_name}{RESET_FORMAT}" + return Color.apply(fluidd.client_config.display_name, Color.CYAN) else: # there are the same amount of non-conflicting includes for each config # or more instances include mainsail than fluidd - return f"{COLOR_CYAN}{mainsail.client_config.display_name}{RESET_FORMAT}" + return Color.apply(mainsail.client_config.display_name, Color.CYAN) def enable_mainsail_remotemode() -> None: diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py index ae3ad42..d2dcce1 100644 --- a/kiauh/components/webui_client/menus/client_install_menu.py +++ b/kiauh/components/webui_client/menus/client_install_menu.py @@ -18,11 +18,11 @@ from components.webui_client.client_utils import ( get_nginx_listen_port, set_listen_port, ) -from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT -from core.logger import DialogCustomColor, DialogType, Logger +from core.logger import DialogType, Logger from core.menus import Option from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings, WebUiSettings +from core.types.color import Color from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr @@ -32,6 +32,8 @@ class ClientInstallMenu(BaseMenu): self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None ): super().__init__() + self.title = f"Installation Menu > {client.display_name}" + self.title_color = Color.GREEN self.previous_menu: Type[BaseMenu] | None = previous_menu self.client: BaseWebClient = client self.settings = KiauhSettings() @@ -50,15 +52,9 @@ class ClientInstallMenu(BaseMenu): def print_menu(self) -> None: client_name = self.client.display_name - - header = f" [ Installation Menu > {client_name} ] " - color = COLOR_GREEN - count = 62 - len(color) - len(RESET_FORMAT) - port = f"(Current: {COLOR_CYAN}{self._get_current_port()}{RESET_FORMAT})" + port = f"(Current: {Color.apply(self._get_current_port(), Color.GREEN)})" menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) Reinstall {client_name:16} ║ ║ 2) Reconfigure Listen Port {port:<34} ║ @@ -92,7 +88,7 @@ class ClientInstallMenu(BaseMenu): Logger.print_dialog( DialogType.CUSTOM, custom_title="Port reconfiguration complete!", - custom_color=DialogCustomColor.GREEN, + custom_color=Color.GREEN, center_content=True, content=[ f"Open {self.client.display_name} now on: " diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index cc16049..b94c6e7 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -13,9 +13,9 @@ from typing import Type from components.webui_client import client_remove from components.webui_client.base_data import BaseWebClient -from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color # noinspection PyUnusedLocal @@ -24,6 +24,8 @@ class ClientRemoveMenu(BaseMenu): self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None ): super().__init__() + self.title = f"Remove {client.display_name}" + self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu self.client: BaseWebClient = client self.remove_client: bool = False @@ -50,18 +52,13 @@ class ClientRemoveMenu(BaseMenu): client_config = self.client.client_config client_config_name = client_config.display_name - header = f" [ Remove {client_name} ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) - checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]" + checked = f"[{Color.apply('x', Color.CYAN)}]" unchecked = "[ ]" o1 = checked if self.remove_client else unchecked o2 = checked if self.remove_client_cfg else unchecked o3 = checked if self.backup_config_json else unchecked menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ @@ -99,8 +96,7 @@ class ClientRemoveMenu(BaseMenu): and not self.remove_client_cfg and not self.backup_config_json ): - error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" - print(error) + print(Color.apply("Nothing selected ...", Color.RED)) return client_remove.run_client_removal( diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py index ea75928..099fe24 100644 --- a/kiauh/core/constants.py +++ b/kiauh/core/constants.py @@ -13,15 +13,6 @@ from pathlib import Path from core.backup_manager import BACKUP_ROOT_DIR -# text colors and formats -COLOR_WHITE = "\033[37m" # white -COLOR_MAGENTA = "\033[35m" # magenta -COLOR_GREEN = "\033[92m" # bright green -COLOR_YELLOW = "\033[93m" # bright yellow -COLOR_RED = "\033[91m" # bright red -COLOR_CYAN = "\033[96m" # bright cyan -RESET_FORMAT = "\033[0m" # reset format - # global dependencies GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] diff --git a/kiauh/core/logger.py b/kiauh/core/logger.py index 0387163..0c9db0c 100644 --- a/kiauh/core/logger.py +++ b/kiauh/core/logger.py @@ -12,78 +12,50 @@ import textwrap from enum import Enum from typing import List -from core.constants import ( - COLOR_CYAN, - COLOR_GREEN, - COLOR_MAGENTA, - COLOR_RED, - COLOR_WHITE, - COLOR_YELLOW, - RESET_FORMAT, -) +from core.types.color import Color class DialogType(Enum): - INFO = ("INFO", COLOR_WHITE) - SUCCESS = ("SUCCESS", COLOR_GREEN) - ATTENTION = ("ATTENTION", COLOR_YELLOW) - WARNING = ("WARNING", COLOR_YELLOW) - ERROR = ("ERROR", COLOR_RED) + INFO = ("INFO", Color.WHITE) + SUCCESS = ("SUCCESS", Color.GREEN) + ATTENTION = ("ATTENTION", Color.YELLOW) + WARNING = ("WARNING", Color.YELLOW) + ERROR = ("ERROR", Color.RED) CUSTOM = (None, None) -class DialogCustomColor(Enum): - WHITE = COLOR_WHITE - GREEN = COLOR_GREEN - YELLOW = COLOR_YELLOW - RED = COLOR_RED - CYAN = COLOR_CYAN - MAGENTA = COLOR_MAGENTA - - LINE_WIDTH = 53 class Logger: - @staticmethod - def info(msg) -> None: - # log to kiauh.log - pass - - @staticmethod - def warn(msg) -> None: - # log to kiauh.log - pass - - @staticmethod - def error(msg) -> None: - # log to kiauh.log - pass - @staticmethod def print_info(msg, prefix=True, start="", end="\n") -> None: message = f"[INFO] {msg}" if prefix else msg - print(f"{COLOR_WHITE}{start}{message}{RESET_FORMAT}", end=end) + Logger.__print(Color.WHITE, start, message, end) @staticmethod def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None: message = f"[OK] {msg}" if prefix else msg - print(f"{COLOR_GREEN}{start}{message}{RESET_FORMAT}", end=end) + Logger.__print(Color.GREEN, start, message, end) @staticmethod def print_warn(msg, prefix=True, start="", end="\n") -> None: message = f"[WARN] {msg}" if prefix else msg - print(f"{COLOR_YELLOW}{start}{message}{RESET_FORMAT}", end=end) + Logger.__print(Color.YELLOW, start, message, end) @staticmethod def print_error(msg, prefix=True, start="", end="\n") -> None: message = f"[ERROR] {msg}" if prefix else msg - print(f"{COLOR_RED}{start}{message}{RESET_FORMAT}", end=end) + Logger.__print(Color.RED, start, message, end) @staticmethod def print_status(msg, prefix=True, start="", end="\n") -> None: message = f"\n###### {msg}" if prefix else msg - print(f"{COLOR_MAGENTA}{start}{message}{RESET_FORMAT}", end=end) + Logger.__print(Color.MAGENTA, start, message, end) + + @staticmethod + def __print(color: Color, start: str, message: str, end: str) -> None: + print(Color.apply(f"{start}{message}", color), end=end) @staticmethod def print_dialog( @@ -91,7 +63,7 @@ class Logger: content: List[str], center_content: bool = False, custom_title: str | None = None, - custom_color: DialogCustomColor | None = None, + custom_color: Color | None = None, margin_top: int = 0, margin_bottom: int = 0, ) -> None: @@ -111,10 +83,15 @@ class Logger: """ dialog_color = Logger._get_dialog_color(title, custom_color) dialog_title = Logger._get_dialog_title(title, custom_title) - dialog_title_formatted = Logger._format_dialog_title(dialog_title) - dialog_content = Logger.format_content(content, LINE_WIDTH, center_content) + dialog_title_formatted = Logger._format_dialog_title(dialog_title, dialog_color) + dialog_content = Logger.format_content( + content, + LINE_WIDTH, + dialog_color, + center_content, + ) top = Logger._format_top_border(dialog_color) - bottom = Logger._format_bottom_border() + bottom = Logger._format_bottom_border(dialog_color) print("\n" * margin_top) print( @@ -133,39 +110,45 @@ class Logger: @staticmethod def _get_dialog_color( - title: DialogType, custom_color: DialogCustomColor | None = None - ) -> str: + title: DialogType, custom_color: Color | None = None + ) -> Color: if title == DialogType.CUSTOM and custom_color: - return str(custom_color.value) + return custom_color - color: str = title.value[1] if title.value[1] else DialogCustomColor.WHITE.value + color: Color = title.value[1] if title.value[1] else Color.WHITE return color @staticmethod - def _format_top_border(color: str) -> str: - return f"{color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" - - @staticmethod - def _format_bottom_border() -> str: - return ( - f"\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛{RESET_FORMAT}" + def _format_top_border(color: Color) -> str: + _border = Color.apply( + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", color ) + return _border @staticmethod - def _format_dialog_title(title: str | None) -> str: - if title is not None: - return textwrap.dedent(f""" - ┃ {title:^{LINE_WIDTH}} ┃ - ┠───────────────────────────────────────────────────────┨ - """) - else: + def _format_bottom_border(color: Color) -> str: + _border = Color.apply( + "\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛", color + ) + return _border + + @staticmethod + def _format_dialog_title(title: str | None, color: Color) -> str: + if title is None: return "\n" + _title = Color.apply(f"┃ {title:^{LINE_WIDTH}} ┃\n", color) + _title += Color.apply( + "┠───────────────────────────────────────────────────────┨\n", color + ) + return _title + @staticmethod def format_content( content: List[str], line_width: int, + color: Color = Color.WHITE, center_content: bool = False, border_left: str = "┃", border_right: str = "┃", @@ -184,11 +167,13 @@ class Logger: if not center_content: formatted_lines = [ - f"{border_left} {line:<{line_width}} {border_right}" for line in lines + Color.apply(f"{border_left} {line:<{line_width}} {border_right}", color) + for line in lines ] else: formatted_lines = [ - f"{border_left} {line:^{line_width}} {border_right}" for line in lines + Color.apply(f"{border_left} {line:^{line_width}} {border_right}", color) + for line in lines ] return "\n".join(formatted_lines) diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 484e1ba..4c52536 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -22,9 +22,9 @@ from components.klipper_firmware.menus.klipper_flash_menu import ( ) from components.moonraker import MOONRAKER_DIR from components.moonraker.moonraker import Moonraker -from core.constants import COLOR_YELLOW, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color from procedures.system import change_system_hostname from utils.git_utils import rollback_repository @@ -34,6 +34,8 @@ from utils.git_utils import rollback_repository class AdvancedMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.title = "Advanced Menu" + self.title_color = Color.YELLOW self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -53,13 +55,8 @@ class AdvancedMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Advanced Menu ] " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────┬───────────────────────────╢ ║ Klipper Firmware: │ Repository Rollback: ║ ║ 1) [Build] │ 5) [Klipper] ║ diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py index be998b1..556f4ab 100644 --- a/kiauh/core/menus/backup_menu.py +++ b/kiauh/core/menus/backup_menu.py @@ -23,9 +23,9 @@ from components.webui_client.client_utils import ( ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData -from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color from utils.common import backup_printer_config_dir @@ -34,6 +34,8 @@ from utils.common import backup_printer_config_dir class BackupMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.title = "Backup Menu" + self.title_color = Color.GREEN self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -55,14 +57,11 @@ class BackupMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Backup Menu ] " - line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}" - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) + line1 = Color.apply( + "INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW + ) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {line1:^62} ║ ╟───────────────────────────┬───────────────────────────╢ diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index a97f3a1..97817e9 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -14,17 +14,13 @@ import sys import textwrap import traceback from abc import abstractmethod +from enum import Enum from typing import Dict, Type -from core.constants import ( - COLOR_CYAN, - COLOR_GREEN, - COLOR_RED, - COLOR_YELLOW, - RESET_FORMAT, -) from core.logger import Logger from core.menus import FooterType, Option +from core.spinner import Spinner +from core.types.color import Color from utils.input_utils import get_selection_input @@ -36,14 +32,14 @@ def print_header() -> None: line1 = " [ KIAUH ] " line2 = "Klipper Installation And Update Helper" line3 = "" - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) + color = Color.CYAN + count = 62 - len(str(color)) - len(str(Color.RST)) header = textwrap.dedent( f""" ╔═══════════════════════════════════════════════════════╗ - ║ {color}{line1:~^{count}}{RESET_FORMAT} ║ - ║ {color}{line2:^{count}}{RESET_FORMAT} ║ - ║ {color}{line3:~^{count}}{RESET_FORMAT} ║ + ║ {Color.apply(f"{line1:~^{count}}", color)} ║ + ║ {Color.apply(f"{line2:^{count}}", color)} ║ + ║ {Color.apply(f"{line3:~^{count}}", color)} ║ ╚═══════════════════════════════════════════════════════╝ """ )[1:] @@ -52,11 +48,11 @@ def print_header() -> None: def print_quit_footer() -> None: text = "Q) Quit" - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) + color = Color.RED + count = 62 - len(str(color)) - len(str(Color.RST)) footer = textwrap.dedent( f""" - ║ {color}{text:^{count}}{RESET_FORMAT} ║ + ║ {color}{text:^{count}}{Color.RST} ║ ╚═══════════════════════════════════════════════════════╝ """ )[1:] @@ -65,11 +61,11 @@ def print_quit_footer() -> None: def print_back_footer() -> None: text = "B) « Back" - color = COLOR_GREEN - count = 62 - len(color) - len(RESET_FORMAT) + color = Color.GREEN + count = 62 - len(str(color)) - len(str(Color.RST)) footer = textwrap.dedent( f""" - ║ {color}{text:^{count}}{RESET_FORMAT} ║ + ║ {color}{text:^{count}}{Color.RST} ║ ╚═══════════════════════════════════════════════════════╝ """ )[1:] @@ -79,12 +75,12 @@ def print_back_footer() -> None: def print_back_help_footer() -> None: text1 = "B) « Back" text2 = "H) Help [?]" - color1 = COLOR_GREEN - color2 = COLOR_YELLOW - count = 34 - len(color1) - len(RESET_FORMAT) + color1 = Color.GREEN + color2 = Color.YELLOW + count = 34 - len(str(color1)) - len(str(Color.RST)) footer = textwrap.dedent( f""" - ║ {color1}{text1:^{count}}{RESET_FORMAT} │ {color2}{text2:^{count}}{RESET_FORMAT} ║ + ║ {color1}{text1:^{count}}{Color.RST} │ {color2}{text2:^{count}}{Color.RST} ║ ╚═══════════════════════════╧═══════════════════════════╝ """ )[1:] @@ -95,6 +91,11 @@ 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) @@ -110,6 +111,14 @@ class BaseMenu(metaclass=PostInitCaller): 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 @@ -160,7 +169,32 @@ class BaseMenu(metaclass=PostInitCaller): def print_menu(self) -> None: raise NotImplementedError - def print_footer(self) -> None: + 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: @@ -172,16 +206,17 @@ class BaseMenu(metaclass=PostInitCaller): else: raise NotImplementedError("FooterType not correctly implemented!") - def display_menu(self) -> None: + def __display_menu(self) -> None: if self.header: print_header() + self.__print_menu_title() self.print_menu() - self.print_footer() + 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() + self.__display_menu() option = get_selection_input(self.input_label_txt, self.options) selected_option: Option = self.options.get(option) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py index b3f7576..062a8cf 100644 --- a/kiauh/core/menus/install_menu.py +++ b/kiauh/core/menus/install_menu.py @@ -22,10 +22,10 @@ from components.webui_client.client_setup import install_client from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from components.webui_client.menus.client_install_menu import ClientInstallMenu -from core.constants import COLOR_GREEN, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings +from core.types.color import Color # noinspection PyUnusedLocal @@ -33,6 +33,8 @@ from core.settings.kiauh_settings import KiauhSettings class InstallMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.title = "Installation Menu" + self.title_color = Color.GREEN self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -53,13 +55,8 @@ class InstallMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Installation Menu ] " - color = COLOR_GREEN - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────┬───────────────────────────╢ ║ Firmware & API: │ Touchscreen GUI: ║ ║ 1) [Klipper] │ 7) [KlipperScreen] ║ diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 8ca1b5d..7ab4c8b 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -23,14 +23,6 @@ from components.webui_client.client_utils import ( ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData -from core.constants import ( - COLOR_CYAN, - COLOR_GREEN, - COLOR_MAGENTA, - COLOR_RED, - COLOR_YELLOW, - RESET_FORMAT, -) from core.logger import Logger from core.menus import FooterType from core.menus.advanced_menu import AdvancedMenu @@ -40,7 +32,8 @@ from core.menus.install_menu import InstallMenu from core.menus.remove_menu import RemoveMenu from core.menus.settings_menu import SettingsMenu from core.menus.update_menu import UpdateMenu -from core.types import ComponentStatus, StatusMap, StatusText +from core.types.color import Color +from core.types.component_status import ComponentStatus, StatusMap, StatusText from extensions.extensions_menu import ExtensionsMenu from utils.common import get_kiauh_version, trunc_string @@ -52,6 +45,8 @@ class MainMenu(BaseMenu): super().__init__() self.header: bool = True + self.title = "Main Menu" + self.title_color = Color.CYAN self.footer_type: FooterType = FooterType.QUIT self.version = "" @@ -83,7 +78,7 @@ class MainMenu(BaseMenu): setattr( self, f"{var}_status", - f"{COLOR_RED}Not installed{RESET_FORMAT}", + Color.apply("Not installed", Color.RED), ) def _fetch_status(self) -> None: @@ -109,34 +104,30 @@ class MainMenu(BaseMenu): count_txt = f": {instance_count}" setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) - setattr(self, f"{name}_owner", f"{COLOR_CYAN}{owner}{RESET_FORMAT}") - setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}") + setattr(self, f"{name}_owner", Color.apply(owner, Color.CYAN)) + setattr(self, f"{name}_repo", Color.apply(repo, Color.CYAN)) def _format_by_code(self, code: int, status: str, count: str) -> str: - color = COLOR_RED + color = Color.RED if code == 0: - color = COLOR_RED + color = Color.RED elif code == 1: - color = COLOR_YELLOW + color = Color.YELLOW elif code == 2: - color = COLOR_GREEN + color = Color.GREEN - return f"{color}{status}{count}{RESET_FORMAT}" + return Color.apply(f"{status}{count}", color) def print_menu(self) -> None: self._fetch_status() - header = " [ Main Menu ] " - footer1 = f"{COLOR_CYAN}{self.version}{RESET_FORMAT}" - footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" - color = COLOR_CYAN - count = 62 - len(color) - len(RESET_FORMAT) + footer1 = Color.apply(self.version, Color.CYAN) + link = Color.apply("https://git.io/JnmlX", Color.MAGENTA) + footer2 = f"Changelog: {link}" pad1 = 32 pad2 = 26 menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟──────────────────┬────────────────────────────────────╢ ║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}} ║ ║ │ Owner: {self.kl_owner:<{pad1}} ║ diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py index 005e950..e471ba7 100644 --- a/kiauh/core/menus/remove_menu.py +++ b/kiauh/core/menus/remove_menu.py @@ -20,9 +20,9 @@ from components.moonraker.menus.moonraker_remove_menu import ( from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData from components.webui_client.menus.client_remove_menu import ClientRemoveMenu -from core.constants import COLOR_RED, RESET_FORMAT from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color # noinspection PyUnusedLocal @@ -30,6 +30,8 @@ from core.menus.base_menu import BaseMenu class RemoveMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.title = "Remove Menu" + self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: @@ -48,13 +50,8 @@ class RemoveMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Remove Menu ] " - color = COLOR_RED - count = 62 - len(color) - len(RESET_FORMAT) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────────────────────────────────╢ ║ INFO: Configurations and/or any backups will be kept! ║ ╟───────────────────────────┬───────────────────────────╢ diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index dd67d2d..c8e4de4 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -13,11 +13,11 @@ from typing import Literal, Tuple, Type from components.klipper.klipper_utils import get_klipper_status from components.moonraker.moonraker_utils import get_moonraker_status -from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT from core.logger import DialogType, Logger from core.menus import Option from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings, RepoSettings +from core.types.color import Color from procedures.switch_repo import run_switch_repo_routine from utils.input_utils import get_confirm, get_string_input @@ -27,6 +27,8 @@ from utils.input_utils import get_confirm, get_string_input class SettingsMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.title = "Settings Menu" + self.title_color = Color.CYAN self.previous_menu: Type[BaseMenu] | None = previous_menu self.klipper_status = get_klipper_status() self.moonraker_status = get_moonraker_status() @@ -50,25 +52,21 @@ class SettingsMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ KIAUH Settings ] " - color, rst = COLOR_CYAN, RESET_FORMAT - count = 62 - len(color) - len(rst) - checked = f"[{COLOR_GREEN}x{rst}]" + color = Color.CYAN + checked = f"[{Color.apply('x', Color.GREEN)}]" unchecked = "[ ]" - kl_repo: str = f"{color}{self.klipper_status.repo}{rst}" - kl_branch: str = f"{color}{self.klipper_status.branch}{rst}" - kl_owner: str = f"{color}{self.klipper_status.owner}{rst}" - mr_repo: str = f"{color}{self.moonraker_status.repo}{rst}" - mr_branch: str = f"{color}{self.moonraker_status.branch}{rst}" - mr_owner: str = f"{color}{self.moonraker_status.owner}{rst}" + kl_repo: str = Color.apply(self.klipper_status.repo, color) + kl_branch: str = Color.apply(self.klipper_status.branch, color) + kl_owner: str = Color.apply(self.klipper_status.owner, color) + mr_repo: str = Color.apply(self.moonraker_status.repo, color) + mr_branch: str = Color.apply(self.moonraker_status.branch, color) + mr_owner: str = Color.apply(self.moonraker_status.owner, color) o1 = checked if self.mainsail_unstable else unchecked o2 = checked if self.fluidd_unstable else unchecked o3 = checked if self.auto_backups_enabled else unchecked menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{rst} ║ ╟───────────────────────────────────────────────────────╢ ║ Klipper: ║ ║ ● Repo: {kl_repo:51} ║ diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py index b07d8a8..c48afe9 100644 --- a/kiauh/core/menus/update_menu.py +++ b/kiauh/core/menus/update_menu.py @@ -32,17 +32,11 @@ from components.webui_client.client_utils import ( ) from components.webui_client.fluidd_data import FluiddData from components.webui_client.mainsail_data import MainsailData -from core.constants import ( - COLOR_GREEN, - COLOR_RED, - COLOR_YELLOW, - RESET_FORMAT, -) from core.logger import DialogType, Logger from core.menus import Option from core.menus.base_menu import BaseMenu -from core.spinner import Spinner -from core.types import ComponentStatus +from core.types.color import Color +from core.types.component_status import ComponentStatus from utils.input_utils import get_confirm from utils.sys_utils import ( get_upgradable_packages, @@ -56,6 +50,11 @@ from utils.sys_utils import ( class UpdateMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: super().__init__() + self.loading_msg = "Loading update menu, please wait" + self.is_loading(True) + + self.title = "Update Menu" + self.title_color = Color.GREEN self.previous_menu: Type[BaseMenu] | None = previous_menu self.packages: List[str] = [] @@ -123,6 +122,9 @@ class UpdateMenu(BaseMenu): }, } + self._fetch_update_status() + self.is_loading(False) + def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.main_menu import MainMenu @@ -143,29 +145,16 @@ class UpdateMenu(BaseMenu): } def print_menu(self) -> None: - spinner = Spinner("Loading update menu, please wait", color="green") - spinner.start() - - self._fetch_update_status() - - spinner.stop() - - header = " [ Update Menu ] " - color = COLOR_GREEN - count = 62 - len(color) - len(RESET_FORMAT) - sysupgrades: str = "No upgrades available." padding = 29 if self.package_count > 0: - sysupgrades = ( - f"{COLOR_GREEN}{self.package_count} upgrades available!{RESET_FORMAT}" + sysupgrades = Color.apply( + f"{self.package_count} upgrades available!", Color.GREEN ) padding = 38 menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────┬───────────────┬───────────────╢ ║ a) Update all │ │ ║ ║ │ Current: │ Latest: ║ @@ -265,15 +254,15 @@ class UpdateMenu(BaseMenu): self.package_count = len(self.packages) def _format_local_status(self, local_version, remote_version) -> str: - color = COLOR_RED + color = Color.RED if not local_version: - color = COLOR_RED + color = Color.RED elif local_version == remote_version: - color = COLOR_GREEN + color = Color.GREEN elif local_version != remote_version: - color = COLOR_YELLOW + color = Color.YELLOW - return f"{color}{local_version or '-'}{RESET_FORMAT}" + return Color.apply(local_version or "-", color) def _set_status_data(self, name: str, status_fn: Callable, *args) -> None: comp_status: ComponentStatus = status_fn(*args) @@ -288,9 +277,9 @@ class UpdateMenu(BaseMenu): local_status = self.status_data[name].get("local", None) remote_status = self.status_data[name].get("remote", None) - color = COLOR_GREEN if remote_status else COLOR_RED + color = Color.GREEN if remote_status else Color.RED local_txt = self._format_local_status(local_status, remote_status) - remote_txt = f"{color}{remote_status or '-'}{RESET_FORMAT}" + remote_txt = Color.apply(remote_status or "-", color) setattr(self, f"{name}_local", local_txt) setattr(self, f"{name}_remote", remote_txt) diff --git a/kiauh/core/spinner.py b/kiauh/core/spinner.py index 55da0f1..db603ac 100644 --- a/kiauh/core/spinner.py +++ b/kiauh/core/spinner.py @@ -3,13 +3,7 @@ import threading import time from typing import List, Literal -from core.constants import ( - COLOR_GREEN, - COLOR_RED, - COLOR_WHITE, - COLOR_YELLOW, - RESET_FORMAT, -) +from core.types.color import Color SpinnerColor = Literal["white", "red", "green", "yellow"] @@ -18,21 +12,18 @@ class Spinner: def __init__( self, message: str = "Loading", - color: SpinnerColor = "white", interval: float = 0.2, ) -> None: self.message = f"{message} ..." self.interval = interval self._stop_event = threading.Event() self._thread = threading.Thread(target=self._animate) - self._color = "" - self._set_color(color) def _animate(self) -> None: animation: List[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] while not self._stop_event.is_set(): for char in animation: - sys.stdout.write(f"\r{self._color}{char}{RESET_FORMAT} {self.message}") + sys.stdout.write(f"\r{Color.GREEN}{char}{Color.RST} {self.message}") sys.stdout.flush() time.sleep(self.interval) if self._stop_event.is_set(): @@ -40,16 +31,6 @@ class Spinner: sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r") sys.stdout.flush() - def _set_color(self, color: SpinnerColor) -> None: - if color == "white": - self._color = COLOR_WHITE - elif color == "red": - self._color = COLOR_RED - elif color == "green": - self._color = COLOR_GREEN - elif color == "yellow": - self._color = COLOR_YELLOW - def start(self) -> None: self._stop_event.clear() if not self._thread.is_alive(): diff --git a/kiauh/core/types/__init__.py b/kiauh/core/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/core/types/color.py b/kiauh/core/types/color.py new file mode 100644 index 0000000..f20b296 --- /dev/null +++ b/kiauh/core/types/color.py @@ -0,0 +1,29 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 Dominik Willner # +# # +# 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 + +from enum import Enum + + +class Color(Enum): + WHITE = "\033[37m" # white + MAGENTA = "\033[35m" # magenta + GREEN = "\033[92m" # bright green + YELLOW = "\033[93m" # bright yellow + RED = "\033[91m" # bright red + CYAN = "\033[96m" # bright cyan + RST = "\033[0m" # reset format + + def __str__(self): + return self.value + + @staticmethod + def apply(text: str | int, color: "Color") -> str: + """Apply a given color to a given text string.""" + return f"{color}{text}{Color.RST}" diff --git a/kiauh/core/types.py b/kiauh/core/types/component_status.py similarity index 100% rename from kiauh/core/types.py rename to kiauh/core/types/component_status.py diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py index c44f555..579c6cc 100644 --- a/kiauh/extensions/extensions_menu.py +++ b/kiauh/extensions/extensions_menu.py @@ -15,10 +15,10 @@ import textwrap from pathlib import Path from typing import Dict, List, Type -from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from core.logger import Logger from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color from extensions import EXTENSION_ROOT from extensions.base_extension import BaseExtension @@ -28,6 +28,8 @@ from extensions.base_extension import BaseExtension class ExtensionsMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Extensions Menu" + self.title_color = Color.CYAN self.previous_menu: Type[BaseMenu] | None = previous_menu self.extensions: Dict[str, BaseExtension] = self.discover_extensions() @@ -82,14 +84,9 @@ class ExtensionsMenu(BaseMenu): ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() def print_menu(self) -> None: - header = " [ Extensions Menu ] " - color = COLOR_CYAN - line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}" - count = 62 - len(color) - len(RESET_FORMAT) + line1 = Color.apply("Available Extensions:", Color.YELLOW) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {line1:<62} ║ ║ ║ @@ -112,6 +109,8 @@ class ExtensionSubmenu(BaseMenu): self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None ): super().__init__() + self.title = extension.metadata.get("display_name") + self.title_color = Color.YELLOW self.extension = extension self.previous_menu: Type[BaseMenu] | None = previous_menu @@ -129,9 +128,6 @@ class ExtensionSubmenu(BaseMenu): self.options["2"] = Option(self.extension.remove_extension) def print_menu(self) -> None: - header = f" [ {self.extension.metadata.get('display_name')} ] " - color = COLOR_YELLOW - count = 62 - len(color) - len(RESET_FORMAT) line_width = 53 description: List[str] = self.extension.metadata.get("description", []) description_text = Logger.format_content( @@ -142,9 +138,7 @@ class ExtensionSubmenu(BaseMenu): ) menu = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ + """ ╟───────────────────────────────────────────────────────╢ """ )[1:] diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index c613f04..ef76f2d 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -20,10 +20,10 @@ from components.klipper.klipper_dialogs import ( DisplayType, print_instance_overview, ) -from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from core.logger import Logger from core.menus import Option from core.menus.base_menu import BaseMenu +from core.types.color import Color from extensions.base_extension import BaseExtension from utils.git_utils import git_clone_wrapper from utils.input_utils import get_selection_input @@ -80,6 +80,8 @@ class MainsailThemeInstallMenu(BaseMenu): def __init__(self, instances: List[Klipper]): super().__init__() + self.title = "Mainsail Theme Installer" + self.title_color = Color.YELLOW self.themes: List[ThemeData] = self.load_themes() self.instances = instances @@ -97,14 +99,11 @@ class MainsailThemeInstallMenu(BaseMenu): } def print_menu(self) -> None: - header = " [ Mainsail Theme Installer ] " - color = COLOR_YELLOW - line1 = f"{COLOR_CYAN}A preview of each Mainsail theme can be found here:{RESET_FORMAT}" - count = 62 - len(color) - len(RESET_FORMAT) + line1 = Color.apply( + "A preview of each Mainsail theme can be found here:", Color.YELLOW + ) menu = textwrap.dedent( f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {color}{header:~^{count}}{RESET_FORMAT} ║ ╟───────────────────────────────────────────────────────╢ ║ {line1:<62} ║ ║ https://docs.mainsail.xyz/theming/themes ║ diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index 97dcff7..c432e40 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -16,13 +16,12 @@ from typing import Dict, List, Literal, Optional, Set from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from core.constants import ( - COLOR_CYAN, GLOBAL_DEPS, PRINTER_DATA_BACKUP_DIR, - RESET_FORMAT, ) from core.logger import DialogType, Logger -from core.types import ComponentStatus, StatusCode +from core.types.color import Color +from core.types.component_status import ComponentStatus, StatusCode from utils.git_utils import ( get_current_branch, get_local_commit, @@ -83,7 +82,7 @@ def check_install_dependencies( Logger.print_status("Installing dependencies ...") Logger.print_info("The following packages need installation:") for r in requirements: - print(f"{COLOR_CYAN}● {r}{RESET_FORMAT}") + print(Color.apply(f"● {r}", Color.CYAN)) update_system_package_lists(silent=False) install_system_packages(requirements) diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 75af939..ef3f785 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -11,8 +11,9 @@ from __future__ import annotations import re from typing import Dict, List -from core.constants import COLOR_CYAN, INVALID_CHOICE, RESET_FORMAT +from core.constants import INVALID_CHOICE from core.logger import Logger +from core.types.color import Color def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool | None: @@ -151,7 +152,7 @@ def format_question(question: str, default=None) -> str: if default is not None: formatted_q += f" (default={default})" - return f"{COLOR_CYAN}###### {formatted_q}: {RESET_FORMAT}" + return Color.apply(f"###### {formatted_q}: ", Color.CYAN) def validate_number_input(value: str, min_count: int, max_count: int | None) -> int: From 4925021aa8aa19e20ff40b0f1ec35d5471a3e1d9 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 26 Oct 2024 17:37:48 +0200 Subject: [PATCH 03/18] fix: ensure encoding Use an alternative approach as in #587 as it introduces an unexpected behavior in printing output Signed-off-by: Dominik Willner --- kiauh.py | 6 ------ kiauh/main.py | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/kiauh.py b/kiauh.py index 85e041c..ff930a4 100644 --- a/kiauh.py +++ b/kiauh.py @@ -9,13 +9,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -import io -import sys - from kiauh.main import main -# ensure that all output is utf-8 encoded -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - if __name__ == "__main__": main() diff --git a/kiauh/main.py b/kiauh/main.py index 7844505..e3e635c 100644 --- a/kiauh/main.py +++ b/kiauh/main.py @@ -6,15 +6,24 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # +import io +import sys from core.logger import Logger from core.menus.main_menu import MainMenu from core.settings.kiauh_settings import KiauhSettings +def ensure_encoding() -> None: + if sys.stdout.encoding == "UTF-8" or not isinstance(sys.stdout, io.TextIOWrapper): + return + sys.stdout.reconfigure(encoding="utf-8") + + def main() -> None: try: KiauhSettings() + ensure_encoding() MainMenu().run() except KeyboardInterrupt: Logger.print_ok("\nHappy printing!\n", prefix=False) From a616876ace36999aaff8ca27f65aaab56eca1f44 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 26 Oct 2024 20:41:34 +0200 Subject: [PATCH 04/18] feat: implement message service Signed-off-by: Dominik Willner --- .../webui_client/menus/client_install_menu.py | 18 +++--- kiauh/core/menus/base_menu.py | 6 ++ kiauh/core/services/__init__.py | 0 kiauh/core/services/message_service.py | 57 +++++++++++++++++++ 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 kiauh/core/services/__init__.py create mode 100644 kiauh/core/services/message_service.py diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py index d2dcce1..951baa8 100644 --- a/kiauh/components/webui_client/menus/client_install_menu.py +++ b/kiauh/components/webui_client/menus/client_install_menu.py @@ -18,9 +18,10 @@ from components.webui_client.client_utils import ( get_nginx_listen_port, set_listen_port, ) -from core.logger import DialogType, Logger +from core.logger import Logger from core.menus import Option from core.menus.base_menu import BaseMenu +from core.services.message_service import Message from core.settings.kiauh_settings import KiauhSettings, WebUiSettings from core.types.color import Color from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr @@ -85,16 +86,15 @@ class ClientInstallMenu(BaseMenu): cmd_sysctl_service("nginx", "start") # noinspection HttpUrlsUsage - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Port reconfiguration complete!", - custom_color=Color.GREEN, - center_content=True, - content=[ + message = Message( + title="Port reconfiguration complete!", + text=[ f"Open {self.client.display_name} now on: " f"http://{get_ipv4_addr()}:{new_port}", ], + color=Color.GREEN, ) + self.message_service.set_message(message) def _get_current_port(self) -> int: curr_port = get_nginx_listen_port(self.client.nginx_config) @@ -103,7 +103,3 @@ class ClientInstallMenu(BaseMenu): # the default port from the kiauh settings as fallback return int(self.client_settings.port) return curr_port - - def _go_back(self, **kwargs) -> None: - if self.previous_menu is not None: - self.previous_menu().run() diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py index 97817e9..47690f7 100644 --- a/kiauh/core/menus/base_menu.py +++ b/kiauh/core/menus/base_menu.py @@ -19,6 +19,7 @@ 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 @@ -123,6 +124,8 @@ class BaseMenu(metaclass=PostInitCaller): 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.") @@ -207,8 +210,11 @@ class BaseMenu(metaclass=PostInitCaller): 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() diff --git a/kiauh/core/services/__init__.py b/kiauh/core/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kiauh/core/services/message_service.py b/kiauh/core/services/message_service.py new file mode 100644 index 0000000..b7bb15c --- /dev/null +++ b/kiauh/core/services/message_service.py @@ -0,0 +1,57 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2024 Dominik Willner # +# # +# 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 dataclasses import dataclass +from typing import List + +from core.logger import DialogType, Logger +from core.types.color import Color + + +@dataclass() +class Message: + title: str + text: List[str] + color: Color + + +class MessageService: + _instance = None + + def __new__(cls) -> "MessageService": + if cls._instance is None: + cls._instance = super(MessageService, cls).__new__(cls) + return cls._instance + + def __init__(self) -> None: + if not hasattr(self, "__initialized"): + self.__initialized = False + if self.__initialized: + return + self.__initialized = True + self.message = None + + def set_message(self, message: Message) -> None: + self.message = message + + def display_message(self) -> None: + if self.message is None: + return + + Logger.print_dialog( + title=DialogType.CUSTOM, + content=self.message.text, + custom_title=self.message.title, + custom_color=self.message.color, + ) + + self.__clear_message() + + def __clear_message(self) -> None: + self.message = None From dd99b0e1a6555cdcb07fbd21f25ba3eaffb212e8 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 10:56:54 +0100 Subject: [PATCH 05/18] refactor: make run_remove_routines return boolean Signed-off-by: Dominik Willner --- kiauh/utils/fs_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 095b7fe..8c83ca6 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -73,11 +73,11 @@ def remove_file(file_path: Path, sudo=False) -> None: raise -def run_remove_routines(file: Path) -> None: +def run_remove_routines(file: Path) -> bool: try: if not file.is_symlink() and not file.exists(): Logger.print_info(f"File '{file}' does not exist. Skipped ...") - return + return False if file.is_dir(): shutil.rmtree(file) @@ -86,15 +86,18 @@ def run_remove_routines(file: Path) -> None: else: raise OSError(f"File '{file}' is neither a file nor a directory!") Logger.print_ok(f"File '{file}' was successfully removed!") + return True except OSError as e: Logger.print_error(f"Unable to delete '{file}':\n{e}") try: Logger.print_info("Trying to remove with sudo ...") remove_with_sudo(file) Logger.print_ok(f"File '{file}' was successfully removed!") + return True except CalledProcessError as e: Logger.print_error(f"Error deleting '{file}' with sudo:\n{e}") Logger.print_error("Remove this directory manually!") + return False def unzip(filepath: Path, target_dir: Path) -> None: From e274e3c00dc01623afe42f14b83f5d20387618d4 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 10:57:47 +0100 Subject: [PATCH 06/18] refactor: add defaults to Message and center property Signed-off-by: Dominik Willner --- kiauh/core/services/message_service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kiauh/core/services/message_service.py b/kiauh/core/services/message_service.py index b7bb15c..2022b04 100644 --- a/kiauh/core/services/message_service.py +++ b/kiauh/core/services/message_service.py @@ -7,7 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List from core.logger import DialogType, Logger @@ -16,9 +16,10 @@ from core.types.color import Color @dataclass() class Message: - title: str - text: List[str] - color: Color + title: str = field(default="") + text: List[str] = field(default_factory=list) + color: Color = field(default=Color.WHITE) + centered: bool = field(default=False) class MessageService: @@ -49,6 +50,7 @@ class MessageService: content=self.message.text, custom_title=self.message.title, custom_color=self.message.color, + center_content=self.message.centered, ) self.__clear_message() From 9b1aba207cae61972f944bde748333c02ef9b36f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 11:27:41 +0100 Subject: [PATCH 07/18] feat: implement completion message for klipper remove process Signed-off-by: Dominik Willner --- kiauh/components/klipper/klipper_remove.py | 34 ++++++++++++++---- .../klipper/menus/klipper_remove_menu.py | 36 +++++++------------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/kiauh/components/klipper/klipper_remove.py b/kiauh/components/klipper/klipper_remove.py index 773a604..05b2957 100644 --- a/kiauh/components/klipper/klipper_remove.py +++ b/kiauh/components/klipper/klipper_remove.py @@ -15,6 +15,8 @@ from components.klipper.klipper import Klipper from components.klipper.klipper_dialogs import print_instance_overview from core.instance_manager.instance_manager import InstanceManager from core.logger import Logger +from core.services.message_service import Message +from core.types.color import Color from utils.fs_utils import run_remove_routines from utils.input_utils import get_selection_input from utils.instance_utils import get_instances @@ -25,7 +27,11 @@ def run_klipper_removal( remove_service: bool, remove_dir: bool, remove_env: bool, -) -> None: +) -> Message: + completion_msg = Message( + title="Klipper Removal Process completed", + color=Color.GREEN, + ) klipper_instances: List[Klipper] = get_instances(Klipper) if remove_service: @@ -33,20 +39,36 @@ def run_klipper_removal( if klipper_instances: instances_to_remove = select_instances_to_remove(klipper_instances) remove_instances(instances_to_remove) + instance_names = [i.service_file_path.stem for i in instances_to_remove] + txt = f"● Klipper instances removed: {', '.join(instance_names)}" + completion_msg.text.append(txt) else: Logger.print_info("No Klipper Services installed! Skipped ...") if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"): - Logger.print_info("There are still other Klipper services installed:") - Logger.print_info(f"● '{KLIPPER_DIR}' was not removed.", prefix=False) - Logger.print_info(f"● '{KLIPPER_ENV_DIR}' was not removed.", prefix=False) + completion_msg.text = [ + "Some Klipper services are still installed:", + f"● '{KLIPPER_DIR}' was not removed, even though selected for removal.", + f"● '{KLIPPER_ENV_DIR}' was not removed, even though selected for removal.", + ] else: if remove_dir: Logger.print_status("Removing Klipper local repository ...") - run_remove_routines(KLIPPER_DIR) + if run_remove_routines(KLIPPER_DIR): + completion_msg.text.append("● Klipper local repository removed") if remove_env: Logger.print_status("Removing Klipper Python environment ...") - run_remove_routines(KLIPPER_ENV_DIR) + if run_remove_routines(KLIPPER_ENV_DIR): + completion_msg.text.append("● Klipper Python environment removed") + + if completion_msg.text: + completion_msg.text.insert(0, "The following actions were performed:") + else: + completion_msg.color = Color.YELLOW + completion_msg.centered = True + completion_msg.text = ["Nothing to remove."] + + return completion_msg def select_instances_to_remove(instances: List[Klipper]) -> List[Klipper] | None: diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 4785806..344686e 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -21,14 +21,16 @@ from core.types.color import Color class KlipperRemoveMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): super().__init__() + self.title = "Remove Klipper" self.title_color = Color.RED self.previous_menu: Type[BaseMenu] | None = previous_menu self.footer_type = FooterType.BACK + self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - self.selection_state = False + self.select_state = False def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.remove_menu import RemoveMenu @@ -50,13 +52,14 @@ class KlipperRemoveMenu(BaseMenu): o1 = checked if self.remove_klipper_service else unchecked o2 = checked if self.remove_klipper_dir else unchecked o3 = checked if self.remove_klipper_env else unchecked + sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything" menu = textwrap.dedent( f""" ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ a) {self._get_selection_state_str():37} ║ + ║ a) {sel_state:49} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove Service ║ ║ 2) {o2} Remove Local Repository ║ @@ -69,10 +72,10 @@ class KlipperRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.selection_state = not self.selection_state - self.remove_klipper_service = self.selection_state - self.remove_klipper_dir = self.selection_state - self.remove_klipper_env = self.selection_state + self.select_state = not self.select_state + self.remove_klipper_service = self.select_state + self.remove_klipper_dir = self.select_state + self.remove_klipper_env = self.select_state def toggle_remove_klipper_service(self, **kwargs) -> None: self.remove_klipper_service = not self.remove_klipper_service @@ -89,30 +92,17 @@ class KlipperRemoveMenu(BaseMenu): and not self.remove_klipper_dir and not self.remove_klipper_env ): - print( - Color.apply( - "Nothing selected! Select options to remove first.", Color.RED - ) - ) + msg = "Nothing selected! Select options to remove first." + print(Color.apply(msg, Color.RED)) return - klipper_remove.run_klipper_removal( + completion_msg = klipper_remove.run_klipper_removal( self.remove_klipper_service, self.remove_klipper_dir, self.remove_klipper_env, ) + self.message_service.set_message(completion_msg) self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False - - self._go_back() - - def _get_selection_state_str(self) -> str: - return ( - "Select everything" if not self.selection_state else "Deselect everything" - ) - - def _go_back(self, **kwargs) -> None: - if self.previous_menu is not None: - self.previous_menu().run() From 66a5cdf9b10a5b0aa74e1037dda74f79c3f45902 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 14:59:21 +0100 Subject: [PATCH 08/18] feat: implement completion message for webclient remove process Signed-off-by: Dominik Willner --- .../components/webui_client/client_remove.py | 21 ++++++++++++++++++- .../webui_client/menus/client_remove_menu.py | 5 ++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index 92e844d..ac08298 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -19,6 +19,8 @@ from components.webui_client.client_config.client_config_remove import ( from core.backup_manager.backup_manager import BackupManager from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.logger import Logger +from core.services.message_service import Message +from core.types.color import Color from utils.config_utils import remove_config_section from utils.fs_utils import ( remove_with_sudo, @@ -32,13 +34,18 @@ def run_client_removal( remove_client: bool, remove_client_cfg: bool, backup_config: bool, -) -> None: +) -> Message: + completion_msg = Message( + title=f"{client.display_name} Removal Process completed", + color=Color.GREEN, + ) mr_instances: List[Moonraker] = get_instances(Moonraker) kl_instances: List[Klipper] = get_instances(Klipper) if backup_config: bm = BackupManager() bm.backup_file(client.config_file) + completion_msg.text.append(f"● {client.config_file.name} backup created") if remove_client: client_name = client.name @@ -48,13 +55,25 @@ def run_client_removal( section = f"update_manager {client_name}" remove_config_section(section, mr_instances) + completion_msg.text.append(f"● {client.display_name} removed") if remove_client_cfg: + # todo: return a Message here as well to display correct actions taken run_client_config_removal( client.client_config, kl_instances, mr_instances, ) + completion_msg.text.append(f"● {client.client_config.display_name} removed") + + if not completion_msg.text: + completion_msg.color = Color.YELLOW + completion_msg.centered = True + completion_msg.text.append("Nothing to remove.") + else: + completion_msg.text.insert(0, "The following actions were performed:") + + return completion_msg def remove_client_dir(client: BaseWebClient) -> None: diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index b94c6e7..9a71c4f 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -99,19 +99,18 @@ class ClientRemoveMenu(BaseMenu): print(Color.apply("Nothing selected ...", Color.RED)) return - client_remove.run_client_removal( + completion_msg = client_remove.run_client_removal( client=self.client, remove_client=self.remove_client, remove_client_cfg=self.remove_client_cfg, backup_config=self.backup_config_json, ) + self.message_service.set_message(completion_msg) self.remove_client = False self.remove_client_cfg = False self.backup_config_json = False - self._go_back() - def _get_selection_state_str(self) -> str: return ( "Select everything" if not self.selection_state else "Deselect everything" From 12127efa2115d6b773248e54ad171c4b06b2f559 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 15:00:40 +0100 Subject: [PATCH 09/18] fix: remove extra newline Signed-off-by: Dominik Willner --- kiauh/core/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiauh/core/logger.py b/kiauh/core/logger.py index 0c9db0c..4102f2d 100644 --- a/kiauh/core/logger.py +++ b/kiauh/core/logger.py @@ -136,7 +136,7 @@ class Logger: @staticmethod def _format_dialog_title(title: str | None, color: Color) -> str: if title is None: - return "\n" + return "" _title = Color.apply(f"┃ {title:^{LINE_WIDTH}} ┃\n", color) _title += Color.apply( From 1ca1e8ff6fde85cd43b69d886e0bd093eb4a37ac Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 18:41:49 +0100 Subject: [PATCH 10/18] feat: rework completion message for webclient remove process Signed-off-by: Dominik Willner --- .../klipper/menus/klipper_remove_menu.py | 1 + .../client_config/client_config_remove.py | 54 ++++++++++++---- .../components/webui_client/client_remove.py | 62 +++++++++++-------- .../webui_client/menus/client_remove_menu.py | 23 +++---- kiauh/core/backup_manager/backup_manager.py | 9 ++- kiauh/utils/config_utils.py | 8 ++- kiauh/utils/fs_utils.py | 40 ++++++++---- 7 files changed, 128 insertions(+), 69 deletions(-) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py index 344686e..7f3760f 100644 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ b/kiauh/components/klipper/menus/klipper_remove_menu.py @@ -106,3 +106,4 @@ class KlipperRemoveMenu(BaseMenu): self.remove_klipper_service = False self.remove_klipper_dir = False self.remove_klipper_env = False + self.select_state = False diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index f0f5170..c2d9862 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -14,6 +14,8 @@ from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker from components.webui_client.base_data import BaseWebClientConfig from core.logger import Logger +from core.services.message_service import Message +from core.types.color import Color from utils.config_utils import remove_config_section from utils.fs_utils import run_remove_routines from utils.instance_utils import get_instances @@ -23,21 +25,49 @@ def run_client_config_removal( client_config: BaseWebClientConfig, kl_instances: List[Klipper], mr_instances: List[Moonraker], -) -> None: - remove_client_config_dir(client_config) - remove_client_config_symlink(client_config) - remove_config_section(f"update_manager {client_config.name}", mr_instances) - remove_config_section(client_config.config_section, kl_instances) - - -def remove_client_config_dir(client_config: BaseWebClientConfig) -> None: +) -> Message: + completion_msg = Message( + title=f"{client_config.display_name} Removal Process completed", + color=Color.GREEN, + ) Logger.print_status(f"Removing {client_config.display_name} ...") - run_remove_routines(client_config.config_dir) + if run_remove_routines(client_config.config_dir): + completion_msg.text.append(f"● {client_config.display_name} removed") - -def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None: instances: List[Klipper] = get_instances(Klipper) + handled_configs = [] for instance in instances: - run_remove_routines( + if run_remove_routines( instance.base.cfg_dir.joinpath(client_config.config_filename) + ): + handled_configs.append(instance) + if handled_configs: + instance_names = [i.service_file_path.stem for i in handled_configs] + completion_msg.text.append( + f"● {client_config.display_name} removed from instances: {', '.join(instance_names)}" ) + + mr_section = f"update_manager {client_config.name}" + handled_mr_instances = remove_config_section(mr_section, mr_instances) + if handled_mr_instances: + instance_names = [i.service_file_path.stem for i in handled_mr_instances] + completion_msg.text.append( + f"● Moonraker config section '{mr_section}' removed for instance: {', '.join(instance_names)}" + ) + + kl_section = client_config.config_section + handled_kl_instances = remove_config_section(kl_section, kl_instances) + if handled_kl_instances: + instance_names = [i.service_file_path.stem for i in handled_kl_instances] + completion_msg.text.append( + f"● Klipper config section '{mr_section}' removed for instance: {', '.join(instance_names)}" + ) + + if not completion_msg.text: + completion_msg.color = Color.YELLOW + completion_msg.centered = True + completion_msg.text.append("Nothing to remove.") + else: + completion_msg.text.insert(0, "The following actions were performed:") + + return completion_msg diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py index ac08298..a017628 100644 --- a/kiauh/components/webui_client/client_remove.py +++ b/kiauh/components/webui_client/client_remove.py @@ -44,27 +44,36 @@ def run_client_removal( if backup_config: bm = BackupManager() - bm.backup_file(client.config_file) - completion_msg.text.append(f"● {client.config_file.name} backup created") + if bm.backup_file(client.config_file): + completion_msg.text.append(f"● {client.config_file.name} backup created") if remove_client: client_name = client.name - remove_client_dir(client) - remove_client_nginx_config(client_name) - remove_client_nginx_logs(client, kl_instances) + if remove_client_dir(client): + completion_msg.text.append(f"● {client.display_name} removed") + if remove_client_nginx_config(client_name): + completion_msg.text.append("● NGINX config removed") + if remove_client_nginx_logs(client, kl_instances): + completion_msg.text.append("● NGINX logs removed") section = f"update_manager {client_name}" - remove_config_section(section, mr_instances) - completion_msg.text.append(f"● {client.display_name} removed") + handled_instances: List[Moonraker] = remove_config_section( + section, mr_instances + ) + if handled_instances: + names = [i.service_file_path.stem for i in handled_instances] + completion_msg.text.append( + f"● Moonraker config section '{section}' removed for instance: {', '.join(names)}" + ) if remove_client_cfg: - # todo: return a Message here as well to display correct actions taken - run_client_config_removal( + cfg_completion_msg = run_client_config_removal( client.client_config, kl_instances, mr_instances, ) - completion_msg.text.append(f"● {client.client_config.display_name} removed") + if cfg_completion_msg.color == Color.GREEN: + completion_msg.text.extend(cfg_completion_msg.text[1:]) if not completion_msg.text: completion_msg.color = Color.YELLOW @@ -76,29 +85,28 @@ def run_client_removal( return completion_msg -def remove_client_dir(client: BaseWebClient) -> None: +def remove_client_dir(client: BaseWebClient) -> bool: Logger.print_status(f"Removing {client.display_name} ...") - run_remove_routines(client.client_dir) + return run_remove_routines(client.client_dir) -def remove_client_nginx_config(name: str) -> None: +def remove_client_nginx_config(name: str) -> bool: Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") - - remove_with_sudo(NGINX_SITES_AVAILABLE.joinpath(name)) - remove_with_sudo(NGINX_SITES_ENABLED.joinpath(name)) + return remove_with_sudo( + [ + NGINX_SITES_AVAILABLE.joinpath(name), + NGINX_SITES_ENABLED.joinpath(name), + ] + ) -def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> None: +def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> bool: Logger.print_status(f"Removing NGINX logs for {client.display_name} ...") - remove_with_sudo(client.nginx_access_log) - remove_with_sudo(client.nginx_error_log) + files = [client.nginx_access_log, client.nginx_error_log] + if instances: + for instance in instances: + files.append(instance.base.log_dir.joinpath(client.nginx_access_log.name)) + files.append(instance.base.log_dir.joinpath(client.nginx_error_log.name)) - if not instances: - return - - for instance in instances: - run_remove_routines( - instance.base.log_dir.joinpath(client.nginx_access_log.name) - ) - run_remove_routines(instance.base.log_dir.joinpath(client.nginx_error_log.name)) + return remove_with_sudo(files) diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py index 9a71c4f..2b16aca 100644 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ b/kiauh/components/webui_client/menus/client_remove_menu.py @@ -31,7 +31,7 @@ class ClientRemoveMenu(BaseMenu): self.remove_client: bool = False self.remove_client_cfg: bool = False self.backup_config_json: bool = False - self.selection_state: bool = False + self.select_state: bool = False def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.remove_menu import RemoveMenu @@ -57,13 +57,14 @@ class ClientRemoveMenu(BaseMenu): o1 = checked if self.remove_client else unchecked o2 = checked if self.remove_client_cfg else unchecked o3 = checked if self.backup_config_json else unchecked + sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything" menu = textwrap.dedent( f""" ╟───────────────────────────────────────────────────────╢ ║ Enter a number and hit enter to select / deselect ║ ║ the specific option for removal. ║ ╟───────────────────────────────────────────────────────╢ - ║ a) {self._get_selection_state_str():37} ║ + ║ a) {sel_state:49} ║ ╟───────────────────────────────────────────────────────╢ ║ 1) {o1} Remove {client_name:16} ║ ║ 2) {o2} Remove {client_config_name:24} ║ @@ -76,10 +77,10 @@ class ClientRemoveMenu(BaseMenu): print(menu, end="") def toggle_all(self, **kwargs) -> None: - self.selection_state = not self.selection_state - self.remove_client = self.selection_state - self.remove_client_cfg = self.selection_state - self.backup_config_json = self.selection_state + self.select_state = not self.select_state + self.remove_client = self.select_state + self.remove_client_cfg = self.select_state + self.backup_config_json = self.select_state def toggle_rm_client(self, **kwargs) -> None: self.remove_client = not self.remove_client @@ -110,12 +111,4 @@ class ClientRemoveMenu(BaseMenu): self.remove_client = False self.remove_client_cfg = False self.backup_config_json = False - - def _get_selection_state_str(self) -> str: - return ( - "Select everything" if not self.selection_state else "Deselect everything" - ) - - def _go_back(self, **kwargs) -> None: - if self.previous_menu is not None: - self.previous_menu().run() + self.select_state = False diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py index ad9ea5b..703be60 100644 --- a/kiauh/core/backup_manager/backup_manager.py +++ b/kiauh/core/backup_manager/backup_manager.py @@ -44,12 +44,14 @@ class BackupManager: def ignore_folders(self, value: List[str]): self._ignore_folders = value - def backup_file(self, file: Path, target: Path | None = None, custom_filename=None): + def backup_file( + self, file: Path, target: Path | None = None, custom_filename=None + ) -> bool: Logger.print_status(f"Creating backup of {file} ...") if not file.exists(): Logger.print_info("File does not exist! Skipping ...") - return + return False target = self.backup_root_dir if target is None else target @@ -62,10 +64,13 @@ class BackupManager: Path(target).mkdir(exist_ok=True) shutil.copyfile(file, target.joinpath(filename)) Logger.print_ok("Backup successful!") + return True except OSError as e: Logger.print_error(f"Unable to backup '{file}':\n{e}") + return False else: Logger.print_info(f"File '{file}' not found ...") + return False def backup_directory( self, name: str, source: Path, target: Path | None = None diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py index e9bbf14..18ca608 100644 --- a/kiauh/utils/config_utils.py +++ b/kiauh/utils/config_utils.py @@ -78,7 +78,10 @@ def add_config_section_at_top(section: str, instances: List[InstanceType]) -> No Logger.print_ok("OK!") -def remove_config_section(section: str, instances: List[InstanceType]) -> None: +def remove_config_section( + section: str, instances: List[InstanceType] +) -> List[InstanceType]: + removed_from: List[instances] = [] for instance in instances: cfg_file = instance.cfg_file Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") @@ -96,4 +99,7 @@ def remove_config_section(section: str, instances: List[InstanceType]) -> None: scp.remove_section(section) scp.write_file(cfg_file) + removed_from.append(instance) Logger.print_ok("OK!") + + return removed_from diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py index 8c83ca6..91f315b 100644 --- a/kiauh/utils/fs_utils.py +++ b/kiauh/utils/fs_utils.py @@ -13,7 +13,7 @@ from __future__ import annotations import re import shutil from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run +from subprocess import DEVNULL, PIPE, CalledProcessError, call, check_output, run from typing import List from zipfile import ZipFile @@ -53,13 +53,28 @@ def create_symlink(source: Path, target: Path, sudo=False) -> None: raise -def remove_with_sudo(file: Path) -> None: - try: - cmd = ["sudo", "rm", "-rf", file.as_posix()] - run(cmd, stderr=PIPE, check=True) - except CalledProcessError as e: - Logger.print_error(f"Failed to remove {file}: {e}") - raise +def remove_with_sudo(files: Path | List[Path]) -> bool: + _files = [] + _removed = [] + if isinstance(files, list): + _files = files + else: + _files.append(files) + + for f in _files: + try: + cmd = ["sudo", "find", f.as_posix()] + if call(cmd, stderr=DEVNULL, stdout=DEVNULL) == 1: + Logger.print_info(f"File '{f}' does not exist. Skipped ...") + continue + cmd = ["sudo", "rm", "-rf", f.as_posix()] + run(cmd, stderr=PIPE, check=True) + Logger.print_ok(f"File '{f}' was successfully removed!") + _removed.append(f) + except CalledProcessError as e: + Logger.print_error(f"Error removing file '{f}': {e}") + + return len(_removed) > 0 @deprecated(info="Use remove_with_sudo instead", replaced_by=remove_with_sudo) @@ -84,16 +99,17 @@ def run_remove_routines(file: Path) -> bool: elif file.is_file() or file.is_symlink(): file.unlink() else: - raise OSError(f"File '{file}' is neither a file nor a directory!") + Logger.print_error(f"File '{file}' is neither a file nor a directory!") + return False Logger.print_ok(f"File '{file}' was successfully removed!") return True except OSError as e: Logger.print_error(f"Unable to delete '{file}':\n{e}") try: Logger.print_info("Trying to remove with sudo ...") - remove_with_sudo(file) - Logger.print_ok(f"File '{file}' was successfully removed!") - return True + if remove_with_sudo(file): + Logger.print_ok(f"File '{file}' was successfully removed!") + return True except CalledProcessError as e: Logger.print_error(f"Error deleting '{file}' with sudo:\n{e}") Logger.print_error("Remove this directory manually!") From dd14de9a412777d9d4cbc060ce439afb179a9629 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 20:35:50 +0100 Subject: [PATCH 11/18] fix: test if `checks` is empty Signed-off-by: Dominik Willner --- kiauh/utils/common.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py index c432e40..004613b 100644 --- a/kiauh/utils/common.py +++ b/kiauh/utils/common.py @@ -6,12 +6,13 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # + from __future__ import annotations import re from datetime import datetime from pathlib import Path -from typing import Dict, List, Literal, Optional, Set +from typing import Dict, List, Literal, Set from components.klipper.klipper import Klipper from components.moonraker.moonraker import Moonraker @@ -89,9 +90,9 @@ def check_install_dependencies( def get_install_status( repo_dir: Path, - env_dir: Optional[Path] = None, + env_dir: Path | None = None, instance_type: type | None = None, - files: Optional[List[Path]] = None, + files: List[Path] | None = None, ) -> ComponentStatus: """ Helper method to get the installation status of software components @@ -123,7 +124,7 @@ def get_install_status( checks.append(f.exists()) status: StatusCode - if all(checks): + if checks and all(checks): status = 2 # installed elif not any(checks): status = 0 # not installed From 8aee23830aab733dd17f0e1a9ab3cc3e2f1d2d84 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 27 Oct 2024 22:27:01 +0100 Subject: [PATCH 12/18] feat: implement completion message for webclient config remove process Signed-off-by: Dominik Willner --- .../client_config/client_config_remove.py | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py index c2d9862..f64e022 100644 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ b/kiauh/components/webui_client/client_config/client_config_remove.py @@ -18,6 +18,7 @@ from core.services.message_service import Message from core.types.color import Color from utils.config_utils import remove_config_section from utils.fs_utils import run_remove_routines +from utils.instance_type import InstanceType from utils.instance_utils import get_instances @@ -34,40 +35,57 @@ def run_client_config_removal( if run_remove_routines(client_config.config_dir): completion_msg.text.append(f"● {client_config.display_name} removed") - instances: List[Klipper] = get_instances(Klipper) - handled_configs = [] - for instance in instances: - if run_remove_routines( - instance.base.cfg_dir.joinpath(client_config.config_filename) - ): - handled_configs.append(instance) - if handled_configs: - instance_names = [i.service_file_path.stem for i in handled_configs] - completion_msg.text.append( - f"● {client_config.display_name} removed from instances: {', '.join(instance_names)}" - ) + completion_msg = remove_moonraker_config_section( + completion_msg, client_config, mr_instances + ) - mr_section = f"update_manager {client_config.name}" - handled_mr_instances = remove_config_section(mr_section, mr_instances) - if handled_mr_instances: - instance_names = [i.service_file_path.stem for i in handled_mr_instances] - completion_msg.text.append( - f"● Moonraker config section '{mr_section}' removed for instance: {', '.join(instance_names)}" - ) + completion_msg = remove_printer_config_section( + completion_msg, client_config, kl_instances + ) - kl_section = client_config.config_section - handled_kl_instances = remove_config_section(kl_section, kl_instances) - if handled_kl_instances: - instance_names = [i.service_file_path.stem for i in handled_kl_instances] - completion_msg.text.append( - f"● Klipper config section '{mr_section}' removed for instance: {', '.join(instance_names)}" - ) - - if not completion_msg.text: + if completion_msg.text: + completion_msg.text.insert(0, "The following actions were performed:") + else: completion_msg.color = Color.YELLOW completion_msg.centered = True - completion_msg.text.append("Nothing to remove.") - else: - completion_msg.text.insert(0, "The following actions were performed:") + completion_msg.text = ["Nothing to remove."] return completion_msg + + +def remove_cfg_symlink(client_config: BaseWebClientConfig, message: Message) -> Message: + instances: List[Klipper] = get_instances(Klipper) + kl_instances = [] + for instance in instances: + cfg = instance.base.cfg_dir.joinpath(client_config.config_filename) + if run_remove_routines(cfg): + kl_instances.append(instance) + text = f"{client_config.display_name} removed from instance" + return update_msg(kl_instances, message, text) + + +def remove_printer_config_section( + message: Message, client_config: BaseWebClientConfig, kl_instances: List[Klipper] +) -> Message: + kl_section = client_config.config_section + kl_instances = remove_config_section(kl_section, kl_instances) + text = f"Klipper config section '{kl_section}' removed for instance" + return update_msg(kl_instances, message, text) + + +def remove_moonraker_config_section( + message: Message, client_config: BaseWebClientConfig, mr_instances: List[Moonraker] +) -> Message: + mr_section = f"update_manager {client_config.name}" + mr_instances = remove_config_section(mr_section, mr_instances) + text = f"Moonraker config section '{mr_section}' removed for instance" + return update_msg(mr_instances, message, text) + + +def update_msg(instances: List[InstanceType], message: Message, text: str) -> Message: + if not instances: + return message + + instance_names = [i.service_file_path.stem for i in instances] + message.text.append(f"● {text}: {', '.join(instance_names)}") + return message From a3fb57aee3f26571aac4595c6a299c60c98d2143 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 10 Nov 2024 19:54:26 +0100 Subject: [PATCH 13/18] refactor: return `-` if branch cannot be read Signed-off-by: Dominik Willner --- kiauh/utils/git_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py index ee7073e..1ffdd4b 100644 --- a/kiauh/utils/git_utils.py +++ b/kiauh/utils/git_utils.py @@ -104,10 +104,10 @@ def get_current_branch(repo: Path) -> str: result: str = check_output(cmd, stderr=DEVNULL, cwd=repo).decode( encoding="utf-8" ) - return result.strip() + return result.strip() if result else "-" except CalledProcessError: - return "" + return "-" def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]: From d37d047aaa383393d1cd1d2ddff14cccbbb9470f Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 10 Nov 2024 19:56:47 +0100 Subject: [PATCH 14/18] refactor: fallback to config settings for repos in settings menu Signed-off-by: Dominik Willner --- kiauh/core/menus/settings_menu.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index c8e4de4..8204f29 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -19,6 +19,7 @@ from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings, RepoSettings from core.types.color import Color from procedures.switch_repo import run_switch_repo_routine +from utils.git_utils import get_repo_name from utils.input_utils import get_confirm, get_string_input @@ -30,12 +31,13 @@ class SettingsMenu(BaseMenu): self.title = "Settings Menu" self.title_color = Color.CYAN self.previous_menu: Type[BaseMenu] | None = previous_menu - self.klipper_status = get_klipper_status() - self.moonraker_status = get_moonraker_status() + self.mainsail_unstable: bool | None = None self.fluidd_unstable: bool | None = None self.auto_backups_enabled: bool | None = None self._load_settings() + print(self.klipper_status) + def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.main_menu import MainMenu @@ -103,6 +105,21 @@ class SettingsMenu(BaseMenu): self.mainsail_unstable = self.settings.mainsail.unstable_releases self.fluidd_unstable = self.settings.fluidd.unstable_releases + # by default, we show the status of the installed repositories + self.klipper_status = get_klipper_status() + self.moonraker_status = get_moonraker_status() + # if the repository is not installed, we show the status of the settings from the config file + if self.klipper_status.repo == "-": + url_parts = self.settings.klipper.repo_url.split("/") + self.klipper_status.repo = url_parts[-1] + self.klipper_status.owner = url_parts[-2] + self.klipper_status.branch = self.settings.klipper.branch + if self.moonraker_status.repo == "-": + url_parts = self.settings.moonraker.repo_url.split("/") + self.moonraker_status.repo = url_parts[-1] + self.moonraker_status.owner = url_parts[-2] + self.moonraker_status.branch = self.settings.moonraker.branch + def _gather_input(self) -> Tuple[str, str]: Logger.print_dialog( DialogType.ATTENTION, From b9c9feef3cf49cda04f32559d5c8284c0a69a1c6 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 10 Nov 2024 20:14:47 +0100 Subject: [PATCH 15/18] refactor: clone repo in repo switch routine only if there is already a repo present Signed-off-by: Dominik Willner --- kiauh/core/menus/settings_menu.py | 22 +++++++++++++++------- kiauh/procedures/switch_repo.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 8204f29..278301b 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -9,15 +9,19 @@ from __future__ import annotations import textwrap +from pathlib import Path from typing import Literal, Tuple, Type +from components.klipper import KLIPPER_DIR from components.klipper.klipper_utils import get_klipper_status +from components.moonraker import MOONRAKER_DIR from components.moonraker.moonraker_utils import get_moonraker_status from core.logger import DialogType, Logger from core.menus import Option from core.menus.base_menu import BaseMenu from core.settings.kiauh_settings import KiauhSettings, RepoSettings from core.types.color import Color +from core.types.component_status import ComponentStatus from procedures.switch_repo import run_switch_repo_routine from utils.git_utils import get_repo_name from utils.input_utils import get_confirm, get_string_input @@ -144,7 +148,7 @@ class SettingsMenu(BaseMenu): return repo, branch - def _set_repo(self, repo_name: Literal["klipper", "moonraker"]) -> None: + def _set_repo(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> None: repo_url, branch = self._gather_input() display_name = repo_name.capitalize() Logger.print_dialog( @@ -168,22 +172,26 @@ class SettingsMenu(BaseMenu): Logger.print_ok("Changes saved!") else: Logger.print_info( - f"Skipping change of {display_name} source repository ..." + f"Changing of {display_name} source repository canceled ..." ) return - Logger.print_status(f"Switching to {display_name}'s new source repository ...") - self._switch_repo(repo_name) + self._switch_repo(repo_name, repo_dir) + + def _switch_repo(self, name: Literal["klipper", "moonraker"], repo_dir: Path ) -> None: + if not repo_dir.exists(): + return + + Logger.print_status(f"Switching to {name.capitalize()}'s new source repository ...") - def _switch_repo(self, name: Literal["klipper", "moonraker"]) -> None: repo: RepoSettings = self.settings[name] run_switch_repo_routine(name, repo) def set_klipper_repo(self, **kwargs) -> None: - self._set_repo("klipper") + self._set_repo("klipper", KLIPPER_DIR) def set_moonraker_repo(self, **kwargs) -> None: - self._set_repo("moonraker") + self._set_repo("moonraker", MOONRAKER_DIR) def toggle_mainsail_release(self, **kwargs) -> None: self.mainsail_unstable = not self.mainsail_unstable diff --git a/kiauh/procedures/switch_repo.py b/kiauh/procedures/switch_repo.py index 4ddcab7..7bc4ba2 100644 --- a/kiauh/procedures/switch_repo.py +++ b/kiauh/procedures/switch_repo.py @@ -64,7 +64,7 @@ def run_switch_repo_routine( try: # step 2: backup old repo and env - org, repo = get_repo_name(repo_dir) + org, _ = get_repo_name(repo_dir) backup_dir = backup_dir.joinpath(org) bm = BackupManager() repo_dir_backup_path = bm.backup_directory( From 6ff45aab418c0b3114cf3999ef8dd1ea010553b7 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 10 Nov 2024 20:58:37 +0100 Subject: [PATCH 16/18] refactor: implement basic input validation for repo switch feature Signed-off-by: Dominik Willner --- kiauh/components/klipper/__init__.py | 2 ++ kiauh/components/moonraker/__init__.py | 2 ++ kiauh/core/menus/settings_menu.py | 36 ++++++++++++++------------ kiauh/utils/input_utils.py | 10 ++++--- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 2e282e4..908ed4e 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent +KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" + # names KLIPPER_LOG_NAME = "klippy.log" KLIPPER_CFG_NAME = "printer.cfg" diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py index 8924129..b48a894 100644 --- a/kiauh/components/moonraker/__init__.py +++ b/kiauh/components/moonraker/__init__.py @@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR MODULE_PATH = Path(__file__).resolve().parent +MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" + # names MOONRAKER_CFG_NAME = "moonraker.conf" MOONRAKER_LOG_NAME = "moonraker.log" diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 278301b..1823214 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -12,9 +12,9 @@ import textwrap from pathlib import Path from typing import Literal, Tuple, Type -from components.klipper import KLIPPER_DIR +from components.klipper import KLIPPER_DIR, KLIPPER_REPO_URL from components.klipper.klipper_utils import get_klipper_status -from components.moonraker import MOONRAKER_DIR +from components.moonraker import MOONRAKER_DIR, MOONRAKER_REPO_URL from components.moonraker.moonraker_utils import get_moonraker_status from core.logger import DialogType, Logger from core.menus import Option @@ -124,32 +124,36 @@ class SettingsMenu(BaseMenu): self.moonraker_status.owner = url_parts[-2] self.moonraker_status.branch = self.settings.moonraker.branch - def _gather_input(self) -> Tuple[str, str]: - Logger.print_dialog( - DialogType.ATTENTION, - [ - "There is no input validation in place! Make sure your the input is " - "valid and has no typos or invalid characters! For the change to take " - "effect, the new repository will be cloned. A backup of the old " - "repository will be created.", + def _gather_input(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> Tuple[str, str]: + warn_msg = [ + "There is only basic input validation in place! " + "Make sure your the input is valid and has no typos or invalid characters!"] + + if repo_dir.exists(): + warn_msg.extend([ + "For the change to take effect, the new repository will be cloned. " + "A backup of the old repository will be created.", "\n\n", "Make sure you don't have any ongoing prints running, as the services " - "will be restarted during this process! You will loose any ongoing print!", - ], - ) + "will be restarted during this process! You will loose any ongoing print!"]) + + Logger.print_dialog(DialogType.ATTENTION, warn_msg) + repo = get_string_input( "Enter new repository URL", - allow_special_chars=True, + regex="^[\w/.:-]+$", + default=KLIPPER_REPO_URL if repo_name == "klipper" else MOONRAKER_REPO_URL, ) branch = get_string_input( "Enter new branch name", - allow_special_chars=True, + regex="^.+$", + default="master" ) return repo, branch def _set_repo(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> None: - repo_url, branch = self._gather_input() + repo_url, branch = self._gather_input(repo_name, repo_dir) display_name = repo_name.capitalize() Logger.print_dialog( DialogType.CUSTOM, diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index ef3f785..fecb616 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -86,6 +86,7 @@ def get_string_input( question: str, regex: str | None = None, exclude: List[str] | None = None, + allow_empty: bool = False, allow_special_chars: bool = False, default: str | None = None, ) -> str: @@ -94,6 +95,7 @@ def get_string_input( :param question: The question to display :param regex: An optional regex pattern to validate the input against :param exclude: List of strings which are not allowed + :param allow_empty: Whether to allow empty input :param allow_special_chars: Wheter to allow special characters in the input :param default: Optional default value :return: The validated string value @@ -104,12 +106,14 @@ def get_string_input( while True: _input = input(_question) - if _input.lower() in _exclude: - Logger.print_error("This value is already in use/reserved.") - elif default is not None and _input == "": + if default is not None and _input == "": return default + elif _input == "" and not allow_empty: + Logger.print_error("Input must not be empty!") elif _pattern is not None and _pattern.match(_input): return _input + elif _input.lower() in _exclude: + Logger.print_error("This value is already in use/reserved.") elif allow_special_chars: return _input elif not allow_special_chars and _input.isalnum(): From 3fc190ff253c3c7ff670fb6324b03a4ba1f1b207 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sun, 10 Nov 2024 21:15:08 +0100 Subject: [PATCH 17/18] fix: actually raise exception on empty config value Signed-off-by: Dominik Willner --- kiauh/core/settings/kiauh_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index dc5a2ae..6898e82 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -145,7 +145,8 @@ class KiauhSettings: def _validate_str(self, section: str, option: str) -> None: self._v_section, self._v_option = (section, option) v = self.config.getval(section, option) - if v.isdigit() or v.lower() == "true" or v.lower() == "false": + + if not v: raise ValueError def _apply_settings_from_file(self) -> None: From 91cba3637e110a4219a3cb7b05d888ca5b0b8440 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 23 Nov 2024 21:12:35 +0100 Subject: [PATCH 18/18] readme: update README.md Signed-off-by: Dominik Willner --- README.md | 130 +++++++++++++++++++++++++++++------------------------- 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 74df0a7..72ece02 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ ### 📋 Prerequisites KIAUH is a script that assists you in installing Klipper on a Linux operating system that has -already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure -that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image -if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/) +already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure +that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image +if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/) is the simplest way to flash an image like this to an SD card. -* Once you have downloaded, installed and launched the Raspberry Pi Imager, +* Once you have downloaded, installed and launched the Raspberry Pi Imager, select `Choose OS -> Raspberry Pi OS (other)`: \

KIAUH logo @@ -44,7 +44,7 @@ select `Choose OS -> Raspberry Pi OS (other)`: \ KIAUH logo

-* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which +* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which you want to flash the image. * Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu) @@ -52,9 +52,9 @@ and enable SSH and configure Wi-Fi. * If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html). -These steps **only** apply if you are actually using a Raspberry Pi. In case you want -to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed -to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run +These steps **only** apply if you are actually using a Raspberry Pi. In case you want +to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed +to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run and operate on the Linux Distribution you are going to flash. You likely will have the most success with distributions based on Debian 11 Bullseye. Read the notes further down below in this document. @@ -82,8 +82,8 @@ Finally, start KIAUH by running the next command: ``` * **Step 4:** \ -You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending -on what you want to do. To choose an action, simply type the corresponding number into the "Perform action" +You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending +on what you want to do. To choose an action, simply type the corresponding number into the "Perform action" prompt and confirm by hitting ENTER.
@@ -101,77 +101,83 @@ prompt and confirm by hitting ENTER.

🌐 Sources & Further Information

- +
- - - + + + - - - + + + - - - - - - - - - - - - - - - - - - + + + - - - + + + + + + + + + + + + + - - - + + + + + + + + + + + + + - - - + + + + + + + + + + + + + - - - - + + - - - - + + - - - - - + + - -

Klipper

Moonraker

Mainsail

Klipper

Moonraker

Mainsail

Klipper LogoArksine avatarMainsail LogoKlipper LogoArksine avatarMainsail Logo
by KevinOConnorby Arksineby mainsail-crew

Fluidd

KlipperScreen

OctoPrint

Fluidd Logojordanruthe avatarOctoPrint Logo
by fluidd-coreby jordanrutheby OctoPrintby KevinOConnorby Arksineby mainsail-crew

Moonraker-Telegram-Bot

PrettyGCode for Klipper

Obico for Klipper

Fluidd

KlipperScreen

OctoPrint

Fluidd Logojordanruthe avatarOctoPrint Logo
by fluidd-coreby jordanrutheby OctoPrint
nlef avatarKragrathea avatarObico logo

Moonraker-Telegram-Bot

PrettyGCode for Klipper

Obico for Klipper

nlef avatarKragrathea avatarObico logo
by nlefby Kragratheaby Obico
by nlefby Kragratheaby Obico

Mobileraker's Companion

OctoEverywhere For Klipper

OctoApp For Klipper

Mobileraker LogoOctoEverywhere LogoOctoApp Logo
by Patrick Schmidtby Quinn Damerellby Christian Würthner

Mobileraker's Companion

OctoEverywhere For Klipper

OctoApp For Klipper

Klipper-Backup

SimplyPrint for Klipper

OctoEverywhere LogoOctoEverywhere LogoOctoApp LogoStaubgeroner Avatar
by Patrick Schmidtby Quinn Damerellby Christian Würthnerby Staubgeborenerby SimplyPrint

@@ -186,6 +192,12 @@ prompt and confirm by hitting ENTER.
+
+ Repobeats analytics image +
+ +
+

✨ Credits ✨

* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!