Release v6.0.0-alpha.10

Merge develop into master (Release v6.0.0-alpha.10)
This commit is contained in:
dw-0
2024-11-23 21:17:51 +01:00
committed by GitHub
54 changed files with 868 additions and 649 deletions

130
README.md
View File

@@ -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)`: \
<p align="center">
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350">
@@ -44,7 +44,7 @@ select `Choose OS -> Raspberry Pi OS (other)`: \
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
</p>
* 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.
<hr>
@@ -101,77 +101,83 @@ prompt and confirm by hitting ENTER.
<h2 align="center">🌐 Sources & Further Information</h2>
<table>
<table align="center">
<tr>
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th>
<th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th>
<th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th>
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th>
<th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th>
<th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
<th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
<th>by <a href="https://github.com/Arksine">Arksine</a></th>
<th>by <a href="https://github.com/mainsail-crew">mainsail-crew</a></th>
</tr>
<tr>
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
<th>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
<th>by <a href="https://github.com/Arksine">Arksine</a></th>
<th>by <a href="https://github.com/mainsail-crew">mainsail-crew</a></th>
</tr>
<tr>
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
</tr>
<tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
</tr>
<tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/nlef">nlef</a></th>
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
</tr>
<tr>
<th>by <a href="https://github.com/nlef">nlef</a></th>
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
<th><h3><a href="https://github.com/crysxd/OctoApp-Plugin">OctoApp For Klipper</a></h3></th>
</tr>
<tr>
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="Mobileraker Logo" height="64"></a></th>
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th>
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
</tr>
<tr>
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
</tr>
<tr>
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
<th><h3><a href="https://github.com/crysxd/OctoApp-Plugin">OctoApp For Klipper</a></h3></th>
<th><h3></h3></th>
<th><h3><a href="https://github.com/staubgeborener/klipper-backup">Klipper-Backup</a></h3></th>
<th><h3><a href="https://simplyprint.io/">SimplyPrint for Klipper</a></h3></th>
</tr>
<tr>
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="OctoEverywhere Logo" height="64"></a></th>
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th>
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
<th><a href="https://github.com/staubgeborener/klipper-backup"><img src="https://avatars.githubusercontent.com/u/28908603?v=4" alt="Staubgeroner Avatar" height="64"></a></th>
<th><a href="https://github.com/SimplyPrint"><img src="https://avatars.githubusercontent.com/u/64896552?s=200&v=4" alt="" height="64"></a></th>
</tr>
<tr>
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
<th></th>
<th>by <a href="https://github.com/Staubgeborener">Staubgeborener</a></th>
<th>by <a href="https://github.com/SimplyPrint">SimplyPrint</a></th>
</tr>
</table>
<hr>
@@ -186,6 +192,12 @@ prompt and confirm by hitting ENTER.
<hr>
<div align="center">
<img src="https://repobeats.axiom.co/api/embed/a1afbda9190c04a90cf4bd3061e5573bc836cb05.svg" alt="Repobeats analytics image"/>
</div>
<hr>
<h2 align="center">✨ Credits ✨</h2>
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!

View File

@@ -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()

View File

@@ -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,

View File

@@ -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"

View File

@@ -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"""
╔═══════════════════════════════════════════════════════╗

View File

@@ -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:

View File

@@ -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

View File

@@ -12,21 +12,25 @@ 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
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
@@ -43,23 +47,19 @@ 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
sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ 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 ║
@@ -72,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
@@ -92,27 +92,18 @@ 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)
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()
self.select_state = False

View File

@@ -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="")

View File

@@ -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 ║

View File

@@ -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 ║

View File

@@ -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:]

View File

@@ -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,

View File

@@ -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: ║
║ ║

View File

@@ -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"

View File

@@ -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(

View File

@@ -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"""
║ ║

View File

@@ -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 (

View File

@@ -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

View File

@@ -14,8 +14,11 @@ 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_type import InstanceType
from utils.instance_utils import get_instances
@@ -23,21 +26,66 @@ 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")
completion_msg = remove_moonraker_config_section(
completion_msg, client_config, mr_instances
)
completion_msg = remove_printer_config_section(
completion_msg, client_config, kl_instances
)
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 remove_client_config_symlink(client_config: BaseWebClientConfig) -> None:
def remove_cfg_symlink(client_config: BaseWebClientConfig, message: Message) -> Message:
instances: List[Klipper] = get_instances(Klipper)
kl_instances = []
for instance in instances:
run_remove_routines(
instance.base.cfg_dir.joinpath(client_config.config_filename)
)
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

View File

@@ -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,54 +34,79 @@ 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)
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)
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:
run_client_config_removal(
cfg_completion_msg = run_client_config_removal(
client.client_config,
kl_instances,
mr_instances,
)
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
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:
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)

View File

@@ -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}",

View File

@@ -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:
@@ -338,36 +336,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(

View File

@@ -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

View File

@@ -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

View File

@@ -15,13 +15,15 @@ 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
from core.logger import DialogCustomColor, 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
@@ -31,6 +33,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()
@@ -49,15 +53,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}{int(self.client_settings.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}
@@ -70,7 +68,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,
@@ -88,17 +86,20 @@ class ClientInstallMenu(BaseMenu):
cmd_sysctl_service("nginx", "start")
# noinspection HttpUrlsUsage
Logger.print_dialog(
DialogType.CUSTOM,
custom_title="Port reconfiguration complete!",
custom_color=DialogCustomColor.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 _go_back(self, **kwargs) -> None:
if self.previous_menu is not None:
self.previous_menu().run()
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

View File

@@ -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,12 +24,14 @@ 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
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
@@ -50,23 +52,19 @@ 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
sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ 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}
@@ -79,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
@@ -99,28 +97,18 @@ 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(
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"
)
def _go_back(self, **kwargs) -> None:
if self.previous_menu is not None:
self.previous_menu().run()
self.select_state = False

View File

@@ -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

View File

@@ -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"]

View File

@@ -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:
return "\n"
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 ""
_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)

View File

@@ -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] ║

View File

@@ -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}
╟───────────────────────────┬───────────────────────────╢

View File

@@ -14,17 +14,14 @@ 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.services.message_service import MessageService
from core.spinner import Spinner
from core.types.color import Color
from utils.input_utils import get_selection_input
@@ -36,14 +33,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 +49,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 +62,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 +76,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 +92,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,10 +112,20 @@ 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
message_service = MessageService()
def __init__(self, **kwargs) -> None:
if type(self) is BaseMenu:
raise NotImplementedError("BaseMenu cannot be instantiated directly.")
@@ -160,7 +172,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 +209,20 @@ class BaseMenu(metaclass=PostInitCaller):
else:
raise NotImplementedError("FooterType not correctly implemented!")
def display_menu(self) -> None:
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()
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)

View File

@@ -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] ║

View File

@@ -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}}

View File

@@ -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! ║
╟───────────────────────────┬───────────────────────────╢

View File

@@ -9,16 +9,21 @@
from __future__ import annotations
import textwrap
from pathlib import Path
from typing import Literal, Tuple, Type
from components.klipper import KLIPPER_DIR, KLIPPER_REPO_URL
from components.klipper.klipper_utils import get_klipper_status
from components.moonraker import MOONRAKER_DIR, MOONRAKER_REPO_URL
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 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
@@ -27,13 +32,16 @@ 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()
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
@@ -50,25 +58,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}
@@ -105,32 +109,51 @@ class SettingsMenu(BaseMenu):
self.mainsail_unstable = self.settings.mainsail.unstable_releases
self.fluidd_unstable = self.settings.fluidd.unstable_releases
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.",
# 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, 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"]) -> None:
repo_url, branch = self._gather_input()
def _set_repo(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> None:
repo_url, branch = self._gather_input(repo_name, repo_dir)
display_name = repo_name.capitalize()
Logger.print_dialog(
DialogType.CUSTOM,
@@ -153,22 +176,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

View File

@@ -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)

View File

View File

@@ -0,0 +1,59 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from dataclasses import dataclass, field
from typing import List
from core.logger import DialogType, Logger
from core.types.color import Color
@dataclass()
class Message:
title: str = field(default="")
text: List[str] = field(default_factory=list)
color: Color = field(default=Color.WHITE)
centered: bool = field(default=False)
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,
center_content=self.message.centered,
)
self.__clear_message()
def __clear_message(self) -> None:
self.message = None

View File

@@ -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:

View File

@@ -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():

View File

29
kiauh/core/types/color.py Normal file
View File

@@ -0,0 +1,29 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
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}"

View File

@@ -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:]

View File

@@ -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 ║

View File

@@ -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)

View File

@@ -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(

View File

@@ -6,23 +6,23 @@
# #
# 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
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,16 +83,16 @@ 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)
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
@@ -124,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

View File

@@ -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

View File

@@ -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)
@@ -73,28 +88,32 @@ 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)
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!")
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!")
return False
def unzip(filepath: Path, target_dir: Path) -> None:

View File

@@ -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]:

View File

@@ -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:
@@ -85,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:
@@ -93,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
@@ -103,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():
@@ -151,7 +156,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: