Compare commits

...

59 Commits

Author SHA1 Message Date
dw-0
760f131d1c fix(klipper): handle file access exception for dietpi version file (#658)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-03-09 08:32:14 +01:00
dw-0
41804f0eaa style: ruff format
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-03-09 08:32:14 +01:00
dw-0
d3c9bcc38c refactor(klipper): move setup functions into KlipperSetupService
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-03-09 08:32:14 +01:00
dw-0
7fc36f3e68 feat(moonraker): display moonraker ip address after install
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-03-08 11:41:29 +01:00
CODeRUS
a4942b9404 fix: Add pkg-config to klipper packages (#655)
Signed-off-by: Andrey Kozhevnikov <coderusinbox@gmail.com>
2025-03-08 11:41:29 +01:00
dw-0
9e0a8a0081 Release v5.1.3
Release v5.1.3
2025-02-23 12:42:44 +01:00
dw-0
6082528628 Merge pull request #648 from Arksine/dev-v5-moonraker-deps-fix
fix: add support for Moonraker's dependency requirement specifiers to V5
2025-02-23 12:32:17 +01:00
Eric Callahan
9e92e4a36a fix: parse moonraker deps with requirement specifiers
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2025-02-22 16:50:28 -05:00
dw-0
7e8f1f3d81 Release v6.0.0-alpha.16
Merge develop into master (Release v6.0.0-alpha.16)
2025-02-22 16:25:49 +01:00
dw-0
234cf2c751 chore(copyright): update year (#645)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-22 16:21:34 +01:00
dw-0
3bc98eed13 fix(moonraker): adapt to new moonraker system_dependency.json syntax (#644)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-22 16:19:00 +01:00
dw-0
777f5e45e7 master -> develop
master -> develop
2025-02-20 21:00:21 +01:00
Paul Fertser
acf0faf158 fix: add ULA to trusted_clients in moonraker.conf (#637)
Signed-off-by: Paul Fertser <fercerpav@gmail.com>
2025-02-16 16:47:00 +01:00
dw-0
5c219ec544 master -> develop (#635) 2025-02-15 11:26:04 +01:00
dw-0
70055e891e Release v6.0.0-alpha.15 2025-02-15 11:17:54 +01:00
dw-0
e3a0a9dec0 Release v6.0.0-alpha.15
fixes #632
2025-02-15 11:15:45 +01:00
dw-0
1cf81377ee Release v6.0.0-alpha.14
fixes #632
2025-02-15 11:11:04 +01:00
dw-0
aa4ea99c5c fix(moonraker): use os-release file to get distro info (#633)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-15 11:09:32 +01:00
marbocub
20ffc82a04 feat: add .internal as a CORS domain in moonraker.conf (#631)
This adds .internal as a CORS domain in moonraker.conf, which is reserved by ICANN as a domain name for private top-level domains (TLDs).

Co-authored-by: dw-0 <th33xitus@gmail.com>
Co-authored-by: dw-0 <domwil1091+github@gmail.com>
2025-02-13 16:21:47 +01:00
dw-0
0becf9d574 Release v6.0.0-alpha.14
fixes #607
fixes #615
fixes #618
fixes #619
fixes #620
fixes #623
fixes #627
2025-02-09 21:15:42 +01:00
dw-0
ed1bfcdeb4 fix(moonraker): correctly install ubuntu 24.10 dependencies (#630)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-09 21:12:59 +01:00
dw-0
033916216c refactor: skip build firmware dependency screen if all are met (#629)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-02-09 17:04:46 +01:00
CODeRUS
d8f47c0960 feature: save and select kconfig (#621)
* feature: save and select kconfig

Signed-off-by: Andrey Kozhevnikov <coderusinbox@gmail.com>

* chore: clean up and sort imports

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

* refactor: replace os.path with Pathlib

- use config paths as type Paths instead of strings.
- tweak some menu visuals.

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

---------

Signed-off-by: Andrey Kozhevnikov <coderusinbox@gmail.com>
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2025-02-09 15:45:05 +01:00
Mathijs Groothuis
4978f22101 fix(typo): Successfull > Successful
Co-authored-by: dw-0 <th33xitus@gmail.com>
Co-authored-by: dw-0 <domwil1091+github@gmail.com>
2025-02-08 13:30:37 +01:00
Mathijs Groothuis
8330f90b56 Fix typo: tyoing > typing
Fix typo: tyoing > typing

Co-authored-by: dw-0 <th33xitus@gmail.com>
Co-authored-by: dw-0 <domwil1091+github@gmail.com>
2025-02-05 19:07:19 +01:00
dw-0
2a08e3eb15 refactor: omit port 80 for IP in success message after webclient installation (#618)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-01-18 17:38:40 +01:00
dw-0
a2a3e92b50 refactor: remove BASE_USER argument from crowsnest install command (#617)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-01-18 17:38:00 +01:00
dw-0
a58288e7e3 Release v6.0.0-alpha.13
Merge develop into master (Release v6.0.0-alpha.13)
2025-01-03 22:13:12 +01:00
dw-0
3852464ab7 fix: use raw strings for regex parameter in get_string_input (#612)
fixes #602

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2025-01-03 22:10:39 +01:00
dw-0
d9626adc98 Release v6.0.0-alpha.12
Merge develop into master (Release v6.0.0-alpha.12)
2024-11-28 19:38:23 +01:00
dw-0
4ae5a37ec6 fix: most recent tag not shown correctly in main menu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-24 21:43:10 +01:00
dw-0
935f81aab6 fix: backup fails in case of dangling symlink
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-24 21:26:12 +01:00
dw-0
b02df9a1e0 Release v6.0.0-alpha.11
Merge develop into master (Release v6.0.0-alpha.11)
2024-11-24 15:55:04 +01:00
nlef
dbbc87f18e fix: use correct telegram bot config path (#600)
* fix telegram bot config path

* use _post)init_value

---------

Co-authored-by: dw-0 <th33xitus@gmail.com>
Co-authored-by: dw-0 <domwil1091+github@gmail.com>
2024-11-24 15:53:49 +01:00
dw-0
243ea6582a Release v6.0.0-alpha.10
Merge develop into master (Release v6.0.0-alpha.10)
2024-11-23 21:17:51 +01:00
dw-0
91cba3637e readme: update README.md
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-23 21:12:35 +01:00
dw-0
3fc190ff25 fix: actually raise exception on empty config value
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-10 21:15:08 +01:00
dw-0
6ff45aab41 refactor: implement basic input validation for repo switch feature
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-10 20:58:37 +01:00
dw-0
b9c9feef3c refactor: clone repo in repo switch routine only if there is already a repo present
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-10 20:14:47 +01:00
dw-0
d37d047aaa refactor: fallback to config settings for repos in settings menu
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-10 19:56:47 +01:00
dw-0
a3fb57aee3 refactor: return - if branch cannot be read
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-11-10 19:54:26 +01:00
dw-0
8aee23830a feat: implement completion message for webclient config remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:31:43 +01:00
dw-0
dd14de9a41 fix: test if checks is empty
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:31:43 +01:00
dw-0
1ca1e8ff6f feat: rework completion message for webclient remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:31:43 +01:00
dw-0
12127efa21 fix: remove extra newline
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:31:43 +01:00
dw-0
66a5cdf9b1 feat: implement completion message for webclient remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:31:37 +01:00
dw-0
9b1aba207c feat: implement completion message for klipper remove process
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 22:30:54 +01:00
dw-0
e274e3c00d refactor: add defaults to Message and center property
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 10:57:47 +01:00
dw-0
dd99b0e1a6 refactor: make run_remove_routines return boolean
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-27 10:56:54 +01:00
dw-0
a616876ace feat: implement message service
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-26 20:41:34 +02:00
dw-0
4925021aa8 fix: ensure encoding
Use an alternative approach as in #587 as it introduces an unexpected behavior in printing output

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-26 18:14:56 +02:00
dw-0
e63d9d67ec refactor: overhaul color mechanics
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-26 18:14:56 +02:00
dw-0
106bf7675f fix: port reconfiguration menu displays wrong port
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-25 13:01:12 +02:00
dw-0
a63cf8c9d9 Release v6.0.0-alpha.9
Merge develop into master (Release v6.0.0-alpha.9)
2024-10-24 12:29:24 +02:00
dw-0
02ed3e7da0 feat: show actual current branch in settings menu (#588)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:26:18 +02:00
dw-0
4427ae94af fix: make sure all output is utf-8 encoded (#587)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:26:01 +02:00
dw-0
81b7b156b9 feat: implement port reconfiguration for webclients (#586)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:25:44 +02:00
dw-0
2df364512b fix: Path.rename() not working across devices (#584)
causes `[Errno 18] Invalid cross-device link` on tmpfs filesystems

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:25:22 +02:00
dw-0
dfa0036326 refactor: don't clear scrollback on clear
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-24 12:24:05 +02:00
115 changed files with 2437 additions and 1252 deletions

130
README.md
View File

@@ -28,12 +28,12 @@
### 📋 Prerequisites ### 📋 Prerequisites
KIAUH is a script that assists you in installing Klipper on a Linux operating system that has 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 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 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/) 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. 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)`: \ select `Choose OS -> Raspberry Pi OS (other)`: \
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350"> <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"> <img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
</p> </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. 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) * 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). * 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 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 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 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 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. 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:** \ * **Step 4:** \
You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending 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" 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. prompt and confirm by hitting ENTER.
<hr> <hr>
@@ -101,77 +101,83 @@ prompt and confirm by hitting ENTER.
<h2 align="center">🌐 Sources & Further Information</h2> <h2 align="center">🌐 Sources & Further Information</h2>
<table> <table align="center">
<tr> <tr>
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</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/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/mainsail-crew/mainsail">Mainsail</a></h3></th>
</tr> </tr>
<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://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://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/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/KevinOConnor">KevinOConnor</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/Arksine">Arksine</a></th>
<th>by <a href="https://github.com/mainsail-crew">mainsail-crew</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>
</tr> </tr>
<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/fluidd-core/fluidd">Fluidd</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/jordanruthe/KlipperScreen">KlipperScreen</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/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>
<tr> <tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th> <th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th> <th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></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/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>
<tr> <tr>
<th>by <a href="https://github.com/nlef">nlef</a></th> <th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th> <th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></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>
<tr> <tr>
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th> <th><h3><a href="https://github.com/staubgeborener/klipper-backup">Klipper-Backup</a></h3></th>
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th> <th><h3><a href="https://simplyprint.io/">SimplyPrint 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>
</tr> </tr>
<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://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://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" 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>
<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>
<tr> <tr>
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th> <th>by <a href="https://github.com/Staubgeborener">Staubgeborener</a></th>
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th> <th>by <a href="https://github.com/SimplyPrint">SimplyPrint</a></th>
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
<th></th>
</tr> </tr>
</table> </table>
<hr> <hr>
@@ -186,6 +192,12 @@ prompt and confirm by hitting ENTER.
<hr> <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> <h2 align="center">✨ Credits ✨</h2>
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo! * A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -10,7 +10,7 @@
#=======================================================================# #=======================================================================#
set -e set -e
clear clear -x
# make sure we have the correct permissions while running the script # make sure we have the correct permissions while running the script
umask 022 umask 022
@@ -110,7 +110,7 @@ function launch_kiauh_v6() {
export PYTHONPATH="${entrypoint}" export PYTHONPATH="${entrypoint}"
clear clear -x
python3 "${entrypoint}/kiauh.py" python3 "${entrypoint}/kiauh.py"
} }

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -27,10 +27,9 @@ from components.crowsnest import (
) )
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
from core.constants import CURRENT_USER
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import ( from utils.common import (
check_install_dependencies, check_install_dependencies,
get_install_status, get_install_status,
@@ -73,7 +72,7 @@ def install_crowsnest() -> None:
Logger.print_info("Installer will prompt you for sudo password!") Logger.print_info("Installer will prompt you for sudo password!")
try: try:
run( run(
f"sudo make install BASE_USER={CURRENT_USER}", f"sudo make install",
cwd=CROWSNEST_DIR, cwd=CROWSNEST_DIR,
shell=True, shell=True,
check=True, check=True,

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent MODULE_PATH = Path(__file__).resolve().parent
KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git"
# names # names
KLIPPER_LOG_NAME = "klippy.log" KLIPPER_LOG_NAME = "klippy.log"
KLIPPER_CFG_NAME = "printer.cfg" KLIPPER_CFG_NAME = "printer.cfg"
@@ -23,6 +25,7 @@ KLIPPER_SERVICE_NAME = "klipper.service"
# directories # directories
KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_DIR = Path.home().joinpath("klipper")
KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs")
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups")

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -11,13 +11,8 @@ import textwrap
from enum import Enum, unique from enum import Enum, unique
from typing import List 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.menus.base_menu import print_back_footer
from core.types.color import Color
from utils.instance_type import InstanceType from utils.instance_type import InstanceType
@@ -42,12 +37,12 @@ def print_instance_overview(
if display_type is DisplayType.SERVICE_NAME if display_type is DisplayType.SERVICE_NAME
else "printer directories" 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 += f"{headline:^64}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n" dialog += "╟───────────────────────────────────────────────────────╢\n"
if show_select_all: 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 += f"{select_all:<63}\n"
dialog += "║ ║\n" dialog += "║ ║\n"
@@ -56,7 +51,9 @@ def print_instance_overview(
name = s.service_file_path.stem name = s.service_file_path.stem
else: else:
name = s.data_dir 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 += f"{line:<63}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n" dialog += "╟───────────────────────────────────────────────────────╢\n"
@@ -65,8 +62,10 @@ def print_instance_overview(
def print_select_instance_count_dialog() -> None: def print_select_instance_count_dialog() -> None:
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" line1 = Color.apply("WARNING:", Color.YELLOW)
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}" line2 = Color.apply(
"Setting up too many instances may crash your system.", Color.YELLOW
)
dialog = textwrap.dedent( dialog = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
@@ -85,8 +84,8 @@ def print_select_instance_count_dialog() -> None:
def print_select_custom_name_dialog() -> None: def print_select_custom_name_dialog() -> None:
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" line1 = Color.apply("INFO:", Color.YELLOW)
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}" line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW)
dialog = textwrap.dedent( dialog = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗

View File

@@ -1,95 +0,0 @@
# ======================================================================= #
# 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 typing import List
from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR
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 utils.fs_utils import run_remove_routines
from utils.input_utils import get_selection_input
from utils.instance_utils import get_instances
from utils.sys_utils import unit_file_exists
def run_klipper_removal(
remove_service: bool,
remove_dir: bool,
remove_env: bool,
) -> None:
klipper_instances: List[Klipper] = get_instances(Klipper)
if remove_service:
Logger.print_status("Removing Klipper instances ...")
if klipper_instances:
instances_to_remove = select_instances_to_remove(klipper_instances)
remove_instances(instances_to_remove)
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)
else:
if remove_dir:
Logger.print_status("Removing Klipper local repository ...")
run_remove_routines(KLIPPER_DIR)
if remove_env:
Logger.print_status("Removing Klipper Python environment ...")
run_remove_routines(KLIPPER_ENV_DIR)
def select_instances_to_remove(instances: List[Klipper]) -> List[Klipper] | None:
start_index = 1
options = [str(i + start_index) for i in range(len(instances))]
options.extend(["a", "b"])
instance_map = {options[i]: instances[i] for i in range(len(instances))}
print_instance_overview(
instances,
start_index=start_index,
show_index=True,
show_select_all=True,
)
selection = get_selection_input("Select Klipper instance to remove", options)
instances_to_remove = []
if selection == "b":
return None
elif selection == "a":
instances_to_remove.extend(instances)
else:
instances_to_remove.append(instance_map[selection])
return instances_to_remove
def remove_instances(
instance_list: List[Klipper] | None,
) -> None:
if not instance_list:
return
for instance in instance_list:
Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...")
InstanceManager.remove(instance)
delete_klipper_env_file(instance)
def delete_klipper_env_file(instance: Klipper):
Logger.print_status(f"Remove '{instance.env_file}'")
if not instance.env_file.exists():
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
Logger.print_info(msg)
return
run_remove_routines(instance.env_file)

View File

@@ -1,239 +0,0 @@
# ======================================================================= #
# 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 pathlib import Path
from typing import Dict, List, Tuple
from components.klipper import (
EXIT_KLIPPER_SETUP,
KLIPPER_DIR,
KLIPPER_ENV_DIR,
KLIPPER_INSTALL_SCRIPT,
KLIPPER_REQ_FILE,
)
from components.klipper.klipper import Klipper
from components.klipper.klipper_dialogs import (
print_select_custom_name_dialog,
)
from components.klipper.klipper_utils import (
assign_custom_name,
backup_klipper_dir,
check_user_groups,
create_example_printer_cfg,
get_install_count,
handle_disruptive_system_packages,
)
from components.moonraker.moonraker import Moonraker
from components.webui_client.client_utils import (
get_existing_clients,
)
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings
from utils.common import check_install_dependencies
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
from utils.sys_utils import (
cmd_sysctl_manage,
cmd_sysctl_service,
create_python_venv,
install_python_requirements,
parse_packages_from_file,
)
def install_klipper() -> None:
Logger.print_status("Installing Klipper ...")
klipper_list: List[Klipper] = get_instances(Klipper)
moonraker_list: List[Moonraker] = get_instances(Moonraker)
match_moonraker: bool = False
# if there are more moonraker instances than klipper instances, ask the user to
# match the klipper instance count to the count of moonraker instances with the same suffix
if len(moonraker_list) > len(klipper_list):
is_confirmed = display_moonraker_info(moonraker_list)
if not is_confirmed:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
match_moonraker = True
install_count, name_dict = get_install_count_and_name_dict(
klipper_list, moonraker_list
)
if install_count == 0:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
is_multi_install = install_count > 1 or (len(name_dict) >= 1 and install_count >= 1)
if not name_dict and install_count == 1:
name_dict = {0: ""}
elif is_multi_install and not match_moonraker:
custom_names = use_custom_names_or_go_back()
if custom_names is None:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
handle_instance_names(install_count, name_dict, custom_names)
create_example_cfg = get_confirm("Create example printer.cfg?")
# run the actual installation
try:
run_klipper_setup(klipper_list, name_dict, create_example_cfg)
except Exception as e:
Logger.print_error(e)
Logger.print_error("Klipper installation failed!")
return
def run_klipper_setup(
klipper_list: List[Klipper], name_dict: Dict[int, str], create_example_cfg: bool
) -> None:
if not klipper_list:
setup_klipper_prerequesites()
for i in name_dict:
# skip this iteration if there is already an instance with the name
if name_dict[i] in [n.suffix for n in klipper_list]:
continue
instance = Klipper(suffix=name_dict[i])
instance.create()
cmd_sysctl_service(instance.service_file_path.name, "enable")
if create_example_cfg:
# if a client-config is installed, include it in the new example cfg
clients = get_existing_clients()
create_example_printer_cfg(instance, clients)
cmd_sysctl_service(instance.service_file_path.name, "start")
cmd_sysctl_manage("daemon-reload")
# step 4: check/handle conflicting packages/services
handle_disruptive_system_packages()
# step 5: check for required group membership
check_user_groups()
def handle_instance_names(
install_count: int, name_dict: Dict[int, str], custom_names: bool
) -> None:
for i in range(install_count): # 3
key: int = len(name_dict.keys()) + 1
if custom_names:
assign_custom_name(key, name_dict)
else:
name_dict[key] = str(len(name_dict) + 1)
def get_install_count_and_name_dict(
klipper_list: List[Klipper], moonraker_list: List[Moonraker]
) -> Tuple[int, Dict[int, str]]:
install_count: int | None
if len(moonraker_list) > len(klipper_list):
install_count = len(moonraker_list)
name_dict = {i: moonraker.suffix for i, moonraker in enumerate(moonraker_list)}
else:
install_count = get_install_count()
name_dict = {i: klipper.suffix for i, klipper in enumerate(klipper_list)}
if install_count is None:
Logger.print_status(EXIT_KLIPPER_SETUP)
return 0, {}
return install_count, name_dict
def setup_klipper_prerequesites() -> None:
settings = KiauhSettings()
repo = settings.klipper.repo_url
branch = settings.klipper.branch
git_clone_wrapper(repo, KLIPPER_DIR, branch)
# install klipper dependencies and create python virtualenv
try:
install_klipper_packages()
if create_python_venv(KLIPPER_ENV_DIR):
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
except Exception:
Logger.print_error("Error during installation of Klipper requirements!")
raise
def install_klipper_packages() -> None:
script = KLIPPER_INSTALL_SCRIPT
packages = parse_packages_from_file(script)
# Add dbus requirement for DietPi distro
if Path("/boot/dietpi/.version").exists():
packages.append("dbus")
check_install_dependencies({*packages})
def update_klipper() -> None:
Logger.print_dialog(
DialogType.WARNING,
[
"Do NOT continue if there are ongoing prints running!",
"All Klipper instances will be restarted during the update process and "
"ongoing prints WILL FAIL.",
],
)
if not get_confirm("Update Klipper now?"):
return
settings = KiauhSettings()
if settings.kiauh.backup_before_update:
backup_klipper_dir()
instances = get_instances(Klipper)
InstanceManager.stop_all(instances)
git_pull_wrapper(repo=settings.klipper.repo_url, target_dir=KLIPPER_DIR)
# install possible new system packages
install_klipper_packages()
# install possible new python dependencies
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
InstanceManager.start_all(instances)
def use_custom_names_or_go_back() -> bool | None:
print_select_custom_name_dialog()
_input: bool | None = get_confirm(
"Assign custom names?",
False,
allow_go_back=True,
)
return _input
def display_moonraker_info(moonraker_list: List[Moonraker]) -> bool:
# todo: only show the klipper instances that are not already installed
Logger.print_dialog(
DialogType.INFO,
[
"Existing Moonraker instances detected:",
*[f"{m.service_file_path.stem}" for m in moonraker_list],
"\n\n",
"The following Klipper instances will be installed:",
*[f"● klipper-{m.suffix}" for m in moonraker_list],
],
)
_input: bool = get_confirm("Proceed with installation?")
return _input

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -11,6 +11,7 @@ from __future__ import annotations
import grp import grp
import os import os
import shutil import shutil
from pathlib import Path
from subprocess import CalledProcessError, run from subprocess import CalledProcessError, run
from typing import Dict, List from typing import Dict, List
@@ -18,6 +19,7 @@ from components.klipper import (
KLIPPER_BACKUP_DIR, KLIPPER_BACKUP_DIR,
KLIPPER_DIR, KLIPPER_DIR,
KLIPPER_ENV_DIR, KLIPPER_ENV_DIR,
KLIPPER_INSTALL_SCRIPT,
MODULE_PATH, MODULE_PATH,
) )
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
@@ -36,11 +38,12 @@ from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
) )
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import get_install_status from utils.common import check_install_dependencies, get_install_status
from utils.fs_utils import check_file_exist
from utils.input_utils import get_confirm, get_number_input, get_string_input from utils.input_utils import get_confirm, get_number_input, get_string_input
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import cmd_sysctl_service from utils.sys_utils import cmd_sysctl_service, parse_packages_from_file
def get_klipper_status() -> ComponentStatus: def get_klipper_status() -> ComponentStatus:
@@ -194,3 +197,17 @@ def backup_klipper_dir() -> None:
bm = BackupManager() bm = BackupManager()
bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR) bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR)
bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR) bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR)
def install_klipper_packages() -> None:
script = KLIPPER_INSTALL_SCRIPT
packages = parse_packages_from_file(script)
# Add pkg-config for rp2040 build
packages.append("pkg-config")
# Add dbus requirement for DietPi distro
if check_file_exist(Path("/boot/dietpi/.version")):
packages.append("dbus")
check_install_dependencies({*packages})

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -11,22 +11,28 @@ from __future__ import annotations
import textwrap import textwrap
from typing import Type from typing import Type
from components.klipper import klipper_remove from components.klipper.services.klipper_setup_service import KlipperSetupService
from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
from core.menus import FooterType, Option from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class KlipperRemoveMenu(BaseMenu): class KlipperRemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Remove Klipper"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.footer_type = FooterType.BACK self.footer_type = FooterType.BACK
self.remove_klipper_service = False
self.remove_klipper_dir = False self.rm_svc = False
self.remove_klipper_env = False self.rm_dir = False
self.selection_state = False self.rm_env = False
self.select_state = False
self.klsvc = KlipperSetupService()
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.remove_menu import RemoveMenu from core.menus.remove_menu import RemoveMenu
@@ -43,23 +49,19 @@ class KlipperRemoveMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Remove Klipper ] " checked = f"[{Color.apply('x', Color.CYAN)}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]" unchecked = "[ ]"
o1 = checked if self.remove_klipper_service else unchecked o1 = checked if self.rm_svc else unchecked
o2 = checked if self.remove_klipper_dir else unchecked o2 = checked if self.rm_dir else unchecked
o3 = checked if self.remove_klipper_env else unchecked o3 = checked if self.rm_env else unchecked
sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║ ║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║ ║ the specific option for removal. ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ a) {self._get_selection_state_str():37} ║ a) {sel_state:49}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) {o1} Remove Service ║ ║ 1) {o1} Remove Service ║
║ 2) {o2} Remove Local Repository ║ ║ 2) {o2} Remove Local Repository ║
@@ -72,47 +74,29 @@ class KlipperRemoveMenu(BaseMenu):
print(menu, end="") print(menu, end="")
def toggle_all(self, **kwargs) -> None: def toggle_all(self, **kwargs) -> None:
self.selection_state = not self.selection_state self.select_state = not self.select_state
self.remove_klipper_service = self.selection_state self.rm_svc = self.select_state
self.remove_klipper_dir = self.selection_state self.rm_dir = self.select_state
self.remove_klipper_env = self.selection_state self.rm_env = self.select_state
def toggle_remove_klipper_service(self, **kwargs) -> None: def toggle_remove_klipper_service(self, **kwargs) -> None:
self.remove_klipper_service = not self.remove_klipper_service self.rm_svc = not self.rm_svc
def toggle_remove_klipper_dir(self, **kwargs) -> None: def toggle_remove_klipper_dir(self, **kwargs) -> None:
self.remove_klipper_dir = not self.remove_klipper_dir self.rm_dir = not self.rm_dir
def toggle_remove_klipper_env(self, **kwargs) -> None: def toggle_remove_klipper_env(self, **kwargs) -> None:
self.remove_klipper_env = not self.remove_klipper_env self.rm_env = not self.rm_env
def run_removal_process(self, **kwargs) -> None: def run_removal_process(self, **kwargs) -> None:
if ( if not self.rm_svc and not self.rm_dir and not self.rm_env:
not self.remove_klipper_service msg = "Nothing selected! Select options to remove first."
and not self.remove_klipper_dir print(Color.apply(msg, Color.RED))
and not self.remove_klipper_env
):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
print(error)
return return
klipper_remove.run_klipper_removal( self.klsvc.remove(self.rm_svc, self.rm_dir, self.rm_env)
self.remove_klipper_service,
self.remove_klipper_dir,
self.remove_klipper_env,
)
self.remove_klipper_service = False self.rm_svc = False
self.remove_klipper_dir = False self.rm_dir = False
self.remove_klipper_env = False self.rm_env = False
self.select_state = 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()

View File

@@ -0,0 +1,46 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
from typing import List
from components.klipper.klipper import Klipper
from utils.instance_utils import get_instances
class KlipperInstanceService:
__cls_instance = None
__instances: List[Klipper] = []
def __new__(cls) -> "KlipperInstanceService":
if cls.__cls_instance is None:
cls.__cls_instance = super(KlipperInstanceService, cls).__new__(cls)
return cls.__cls_instance
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
def load_instances(self) -> None:
self.__instances = get_instances(Klipper)
def create_new_instance(self, suffix: str) -> Klipper:
instance = Klipper(suffix)
self.__instances.append(instance)
return instance
def get_all_instances(self) -> List[Klipper]:
return self.__instances
def get_instance_by_suffix(self, suffix: str) -> Klipper | None:
instances: List[Klipper] = [i for i in self.__instances if i.suffix == suffix]
return instances[0] if instances else None

View File

@@ -0,0 +1,362 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
from copy import copy
from typing import Dict, List, Tuple
from components.klipper import (
EXIT_KLIPPER_SETUP,
KLIPPER_DIR,
KLIPPER_ENV_DIR,
KLIPPER_REQ_FILE,
)
from components.klipper.klipper import Klipper
from components.klipper.klipper_dialogs import (
print_instance_overview,
print_select_custom_name_dialog,
)
from components.klipper.klipper_utils import (
assign_custom_name,
backup_klipper_dir,
check_user_groups,
create_example_printer_cfg,
get_install_count,
handle_disruptive_system_packages,
install_klipper_packages,
)
from components.klipper.services.klipper_instance_service import KlipperInstanceService
from components.moonraker.moonraker import Moonraker
from components.moonraker.services.moonraker_instance_service import (
MoonrakerInstanceService,
)
from components.webui_client.client_utils import (
get_existing_clients,
)
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.services.message_service import Message, MessageService
from core.settings.kiauh_settings import KiauhSettings
from core.types.color import Color
from utils.fs_utils import run_remove_routines
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm, get_selection_input
from utils.sys_utils import (
cmd_sysctl_manage,
create_python_venv,
install_python_requirements,
unit_file_exists,
)
# noinspection PyMethodMayBeStatic
class KlipperSetupService:
__cls_instance = None
kisvc: KlipperInstanceService
misvc: MoonrakerInstanceService
msgsvc = MessageService
settings: KiauhSettings
klipper_list: List[Klipper]
moonraker_list: List[Moonraker]
def __new__(cls) -> "KlipperSetupService":
if cls.__cls_instance is None:
cls.__cls_instance = super(KlipperSetupService, cls).__new__(cls)
return cls.__cls_instance
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
self.__init_state()
def __init_state(self) -> None:
self.settings = KiauhSettings()
self.kisvc = KlipperInstanceService()
self.kisvc.load_instances()
self.klipper_list = self.kisvc.get_all_instances()
self.misvc = MoonrakerInstanceService()
self.misvc.load_instances()
self.moonraker_list = self.misvc.get_all_instances()
self.msgsvc = MessageService()
def __refresh_state(self) -> None:
self.kisvc.load_instances()
self.klipper_list = self.kisvc.get_all_instances()
self.misvc.load_instances()
self.moonraker_list = self.misvc.get_all_instances()
def install(self) -> None:
Logger.print_status("Installing Klipper ...")
match_moonraker: bool = False
# if there are more moonraker instances than klipper instances, ask the user to
# match the klipper instance count to the count of moonraker instances with the same suffix
if len(self.moonraker_list) > len(self.klipper_list):
is_confirmed = self.__display_moonraker_info()
if not is_confirmed:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
match_moonraker = True
install_count, name_dict = self.__get_install_count_and_name_dict()
if install_count == 0:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
is_multi_install = install_count > 1 or (
len(name_dict) >= 1 and install_count >= 1
)
if not name_dict and install_count == 1:
name_dict = {0: ""}
elif is_multi_install and not match_moonraker:
custom_names = self.__use_custom_names_or_go_back()
if custom_names is None:
Logger.print_status(EXIT_KLIPPER_SETUP)
return
self.__handle_instance_names(install_count, name_dict, custom_names)
create_example_cfg = get_confirm("Create example printer.cfg?")
# run the actual installation
try:
self.__run_setup(name_dict, create_example_cfg)
except Exception as e:
Logger.print_error(e)
Logger.print_error("Klipper installation failed!")
return
def update(self) -> None:
Logger.print_dialog(
DialogType.WARNING,
[
"Do NOT continue if there are ongoing prints running!",
"All Klipper instances will be restarted during the update process and "
"ongoing prints WILL FAIL.",
],
)
if not get_confirm("Update Klipper now?"):
return
self.__refresh_state()
if self.settings.kiauh.backup_before_update:
backup_klipper_dir()
InstanceManager.stop_all(self.klipper_list)
git_pull_wrapper(self.settings.klipper.repo_url, KLIPPER_DIR)
install_klipper_packages()
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
InstanceManager.start_all(self.klipper_list)
def remove(
self,
remove_service: bool,
remove_dir: bool,
remove_env: bool,
) -> None:
self.__refresh_state()
completion_msg = Message(
title="Klipper Removal Process completed",
color=Color.GREEN,
)
if remove_service:
Logger.print_status("Removing Klipper instances ...")
if self.klipper_list:
instances_to_remove = self.__get_instances_to_remove()
self.__remove_instances(instances_to_remove)
if 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"):
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 ...")
if run_remove_routines(KLIPPER_DIR):
completion_msg.text.append("● Klipper local repository removed")
if remove_env:
Logger.print_status("Removing Klipper Python environment ...")
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."]
self.msgsvc.set_message(completion_msg)
def __get_install_count_and_name_dict(self) -> Tuple[int, Dict[int, str]]:
install_count: int | None
if len(self.moonraker_list) > len(self.klipper_list):
install_count = len(self.moonraker_list)
name_dict = {
i: moonraker.suffix for i, moonraker in enumerate(self.moonraker_list)
}
else:
install_count = get_install_count()
name_dict = {
i: klipper.suffix for i, klipper in enumerate(self.klipper_list)
}
if install_count is None:
Logger.print_status(EXIT_KLIPPER_SETUP)
return 0, {}
return install_count, name_dict
def __run_setup(self, name_dict: Dict[int, str], create_example_cfg: bool) -> None:
if not self.klipper_list:
self.__install_deps()
for i in name_dict:
# skip this iteration if there is already an instance with the name
if name_dict[i] in [n.suffix for n in self.klipper_list]:
continue
instance = Klipper(suffix=name_dict[i])
instance.create()
InstanceManager.enable(instance)
if create_example_cfg:
# if a client-config is installed, include it in the new example cfg
clients = get_existing_clients()
create_example_printer_cfg(instance, clients)
InstanceManager.start(instance)
cmd_sysctl_manage("daemon-reload")
# step 4: check/handle conflicting packages/services
handle_disruptive_system_packages()
# step 5: check for required group membership
check_user_groups()
def __install_deps(self) -> None:
repo = self.settings.klipper.repo_url
branch = self.settings.klipper.branch
git_clone_wrapper(repo, KLIPPER_DIR, branch)
try:
install_klipper_packages()
if create_python_venv(KLIPPER_ENV_DIR):
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE)
except Exception:
Logger.print_error("Error during installation of Klipper requirements!")
raise
def __display_moonraker_info(self) -> bool:
# todo: only show the klipper instances that are not already installed
Logger.print_dialog(
DialogType.INFO,
[
"Existing Moonraker instances detected:",
*[f"{m.service_file_path.stem}" for m in self.moonraker_list],
"\n\n",
"The following Klipper instances will be installed:",
*[f"● klipper-{m.suffix}" for m in self.moonraker_list],
],
)
_input: bool = get_confirm("Proceed with installation?")
return _input
def __handle_instance_names(
self, install_count: int, name_dict: Dict[int, str], custom_names: bool
) -> None:
for i in range(install_count): # 3
key: int = len(name_dict.keys()) + 1
if custom_names:
assign_custom_name(key, name_dict)
else:
name_dict[key] = str(len(name_dict) + 1)
def __use_custom_names_or_go_back(self) -> bool | None:
print_select_custom_name_dialog()
_input: bool | None = get_confirm(
"Assign custom names?",
False,
allow_go_back=True,
)
return _input
def __get_instances_to_remove(self) -> List[Klipper] | None:
start_index = 1
curr_instances: List[Klipper] = self.klipper_list
instance_count = len(curr_instances)
options = [str(i + start_index) for i in range(instance_count)]
options.extend(["a", "b"])
instance_map = {options[i]: self.klipper_list[i] for i in range(instance_count)}
print_instance_overview(
self.klipper_list,
start_index=start_index,
show_index=True,
show_select_all=True,
)
selection = get_selection_input("Select Klipper instance to remove", options)
if selection == "b":
return None
elif selection == "a":
return copy(self.klipper_list)
return [instance_map[selection]]
def __remove_instances(
self,
instance_list: List[Klipper] | None,
) -> None:
if not instance_list:
return
for instance in instance_list:
Logger.print_status(
f"Removing instance {instance.service_file_path.stem} ..."
)
InstanceManager.remove(instance)
self.__delete_klipper_env_file(instance)
self.__refresh_state()
def __delete_klipper_env_file(self, instance: Klipper):
Logger.print_status(f"Remove '{instance.env_file}'")
if not instance.env_file.exists():
msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
Logger.print_info(msg)
return
run_remove_routines(instance.env_file)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -7,6 +7,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import re import re
from pathlib import Path
from subprocess import ( from subprocess import (
DEVNULL, DEVNULL,
PIPE, PIPE,
@@ -138,6 +139,7 @@ def start_flash_process(flash_options: FlashOptions) -> None:
if flash_options.flash_method is FlashMethod.REGULAR: if flash_options.flash_method is FlashMethod.REGULAR:
cmd = [ cmd = [
"make", "make",
f"KCONFIG_CONFIG={flash_options.selected_kconfig}",
flash_options.flash_command.value, flash_options.flash_command.value,
f"FLASH_DEVICE={flash_options.selected_mcu}", f"FLASH_DEVICE={flash_options.selected_mcu}",
] ]
@@ -165,17 +167,17 @@ def start_flash_process(flash_options: FlashOptions) -> None:
if rc != 0: if rc != 0:
raise Exception(f"Flashing failed with returncode: {rc}") raise Exception(f"Flashing failed with returncode: {rc}")
else: else:
Logger.print_ok("Flashing successfull!", start="\n", end="\n\n") Logger.print_ok("Flashing successful!", start="\n", end="\n\n")
except (Exception, CalledProcessError): except (Exception, CalledProcessError):
Logger.print_error("Flashing failed!", start="\n") Logger.print_error("Flashing failed!", start="\n")
Logger.print_error("See the console output above!", end="\n\n") Logger.print_error("See the console output above!", end="\n\n")
def run_make_clean() -> None: def run_make_clean(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try: try:
run( run(
"make clean", f"make KCONFIG_CONFIG={kconfig} clean",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,
@@ -185,10 +187,10 @@ def run_make_clean() -> None:
raise raise
def run_make_menuconfig() -> None: def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try: try:
run( run(
"make PYTHON=python3 menuconfig", f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,
@@ -198,10 +200,10 @@ def run_make_menuconfig() -> None:
raise raise
def run_make() -> None: def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try: try:
run( run(
"make PYTHON=python3", f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -39,6 +39,7 @@ class FlashOptions:
_selected_mcu: str = "" _selected_mcu: str = ""
_selected_board: str = "" _selected_board: str = ""
_selected_baudrate: int = 250000 _selected_baudrate: int = 250000
_selected_kconfig: str = ".config"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not cls._instance: if not cls._instance:
@@ -104,3 +105,11 @@ class FlashOptions:
@selected_baudrate.setter @selected_baudrate.setter
def selected_baudrate(self, value: int) -> None: def selected_baudrate(self, value: int) -> None:
self._selected_baudrate = value self._selected_baudrate = value
@property
def selected_kconfig(self) -> str:
return self._selected_kconfig
@selected_kconfig.setter
def selected_kconfig(self, value: str) -> None:
self._selected_kconfig = value

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -9,18 +9,22 @@
from __future__ import annotations from __future__ import annotations
import textwrap import textwrap
from pathlib import Path
from shutil import copyfile
from typing import List, Set, Type from typing import List, Set, Type
from components.klipper import KLIPPER_DIR from components.klipper import KLIPPER_DIR, KLIPPER_KCONFIGS_DIR
from components.klipper_firmware.firmware_utils import ( from components.klipper_firmware.firmware_utils import (
run_make, run_make,
run_make_clean, run_make_clean,
run_make_menuconfig, run_make_menuconfig,
) )
from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT from components.klipper_firmware.flash_options import FlashOptions
from core.logger import Logger from core.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from utils.input_utils import get_confirm, get_string_input
from utils.sys_utils import ( from utils.sys_utils import (
check_package_install, check_package_install,
install_system_packages, install_system_packages,
@@ -30,12 +34,25 @@ from utils.sys_utils import (
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
class KlipperBuildFirmwareMenu(BaseMenu): class KlipperKConfigMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Firmware Config Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} self.flash_options = FlashOptions()
self.missing_deps: List[str] = check_package_install(self.deps) self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR
self.kconfig_default = KLIPPER_DIR.joinpath(".config")
self.configs: List[Path] = []
self.kconfig = (
self.kconfig_default if not Path(self.kconfigs_dirname).is_dir() else None
)
def run(self) -> None:
if not self.kconfig:
super().run()
else:
self.flash_options.selected_kconfig = self.kconfig
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.advanced_menu import AdvancedMenu from core.menus.advanced_menu import AdvancedMenu
@@ -45,21 +62,107 @@ class KlipperBuildFirmwareMenu(BaseMenu):
) )
def set_options(self) -> None: def set_options(self) -> None:
if len(self.missing_deps) == 0: if not Path(self.kconfigs_dirname).is_dir():
self.input_label_txt = "Press ENTER to continue" return
self.default_option = Option(method=self.start_build_process)
else: self.input_label_txt = "Select config or action to continue (default=N)"
self.input_label_txt = "Press ENTER to install dependencies" self.default_option = Option(
self.default_option = Option(method=self.install_missing_deps) method=self.select_config, opt_data=self.kconfig_default
)
option_index = 1
for kconfig in Path(self.kconfigs_dirname).iterdir():
if not kconfig.name.endswith(".config"):
continue
kconfig_path = self.kconfigs_dirname.joinpath(kconfig)
if Path(kconfig_path).is_file():
self.configs += [kconfig]
self.options[str(option_index)] = Option(
method=self.select_config, opt_data=kconfig_path
)
option_index += 1
self.options["n"] = Option(
method=self.select_config, opt_data=self.kconfig_default
)
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Build Firmware Menu ] " cfg_found_str = Color.apply(
color = COLOR_CYAN "Previously saved firmware configs found!", Color.GREEN
count = 62 - len(color) - len(RESET_FORMAT) )
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╟───────────────────────────────────────────────────────╢
{color}{header:~^{count}}{RESET_FORMAT} {cfg_found_str:^62}
║ ║
║ Select an existing config or create a new one. ║
╟───────────────────────────────────────────────────────╢
║ Available firmware configs: ║
"""
)[1:]
start_index = 1
for i, s in enumerate(self.configs):
line = f"{start_index + i}) {s.name}"
menu += f"{line:<54}\n"
new_config = Color.apply("N) Create new firmware config", Color.GREEN)
menu += "║ ║\n"
menu += f"{new_config:<62}\n"
menu += "╟───────────────────────────────────────────────────────╢\n"
print(menu, end="")
def select_config(self, **kwargs) -> None:
selection: str | None = kwargs.get("opt_data", None)
if selection is None:
raise Exception("opt_data is None")
if not Path(selection).is_file() and selection != self.kconfig_default:
raise Exception("opt_data does not exists")
self.kconfig = selection
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperBuildFirmwareMenu(BaseMenu):
def __init__(
self, kconfig: str | None = None, 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)
self.flash_options = FlashOptions()
self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR
self.kconfig_default = KLIPPER_DIR.joinpath(".config")
self.kconfig = self.flash_options.selected_kconfig
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.advanced_menu import AdvancedMenu
self.previous_menu = (
previous_menu if previous_menu is not None else AdvancedMenu
)
def set_options(self) -> None:
self.input_label_txt = "Press ENTER to install dependencies"
self.default_option = Option(method=self.install_missing_deps)
def run(self):
# immediately start the build process if all dependencies are met
if len(self.missing_deps) == 0:
self.start_build_process()
else:
super().run()
def print_menu(self) -> None:
txt = Color.apply("Dependencies are missing!", Color.RED)
menu = textwrap.dedent(
f"""
╟───────────────────────────────────────────────────────╢
{txt:^62}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ The following dependencies are required: ║ ║ The following dependencies are required: ║
║ ║ ║ ║
@@ -67,20 +170,14 @@ class KlipperBuildFirmwareMenu(BaseMenu):
)[1:] )[1:]
for d in self.deps: for d in self.deps:
status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}" status_ok = Color.apply("*INSTALLED*", Color.GREEN)
status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}" status_missing = Color.apply("*MISSING*", Color.RED)
status = status_missing if d in self.missing_deps else status_ok status = status_missing if d in self.missing_deps else status_ok
padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) padding = 40 - 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 += f"{d}{status:>{padding}}\n"
menu += "║ ║\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}"
menu += f"{line:<62}\n"
menu += "╟───────────────────────────────────────────────────────╢\n" menu += "╟───────────────────────────────────────────────────────╢\n"
print(menu, end="") print(menu, end="")
@@ -99,13 +196,16 @@ class KlipperBuildFirmwareMenu(BaseMenu):
def start_build_process(self, **kwargs) -> None: def start_build_process(self, **kwargs) -> None:
try: try:
run_make_clean() run_make_clean(self.kconfig)
run_make_menuconfig() run_make_menuconfig(self.kconfig)
run_make() run_make(self.kconfig)
Logger.print_ok("Firmware successfully built!") Logger.print_ok("Firmware successfully built!")
Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!")
if self.kconfig == self.kconfig_default:
self.save_firmware_config()
except Exception as e: except Exception as e:
Logger.print_error(e) Logger.print_error(e)
Logger.print_error("Building Klipper Firmware failed!") Logger.print_error("Building Klipper Firmware failed!")
@@ -113,3 +213,62 @@ class KlipperBuildFirmwareMenu(BaseMenu):
finally: finally:
if self.previous_menu is not None: if self.previous_menu is not None:
self.previous_menu().run() self.previous_menu().run()
def save_firmware_config(self) -> None:
Logger.print_dialog(
DialogType.CUSTOM,
[
"You can save the firmware build configs for multiple MCUs,"
" and use them to update the firmware after a Klipper version upgrade"
],
custom_title="Save firmware config",
)
if not get_confirm(
"Do you want to save firmware config?", default_choice=False
):
return
filename = self.kconfig_default
while True:
Logger.print_dialog(
DialogType.CUSTOM,
[
"Allowed characters: a-z, 0-9 and '-'",
"The name must not contain the following:",
"\n\n",
"● Any special characters",
"● No leading or trailing '-'",
],
)
input_name = get_string_input(
"Enter the new firmware config name",
regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$",
)
filename = self.kconfigs_dirname.joinpath(f"{input_name}.config")
if Path(filename).is_file():
if get_confirm(
f"Firmware config {input_name} already exists, overwrite?",
default_choice=False,
):
break
if Path(filename).is_dir():
Logger.print_error(f"Path {filename} exists and it's a directory")
if not Path(filename).exists():
break
if not get_confirm(
f"Save firmware config to '{filename}'?", default_choice=True
):
Logger.print_info("Aborted saving firmware config ...")
return
if not Path(self.kconfigs_dirname).exists():
Path(self.kconfigs_dirname).mkdir()
copyfile(self.kconfig_default, filename)
Logger.print_ok()
Logger.print_ok(f"Firmware config successfully saved to {filename}")

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,9 +12,9 @@ import textwrap
from typing import Type from typing import Type
from components.klipper_firmware.flash_options import FlashMethod, FlashOptions 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 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 # noinspection PyUnusedLocal
@@ -22,6 +22,9 @@ from core.menus.base_menu import BaseMenu
class KlipperNoFirmwareErrorMenu(BaseMenu): class KlipperNoFirmwareErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() 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.previous_menu: Type[BaseMenu] | None = previous_menu
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
@@ -35,16 +38,11 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
self.default_option = Option(method=self.go_back) self.default_option = Option(method=self.go_back)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! NO FIRMWARE FILE FOUND !!!" line1 = "Unable to find a compiled firmware file!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {Color.apply(line1, Color.RED):<62}
║ ║ ║ ║
║ Make sure, that: ║ ║ Make sure, that: ║
║ ● the folder '~/klipper/out' and its content exist ║ ║ ● the folder '~/klipper/out' and its content exist ║
@@ -71,6 +69,9 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
class KlipperNoBoardTypesErrorMenu(BaseMenu): class KlipperNoBoardTypesErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() 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.previous_menu: Type[BaseMenu] | None = previous_menu
self.footer_type = FooterType.BLANK self.footer_type = FooterType.BLANK
self.input_label_txt = "Press ENTER to go back to [Main Menu]" 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) self.default_option = Option(method=self.go_back)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ERROR GETTING BOARD LIST !!!" line1 = "Reading the list of supported boards failed!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {Color.apply(line1, Color.RED):<62}
║ ║ ║ ║
║ Make sure, that: ║ ║ Make sure, that: ║
║ ● the folder '~/klipper' and all its content exist ║ ║ ● the folder '~/klipper' and all its content exist ║

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -9,16 +9,21 @@
from __future__ import annotations from __future__ import annotations
import textwrap 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, MenuTitleStyle
from core.menus.base_menu import BaseMenu from core.types.color import Color
def __title_config__() -> Tuple[str, Color, MenuTitleStyle]:
return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN
# noinspection DuplicatedCode # noinspection DuplicatedCode
class KlipperFlashMethodHelpMenu(BaseMenu): class KlipperFlashMethodHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -34,15 +39,10 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("Regular flashing method:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN)
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}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ The default method to flash controller boards which ║ ║ The default method to flash controller boards which ║
@@ -77,6 +77,7 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
class KlipperFlashCommandHelpMenu(BaseMenu): class KlipperFlashCommandHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -92,15 +93,10 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("make flash:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("make serialflash:", Color.CYAN)
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ The default command to flash controller board, it ║ ║ The default command to flash controller board, it ║
@@ -121,6 +117,7 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
class KlipperMcuConnectionHelpMenu(BaseMenu): class KlipperMcuConnectionHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -138,17 +135,12 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("USB:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("UART:", Color.CYAN)
count = 62 - len(color) - len(RESET_FORMAT) subheader3 = Color.apply("USB DFU:", Color.CYAN)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN)
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
subheader3 = f"{COLOR_CYAN}USB DFU:{RESET_FORMAT}"
subheader4 = f"{COLOR_CYAN}USB RP2040 Boot:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ Selecting USB as the connection method will scan the ║ ║ Selecting USB as the connection method will scan the ║

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -10,6 +10,7 @@ from __future__ import annotations
import textwrap import textwrap
import time import time
from pathlib import Path
from typing import Type from typing import Type
from components.klipper_firmware.firmware_utils import ( from components.klipper_firmware.firmware_utils import (
@@ -36,10 +37,10 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import (
KlipperFlashMethodHelpMenu, KlipperFlashMethodHelpMenu,
KlipperMcuConnectionHelpMenu, KlipperMcuConnectionHelpMenu,
) )
from core.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import FooterType, Option 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 from utils.input_utils import get_number_input
@@ -48,6 +49,8 @@ from utils.input_utils import get_number_input
class KlipperFlashMethodMenu(BaseMenu): class KlipperFlashMethodMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "MCU Flash Menu"
self.title_color = Color.CYAN
self.help_menu = KlipperFlashMethodHelpMenu self.help_menu = KlipperFlashMethodHelpMenu
self.input_label_txt = "Select flash method" self.input_label_txt = "Select flash method"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -67,17 +70,13 @@ class KlipperFlashMethodMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ MCU Flash Menu ] " subheader = Color.apply("ATTENTION:", Color.YELLOW)
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}" subline1 = Color.apply(
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}" "Make sure to select the correct method for the MCU!", Color.YELLOW
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}" )
subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW)
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Select the flash method for flashing the MCU. ║ ║ Select the flash method for flashing the MCU. ║
║ ║ ║ ║
@@ -112,6 +111,9 @@ class KlipperFlashMethodMenu(BaseMenu):
class KlipperFlashCommandMenu(BaseMenu): class KlipperFlashCommandMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() 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.help_menu = KlipperFlashCommandHelpMenu
self.input_label_txt = "Select flash command" self.input_label_txt = "Select flash command"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -132,8 +134,6 @@ class KlipperFlashCommandMenu(BaseMenu):
def print_menu(self) -> None: def print_menu(self) -> None:
menu = textwrap.dedent( menu = textwrap.dedent(
""" """
╔═══════════════════════════════════════════════════════╗
║ Which flash command to use for flashing the MCU? ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) make flash (default) ║ ║ 1) make flash (default) ║
║ 2) make serialflash (stm32flash) ║ ║ 2) make serialflash (stm32flash) ║
@@ -161,6 +161,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False
): ):
super().__init__() 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.previous_menu: Type[BaseMenu] | None = previous_menu
self.__standalone = standalone self.__standalone = standalone
self.help_menu = KlipperMcuConnectionHelpMenu self.help_menu = KlipperMcuConnectionHelpMenu
@@ -182,13 +185,8 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
} }
def print_menu(self) -> None: 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( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ How is the controller board connected to the host? ║ ║ How is the controller board connected to the host? ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
@@ -230,7 +228,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
Logger.print_status("Identifying MCU connected via USB in DFU mode ...") Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
self.flash_options.mcu_list = find_usb_dfu_device() self.flash_options.mcu_list = find_usb_dfu_device()
elif conn_type is ConnectionType.USB_RP2040: 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() self.flash_options.mcu_list = find_usb_rp2_boot_device()
if len(self.flash_options.mcu_list) < 1: if len(self.flash_options.mcu_list) < 1:
@@ -241,7 +241,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
if self.__standalone and len(self.flash_options.mcu_list) > 0: if self.__standalone and len(self.flash_options.mcu_list) > 0:
Logger.print_ok("The following MCUs were found:", prefix=False) Logger.print_ok("The following MCUs were found:", prefix=False)
for i, mcu in enumerate(self.flash_options.mcu_list): 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) time.sleep(3)
return return
@@ -256,6 +256,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
class KlipperSelectMcuIdMenu(BaseMenu): class KlipperSelectMcuIdMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
self.mcu_list = self.flash_options.mcu_list self.mcu_list = self.flash_options.mcu_list
self.input_label_txt = "Select MCU to flash" self.input_label_txt = "Select MCU to flash"
@@ -274,14 +277,9 @@ class KlipperSelectMcuIdMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ATTENTION !!!" header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]"
header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Make sure, to select the correct MCU! ║ ║ Make sure, to select the correct MCU! ║
║ ONLY flash a firmware created for the respective MCU! ║ ║ ONLY flash a firmware created for the respective MCU! ║
@@ -293,7 +291,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
for i, mcu in enumerate(self.mcu_list): for i, mcu in enumerate(self.mcu_list):
mcu = mcu.split("/")[-1] 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( menu += textwrap.dedent(
""" """
@@ -348,7 +346,6 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
else: else:
menu = textwrap.dedent( menu = textwrap.dedent(
""" """
╔═══════════════════════════════════════════════════════╗
║ Please select the type of board that corresponds to ║ ║ Please select the type of board that corresponds to ║
║ the currently selected MCU ID you chose before. ║ ║ the currently selected MCU ID you chose before. ║
║ ║ ║ ║
@@ -400,6 +397,9 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
class KlipperFlashOverviewMenu(BaseMenu): class KlipperFlashOverviewMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
self.input_label_txt = "Perform action (default=Y)" self.input_label_txt = "Perform action (default=Y)"
@@ -415,21 +415,17 @@ class KlipperFlashOverviewMenu(BaseMenu):
self.default_option = Option(self.execute_flash) self.default_option = Option(self.execute_flash)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
method = self.flash_options.flash_method.value method = self.flash_options.flash_method.value
command = self.flash_options.flash_command.value command = self.flash_options.flash_command.value
conn_type = self.flash_options.connection_type.value conn_type = self.flash_options.connection_type.value
mcu = self.flash_options.selected_mcu.split("/")[-1] mcu = self.flash_options.selected_mcu.split("/")[-1]
board = self.flash_options.selected_board board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" kconfig = Path(self.flash_options.selected_kconfig).name
color = Color.CYAN
subheader = f"[{Color.apply('Overview', color)}]"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Before contuining the flashing process, please check ║ ║ Before contuining the flashing process, please check ║
║ if all parameters were set correctly! Once you made ║ ║ if all parameters were set correctly! Once you made ║
@@ -443,18 +439,25 @@ class KlipperFlashOverviewMenu(BaseMenu):
menu += textwrap.dedent( menu += textwrap.dedent(
f""" f"""
║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT} ║ MCU: {Color.apply(f"{mcu:<48}", color)}
║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT} ║ Connection: {Color.apply(f"{conn_type:<41}", color)}
║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT} ║ Flash method: {Color.apply(f"{method:<39}", color)}
║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT} ║ Flash command: {Color.apply(f"{command:<38}", color)}
""" """
)[1:] )[1:]
if self.flash_options.flash_method is FlashMethod.SD_CARD: if self.flash_options.flash_method is FlashMethod.SD_CARD:
menu += textwrap.dedent( menu += textwrap.dedent(
f""" f"""
║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT} ║ Board type: {Color.apply(f"{board:<41}", color)}
║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT} ║ Baudrate: {Color.apply(f"{baudrate:<43}", color)}
"""
)[1:]
if self.flash_options.flash_method is FlashMethod.REGULAR:
menu += textwrap.dedent(
f"""
║ Firmware config: {Color.apply(f"{kconfig:<36}", color)}
""" """
)[1:] )[1:]

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -30,7 +30,7 @@ from core.constants import SYSTEMD
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import ( from utils.common import (
check_install_dependencies, check_install_dependencies,
get_install_status, get_install_status,

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,16 +12,18 @@ import textwrap
from typing import Type from typing import Type
from components.log_uploads.log_upload_utils import get_logfile_list, upload_logfile 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.logger import Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
class LogUploadMenu(BaseMenu): class LogUploadMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Log Upload"
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.logfile_list = get_logfile_list() self.logfile_list = get_logfile_list()
@@ -37,13 +39,8 @@ class LogUploadMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Log Upload ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ You can select the following logfiles for uploading: ║ ║ You can select the following logfiles for uploading: ║
║ ║ ║ ║

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent MODULE_PATH = Path(__file__).resolve().parent
MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git"
# names # names
MOONRAKER_CFG_NAME = "moonraker.conf" MOONRAKER_CFG_NAME = "moonraker.conf"
MOONRAKER_LOG_NAME = "moonraker.log" MOONRAKER_LOG_NAME = "moonraker.log"

View File

@@ -10,6 +10,7 @@ trusted_clients:
169.254.0.0/16 169.254.0.0/16
172.16.0.0/12 172.16.0.0/12
192.168.0.0/16 192.168.0.0/16
FC00::/7
FE80::/10 FE80::/10
::1/128 ::1/128
cors_domains: cors_domains:

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,15 +12,17 @@ import textwrap
from typing import Type from typing import Type
from components.moonraker import moonraker_remove from components.moonraker import moonraker_remove
from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class MoonrakerRemoveMenu(BaseMenu): class MoonrakerRemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Remove Moonraker"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.remove_moonraker_service = False self.remove_moonraker_service = False
self.remove_moonraker_dir = False self.remove_moonraker_dir = False
@@ -44,10 +46,7 @@ class MoonrakerRemoveMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Remove Moonraker ] " checked = f"[{Color.apply('x', Color.CYAN)}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]" unchecked = "[ ]"
o1 = checked if self.remove_moonraker_service else unchecked o1 = checked if self.remove_moonraker_service else unchecked
o2 = checked if self.remove_moonraker_dir 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 o4 = checked if self.remove_moonraker_polkit else unchecked
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║ ║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║ ║ the specific option for removal. ║
@@ -100,8 +97,11 @@ class MoonrakerRemoveMenu(BaseMenu):
and not self.remove_moonraker_env and not self.remove_moonraker_env
and not self.remove_moonraker_polkit and not self.remove_moonraker_polkit
): ):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}" print(
print(error) Color.apply(
"Nothing selected! Select options to remove first.", Color.RED
)
)
return return
moonraker_remove.run_moonraker_removal( moonraker_remove.run_moonraker_removal(

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,8 +12,8 @@ from typing import List
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker 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.menus.base_menu import print_back_footer
from core.types.color import Color
def print_moonraker_overview( def print_moonraker_overview(
@@ -22,7 +22,7 @@ def print_moonraker_overview(
show_index=False, show_index=False,
show_select_all=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( dialog = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
@@ -32,7 +32,7 @@ def print_moonraker_overview(
)[1:] )[1:]
if show_select_all: 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 += f"{select_all:<63}\n"
dialog += "║ ║\n" dialog += "║ ║\n"
@@ -48,12 +48,16 @@ def print_moonraker_overview(
for i, k in enumerate(instance_map): for i, k in enumerate(instance_map):
mr_name = instance_map.get(k) mr_name = instance_map.get(k)
m = f"<-> {mr_name}" if mr_name != "" else "" 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" dialog += f"{line:<63}\n"
warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}" warn_l1 = Color.apply("PLEASE NOTE:", Color.YELLOW)
warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}" warn_l2 = Color.apply(
warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}" "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( warning = textwrap.dedent(
f""" f"""
║ ║ ║ ║

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -8,7 +8,6 @@
# ======================================================================= # # ======================================================================= #
from __future__ import annotations from __future__ import annotations
import json
import subprocess import subprocess
from typing import List from typing import List
@@ -28,9 +27,14 @@ from components.moonraker import (
) )
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from components.moonraker.moonraker_dialogs import print_moonraker_overview from components.moonraker.moonraker_dialogs import print_moonraker_overview
from components.moonraker.moonraker_utils import ( from components.moonraker.services.moonraker_instance_service import (
MoonrakerInstanceService,
)
from components.moonraker.utils.sysdeps_parser import SysDepsParser
from components.moonraker.utils.utils import (
backup_moonraker_dir, backup_moonraker_dir,
create_example_moonraker_conf, create_example_moonraker_conf,
load_sysdeps_json,
) )
from components.webui_client.client_utils import ( from components.webui_client.client_utils import (
enable_mainsail_remotemode, enable_mainsail_remotemode,
@@ -38,8 +42,9 @@ from components.webui_client.client_utils import (
) )
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from core.types.color import Color
from utils.common import check_install_dependencies from utils.common import check_install_dependencies
from utils.fs_utils import check_file_exist from utils.fs_utils import check_file_exist
from utils.git_utils import git_clone_wrapper, git_pull_wrapper from utils.git_utils import git_clone_wrapper, git_pull_wrapper
@@ -53,6 +58,7 @@ from utils.sys_utils import (
cmd_sysctl_manage, cmd_sysctl_manage,
cmd_sysctl_service, cmd_sysctl_service,
create_python_venv, create_python_venv,
get_ipv4_addr,
install_python_requirements, install_python_requirements,
parse_packages_from_file, parse_packages_from_file,
) )
@@ -64,12 +70,18 @@ def install_moonraker() -> None:
if not check_moonraker_install_requirements(klipper_list): if not check_moonraker_install_requirements(klipper_list):
return return
moonraker_list: List[Moonraker] = get_instances(Moonraker) instance_service = MoonrakerInstanceService()
instances: List[Moonraker] = [] instance_service.load_instances()
moonraker_list: List[Moonraker] = instance_service.get_all_instances()
new_instances: List[Moonraker] = []
selected_option: str | Klipper selected_option: str | Klipper
if len(klipper_list) == 1: if len(klipper_list) == 1:
instances.append(Moonraker(klipper_list[0].suffix)) suffix: str = klipper_list[0].suffix
new_inst = instance_service.create_new_instance(suffix)
new_instances.append(new_inst)
else: else:
print_moonraker_overview( print_moonraker_overview(
klipper_list, klipper_list,
@@ -88,12 +100,16 @@ def install_moonraker() -> None:
return return
if selected_option == "a": if selected_option == "a":
instances.extend([Moonraker(k.suffix) for k in klipper_list]) new_inst_list: List[Moonraker] = [
instance_service.create_new_instance(k.suffix) for k in klipper_list
]
new_instances.extend(new_inst_list)
else: else:
klipper_instance: Klipper | None = options.get(selected_option) klipper_instance: Klipper | None = options.get(selected_option)
if klipper_instance is None: if klipper_instance is None:
raise Exception("Error selecting instance!") raise Exception("Error selecting instance!")
instances.append(Moonraker(klipper_instance.suffix)) new_inst = instance_service.create_new_instance(klipper_instance.suffix)
new_instances.append(new_inst)
create_example_cfg = get_confirm("Create example moonraker.conf?") create_example_cfg = get_confirm("Create example moonraker.conf?")
@@ -102,8 +118,8 @@ def install_moonraker() -> None:
setup_moonraker_prerequesites() setup_moonraker_prerequesites()
install_moonraker_polkit() install_moonraker_polkit()
used_ports_map = {m.suffix: m.port for m in moonraker_list} ports_map = instance_service.get_instance_port_map()
for instance in instances: for instance in new_instances:
instance.create() instance.create()
cmd_sysctl_service(instance.service_file_path.name, "enable") cmd_sysctl_service(instance.service_file_path.name, "enable")
@@ -111,7 +127,7 @@ def install_moonraker() -> None:
# if a webclient and/or it's config is installed, patch # if a webclient and/or it's config is installed, patch
# its update section to the config # its update section to the config
clients = get_existing_clients() clients = get_existing_clients()
create_example_moonraker_conf(instance, used_ports_map, clients) create_example_moonraker_conf(instance, ports_map, clients)
cmd_sysctl_service(instance.service_file_path.name, "start") cmd_sysctl_service(instance.service_file_path.name, "start")
@@ -122,6 +138,30 @@ def install_moonraker() -> None:
if MainsailData().client_dir.exists() and len(moonraker_list) > 1: if MainsailData().client_dir.exists() and len(moonraker_list) > 1:
enable_mainsail_remotemode() enable_mainsail_remotemode()
instance_service.load_instances()
new_instances = [
instance_service.get_instance_by_suffix(i.suffix) for i in new_instances
]
ip: str = get_ipv4_addr()
# noinspection HttpUrlsUsage
url_list = [
f"{i.service_file_path.stem}: http://{ip}:{i.port}"
for i in new_instances
if i.port
]
dialog_content = []
if url_list:
dialog_content.append("You can access Moonraker via the following URL:")
dialog_content.extend(url_list)
Logger.print_dialog(
DialogType.CUSTOM,
custom_title="Moonraker successfully installed!",
custom_color=Color.GREEN,
content=dialog_content,
)
except Exception as e: except Exception as e:
Logger.print_error(f"Error while installing Moonraker: {e}") Logger.print_error(f"Error while installing Moonraker: {e}")
return return
@@ -154,16 +194,26 @@ def setup_moonraker_prerequesites() -> None:
def install_moonraker_packages() -> None: def install_moonraker_packages() -> None:
moonraker_deps = [] Logger.print_status("Parsing Moonraker system dependencies ...")
moonraker_deps = []
if MOONRAKER_DEPS_JSON_FILE.exists(): if MOONRAKER_DEPS_JSON_FILE.exists():
with open(MOONRAKER_DEPS_JSON_FILE, "r") as deps: Logger.print_info(
moonraker_deps = json.load(deps).get("debian", []) f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..."
)
parser = SysDepsParser()
sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE)
moonraker_deps.extend(parser.parse_dependencies(sysdeps))
elif MOONRAKER_INSTALL_SCRIPT.exists(): elif MOONRAKER_INSTALL_SCRIPT.exists():
Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!")
Logger.print_info(
f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..."
)
moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT)
if not moonraker_deps: if not moonraker_deps:
raise ValueError("Error reading Moonraker dependencies!") raise ValueError("Error parsing Moonraker dependencies!")
check_install_dependencies({*moonraker_deps}) check_install_dependencies({*moonraker_deps})

View File

@@ -0,0 +1,49 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
from typing import Dict, List
from components.moonraker.moonraker import Moonraker
from utils.instance_utils import get_instances
class MoonrakerInstanceService:
__cls_instance = None
__instances: List[Moonraker] = []
def __new__(cls) -> "MoonrakerInstanceService":
if cls.__cls_instance is None:
cls.__cls_instance = super(MoonrakerInstanceService, cls).__new__(cls)
return cls.__cls_instance
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
def load_instances(self) -> None:
self.__instances = get_instances(Moonraker)
def create_new_instance(self, suffix: str) -> Moonraker:
instance = Moonraker(suffix)
self.__instances.append(instance)
return instance
def get_all_instances(self) -> List[Moonraker]:
return self.__instances
def get_instance_by_suffix(self, suffix: str) -> Moonraker | None:
instances: List[Moonraker] = [i for i in self.__instances if i.suffix == suffix]
return instances[0] if instances else None
def get_instance_port_map(self) -> Dict[str, int]:
return {i.suffix: i.port for i in self.__instances}

View File

@@ -0,0 +1,173 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# It was modified by Dominik Willner <th33xitus@gmail.com> #
# #
# The original file is part of Moonraker: #
# https://github.com/Arksine/moonraker #
# Copyright (C) 2025 Eric Callahan <arksine.code@gmail.com> #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
import logging
import pathlib
import re
import shlex
from typing import Any, Dict, List, Tuple
def _get_distro_info() -> Dict[str, Any]:
release_file = pathlib.Path("/etc/os-release")
release_info: Dict[str, str] = {}
with release_file.open("r") as f:
lexer = shlex.shlex(f, posix=True)
lexer.whitespace_split = True
for item in list(lexer):
if "=" in item:
key, val = item.split("=", maxsplit=1)
release_info[key] = val
return dict(
distro_id=release_info.get("ID", ""),
distro_version=release_info.get("VERSION_ID", ""),
aliases=release_info.get("ID_LIKE", "").split(),
)
def _convert_version(version: str) -> Tuple[str | int, ...]:
version = version.strip()
ver_match = re.match(r"\d+(\.\d+)*((?:-|\.).+)?", version)
if ver_match is not None:
return tuple(
[
int(part) if part.isdigit() else part
for part in re.split(r"\.|-", version)
]
)
return (version,)
class SysDepsParser:
def __init__(self, distro_info: Dict[str, Any] | None = None) -> None:
if distro_info is None:
distro_info = _get_distro_info()
self.distro_id: str = distro_info.get("distro_id", "")
self.aliases: List[str] = distro_info.get("aliases", [])
self.distro_version: Tuple[int | str, ...] = tuple()
version = distro_info.get("distro_version")
if version:
self.distro_version = _convert_version(version)
def _parse_spec(self, full_spec: str) -> str | None:
parts = full_spec.split(";", maxsplit=1)
if len(parts) == 1:
return full_spec
pkg_name = parts[0].strip()
expressions = re.split(r"( and | or )", parts[1].strip())
if not len(expressions) & 1:
# There should always be an odd number of expressions. Each
# expression is separated by an "and" or "or" operator
logging.info(
f"Requirement specifier is missing an expression "
f"between logical operators : {full_spec}"
)
return None
last_result: bool = True
last_logical_op: str | None = "and"
for idx, exp in enumerate(expressions):
if idx & 1:
if last_logical_op is not None:
logging.info(
"Requirement specifier contains sequential logical "
f"operators: {full_spec}"
)
return None
logical_op = exp.strip()
if logical_op not in ("and", "or"):
logging.info(
f"Invalid logical operator {logical_op} in requirement "
f"specifier: {full_spec}"
)
return None
last_logical_op = logical_op
continue
elif last_logical_op is None:
logging.info(
f"Requirement specifier contains two seqential expressions "
f"without a logical operator: {full_spec}"
)
return None
dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip())
req_var = dep_parts[0].strip().lower()
if len(dep_parts) != 3:
logging.info(f"Invalid comparison, must be 3 parts: {full_spec}")
return None
elif req_var == "distro_id":
left_op: str | Tuple[int | str, ...] = self.distro_id
right_op = dep_parts[2].strip().strip("\"'")
elif req_var == "distro_version":
if not self.distro_version:
logging.info(
"Distro Version not detected, cannot satisfy requirement: "
f"{full_spec}"
)
return None
left_op = self.distro_version
right_op = _convert_version(dep_parts[2].strip().strip("\"'"))
else:
logging.info(f"Invalid requirement specifier: {full_spec}")
return None
operator = dep_parts[1].strip()
try:
compfunc = {
"<": lambda x, y: x < y,
">": lambda x, y: x > y,
"==": lambda x, y: x == y,
"!=": lambda x, y: x != y,
">=": lambda x, y: x >= y,
"<=": lambda x, y: x <= y,
}.get(operator, lambda x, y: False)
result = compfunc(left_op, right_op)
if last_logical_op == "and":
last_result &= result
else:
last_result |= result
last_logical_op = None
except Exception:
logging.exception(f"Error comparing requirements: {full_spec}")
return None
if last_result:
return pkg_name
return None
def parse_dependencies(self, sys_deps: Dict[str, List[str]]) -> List[str]:
if not self.distro_id:
logging.info(
"Failed to detect current distro ID, cannot parse dependencies"
)
return []
all_ids = [self.distro_id] + self.aliases
for distro_id in all_ids:
if distro_id in sys_deps:
if not sys_deps[distro_id]:
logging.info(
f"Dependency data contains an empty package definition "
f"for linux distro '{distro_id}'"
)
continue
processed_deps: List[str] = []
for dep in sys_deps[distro_id]:
parsed_dep = self._parse_spec(dep)
if parsed_dep is not None:
processed_deps.append(parsed_dep)
return processed_deps
else:
logging.info(
f"Dependency data has no package definition for linux "
f"distro '{self.distro_id}'"
)
return []

View File

@@ -1,13 +1,14 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import json
import shutil import shutil
from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from components.moonraker import ( from components.moonraker import (
@@ -25,7 +26,7 @@ from core.logger import Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
) )
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import get_install_status from utils.common import get_install_status
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import ( from utils.sys_utils import (
@@ -138,3 +139,13 @@ def backup_moonraker_db_dir() -> None:
bm.backup_directory( bm.backup_directory(
name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR
) )
def load_sysdeps_json(file: Path) -> Dict[str, List[str]]:
try:
sysdeps: Dict[str, List[str]] = json.loads(file.read_bytes())
except json.JSONDecodeError as e:
Logger.print_error(f"Unable to parse {file.name}:\n{e}")
return {}
else:
return sysdeps

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -37,6 +37,7 @@ class BaseWebClient(ABC):
backup_dir: Path backup_dir: Path
repo_path: str repo_path: str
download_url: str download_url: str
nginx_config: Path
nginx_access_log: Path nginx_access_log: Path
nginx_error_log: Path nginx_error_log: Path
client_config: BaseWebClientConfig client_config: BaseWebClientConfig

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -14,8 +14,11 @@ from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import BaseWebClientConfig from components.webui_client.base_data import BaseWebClientConfig
from core.logger import Logger 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.config_utils import remove_config_section
from utils.fs_utils import run_remove_routines from utils.fs_utils import run_remove_routines
from utils.instance_type import InstanceType
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
@@ -23,21 +26,66 @@ def run_client_config_removal(
client_config: BaseWebClientConfig, client_config: BaseWebClientConfig,
kl_instances: List[Klipper], kl_instances: List[Klipper],
mr_instances: List[Moonraker], mr_instances: List[Moonraker],
) -> None: ) -> Message:
remove_client_config_dir(client_config) completion_msg = Message(
remove_client_config_symlink(client_config) title=f"{client_config.display_name} Removal Process completed",
remove_config_section(f"update_manager {client_config.name}", mr_instances) color=Color.GREEN,
remove_config_section(client_config.config_section, kl_instances) )
def remove_client_config_dir(client_config: BaseWebClientConfig) -> None:
Logger.print_status(f"Removing {client_config.display_name} ...") 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) instances: List[Klipper] = get_instances(Klipper)
kl_instances = []
for instance in instances: for instance in instances:
run_remove_routines( cfg = instance.base.cfg_dir.joinpath(client_config.config_filename)
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

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -34,7 +34,7 @@ from utils.input_utils import get_confirm
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
def install_client_config(client_data: BaseWebClient) -> None: def install_client_config(client_data: BaseWebClient, cfg_backup=True) -> None:
client_config: BaseWebClientConfig = client_data.client_config client_config: BaseWebClientConfig = client_data.client_config
display_name = client_config.display_name display_name = client_config.display_name
@@ -56,7 +56,8 @@ def install_client_config(client_data: BaseWebClient) -> None:
download_client_config(client_config) download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances) create_client_config_symlink(client_config, kl_instances)
backup_printer_config_dir() if cfg_backup:
backup_printer_config_dir()
add_config_section( add_config_section(
section=f"update_manager {client_config.name}", section=f"update_manager {client_config.name}",

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -53,8 +53,8 @@ def print_client_port_select_dialog(
dialog_content.extend( dialog_content.extend(
[ [
"\n\n", "\n\n",
"The following ports were found to be in use already:", "The following ports were found to be already in use:",
*[f"{port}" for port in ports_in_use], *[f"{p}" for p in ports_in_use if p != port],
] ]
) )

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -19,6 +19,8 @@ from components.webui_client.client_config.client_config_remove import (
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from core.logger import Logger 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.config_utils import remove_config_section
from utils.fs_utils import ( from utils.fs_utils import (
remove_with_sudo, remove_with_sudo,
@@ -32,54 +34,79 @@ def run_client_removal(
remove_client: bool, remove_client: bool,
remove_client_cfg: bool, remove_client_cfg: bool,
backup_config: 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) mr_instances: List[Moonraker] = get_instances(Moonraker)
kl_instances: List[Klipper] = get_instances(Klipper) kl_instances: List[Klipper] = get_instances(Klipper)
if backup_config: if backup_config:
bm = BackupManager() 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: if remove_client:
client_name = client.name client_name = client.name
remove_client_dir(client) if remove_client_dir(client):
remove_client_nginx_config(client_name) completion_msg.text.append(f"{client.display_name} removed")
remove_client_nginx_logs(client, kl_instances) 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}" 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: if remove_client_cfg:
run_client_config_removal( cfg_completion_msg = run_client_config_removal(
client.client_config, client.client_config,
kl_instances, kl_instances,
mr_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} ...") 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()} ...") Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
return remove_with_sudo(
remove_with_sudo(NGINX_SITES_AVAILABLE.joinpath(name)) [
remove_with_sudo(NGINX_SITES_ENABLED.joinpath(name)) 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} ...") Logger.print_status(f"Removing NGINX logs for {client.display_name} ...")
remove_with_sudo(client.nginx_access_log) files = [client.nginx_access_log, client.nginx_error_log]
remove_with_sudo(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 remove_with_sudo(files)
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))

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -36,8 +36,10 @@ from components.webui_client.client_utils import (
symlink_webui_nginx_log, symlink_webui_nginx_log,
) )
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import DialogType, Logger
from utils.common import check_install_dependencies 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.config_utils import add_config_section
from utils.fs_utils import unzip from utils.fs_utils import unzip
from utils.input_utils import get_confirm from utils.input_utils import get_confirm
@@ -49,16 +51,11 @@ from utils.sys_utils import (
) )
def install_client(client: BaseWebClient) -> None: def install_client(
if client is None: client: BaseWebClient,
raise ValueError("Missing parameter client_data!") settings: KiauhSettings,
reinstall: bool = False,
if client.client_dir.exists(): ) -> None:
Logger.print_info(
f"{client.display_name} seems to be already installed! Skipped ..."
)
return
mr_instances: List[Moonraker] = get_instances(Moonraker) mr_instances: List[Moonraker] = get_instances(Moonraker)
enable_remotemode = False enable_remotemode = False
@@ -88,7 +85,10 @@ def install_client(client: BaseWebClient) -> None:
question = f"Download the recommended {client_config.display_name}?" question = f"Download the recommended {client_config.display_name}?"
install_client_cfg = get_confirm(question, allow_go_back=False) install_client_cfg = get_confirm(question, allow_go_back=False)
port: int = get_client_port_selection(client) default_port: int = int(settings.get(client.name, "port"))
port: int = (
default_port if reinstall else get_client_port_selection(client, settings)
)
check_install_dependencies({"nginx"}) check_install_dependencies({"nginx"})
@@ -96,20 +96,22 @@ def install_client(client: BaseWebClient) -> None:
download_client(client) download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL: if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode() enable_mainsail_remotemode()
if mr_instances:
add_config_section( backup_printer_config_dir()
section=f"update_manager {client.name}", add_config_section(
instances=mr_instances, section=f"update_manager {client.name}",
options=[ instances=mr_instances,
("type", "web"), options=[
("channel", "stable"), ("type", "web"),
("repo", str(client.repo_path)), ("channel", "stable"),
("path", str(client.client_dir)), ("repo", str(client.repo_path)),
], ("path", str(client.client_dir)),
) ],
InstanceManager.restart_all(mr_instances) )
InstanceManager.restart_all(mr_instances)
if install_client_cfg and kl_instances: if install_client_cfg and kl_instances:
install_client_config(client) install_client_config(client, False)
copy_upstream_nginx_cfg() copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg() copy_common_vars_nginx_cfg()
@@ -127,12 +129,24 @@ def install_client(client: BaseWebClient) -> None:
cmd_sysctl_service("nginx", "restart") cmd_sysctl_service("nginx", "restart")
except Exception as e: except Exception as e:
Logger.print_error(f"{client.display_name} installation failed!\n{e}") Logger.print_error(e)
Logger.print_dialog(
DialogType.ERROR,
center_content=True,
content=[f"{client.display_name} installation failed!"],
)
return return
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}" # noinspection HttpUrlsUsage
Logger.print_ok(f"{client.display_name} installation complete!", start="\n") Logger.print_dialog(
Logger.print_ok(log, prefix=False, end="\n\n") DialogType.CUSTOM,
custom_title=f"{client.display_name} installation complete!",
custom_color=Color.GREEN,
center_content=True,
content=[
f"Open {client.display_name} now on: http://{get_ipv4_addr()}{'' if port == 80 else f':{port}'}",
],
)
def download_client(client: BaseWebClient) -> None: def download_client(client: BaseWebClient) -> None:

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -26,19 +26,17 @@ from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
from core.constants import ( from core.constants import (
COLOR_CYAN,
COLOR_YELLOW,
NGINX_CONFD, NGINX_CONFD,
NGINX_SITES_AVAILABLE, NGINX_SITES_AVAILABLE,
NGINX_SITES_ENABLED, NGINX_SITES_ENABLED,
RESET_FORMAT,
) )
from core.logger import Logger from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings, WebUiSettings from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, 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.common import get_install_status
from utils.fs_utils import create_symlink, remove_file from utils.fs_utils import create_symlink, remove_file
from utils.git_utils import ( 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()] installed = [c for c in clients if c.client_config.config_dir.exists()]
if not installed: if not installed:
return f"{COLOR_CYAN}-{RESET_FORMAT}" return Color.apply("-", Color.CYAN)
elif len(installed) == 1: elif len(installed) == 1:
cfg = installed[0].client_config 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 # 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 # 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 both are included in the same file, we have a potential conflict
if includes_mainsail and includes_fluidd: 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: if not mainsail_includes and not fluidd_includes:
# there are no includes at all, even though the client config folders exist # 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): elif len(fluidd_includes) > len(mainsail_includes):
# there are more instances that include fluidd than mainsail # 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: else:
# there are the same amount of non-conflicting includes for each config # there are the same amount of non-conflicting includes for each config
# or more instances include mainsail than fluidd # 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: def enable_mainsail_remotemode() -> None:
@@ -338,51 +336,84 @@ def create_nginx_cfg(
raise 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]: 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 :return: A sorted list of listen ports
""" """
if not NGINX_SITES_ENABLED.exists(): if not NGINX_SITES_ENABLED.exists():
return [] return []
port_list = [] port_list: List[int] = []
for config in NGINX_SITES_ENABLED.iterdir(): for config in get_nginx_config_list():
if not config.is_file(): port = get_nginx_listen_port(config)
continue if port is not None:
port_list.append(port)
with open(config, "r") as cfg: return sorted(port_list, key=lambda x: int(x))
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))
def get_client_port_selection(client: BaseWebClient) -> int: def get_client_port_selection(
settings = KiauhSettings() client: BaseWebClient,
settings: KiauhSettings,
reconfigure=False,
) -> int:
default_port: int = int(settings.get(client.name, "port")) default_port: int = int(settings.get(client.name, "port"))
ports_in_use: List[int] = read_ports_from_nginx_configs() ports_in_use: List[int] = read_ports_from_nginx_configs()
next_free_port: int = get_next_free_port(ports_in_use) next_free_port: int = get_next_free_port(ports_in_use)
port: int = next_free_port if default_port in ports_in_use else default_port port: int = (
next_free_port
if not reconfigure and default_port in ports_in_use
else default_port
)
print_client_port_select_dialog(client.display_name, port, ports_in_use) print_client_port_select_dialog(client.display_name, port, ports_in_use)
while True: while True:
question = f"Configure {client.display_name} for port" _type = "Reconfigure" if reconfigure else "Configure"
question = f"{_type} {client.display_name} for port"
port_input = get_number_input(question, min_count=80, default=port) port_input = get_number_input(question, min_count=80, default=port)
if port_input not in ports_in_use: if port_input not in ports_in_use:
@@ -400,3 +431,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int:
used_ports = set(map(int, ports_in_use)) used_ports = set(map(int, ports_in_use))
return min(valid_ports - used_ports) return min(valid_ports - used_ports)
def set_listen_port(client: BaseWebClient, curr_port: int, new_port: int) -> None:
"""
Set the port the client should listen on in the NGINX config
:param curr_port: The current port the client listens on
:param new_port: The new port to set
:param client: The client to set the port for
:return: None
"""
config = NGINX_SITES_AVAILABLE.joinpath(client.name)
with open(config, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if "listen" in line:
lines[i] = line.replace(str(curr_port), str(new_port))
with open(config, "w") as f:
f.writelines(lines)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -19,6 +19,7 @@ from components.webui_client.base_data import (
WebClientType, WebClientType,
) )
from core.backup_manager import BACKUP_ROOT_DIR from core.backup_manager import BACKUP_ROOT_DIR
from core.constants import NGINX_SITES_AVAILABLE
@dataclass() @dataclass()
@@ -44,6 +45,7 @@ class FluiddData(BaseWebClient):
config_file: Path = client_dir.joinpath("config.json") config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups") backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
repo_path: str = "fluidd-core/fluidd" 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_access_log: Path = Path("/var/log/nginx/fluidd-access.log")
nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log") nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log")
client_config: BaseWebClientConfig = None client_config: BaseWebClientConfig = None

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -19,6 +19,7 @@ from components.webui_client.base_data import (
WebClientType, WebClientType,
) )
from core.backup_manager import BACKUP_ROOT_DIR from core.backup_manager import BACKUP_ROOT_DIR
from core.constants import NGINX_SITES_AVAILABLE
@dataclass() @dataclass()
@@ -44,6 +45,7 @@ class MainsailData(BaseWebClient):
config_file: Path = client_dir.joinpath("config.json") config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups") backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups")
repo_path: str = "mainsail-crew/mainsail" 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_access_log: Path = Path("/var/log/nginx/mainsail-access.log")
nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log") nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log")
client_config: BaseWebClientConfig = None client_config: BaseWebClientConfig = None

View File

@@ -0,0 +1,105 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
import textwrap
from typing import Type
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.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
# noinspection PyUnusedLocal
class ClientInstallMenu(BaseMenu):
def __init__(
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()
self.client_settings: WebUiSettings = self.settings[client.name]
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.install_menu import InstallMenu
self.previous_menu = previous_menu if previous_menu is not None else InstallMenu
def set_options(self) -> None:
self.options = {
"1": Option(method=self.reinstall_client),
"2": Option(method=self.change_listen_port),
}
def print_menu(self) -> None:
client_name = self.client.display_name
port = f"(Current: {Color.apply(self._get_current_port(), Color.GREEN)})"
menu = textwrap.dedent(
f"""
╟───────────────────────────────────────────────────────╢
║ 1) Reinstall {client_name:16}
║ 2) Reconfigure Listen Port {port:<34}
╟───────────────────────────────────────────────────────╢
"""
)[1:]
print(menu, end="")
def reinstall_client(self, **kwargs) -> None:
install_client(self.client, settings=self.settings, reinstall=True)
def change_listen_port(self, **kwargs) -> None:
curr_port = self._get_current_port()
new_port = get_client_port_selection(
self.client,
self.settings,
reconfigure=True,
)
cmd_sysctl_service("nginx", "stop")
set_listen_port(self.client, curr_port, new_port)
Logger.print_status("Saving new port configuration ...")
self.client_settings.port = new_port
self.settings.save()
Logger.print_ok("Port configuration saved!")
cmd_sysctl_service("nginx", "start")
# noinspection HttpUrlsUsage
message = Message(
title="Port reconfiguration complete!",
text=[
f"Open {self.client.display_name} now on: "
f"http://{get_ipv4_addr()}:{new_port}",
],
color=Color.GREEN,
)
self.message_service.set_message(message)
def _get_current_port(self) -> int:
curr_port = get_nginx_listen_port(self.client.nginx_config)
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

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -13,9 +13,9 @@ from typing import Type
from components.webui_client import client_remove from components.webui_client import client_remove
from components.webui_client.base_data import BaseWebClient 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 import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -24,12 +24,14 @@ class ClientRemoveMenu(BaseMenu):
self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None
): ):
super().__init__() super().__init__()
self.title = f"Remove {client.display_name}"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.client: BaseWebClient = client self.client: BaseWebClient = client
self.remove_client: bool = False self.remove_client: bool = False
self.remove_client_cfg: bool = False self.remove_client_cfg: bool = False
self.backup_config_json: 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: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.remove_menu import RemoveMenu from core.menus.remove_menu import RemoveMenu
@@ -50,23 +52,19 @@ class ClientRemoveMenu(BaseMenu):
client_config = self.client.client_config client_config = self.client.client_config
client_config_name = client_config.display_name client_config_name = client_config.display_name
header = f" [ Remove {client_name} ] " checked = f"[{Color.apply('x', Color.CYAN)}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
unchecked = "[ ]" unchecked = "[ ]"
o1 = checked if self.remove_client else unchecked o1 = checked if self.remove_client else unchecked
o2 = checked if self.remove_client_cfg else unchecked o2 = checked if self.remove_client_cfg else unchecked
o3 = checked if self.backup_config_json 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( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║ ║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║ ║ the specific option for removal. ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ a) {self._get_selection_state_str():37} ║ a) {sel_state:49}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) {o1} Remove {client_name:16} ║ 1) {o1} Remove {client_name:16}
║ 2) {o2} Remove {client_config_name:24} ║ 2) {o2} Remove {client_config_name:24}
@@ -79,10 +77,10 @@ class ClientRemoveMenu(BaseMenu):
print(menu, end="") print(menu, end="")
def toggle_all(self, **kwargs) -> None: def toggle_all(self, **kwargs) -> None:
self.selection_state = not self.selection_state self.select_state = not self.select_state
self.remove_client = self.selection_state self.remove_client = self.select_state
self.remove_client_cfg = self.selection_state self.remove_client_cfg = self.select_state
self.backup_config_json = self.selection_state self.backup_config_json = self.select_state
def toggle_rm_client(self, **kwargs) -> None: def toggle_rm_client(self, **kwargs) -> None:
self.remove_client = not self.remove_client self.remove_client = not self.remove_client
@@ -99,28 +97,18 @@ class ClientRemoveMenu(BaseMenu):
and not self.remove_client_cfg and not self.remove_client_cfg
and not self.backup_config_json and not self.backup_config_json
): ):
error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}" print(Color.apply("Nothing selected ...", Color.RED))
print(error)
return return
client_remove.run_client_removal( completion_msg = client_remove.run_client_removal(
client=self.client, client=self.client,
remove_client=self.remove_client, remove_client=self.remove_client,
remove_client_cfg=self.remove_client_cfg, remove_client_cfg=self.remove_client_cfg,
backup_config=self.backup_config_json, backup_config=self.backup_config_json,
) )
self.message_service.set_message(completion_msg)
self.remove_client = False self.remove_client = False
self.remove_client_cfg = False self.remove_client_cfg = False
self.backup_config_json = False self.backup_config_json = False
self.select_state = 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()

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -44,12 +44,14 @@ class BackupManager:
def ignore_folders(self, value: List[str]): def ignore_folders(self, value: List[str]):
self._ignore_folders = value 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} ...") Logger.print_status(f"Creating backup of {file} ...")
if not file.exists(): if not file.exists():
Logger.print_info("File does not exist! Skipping ...") Logger.print_info("File does not exist! Skipping ...")
return return False
target = self.backup_root_dir if target is None else target target = self.backup_root_dir if target is None else target
@@ -62,10 +64,13 @@ class BackupManager:
Path(target).mkdir(exist_ok=True) Path(target).mkdir(exist_ok=True)
shutil.copyfile(file, target.joinpath(filename)) shutil.copyfile(file, target.joinpath(filename))
Logger.print_ok("Backup successful!") Logger.print_ok("Backup successful!")
return True
except OSError as e: except OSError as e:
Logger.print_error(f"Unable to backup '{file}':\n{e}") Logger.print_error(f"Unable to backup '{file}':\n{e}")
return False
else: else:
Logger.print_info(f"File '{file}' not found ...") Logger.print_info(f"File '{file}' not found ...")
return False
def backup_directory( def backup_directory(
self, name: str, source: Path, target: Path | None = None self, name: str, source: Path, target: Path | None = None
@@ -74,14 +79,19 @@ class BackupManager:
if source is None or not Path(source).exists(): if source is None or not Path(source).exists():
Logger.print_info("Source directory does not exist! Skipping ...") Logger.print_info("Source directory does not exist! Skipping ...")
return return None
target = self.backup_root_dir if target is None else target target = self.backup_root_dir if target is None else target
try: try:
date = get_current_date().get("date") date = get_current_date().get("date")
time = get_current_date().get("time") time = get_current_date().get("time")
backup_target = target.joinpath(f"{name.lower()}-{date}-{time}") backup_target = target.joinpath(f"{name.lower()}-{date}-{time}")
shutil.copytree(source, backup_target, ignore=self.ignore_folders_func) shutil.copytree(
source,
backup_target,
ignore=self.ignore_folders_func,
ignore_dangling_symlinks=True,
)
Logger.print_ok("Backup successful!") Logger.print_ok("Backup successful!")
return backup_target return backup_target

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -13,15 +13,6 @@ from pathlib import Path
from core.backup_manager import BACKUP_ROOT_DIR 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 dependencies
GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"]

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,78 +12,57 @@ import textwrap
from enum import Enum from enum import Enum
from typing import List from typing import List
from core.constants import ( from core.types.color import Color
COLOR_CYAN,
COLOR_GREEN,
COLOR_MAGENTA,
COLOR_RED,
COLOR_WHITE,
COLOR_YELLOW,
RESET_FORMAT,
)
class DialogType(Enum): class DialogType(Enum):
INFO = ("INFO", COLOR_WHITE) INFO = ("INFO", Color.WHITE)
SUCCESS = ("SUCCESS", COLOR_GREEN) SUCCESS = ("SUCCESS", Color.GREEN)
ATTENTION = ("ATTENTION", COLOR_YELLOW) ATTENTION = ("ATTENTION", Color.YELLOW)
WARNING = ("WARNING", COLOR_YELLOW) WARNING = ("WARNING", Color.YELLOW)
ERROR = ("ERROR", COLOR_RED) ERROR = ("ERROR", Color.RED)
CUSTOM = (None, None) 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 LINE_WIDTH = 53
BORDER_TOP: str = "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
BORDER_BOTTOM: str = "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
BORDER_TITLE: str = "┠───────────────────────────────────────────────────────┨"
BORDER_LEFT: str = ""
BORDER_RIGHT: str = ""
class Logger: 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 @staticmethod
def print_info(msg, prefix=True, start="", end="\n") -> None: def print_info(msg, prefix=True, start="", end="\n") -> None:
message = f"[INFO] {msg}" if prefix else msg 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 @staticmethod
def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None: def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None:
message = f"[OK] {msg}" if prefix else msg 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 @staticmethod
def print_warn(msg, prefix=True, start="", end="\n") -> None: def print_warn(msg, prefix=True, start="", end="\n") -> None:
message = f"[WARN] {msg}" if prefix else msg 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 @staticmethod
def print_error(msg, prefix=True, start="", end="\n") -> None: def print_error(msg, prefix=True, start="", end="\n") -> None:
message = f"[ERROR] {msg}" if prefix else msg 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 @staticmethod
def print_status(msg, prefix=True, start="", end="\n") -> None: def print_status(msg, prefix=True, start="", end="\n") -> None:
message = f"\n###### {msg}" if prefix else msg 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 @staticmethod
def print_dialog( def print_dialog(
@@ -91,7 +70,7 @@ class Logger:
content: List[str], content: List[str],
center_content: bool = False, center_content: bool = False,
custom_title: str | None = None, custom_title: str | None = None,
custom_color: DialogCustomColor | None = None, custom_color: Color | None = None,
margin_top: int = 0, margin_top: int = 0,
margin_bottom: int = 0, margin_bottom: int = 0,
) -> None: ) -> None:
@@ -109,18 +88,29 @@ class Logger:
:param margin_top: The number of empty lines to print before the dialog. :param margin_top: The number of empty lines to print before the dialog.
:param margin_bottom: The number of empty lines to print after the dialog. :param margin_bottom: The number of empty lines to print after the dialog.
""" """
dialog_color = Logger._get_dialog_color(title, custom_color) color = Logger._get_dialog_color(title, custom_color)
dialog_title = Logger._get_dialog_title(title, custom_title) 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)
top = Logger._format_top_border(dialog_color)
bottom = Logger._format_bottom_border()
print("\n" * margin_top) print("\n" * margin_top)
print(
f"{top}{dialog_title_formatted}{dialog_content}{bottom}", print(Color.apply(BORDER_TOP, color))
end="",
) if dialog_title:
print(Color.apply(f"{dialog_title:^{LINE_WIDTH}}", color))
print(Color.apply(BORDER_TITLE, color))
if content:
print(
Logger.format_content(
content,
LINE_WIDTH,
color,
center_content,
)
)
print(Color.apply(BORDER_BOTTOM, color))
print("\n" * margin_bottom) print("\n" * margin_bottom)
@staticmethod @staticmethod
@@ -133,39 +123,20 @@ class Logger:
@staticmethod @staticmethod
def _get_dialog_color( def _get_dialog_color(
title: DialogType, custom_color: DialogCustomColor | None = None title: DialogType, custom_color: Color | None = None
) -> str: ) -> Color:
if title == DialogType.CUSTOM and custom_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 return color
@staticmethod
def _format_top_border(color: str) -> str:
return f"{color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
@staticmethod
def _format_bottom_border() -> str:
return (
f"\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛{RESET_FORMAT}"
)
@staticmethod
def _format_dialog_title(title: str | None) -> str:
if title is not None:
return textwrap.dedent(f"""
{title:^{LINE_WIDTH}}
┠───────────────────────────────────────────────────────┨
""")
else:
return "\n"
@staticmethod @staticmethod
def format_content( def format_content(
content: List[str], content: List[str],
line_width: int, line_width: int,
color: Color = Color.WHITE,
center_content: bool = False, center_content: bool = False,
border_left: str = "", border_left: str = "",
border_right: str = "", border_right: str = "",
@@ -184,11 +155,13 @@ class Logger:
if not center_content: if not center_content:
formatted_lines = [ 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: else:
formatted_lines = [ 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) return "\n".join(formatted_lines)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -15,6 +15,7 @@ from components.klipper import KLIPPER_DIR
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.klipper_firmware.menus.klipper_build_menu import ( from components.klipper_firmware.menus.klipper_build_menu import (
KlipperBuildFirmwareMenu, KlipperBuildFirmwareMenu,
KlipperKConfigMenu,
) )
from components.klipper_firmware.menus.klipper_flash_menu import ( from components.klipper_firmware.menus.klipper_flash_menu import (
KlipperFlashMethodMenu, KlipperFlashMethodMenu,
@@ -22,9 +23,9 @@ from components.klipper_firmware.menus.klipper_flash_menu import (
) )
from components.moonraker import MOONRAKER_DIR from components.moonraker import MOONRAKER_DIR
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from core.constants import COLOR_YELLOW, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from procedures.system import change_system_hostname from procedures.system import change_system_hostname
from utils.git_utils import rollback_repository from utils.git_utils import rollback_repository
@@ -34,6 +35,8 @@ from utils.git_utils import rollback_repository
class AdvancedMenu(BaseMenu): class AdvancedMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Advanced Menu"
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -53,13 +56,8 @@ class AdvancedMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Advanced Menu ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
║ Klipper Firmware: │ Repository Rollback: ║ ║ Klipper Firmware: │ Repository Rollback: ║
║ 1) [Build] │ 5) [Klipper] ║ ║ 1) [Build] │ 5) [Klipper] ║
@@ -79,12 +77,15 @@ class AdvancedMenu(BaseMenu):
rollback_repository(MOONRAKER_DIR, Moonraker) rollback_repository(MOONRAKER_DIR, Moonraker)
def build(self, **kwargs) -> None: def build(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() KlipperBuildFirmwareMenu(previous_menu=self.__class__).run()
def flash(self, **kwargs) -> None: def flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperFlashMethodMenu(previous_menu=self.__class__).run() KlipperFlashMethodMenu(previous_menu=self.__class__).run()
def build_flash(self, **kwargs) -> None: def build_flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run()
KlipperFlashMethodMenu(previous_menu=self.__class__).run() KlipperFlashMethodMenu(previous_menu=self.__class__).run()

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -13,7 +13,7 @@ from typing import Type
from components.klipper.klipper_utils import backup_klipper_dir from components.klipper.klipper_utils import backup_klipper_dir
from components.klipperscreen.klipperscreen import backup_klipperscreen_dir from components.klipperscreen.klipperscreen import backup_klipperscreen_dir
from components.moonraker.moonraker_utils import ( from components.moonraker.utils.utils import (
backup_moonraker_db_dir, backup_moonraker_db_dir,
backup_moonraker_dir, backup_moonraker_dir,
) )
@@ -23,9 +23,9 @@ from components.webui_client.client_utils import (
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData 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 import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from utils.common import backup_printer_config_dir from utils.common import backup_printer_config_dir
@@ -34,6 +34,8 @@ from utils.common import backup_printer_config_dir
class BackupMenu(BaseMenu): class BackupMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Backup Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -55,14 +57,11 @@ class BackupMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Backup Menu ] " line1 = Color.apply(
line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}" "INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW
color = COLOR_CYAN )
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:^62} {line1:^62}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -14,36 +14,33 @@ import sys
import textwrap import textwrap
import traceback import traceback
from abc import abstractmethod from abc import abstractmethod
from enum import Enum
from typing import Dict, Type 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.logger import Logger
from core.menus import FooterType, Option 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 from utils.input_utils import get_selection_input
def clear() -> None: def clear() -> None:
subprocess.call("clear", shell=True) subprocess.call("clear -x", shell=True)
def print_header() -> None: def print_header() -> None:
line1 = " [ KIAUH ] " line1 = " [ KIAUH ] "
line2 = "Klipper Installation And Update Helper" line2 = "Klipper Installation And Update Helper"
line3 = "" line3 = ""
color = COLOR_CYAN color = Color.CYAN
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(str(color)) - len(str(Color.RST))
header = textwrap.dedent( header = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
{color}{line1:~^{count}}{RESET_FORMAT} {Color.apply(f"{line1:~^{count}}", color)}
{color}{line2:^{count}}{RESET_FORMAT} {Color.apply(f"{line2:^{count}}", color)}
{color}{line3:~^{count}}{RESET_FORMAT} {Color.apply(f"{line3:~^{count}}", color)}
╚═══════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════╝
""" """
)[1:] )[1:]
@@ -52,11 +49,11 @@ def print_header() -> None:
def print_quit_footer() -> None: def print_quit_footer() -> None:
text = "Q) Quit" text = "Q) Quit"
color = COLOR_RED color = Color.RED
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(str(color)) - len(str(Color.RST))
footer = textwrap.dedent( footer = textwrap.dedent(
f""" f"""
{color}{text:^{count}}{RESET_FORMAT} {color}{text:^{count}}{Color.RST}
╚═══════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════╝
""" """
)[1:] )[1:]
@@ -65,11 +62,11 @@ def print_quit_footer() -> None:
def print_back_footer() -> None: def print_back_footer() -> None:
text = "B) « Back" text = "B) « Back"
color = COLOR_GREEN color = Color.GREEN
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(str(color)) - len(str(Color.RST))
footer = textwrap.dedent( footer = textwrap.dedent(
f""" f"""
{color}{text:^{count}}{RESET_FORMAT} {color}{text:^{count}}{Color.RST}
╚═══════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════╝
""" """
)[1:] )[1:]
@@ -79,12 +76,12 @@ def print_back_footer() -> None:
def print_back_help_footer() -> None: def print_back_help_footer() -> None:
text1 = "B) « Back" text1 = "B) « Back"
text2 = "H) Help [?]" text2 = "H) Help [?]"
color1 = COLOR_GREEN color1 = Color.GREEN
color2 = COLOR_YELLOW color2 = Color.YELLOW
count = 34 - len(color1) - len(RESET_FORMAT) count = 34 - len(str(color1)) - len(str(Color.RST))
footer = textwrap.dedent( footer = textwrap.dedent(
f""" f"""
{color1}{text1:^{count}}{RESET_FORMAT}{color2}{text2:^{count}}{RESET_FORMAT} {color1}{text1:^{count}}{Color.RST}{color2}{text2:^{count}}{Color.RST}
╚═══════════════════════════╧═══════════════════════════╝ ╚═══════════════════════════╧═══════════════════════════╝
""" """
)[1:] )[1:]
@@ -95,6 +92,11 @@ def print_blank_footer() -> None:
print("╚═══════════════════════════════════════════════════════╝") print("╚═══════════════════════════════════════════════════════╝")
class MenuTitleStyle(Enum):
PLAIN = "plain"
STYLED = "styled"
class PostInitCaller(type): class PostInitCaller(type):
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs) obj = type.__call__(cls, *args, **kwargs)
@@ -110,10 +112,20 @@ class BaseMenu(metaclass=PostInitCaller):
default_option: Option = None default_option: Option = None
input_label_txt: str = "Perform action" input_label_txt: str = "Perform action"
header: bool = False 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 previous_menu: Type[BaseMenu] | None = None
help_menu: Type[BaseMenu] | None = None help_menu: Type[BaseMenu] | None = None
footer_type: FooterType = FooterType.BACK footer_type: FooterType = FooterType.BACK
message_service = MessageService()
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
if type(self) is BaseMenu: if type(self) is BaseMenu:
raise NotImplementedError("BaseMenu cannot be instantiated directly.") raise NotImplementedError("BaseMenu cannot be instantiated directly.")
@@ -160,7 +172,32 @@ class BaseMenu(metaclass=PostInitCaller):
def print_menu(self) -> None: def print_menu(self) -> None:
raise NotImplementedError 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: if self.footer_type is FooterType.QUIT:
print_quit_footer() print_quit_footer()
elif self.footer_type is FooterType.BACK: elif self.footer_type is FooterType.BACK:
@@ -172,16 +209,20 @@ class BaseMenu(metaclass=PostInitCaller):
else: else:
raise NotImplementedError("FooterType not correctly implemented!") raise NotImplementedError("FooterType not correctly implemented!")
def display_menu(self) -> None: def __display_menu(self) -> None:
self.message_service.display_message()
if self.header: if self.header:
print_header() print_header()
self.__print_menu_title()
self.print_menu() self.print_menu()
self.print_footer() self.__print_footer()
def run(self) -> None: def run(self) -> None:
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
try: try:
self.display_menu() self.__display_menu()
option = get_selection_input(self.input_label_txt, self.options) option = get_selection_input(self.input_label_txt, self.options)
selected_option: Option = self.options.get(option) selected_option: Option = self.options.get(option)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,16 +12,20 @@ import textwrap
from typing import Type from typing import Type
from components.crowsnest.crowsnest import install_crowsnest from components.crowsnest.crowsnest import install_crowsnest
from components.klipper import klipper_setup from components.klipper.services.klipper_setup_service import KlipperSetupService
from components.klipperscreen.klipperscreen import install_klipperscreen from components.klipperscreen.klipperscreen import install_klipperscreen
from components.moonraker import moonraker_setup from components.moonraker import moonraker_setup
from components.webui_client import client_setup from components.webui_client.client_config.client_config_setup import (
from components.webui_client.client_config import client_config_setup install_client_config,
)
from components.webui_client.client_setup import install_client
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.constants import COLOR_GREEN, RESET_FORMAT from components.webui_client.menus.client_install_menu import ClientInstallMenu
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -29,7 +33,10 @@ from core.menus.base_menu import BaseMenu
class InstallMenu(BaseMenu): class InstallMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Installation Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.klsvc = KlipperSetupService()
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
@@ -49,13 +56,8 @@ class InstallMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Installation Menu ] "
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
║ Firmware & API: │ Touchscreen GUI: ║ ║ Firmware & API: │ Touchscreen GUI: ║
║ 1) [Klipper] │ 7) [KlipperScreen] ║ ║ 1) [Klipper] │ 7) [KlipperScreen] ║
@@ -74,22 +76,30 @@ class InstallMenu(BaseMenu):
print(menu, end="") print(menu, end="")
def install_klipper(self, **kwargs) -> None: def install_klipper(self, **kwargs) -> None:
klipper_setup.install_klipper() self.klsvc.install()
def install_moonraker(self, **kwargs) -> None: def install_moonraker(self, **kwargs) -> None:
moonraker_setup.install_moonraker() moonraker_setup.install_moonraker()
def install_mainsail(self, **kwargs) -> None: def install_mainsail(self, **kwargs) -> None:
client_setup.install_client(MainsailData()) client: MainsailData = MainsailData()
if client.client_dir.exists():
ClientInstallMenu(client, self.__class__).run()
else:
install_client(client, settings=KiauhSettings())
def install_mainsail_config(self, **kwargs) -> None: def install_mainsail_config(self, **kwargs) -> None:
client_config_setup.install_client_config(MainsailData()) install_client_config(MainsailData())
def install_fluidd(self, **kwargs) -> None: def install_fluidd(self, **kwargs) -> None:
client_setup.install_client(FluiddData()) client: FluiddData = FluiddData()
if client.client_dir.exists():
ClientInstallMenu(client, self.__class__).run()
else:
install_client(client, settings=KiauhSettings())
def install_fluidd_config(self, **kwargs) -> None: def install_fluidd_config(self, **kwargs) -> None:
client_config_setup.install_client_config(FluiddData()) install_client_config(FluiddData())
def install_klipperscreen(self, **kwargs) -> None: def install_klipperscreen(self, **kwargs) -> None:
install_klipperscreen() install_klipperscreen()

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -16,21 +16,13 @@ from components.crowsnest.crowsnest import get_crowsnest_status
from components.klipper.klipper_utils import get_klipper_status from components.klipper.klipper_utils import get_klipper_status
from components.klipperscreen.klipperscreen import get_klipperscreen_status from components.klipperscreen.klipperscreen import get_klipperscreen_status
from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.log_uploads.menus.log_upload_menu import LogUploadMenu
from components.moonraker.moonraker_utils import get_moonraker_status from components.moonraker.utils.utils import get_moonraker_status
from components.webui_client.client_utils import ( from components.webui_client.client_utils import (
get_client_status, get_client_status,
get_current_client_config, get_current_client_config,
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData 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.logger import Logger
from core.menus import FooterType from core.menus import FooterType
from core.menus.advanced_menu import AdvancedMenu 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.remove_menu import RemoveMenu
from core.menus.settings_menu import SettingsMenu from core.menus.settings_menu import SettingsMenu
from core.menus.update_menu import UpdateMenu 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 extensions.extensions_menu import ExtensionsMenu
from utils.common import get_kiauh_version, trunc_string from utils.common import get_kiauh_version, trunc_string
@@ -52,6 +45,8 @@ class MainMenu(BaseMenu):
super().__init__() super().__init__()
self.header: bool = True self.header: bool = True
self.title = "Main Menu"
self.title_color = Color.CYAN
self.footer_type: FooterType = FooterType.QUIT self.footer_type: FooterType = FooterType.QUIT
self.version = "" self.version = ""
@@ -83,7 +78,7 @@ class MainMenu(BaseMenu):
setattr( setattr(
self, self,
f"{var}_status", f"{var}_status",
f"{COLOR_RED}Not installed{RESET_FORMAT}", Color.apply("Not installed", Color.RED),
) )
def _fetch_status(self) -> None: def _fetch_status(self) -> None:
@@ -109,34 +104,30 @@ class MainMenu(BaseMenu):
count_txt = f": {instance_count}" count_txt = f": {instance_count}"
setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) 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}_owner", Color.apply(owner, Color.CYAN))
setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}") setattr(self, f"{name}_repo", Color.apply(repo, Color.CYAN))
def _format_by_code(self, code: int, status: str, count: str) -> str: def _format_by_code(self, code: int, status: str, count: str) -> str:
color = COLOR_RED color = Color.RED
if code == 0: if code == 0:
color = COLOR_RED color = Color.RED
elif code == 1: elif code == 1:
color = COLOR_YELLOW color = Color.YELLOW
elif code == 2: 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: def print_menu(self) -> None:
self._fetch_status() self._fetch_status()
header = " [ Main Menu ] " footer1 = Color.apply(self.version, Color.CYAN)
footer1 = f"{COLOR_CYAN}{self.version}{RESET_FORMAT}" link = Color.apply("https://git.io/JnmlX", Color.MAGENTA)
footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" footer2 = f"Changelog: {link}"
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
pad1 = 32 pad1 = 32
pad2 = 26 pad2 = 26
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟──────────────────┬────────────────────────────────────╢ ╟──────────────────┬────────────────────────────────────╢
║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}} ║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}}
║ │ Owner: {self.kl_owner:<{pad1}} ║ │ Owner: {self.kl_owner:<{pad1}}

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -20,9 +20,9 @@ from components.moonraker.menus.moonraker_remove_menu import (
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from components.webui_client.menus.client_remove_menu import ClientRemoveMenu 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 import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -30,6 +30,8 @@ from core.menus.base_menu import BaseMenu
class RemoveMenu(BaseMenu): class RemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Remove Menu"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -48,13 +50,8 @@ class RemoveMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Remove Menu ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ INFO: Configurations and/or any backups will be kept! ║ ║ INFO: Configurations and/or any backups will be kept! ║
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -9,13 +9,18 @@
from __future__ import annotations from __future__ import annotations
import textwrap import textwrap
from pathlib import Path
from typing import Literal, Tuple, Type from typing import Literal, Tuple, Type
from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT 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.utils.utils import get_moonraker_status
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings, RepoSettings from core.settings.kiauh_settings import KiauhSettings, RepoSettings
from core.types.color import Color
from procedures.switch_repo import run_switch_repo_routine from procedures.switch_repo import run_switch_repo_routine
from utils.input_utils import get_confirm, get_string_input from utils.input_utils import get_confirm, get_string_input
@@ -25,13 +30,15 @@ from utils.input_utils import get_confirm, get_string_input
class SettingsMenu(BaseMenu): class SettingsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Settings Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.klipper_repo: str | None = None
self.moonraker_repo: str | None = None
self.mainsail_unstable: bool | None = None self.mainsail_unstable: bool | None = None
self.fluidd_unstable: bool | None = None self.fluidd_unstable: bool | None = None
self.auto_backups_enabled: bool | None = None self.auto_backups_enabled: bool | None = None
self._load_settings() self._load_settings()
print(self.klipper_status)
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
@@ -48,32 +55,38 @@ class SettingsMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ KIAUH Settings ] " color = Color.CYAN
color = COLOR_CYAN checked = f"[{Color.apply('x', Color.GREEN)}]"
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]"
unchecked = "[ ]" unchecked = "[ ]"
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 o1 = checked if self.mainsail_unstable else unchecked
o2 = checked if self.fluidd_unstable else unchecked o2 = checked if self.fluidd_unstable else unchecked
o3 = checked if self.auto_backups_enabled else unchecked o3 = checked if self.auto_backups_enabled else unchecked
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Klipper source repository: ║ Klipper:
{self.klipper_repo:<67} ● Repo: {kl_repo:51}
● Owner: {kl_owner:51}
Moonraker source repository: ● Branch: {kl_branch:51}
║ ● {self.moonraker_repo:<67} ╟───────────────────────────────────────────────────────╢
Moonraker:
Install unstable Webinterface releases: ● Repo: {mr_repo:51}
║ ● Owner: {mr_owner:51}
║ ● Branch: {mr_branch:51}
╟───────────────────────────────────────────────────────╢
║ Install unstable releases: ║
{o1} Mainsail ║ {o1} Mainsail ║
{o2} Fluidd ║ {o2} Fluidd ║
║ ║ ╟───────────────────────────────────────────────────────╢
║ Auto-Backup: ║ ║ Auto-Backup: ║
{o3} Automatic backup before update ║ {o3} Automatic backup before update ║
║ ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) Set Klipper source repository ║ ║ 1) Set Klipper source repository ║
║ 2) Set Moonraker source repository ║ ║ 2) Set Moonraker source repository ║
@@ -89,51 +102,61 @@ class SettingsMenu(BaseMenu):
def _load_settings(self) -> None: def _load_settings(self) -> None:
self.settings = KiauhSettings() self.settings = KiauhSettings()
self._format_repo_str("klipper")
self._format_repo_str("moonraker")
self.auto_backups_enabled = self.settings.kiauh.backup_before_update self.auto_backups_enabled = self.settings.kiauh.backup_before_update
self.mainsail_unstable = self.settings.mainsail.unstable_releases self.mainsail_unstable = self.settings.mainsail.unstable_releases
self.fluidd_unstable = self.settings.fluidd.unstable_releases self.fluidd_unstable = self.settings.fluidd.unstable_releases
def _format_repo_str(self, repo_name: Literal["klipper", "moonraker"]) -> None: # by default, we show the status of the installed repositories
repo: RepoSettings = self.settings[repo_name] self.klipper_status = get_klipper_status()
repo_str = f"{'/'.join(repo.repo_url.rsplit('/', 2)[-2:])}" self.moonraker_status = get_moonraker_status()
branch_str = f"({COLOR_CYAN}@ {repo.branch}{RESET_FORMAT})" # 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
setattr( def _gather_input(
self, self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path
f"{repo_name}_repo", ) -> Tuple[str, str]:
f"{COLOR_CYAN}{repo_str}{RESET_FORMAT} {branch_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!",
]
)
Logger.print_dialog(DialogType.ATTENTION, warn_msg)
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.",
"\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!",
],
)
repo = get_string_input( repo = get_string_input(
"Enter new repository URL", "Enter new repository URL",
allow_special_chars=True, regex=r"^[\w/.:-]+$",
default=KLIPPER_REPO_URL if repo_name == "klipper" else MOONRAKER_REPO_URL,
) )
branch = get_string_input( branch = get_string_input(
"Enter new branch name", "Enter new branch name", regex=r"^.+$", default="master"
allow_special_chars=True,
) )
return repo, branch return repo, branch
def _set_repo(self, repo_name: Literal["klipper", "moonraker"]) -> None: def _set_repo(
repo_url, branch = self._gather_input() 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() display_name = repo_name.capitalize()
Logger.print_dialog( Logger.print_dialog(
DialogType.CUSTOM, DialogType.CUSTOM,
@@ -156,22 +179,30 @@ class SettingsMenu(BaseMenu):
Logger.print_ok("Changes saved!") Logger.print_ok("Changes saved!")
else: else:
Logger.print_info( Logger.print_info(
f"Skipping change of {display_name} source repository ..." f"Changing of {display_name} source repository canceled ..."
) )
return return
Logger.print_status(f"Switching to {display_name}'s new source repository ...") self._switch_repo(repo_name, repo_dir)
self._switch_repo(repo_name)
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] repo: RepoSettings = self.settings[name]
run_switch_repo_routine(name, repo) run_switch_repo_routine(name, repo)
def set_klipper_repo(self, **kwargs) -> None: def set_klipper_repo(self, **kwargs) -> None:
self._set_repo("klipper") self._set_repo("klipper", KLIPPER_DIR)
def set_moonraker_repo(self, **kwargs) -> None: def set_moonraker_repo(self, **kwargs) -> None:
self._set_repo("moonraker") self._set_repo("moonraker", MOONRAKER_DIR)
def toggle_mainsail_release(self, **kwargs) -> None: def toggle_mainsail_release(self, **kwargs) -> None:
self.mainsail_unstable = not self.mainsail_unstable self.mainsail_unstable = not self.mainsail_unstable

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -12,16 +12,16 @@ import textwrap
from typing import Callable, List, Type from typing import Callable, List, Type
from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest
from components.klipper.klipper_setup import update_klipper
from components.klipper.klipper_utils import ( from components.klipper.klipper_utils import (
get_klipper_status, get_klipper_status,
) )
from components.klipper.services.klipper_setup_service import KlipperSetupService
from components.klipperscreen.klipperscreen import ( from components.klipperscreen.klipperscreen import (
get_klipperscreen_status, get_klipperscreen_status,
update_klipperscreen, update_klipperscreen,
) )
from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_setup import update_moonraker
from components.moonraker.moonraker_utils import get_moonraker_status from components.moonraker.utils.utils import get_moonraker_status
from components.webui_client.client_config.client_config_setup import ( from components.webui_client.client_config.client_config_setup import (
update_client_config, update_client_config,
) )
@@ -32,17 +32,11 @@ from components.webui_client.client_utils import (
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData 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.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.spinner import Spinner from core.types.color import Color
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.input_utils import get_confirm from utils.input_utils import get_confirm
from utils.sys_utils import ( from utils.sys_utils import (
get_upgradable_packages, get_upgradable_packages,
@@ -56,6 +50,11 @@ from utils.sys_utils import (
class UpdateMenu(BaseMenu): class UpdateMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() 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.previous_menu: Type[BaseMenu] | None = previous_menu
self.packages: List[str] = [] 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: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
@@ -143,29 +145,16 @@ class UpdateMenu(BaseMenu):
} }
def print_menu(self) -> None: 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." sysupgrades: str = "No upgrades available."
padding = 29 padding = 29
if self.package_count > 0: if self.package_count > 0:
sysupgrades = ( sysupgrades = Color.apply(
f"{COLOR_GREEN}{self.package_count} upgrades available!{RESET_FORMAT}" f"{self.package_count} upgrades available!", Color.GREEN
) )
padding = 38 padding = 38
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────┬───────────────┬───────────────╢ ╟───────────────────────┬───────────────┬───────────────╢
║ a) Update all │ │ ║ ║ a) Update all │ │ ║
║ │ Current: │ Latest: ║ ║ │ Current: │ Latest: ║
@@ -204,7 +193,8 @@ class UpdateMenu(BaseMenu):
self.upgrade_system_packages() self.upgrade_system_packages()
def update_klipper(self, **kwargs) -> None: def update_klipper(self, **kwargs) -> None:
self._run_update_routine("klipper", update_klipper) klsvc = KlipperSetupService()
self._run_update_routine("klipper", klsvc.update)
def update_moonraker(self, **kwargs) -> None: def update_moonraker(self, **kwargs) -> None:
self._run_update_routine("moonraker", update_moonraker) self._run_update_routine("moonraker", update_moonraker)
@@ -265,15 +255,15 @@ class UpdateMenu(BaseMenu):
self.package_count = len(self.packages) self.package_count = len(self.packages)
def _format_local_status(self, local_version, remote_version) -> str: def _format_local_status(self, local_version, remote_version) -> str:
color = COLOR_RED color = Color.RED
if not local_version: if not local_version:
color = COLOR_RED color = Color.RED
elif local_version == remote_version: elif local_version == remote_version:
color = COLOR_GREEN color = Color.GREEN
elif local_version != remote_version: 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: def _set_status_data(self, name: str, status_fn: Callable, *args) -> None:
comp_status: ComponentStatus = status_fn(*args) comp_status: ComponentStatus = status_fn(*args)
@@ -288,9 +278,9 @@ class UpdateMenu(BaseMenu):
local_status = self.status_data[name].get("local", None) local_status = self.status_data[name].get("local", None)
remote_status = self.status_data[name].get("remote", 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) 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}_local", local_txt)
setattr(self, f"{name}_remote", remote_txt) setattr(self, f"{name}_remote", remote_txt)

View File

View File

@@ -0,0 +1,61 @@
# ======================================================================= #
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
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:
__cls_instance = None
__message: Message | None
def __new__(cls) -> "MessageService":
if cls.__cls_instance is None:
cls.__cls_instance = super(MessageService, cls).__new__(cls)
return cls.__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

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -145,7 +145,8 @@ class KiauhSettings:
def _validate_str(self, section: str, option: str) -> None: def _validate_str(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option) self._v_section, self._v_option = (section, option)
v = self.config.getval(section, option) v = self.config.getval(section, option)
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
if not v:
raise ValueError raise ValueError
def _apply_settings_from_file(self) -> None: def _apply_settings_from_file(self) -> None:

View File

@@ -3,13 +3,7 @@ import threading
import time import time
from typing import List, Literal from typing import List, Literal
from core.constants import ( from core.types.color import Color
COLOR_GREEN,
COLOR_RED,
COLOR_WHITE,
COLOR_YELLOW,
RESET_FORMAT,
)
SpinnerColor = Literal["white", "red", "green", "yellow"] SpinnerColor = Literal["white", "red", "green", "yellow"]
@@ -18,21 +12,18 @@ class Spinner:
def __init__( def __init__(
self, self,
message: str = "Loading", message: str = "Loading",
color: SpinnerColor = "white",
interval: float = 0.2, interval: float = 0.2,
) -> None: ) -> None:
self.message = f"{message} ..." self.message = f"{message} ..."
self.interval = interval self.interval = interval
self._stop_event = threading.Event() self._stop_event = threading.Event()
self._thread = threading.Thread(target=self._animate) self._thread = threading.Thread(target=self._animate)
self._color = ""
self._set_color(color)
def _animate(self) -> None: def _animate(self) -> None:
animation: List[str] = ["", "", "", "", "", "", "", "", "", ""] animation: List[str] = ["", "", "", "", "", "", "", "", "", ""]
while not self._stop_event.is_set(): while not self._stop_event.is_set():
for char in animation: 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() sys.stdout.flush()
time.sleep(self.interval) time.sleep(self.interval)
if self._stop_event.is_set(): if self._stop_event.is_set():
@@ -40,16 +31,6 @@ class Spinner:
sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r") sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r")
sys.stdout.flush() 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: def start(self) -> None:
self._stop_event.clear() self._stop_event.clear()
if not self._thread.is_alive(): 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 - 2025 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
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

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -25,6 +25,7 @@ class ComponentStatus:
status: StatusCode status: StatusCode
owner: str | None = None owner: str | None = None
repo: str | None = None repo: str | None = None
branch: str = ""
local: str | None = None local: str | None = None
remote: str | None = None remote: str | None = None
instances: int | None = None instances: int | None = None

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -15,10 +15,10 @@ import textwrap
from pathlib import Path from pathlib import Path
from typing import Dict, List, Type from typing import Dict, List, Type
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.logger import Logger from core.logger import Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from extensions import EXTENSION_ROOT from extensions import EXTENSION_ROOT
from extensions.base_extension import BaseExtension from extensions.base_extension import BaseExtension
@@ -28,6 +28,8 @@ from extensions.base_extension import BaseExtension
class ExtensionsMenu(BaseMenu): class ExtensionsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Extensions Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.extensions: Dict[str, BaseExtension] = self.discover_extensions() self.extensions: Dict[str, BaseExtension] = self.discover_extensions()
@@ -82,14 +84,9 @@ class ExtensionsMenu(BaseMenu):
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Extensions Menu ] " line1 = Color.apply("Available Extensions:", Color.YELLOW)
color = COLOR_CYAN
line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}"
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {line1:<62}
║ ║ ║ ║
@@ -112,6 +109,8 @@ class ExtensionSubmenu(BaseMenu):
self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None
): ):
super().__init__() super().__init__()
self.title = extension.metadata.get("display_name")
self.title_color = Color.YELLOW
self.extension = extension self.extension = extension
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
@@ -129,9 +128,6 @@ class ExtensionSubmenu(BaseMenu):
self.options["2"] = Option(self.extension.remove_extension) self.options["2"] = Option(self.extension.remove_extension)
def print_menu(self) -> None: 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 line_width = 53
description: List[str] = self.extension.metadata.get("description", []) description: List[str] = self.extension.metadata.get("description", [])
description_text = Logger.format_content( description_text = Logger.format_content(
@@ -142,9 +138,7 @@ class ExtensionSubmenu(BaseMenu):
) )
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
""" """
)[1:] )[1:]

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -10,11 +10,11 @@
# ======================================================================= # # ======================================================================= #
import os import os
import shutil
import subprocess import subprocess
from pathlib import Path
from core.constants import SYSTEMD from core.constants import SYSTEMD
from core.logger import Logger from core.logger import Logger
from pathlib import Path
from extensions.base_extension import BaseExtension from extensions.base_extension import BaseExtension
from extensions.klipper_backup import ( from extensions.klipper_backup import (
KLIPPERBACKUP_CONFIG_DIR, KLIPPERBACKUP_CONFIG_DIR,
@@ -29,7 +29,6 @@ from utils.sys_utils import cmd_sysctl_manage, remove_system_service, unit_file_
class KlipperbackupExtension(BaseExtension): class KlipperbackupExtension(BaseExtension):
def remove_extension(self, **kwargs) -> None: def remove_extension(self, **kwargs) -> None:
if not check_file_exist(KLIPPERBACKUP_DIR): if not check_file_exist(KLIPPERBACKUP_DIR):
Logger.print_info("Extension does not seem to be installed! Skipping ...") Logger.print_info("Extension does not seem to be installed! Skipping ...")
@@ -48,29 +47,44 @@ class KlipperbackupExtension(BaseExtension):
cmd_sysctl_manage("daemon-reload") cmd_sysctl_manage("daemon-reload")
cmd_sysctl_manage("reset-failed") cmd_sysctl_manage("reset-failed")
else: else:
Logger.print_error(f"Unknown unit type {unit_type} of {full_service_name}") Logger.print_error(
f"Unknown unit type {unit_type} of {full_service_name}"
)
except: except:
Logger.print_error(f"Failed to remove {full_service_name}: {str(e)}") Logger.print_error(f"Failed to remove {full_service_name}: {str(e)}")
def check_crontab_entry(entry) -> bool: def check_crontab_entry(entry) -> bool:
try: try:
crontab_content = subprocess.check_output(["crontab", "-l"], stderr=subprocess.DEVNULL, text=True) crontab_content = subprocess.check_output(
["crontab", "-l"], stderr=subprocess.DEVNULL, text=True
)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return False return False
return any(entry in line for line in crontab_content.splitlines()) return any(entry in line for line in crontab_content.splitlines())
def remove_moonraker_entry(): def remove_moonraker_entry():
original_file_path = MOONRAKER_CONF original_file_path = MOONRAKER_CONF
comparison_file_path = os.path.join(str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf") comparison_file_path = os.path.join(
if not (os.path.exists(original_file_path) and os.path.exists(comparison_file_path)): str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf"
)
if not (
os.path.exists(original_file_path)
and os.path.exists(comparison_file_path)
):
return False return False
with open(original_file_path, "r") as original_file, open(comparison_file_path, "r") as comparison_file: with open(original_file_path, "r") as original_file, open(
comparison_file_path, "r"
) as comparison_file:
original_content = original_file.read() original_content = original_file.read()
comparison_content = comparison_file.read() comparison_content = comparison_file.read()
if comparison_content in original_content: if comparison_content in original_content:
Logger.print_status("Removing Klipper-Backup moonraker entry ...") Logger.print_status("Removing Klipper-Backup moonraker entry ...")
modified_content = original_content.replace(comparison_content, "").strip() modified_content = original_content.replace(
modified_content = "\n".join(line for line in modified_content.split("\n") if line.strip()) comparison_content, ""
).strip()
modified_content = "\n".join(
line for line in modified_content.split("\n") if line.strip()
)
with open(original_file_path, "w") as original_file: with open(original_file_path, "w") as original_file:
original_file.write(modified_content) original_file.write(modified_content)
Logger.print_ok("Klipper-Backup moonraker entry successfully removed!") Logger.print_ok("Klipper-Backup moonraker entry successfully removed!")
@@ -79,7 +93,11 @@ class KlipperbackupExtension(BaseExtension):
if get_confirm("Do you really want to remove the extension?", True, False): if get_confirm("Do you really want to remove the extension?", True, False):
# Remove systemd timer and services # Remove systemd timer and services
service_names = ["klipper-backup-on-boot", "klipper-backup-filewatch", "klipper-backup"] service_names = [
"klipper-backup-on-boot",
"klipper-backup-filewatch",
"klipper-backup",
]
unit_types = ["timer", "service"] unit_types = ["timer", "service"]
for service_name in service_names: for service_name in service_names:
@@ -91,10 +109,23 @@ class KlipperbackupExtension(BaseExtension):
try: try:
if check_crontab_entry("/klipper-backup/script.sh"): if check_crontab_entry("/klipper-backup/script.sh"):
Logger.print_status("Removing Klipper-Backup crontab entry ...") Logger.print_status("Removing Klipper-Backup crontab entry ...")
crontab_content = subprocess.check_output(["crontab", "-l"], text=True) crontab_content = subprocess.check_output(
modified_content = "\n".join(line for line in crontab_content.splitlines() if "/klipper-backup/script.sh" not in line) ["crontab", "-l"], text=True
subprocess.run(["crontab", "-"], input=modified_content + "\n", text=True, check=True) )
Logger.print_ok("Klipper-Backup crontab entry successfully removed!") modified_content = "\n".join(
line
for line in crontab_content.splitlines()
if "/klipper-backup/script.sh" not in line
)
subprocess.run(
["crontab", "-"],
input=modified_content + "\n",
text=True,
check=True,
)
Logger.print_ok(
"Klipper-Backup crontab entry successfully removed!"
)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
Logger.print_error("Unable to remove the Klipper-Backup cron entry") Logger.print_error("Unable to remove the Klipper-Backup cron entry")
@@ -102,7 +133,9 @@ class KlipperbackupExtension(BaseExtension):
try: try:
remove_moonraker_entry() remove_moonraker_entry()
except: except:
Logger.print_error("Unable to remove the Klipper-Backup moonraker entry") Logger.print_error(
"Unable to remove the Klipper-Backup moonraker entry"
)
# Remove Klipper-backup extension # Remove Klipper-backup extension
Logger.print_status("Removing Klipper-Backup extension ...") Logger.print_status("Removing Klipper-Backup extension ...")
@@ -112,7 +145,7 @@ class KlipperbackupExtension(BaseExtension):
remove_with_sudo(KLIPPERBACKUP_CONFIG_DIR) remove_with_sudo(KLIPPERBACKUP_CONFIG_DIR)
Logger.print_ok("Extension Klipper-Backup successfully removed!") Logger.print_ok("Extension Klipper-Backup successfully removed!")
except: except:
Logger.print_error(f"Unable to remove Klipper-Backup extension") Logger.print_error("Unable to remove Klipper-Backup extension")
def install_extension(self, **kwargs) -> None: def install_extension(self, **kwargs) -> None:
if not KLIPPERBACKUP_DIR.exists(): if not KLIPPERBACKUP_DIR.exists():

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -20,10 +20,10 @@ from components.klipper.klipper_dialogs import (
DisplayType, DisplayType,
print_instance_overview, print_instance_overview,
) )
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.logger import Logger from core.logger import Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from extensions.base_extension import BaseExtension from extensions.base_extension import BaseExtension
from utils.git_utils import git_clone_wrapper from utils.git_utils import git_clone_wrapper
from utils.input_utils import get_selection_input from utils.input_utils import get_selection_input
@@ -80,6 +80,8 @@ class MainsailThemeInstallMenu(BaseMenu):
def __init__(self, instances: List[Klipper]): def __init__(self, instances: List[Klipper]):
super().__init__() super().__init__()
self.title = "Mainsail Theme Installer"
self.title_color = Color.YELLOW
self.themes: List[ThemeData] = self.load_themes() self.themes: List[ThemeData] = self.load_themes()
self.instances = instances self.instances = instances
@@ -97,14 +99,11 @@ class MainsailThemeInstallMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Mainsail Theme Installer ] " line1 = Color.apply(
color = COLOR_YELLOW "A preview of each Mainsail theme can be found here:", 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)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {line1:<62}
║ https://docs.mainsail.xyz/theming/themes ║ ║ https://docs.mainsail.xyz/theming/themes ║

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -11,8 +11,8 @@ from typing import List
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from core.instance_manager.instance_manager import InstanceManager
from core.instance_manager.base_instance import SUFFIX_BLACKLIST from core.instance_manager.base_instance import SUFFIX_BLACKLIST
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
@@ -309,8 +309,12 @@ class ObicoExtension(BaseExtension):
def _check_and_opt_link_instances(self) -> None: def _check_and_opt_link_instances(self) -> None:
Logger.print_status("Checking link status of Obico instances ...") Logger.print_status("Checking link status of Obico instances ...")
suffix_blacklist: List[str] = [suffix for suffix in SUFFIX_BLACKLIST if suffix != 'obico'] suffix_blacklist: List[str] = [
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico, suffix_blacklist=suffix_blacklist) suffix for suffix in SUFFIX_BLACKLIST if suffix != "obico"
]
ob_instances: List[MoonrakerObico] = get_instances(
MoonrakerObico, suffix_blacklist=suffix_blacklist
)
unlinked_instances: List[MoonrakerObico] = [ unlinked_instances: List[MoonrakerObico] = [
obico for obico in ob_instances if not obico.is_linked obico for obico in ob_instances if not obico.is_linked
] ]

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -45,9 +45,7 @@ class Octoapp:
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
self.base.log_file_name = self.log_file_name self.base.log_file_name = self.log_file_name
self.service_file_path: Path = get_service_file_path( self.service_file_path: Path = get_service_file_path(Octoapp, self.suffix)
Octoapp, self.suffix
)
self.store_dir = self.base.data_dir.joinpath("store") self.store_dir = self.base.data_dir.joinpath("store")
self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME) self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME)
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME) self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -9,8 +9,8 @@
import json import json
from typing import List from typing import List
from components.moonraker.moonraker import Moonraker
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from extensions.base_extension import BaseExtension from extensions.base_extension import BaseExtension
@@ -107,9 +107,7 @@ class OctoappExtension(BaseExtension):
) )
except Exception as e: except Exception as e:
Logger.print_error( Logger.print_error(f"Error during OctoApp for Klipper installation:\n{e}")
f"Error during OctoApp for Klipper installation:\n{e}"
)
def update_extension(self, **kwargs) -> None: def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating OctoApp for Klipper ...") Logger.print_status("Updating OctoApp for Klipper ...")
@@ -183,12 +181,11 @@ class OctoappExtension(BaseExtension):
run_remove_routines(OA_DIR) run_remove_routines(OA_DIR)
def _remove_OA_store_dirs(self) -> None: def _remove_OA_store_dirs(self) -> None:
Logger.print_status("Removing OctoApp for Klipper store directory ...") Logger.print_status("Removing OctoApp for Klipper store directory ...")
klipper_instances: List[Moonraker] = get_instances(Klipper) klipper_instances: List[Moonraker] = get_instances(Klipper)
for instance in klipper_instances: for instance in klipper_instances:
store_dir = instance.data_dir.joinpath("octoapp-store") store_dir = instance.data_dir.joinpath("octoapp-store")
if not store_dir.exists(): if not store_dir.exists():
@@ -197,7 +194,6 @@ class OctoappExtension(BaseExtension):
run_remove_routines(store_dir) run_remove_routines(store_dir)
def _remove_OA_env(self) -> None: def _remove_OA_env(self) -> None:
Logger.print_status("Removing OctoApp for Klipper environment ...") Logger.print_status("Removing OctoApp for Klipper environment ...")

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -116,10 +116,7 @@ class MoonrakerTelegramBot:
"%TELEGRAM_BOT_DIR%", "%TELEGRAM_BOT_DIR%",
self.bot_dir.as_posix(), self.bot_dir.as_posix(),
) )
env_file_content = env_file_content.replace( env_file_content = env_file_content.replace("%CFG%", self.cfg_file.as_posix())
"%CFG%",
f"{self.base.cfg_dir}/printer.cfg",
)
env_file_content = env_file_content.replace( env_file_content = env_file_content.replace(
"%LOG%", "%LOG%",
self.base.log_dir.joinpath(self.log_file_name).as_posix(), self.base.log_dir.joinpath(self.log_file_name).as_posix(),

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #

View File

@@ -1,20 +1,29 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import io
import sys
from core.logger import Logger from core.logger import Logger
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
from core.settings.kiauh_settings import KiauhSettings 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: def main() -> None:
try: try:
KiauhSettings() KiauhSettings()
ensure_encoding()
MainMenu().run() MainMenu().run()
except KeyboardInterrupt: except KeyboardInterrupt:
Logger.print_ok("\nHappy printing!\n", prefix=False) Logger.print_ok("\nHappy printing!\n", prefix=False)

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
# # # #
# This file is part of KIAUH - Klipper Installation And Update Helper # # This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh # # https://github.com/dw-0/kiauh #
@@ -19,7 +19,7 @@ from components.klipper import (
KLIPPER_REQ_FILE, KLIPPER_REQ_FILE,
) )
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.klipper.klipper_setup import install_klipper_packages from components.klipper.klipper_utils import install_klipper_packages
from components.moonraker import ( from components.moonraker import (
MOONRAKER_BACKUP_DIR, MOONRAKER_BACKUP_DIR,
MOONRAKER_DIR, MOONRAKER_DIR,
@@ -64,7 +64,7 @@ def run_switch_repo_routine(
try: try:
# step 2: backup old repo and env # 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) backup_dir = backup_dir.joinpath(org)
bm = BackupManager() bm = BackupManager()
repo_dir_backup_path = bm.backup_directory( repo_dir_backup_path = bm.backup_directory(

Some files were not shown because too many files have changed in this diff Show More