diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py index 67a61ee..35291bc 100644 --- a/kiauh/core/menus/main_menu.py +++ b/kiauh/core/menus/main_menu.py @@ -166,7 +166,7 @@ class MainMenu(BaseMenu): BackupMenu(previous_menu=self.__class__).run() def settings_menu(self, **kwargs): - SettingsMenu().run() + SettingsMenu(previous_menu=self.__class__).run() def extension_menu(self, **kwargs): ExtensionsMenu(previous_menu=self.__class__).run() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py index 8ed407b..f049462 100644 --- a/kiauh/core/menus/settings_menu.py +++ b/kiauh/core/menus/settings_menu.py @@ -6,16 +6,37 @@ # # # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import Type, Optional +import shutil +import textwrap +from pathlib import Path +from typing import Type, Optional, Tuple +from components.klipper import KLIPPER_DIR +from components.klipper.klipper import Klipper +from components.moonraker import MOONRAKER_DIR +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from core.menus import Option from core.menus.base_menu import BaseMenu +from core.repo_manager.repo_manager import RepoManager +from core.settings.kiauh_settings import KiauhSettings +from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_GREEN, COLOR_YELLOW +from utils.input_utils import get_string_input, get_confirm +from utils.logger import Logger +# noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic class SettingsMenu(BaseMenu): def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None): super().__init__() self.previous_menu = previous_menu + self.klipper_repo = None + self.moonraker_repo = None + self.mainsail_unstable = None + self.fluidd_unstable = None + self.auto_backups_enabled = None + self._load_settings() def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None: from core.menus.main_menu import MainMenu @@ -25,19 +46,183 @@ class SettingsMenu(BaseMenu): ) def set_options(self) -> None: - pass + self.options = { + "1": Option(method=self.set_klipper_repo, menu=True), + "2": Option(method=self.set_moonraker_repo, menu=True), + "3": Option(method=self.toggle_mainsail_release, menu=True), + "4": Option(method=self.toggle_fluidd_release, menu=False), + "5": Option(method=self.toggle_backup_before_update, menu=False), + } def print_menu(self): - print("self") + header = " [ KIAUH Settings ] " + color = COLOR_CYAN + count = 62 - len(color) - len(RESET_FORMAT) + checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]" + unchecked = "[ ]" + 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}}{RESET_FORMAT} | + |-------------------------------------------------------| + | Klipper source repository: | + | ● {self.klipper_repo:<67} | + | | + | Moonraker source repository: | + | ● {self.moonraker_repo:<67} | + | | + | Install unstable Webinterface releases: | + | {o1} Mainsail | + | {o2} Fluidd | + | | + | Auto-Backup: | + | {o3} Automatic backup before update | + | | + |-------------------------------------------------------| + | 1) Set Klipper source repository | + | 2) Set Moonraker source repository | + | | + | 3) Toggle unstable Mainsail releases | + | 4) Toggle unstable Fluidd releases | + | | + | 5) Toggle automatic backups before updates | + """ + )[1:] + print(menu, end="") - def execute_option_p(self): - # Implement the functionality for Option P - print("Executing Option P") + def _load_settings(self): + self.kiauh_settings = KiauhSettings() - def execute_option_q(self): - # Implement the functionality for Option Q - print("Executing Option Q") + self._format_repo_str("klipper") + self._format_repo_str("moonraker") - def execute_option_r(self): - # Implement the functionality for Option R - print("Executing Option R") + self.auto_backups_enabled = self.kiauh_settings.get( + "kiauh", "backup_before_update" + ) + self.mainsail_unstable = self.kiauh_settings.get( + "mainsail", "unstable_releases" + ) + self.fluidd_unstable = self.kiauh_settings.get("fluidd", "unstable_releases") + + def _format_repo_str(self, repo_name: str) -> None: + repo = self.kiauh_settings.get(repo_name, "repo_url") + repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}" + branch = self.kiauh_settings.get(repo_name, "branch") + branch = f"({COLOR_CYAN}@ {branch}{RESET_FORMAT})" + setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}") + + def _gather_input(self) -> Tuple[str, str]: + l2 = "Make sure your input is correct!" + error = textwrap.dedent( + f"""\n + {COLOR_YELLOW}/=======================================================\\ + | ATTENTION: | + | There is no input validation in place! Make sure your | + | input is valid and has no typos! For any change to | + | take effect, the repository must be cloned again. | + | Make sure you don't have any ongoing prints running, | + | as the services will be restarted! | + \=======================================================/{RESET_FORMAT} + """ + )[1:] + print(error, end="\n") + repo = get_string_input( + "Enter new repository URL", + allow_special_chars=True, + ) + branch = get_string_input( + "Enter new branch name", + allow_special_chars=True, + ) + + return repo, branch + + def _display_summary(self, name: str, repo: str, branch: str): + l1 = f"New {name} repository URL:" + l2 = f"● {repo}" + l3 = f"New {name} repository branch:" + l4 = f"● {branch}" + summary = textwrap.dedent( + f"""\n + /=======================================================\\ + | {l1:<52} | + | {l2:<52} | + | {l3:<52} | + | {l4:<52} | + \=======================================================/ + """ + )[1:] + print(summary, end="") + + def _set_repo(self, repo_name: str): + repo, branch = self._gather_input() + self._display_summary(repo_name.capitalize(), repo, branch) + + if get_confirm("Apply changes?", allow_go_back=True): + self.kiauh_settings.set(repo_name, "repo_url", repo) + self.kiauh_settings.set(repo_name, "branch", branch) + self.kiauh_settings.save() + self._load_settings() + Logger.print_ok("Changes saved!") + else: + Logger.print_info( + f"Skipping change of {repo_name.capitalize()} source repository ..." + ) + return + + Logger.print_status( + f"Switching to {repo_name.capitalize()}'s new source repository ..." + ) + self._switch_repo(repo_name) + Logger.print_ok(f"Switched to {repo} at branch {branch}!") + + def _switch_repo(self, name: str) -> None: + target_dir: Path + if name == "klipper": + target_dir = KLIPPER_DIR + _type = Klipper + elif name == "moonraker": + target_dir = MOONRAKER_DIR + _type = Moonraker + else: + Logger.print_error("Invalid repository name!") + return + + if target_dir.exists(): + shutil.rmtree(target_dir) + + im = InstanceManager(_type) + im.stop_all_instance() + + repo = self.kiauh_settings.get(name, "repo_url") + branch = self.kiauh_settings.get(name, "branch") + repman = RepoManager(repo, str(target_dir), branch) + repman.clone_repo() + + im.start_all_instance() + + def set_klipper_repo(self, **kwargs): + self._set_repo("klipper") + + def set_moonraker_repo(self, **kwargs): + self._set_repo("moonraker") + + def toggle_mainsail_release(self, **kwargs): + self.mainsail_unstable = not self.mainsail_unstable + self.kiauh_settings.set("mainsail", "unstable_releases", self.mainsail_unstable) + self.kiauh_settings.save() + + def toggle_fluidd_release(self, **kwargs): + self.fluidd_unstable = not self.fluidd_unstable + self.kiauh_settings.set("fluidd", "unstable_releases", self.fluidd_unstable) + self.kiauh_settings.save() + + def toggle_backup_before_update(self, **kwargs): + self.auto_backups_enabled = not self.auto_backups_enabled + self.kiauh_settings.set( + "kiauh", "backup_before_update", self.auto_backups_enabled + ) + self.kiauh_settings.save() diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py index 7a9ef86..8eddb12 100644 --- a/kiauh/core/settings/kiauh_settings.py +++ b/kiauh/core/settings/kiauh_settings.py @@ -9,8 +9,9 @@ import textwrap import configparser -from configparser import ConfigParser from typing import Dict, Union + +from core.config_manager.config_manager import CustomConfigParser from kiauh import PROJECT_ROOT from utils.constants import RESET_FORMAT, COLOR_RED from utils.logger import Logger @@ -34,33 +35,32 @@ class KiauhSettings: if self.__initialized: return self.__initialized = True + self.config = CustomConfigParser() self.settings: Dict[str, Dict[str, Union[str, int, bool]]] = {} + self._load_settings() - self.config = ConfigParser() - if self._custom_cfg.exists(): - self.config.read(self._custom_cfg) - elif self._default_cfg.exists(): - self.config.read(self._default_cfg) - else: - self._kill() + def get(self, section: str, option: str) -> Union[str, int, bool]: + return self.settings[section][option] - sections = self.config.sections() - for s in sections: - self.settings[s] = dict(self.config[s]) - - self._validate_cfg() - - def get(self, key: str, value: Union[str, int, bool]) -> Union[str, int, bool]: - return self.settings[key][value] - - def set(self, key: str, value: Union[str, int, bool]) -> None: - self.settings[key][value] = value + def set(self, section: str, option: str, value: Union[str, int, bool]) -> None: + self.settings[section][option] = value def save(self) -> None: for section, option in self.settings.items(): self.config[section] = option with open(self._custom_cfg, "w") as configfile: self.config.write(configfile) + self._load_settings() + + def _load_settings(self) -> None: + if self._custom_cfg.exists(): + self.config.read(self._custom_cfg) + elif self._default_cfg.exists(): + self.config.read(self._default_cfg) + else: + self._kill() + self._validate_cfg() + self._parse_settings() def _validate_cfg(self) -> None: try: @@ -102,6 +102,19 @@ class KiauhSettings: if v.isdigit() or v.lower() == "true" or v.lower() == "false": raise ValueError + def _parse_settings(self): + for s in self.config.sections(): + self.settings[s] = {} + for o, v in self.config.items(s): + if v.lower() == "true": + self.settings[s][o] = True + elif v.lower() == "false": + self.settings[s][o] = False + elif v.isdigit(): + self.settings[s][o] = int(v) + else: + self.settings[s][o] = v + def _kill(self) -> None: l1 = "!!! ERROR !!!" l2 = "No KIAUH configuration file found!" diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py index 88b9188..4738681 100644 --- a/kiauh/utils/input_utils.py +++ b/kiauh/utils/input_utils.py @@ -7,7 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # -from typing import Optional, List, Union +from typing import List, Union from utils import INVALID_CHOICE from utils.constants import COLOR_CYAN, RESET_FORMAT @@ -82,23 +82,33 @@ def get_number_input( Logger.print_error(INVALID_CHOICE) -def get_string_input(question: str, exclude=Optional[List], default=None) -> str: +def get_string_input( + question: str, + exclude: Union[List, None] = None, + allow_special_chars=False, + default=None, +) -> str: """ Helper method to get a string input from the user :param question: The question to display :param exclude: List of strings which are not allowed + :param allow_special_chars: Wheter to allow special characters in the input :param default: Optional default value :return: The validated string value """ + _exclude = [] if exclude is None else exclude + _question = format_question(question, default) while True: - _input = input(format_question(question, default)).strip() + _input = input(_question) - if _input.isalnum() and _input.lower() not in exclude: - return _input - - Logger.print_error(INVALID_CHOICE) - if _input in exclude: + if _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(): + return _input + else: + Logger.print_error(INVALID_CHOICE) def get_selection_input(question: str, option_list: List, default=None) -> str: