Compare commits

...

73 Commits

Author SHA1 Message Date
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
dw-0
425d86a12f Release v6.0.0-alpha.8
Merge develop into master (Release v6.0.0-alpha.8)
2024-10-21 19:45:55 +02:00
dw-0
ff6162d799 refactor: do not silently configure Fluidd for port 81 (#582)
* refactor: use port 80 as default for fluidd

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

* refactor: improve port selection logic, write last port selection for client to kiauh.cfg

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-21 19:30:03 +02:00
dw-0
674c174224 fix: correctly add Spoolman to moonraker.asvc (#581)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-20 18:11:30 +02:00
CODeRUS
a368331693 feat(flashing): Flash RP2040 in Boot Mode (#580)
* feat(flashing): Flash RP2040 in Boot Mode

* docs: add info about STM32 DFU and RP2040 boot modes
2024-10-18 18:56:21 +02:00
Pedro Lamas
406b64d1e5 refactor: add client name to Moonraker not found dialog (#574)
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-14 17:18:10 +02:00
dw-0
1b5691f2f5 Release v6.0.0-alpha.7
Merge develop into master (v6.0.0-alpha.7)

fixes #561
fixes #564
fixes #565
2024-10-13 11:51:19 +02:00
dw-0
e7eae5a0d1 fix: correctly handle IPs in nginx config files when parsing ports (#568)
* chore: add jupyter files to .gitignore

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

* fix: correctly handle IPs in nginx config files when parsing ports

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 11:21:18 +02:00
dw-0
dc561a562c fix: always return string tuple from get_repo_name() (#567)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 10:27:31 +02:00
dw-0
55cfe124b2 feat: add SimplyPrint extension (#566)
* refactor: correctly sort extensions in extension menu

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

* refactor: use different name for printer_data backup dir

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

* refactor: change return type to List for moonraker_exists function

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

* feat: add SimplyPrint extension

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-13 09:35:15 +02:00
Christian Würthner
43d6598be6 fix: remove octoapp_store dir when uninstalling (#562)
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-05 12:35:14 +02:00
dw-0
dc026a7a2b Release v6.0.0-alpha.6
Merge develop into master (v6.0.0-alpha.6)

fixes #545
fixes #553
fixes #557
2024-10-05 08:29:40 +02:00
dw-0
ac54d04b40 fix: correctly find connected UART devices (#559)
fixes #557

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-05 08:21:39 +02:00
dw-0
c19364360c fix: correctly find connected USB DFU devices (#555)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-05 08:07:47 +02:00
dw-0
2e6c66e524 fix: allow moonraker-telegram-bot-env access to systems site-packages dir (#556)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-10-03 10:22:32 +02:00
Christian Würthner
cd8003add9 feat(extension): add OctoApp (#554)
* Add OctoApp to v6

* fix: set correct index to new extension

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-10-03 08:51:38 +02:00
Kenneth Jiang
1f75395063 fix: self.cfg_file is already a full path (#552)
Signed-off-by: Kenneth Jiang <kenneth.jiang@gmail.com>
2024-09-29 20:33:54 +02:00
Kenneth Jiang
6e1bffa975 fix: remove "obico" from the suffix_blacklist so that it can discover its own instances. (#551)
Signed-off-by: Kenneth Jiang <kenneth.jiang@gmail.com>
2024-09-29 16:41:20 +02:00
dw-0
a8a73249a5 Release v6.0.0-alpha.5
Merge develop into master (v6.0.0-alpha.5)
2024-09-26 20:55:22 +02:00
dw-0
4138c71920 fix: fix section adding and exception handling (#548)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-26 20:52:19 +02:00
123 changed files with 2670 additions and 956 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,10 @@
.idea
.vscode
.pytest_cache
.jupyter
*.ipynb
*.ipynb_checkpoints
*.tmp
__pycache__
.kiauh-env
*.code-workspace

110
README.md
View File

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

View File

@@ -14,5 +14,5 @@ port: 80
unstable_releases: False
[fluidd]
port: 81
port: 80
unstable_releases: False

View File

@@ -1,7 +1,7 @@
#!/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 #
# https://github.com/dw-0/kiauh #

View File

@@ -10,7 +10,7 @@
#=======================================================================#
set -e
clear
clear -x
# make sure we have the correct permissions while running the script
umask 022
@@ -110,7 +110,7 @@ function launch_kiauh_v6() {
export PYTHONPATH="${entrypoint}"
clear
clear -x
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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -27,10 +27,9 @@ from components.crowsnest import (
)
from components.klipper.klipper import Klipper
from core.backup_manager.backup_manager import BackupManager
from core.constants import CURRENT_USER
from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus
from core.types.component_status import ComponentStatus
from utils.common import (
check_install_dependencies,
get_install_status,
@@ -73,7 +72,7 @@ def install_crowsnest() -> None:
Logger.print_info("Installer will prompt you for sudo password!")
try:
run(
f"sudo make install BASE_USER={CURRENT_USER}",
f"sudo make install",
cwd=CROWSNEST_DIR,
shell=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 #
# https://github.com/dw-0/kiauh #
@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent
KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git"
# names
KLIPPER_LOG_NAME = "klippy.log"
KLIPPER_CFG_NAME = "printer.cfg"
@@ -23,6 +25,7 @@ KLIPPER_SERVICE_NAME = "klipper.service"
# directories
KLIPPER_DIR = Path.home().joinpath("klipper")
KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs")
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -11,13 +11,8 @@ import textwrap
from enum import Enum, unique
from typing import List
from core.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.menus.base_menu import print_back_footer
from core.types.color import Color
from utils.instance_type import InstanceType
@@ -42,12 +37,12 @@ def print_instance_overview(
if display_type is DisplayType.SERVICE_NAME
else "printer directories"
)
headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}"
headline = Color.apply(f"The following {d_type} were found:", Color.GREEN)
dialog += f"{headline:^64}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n"
if show_select_all:
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
select_all = Color.apply("a) Select all", Color.YELLOW)
dialog += f"{select_all:<63}\n"
dialog += "║ ║\n"
@@ -56,7 +51,9 @@ def print_instance_overview(
name = s.service_file_path.stem
else:
name = s.data_dir
line = f"{COLOR_CYAN}{f'{i + start_index})' if show_index else ''} {name}{RESET_FORMAT}"
line = Color.apply(
f"{f'{i + start_index})' if show_index else ''} {name}", Color.CYAN
)
dialog += f"{line:<63}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n"
@@ -65,8 +62,10 @@ def print_instance_overview(
def print_select_instance_count_dialog() -> None:
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}"
line1 = Color.apply("WARNING:", Color.YELLOW)
line2 = Color.apply(
"Setting up too many instances may crash your system.", Color.YELLOW
)
dialog = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
@@ -85,8 +84,8 @@ def print_select_instance_count_dialog() -> None:
def print_select_custom_name_dialog() -> None:
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}"
line1 = Color.apply("INFO:", Color.YELLOW)
line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW)
dialog = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -15,6 +15,8 @@ from components.klipper.klipper import Klipper
from components.klipper.klipper_dialogs import print_instance_overview
from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger
from core.services.message_service import Message
from core.types.color import Color
from utils.fs_utils import run_remove_routines
from utils.input_utils import get_selection_input
from utils.instance_utils import get_instances
@@ -25,7 +27,11 @@ def run_klipper_removal(
remove_service: bool,
remove_dir: bool,
remove_env: bool,
) -> None:
) -> Message:
completion_msg = Message(
title="Klipper Removal Process completed",
color=Color.GREEN,
)
klipper_instances: List[Klipper] = get_instances(Klipper)
if remove_service:
@@ -33,20 +39,36 @@ def run_klipper_removal(
if klipper_instances:
instances_to_remove = select_instances_to_remove(klipper_instances)
remove_instances(instances_to_remove)
instance_names = [i.service_file_path.stem for i in instances_to_remove]
txt = f"● Klipper instances removed: {', '.join(instance_names)}"
completion_msg.text.append(txt)
else:
Logger.print_info("No Klipper Services installed! Skipped ...")
if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"):
Logger.print_info("There are still other Klipper services installed:")
Logger.print_info(f"'{KLIPPER_DIR}' was not removed.", prefix=False)
Logger.print_info(f"'{KLIPPER_ENV_DIR}' was not removed.", prefix=False)
completion_msg.text = [
"Some Klipper services are still installed:",
f"'{KLIPPER_DIR}' was not removed, even though selected for removal.",
f"'{KLIPPER_ENV_DIR}' was not removed, even though selected for removal.",
]
else:
if remove_dir:
Logger.print_status("Removing Klipper local repository ...")
run_remove_routines(KLIPPER_DIR)
if run_remove_routines(KLIPPER_DIR):
completion_msg.text.append("● Klipper local repository removed")
if remove_env:
Logger.print_status("Removing Klipper Python environment ...")
run_remove_routines(KLIPPER_ENV_DIR)
if run_remove_routines(KLIPPER_ENV_DIR):
completion_msg.text.append("● Klipper Python environment removed")
if completion_msg.text:
completion_msg.text.insert(0, "The following actions were performed:")
else:
completion_msg.color = Color.YELLOW
completion_msg.centered = True
completion_msg.text = ["Nothing to remove."]
return completion_msg
def select_instances_to_remove(instances: List[Klipper]) -> List[Klipper] | None:

View File

@@ -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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -36,7 +36,7 @@ from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
from core.types import ComponentStatus
from core.types.component_status import ComponentStatus
from utils.common import get_install_status
from utils.input_utils import get_confirm, get_number_input, get_string_input
from utils.instance_utils import get_instances

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -12,21 +12,25 @@ import textwrap
from typing import Type
from components.klipper import klipper_remove
from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal
class KlipperRemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "Remove Klipper"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.footer_type = FooterType.BACK
self.remove_klipper_service = False
self.remove_klipper_dir = False
self.remove_klipper_env = False
self.selection_state = False
self.select_state = False
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.remove_menu import RemoveMenu
@@ -43,23 +47,19 @@ class KlipperRemoveMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Remove Klipper ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
checked = f"[{Color.apply('x', Color.CYAN)}]"
unchecked = "[ ]"
o1 = checked if self.remove_klipper_service else unchecked
o2 = checked if self.remove_klipper_dir else unchecked
o3 = checked if self.remove_klipper_env else unchecked
sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║
╟───────────────────────────────────────────────────────╢
║ a) {self._get_selection_state_str():37}
║ a) {sel_state:49}
╟───────────────────────────────────────────────────────╢
║ 1) {o1} Remove Service ║
║ 2) {o2} Remove Local Repository ║
@@ -72,10 +72,10 @@ class KlipperRemoveMenu(BaseMenu):
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.selection_state = not self.selection_state
self.remove_klipper_service = self.selection_state
self.remove_klipper_dir = self.selection_state
self.remove_klipper_env = self.selection_state
self.select_state = not self.select_state
self.remove_klipper_service = self.select_state
self.remove_klipper_dir = self.select_state
self.remove_klipper_env = self.select_state
def toggle_remove_klipper_service(self, **kwargs) -> None:
self.remove_klipper_service = not self.remove_klipper_service
@@ -92,27 +92,18 @@ class KlipperRemoveMenu(BaseMenu):
and not self.remove_klipper_dir
and not self.remove_klipper_env
):
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
print(error)
msg = "Nothing selected! Select options to remove first."
print(Color.apply(msg, Color.RED))
return
klipper_remove.run_klipper_removal(
completion_msg = klipper_remove.run_klipper_removal(
self.remove_klipper_service,
self.remove_klipper_dir,
self.remove_klipper_env,
)
self.message_service.set_message(completion_msg)
self.remove_klipper_service = False
self.remove_klipper_dir = False
self.remove_klipper_env = False
self._go_back()
def _get_selection_state_str(self) -> str:
return (
"Select everything" if not self.selection_state else "Deselect everything"
)
def _go_back(self, **kwargs) -> None:
if self.previous_menu is not None:
self.previous_menu().run()
self.select_state = False

View File

@@ -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 #
# https://github.com/dw-0/kiauh #

View File

@@ -1,13 +1,22 @@
# ======================================================================= #
# 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 #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run
import re
from pathlib import Path
from subprocess import (
DEVNULL,
PIPE,
STDOUT,
CalledProcessError,
Popen,
check_output,
run,
)
from typing import List
from components.klipper import KLIPPER_DIR
@@ -32,16 +41,18 @@ def find_firmware_file() -> bool:
f3 = "klipper.bin"
f4 = "klipper.uf2"
fw_file_exists: bool = (
target.joinpath(f1).exists() and target.joinpath(f2).exists()
) or target.joinpath(f3).exists() or target.joinpath(f4).exists()
(target.joinpath(f1).exists() and target.joinpath(f2).exists())
or target.joinpath(f3).exists()
or target.joinpath(f4).exists()
)
return target_exists and fw_file_exists
def find_usb_device_by_id() -> List[str]:
try:
command = "find /dev/serial/by-id/* 2>/dev/null"
output = check_output(command, shell=True, text=True)
command = "find /dev/serial/by-id/*"
output = check_output(command, shell=True, text=True, stderr=DEVNULL)
return output.splitlines()
except CalledProcessError as e:
Logger.print_error("Unable to find a USB device!")
@@ -51,9 +62,14 @@ def find_usb_device_by_id() -> List[str]:
def find_uart_device() -> List[str]:
try:
command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"'
output = check_output(command, shell=True, text=True)
return output.splitlines()
cmd = "find /dev -maxdepth 1"
output = check_output(cmd, shell=True, text=True, stderr=DEVNULL)
device_list = []
if output:
pattern = r"^/dev/tty(AMA0|S0)$"
devices = output.splitlines()
device_list = [d for d in devices if re.search(pattern, d)]
return device_list
except CalledProcessError as e:
Logger.print_error("Unable to find a UART device!")
Logger.print_error(e, prefix=False)
@@ -62,15 +78,34 @@ def find_uart_device() -> List[str]:
def find_usb_dfu_device() -> List[str]:
try:
command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"'
output = check_output(command, shell=True, text=True)
return output.splitlines()
output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
device_list = []
if output:
devices = output.splitlines()
device_list = [d.split(" ")[5] for d in devices if "DFU" in d]
return device_list
except CalledProcessError as e:
Logger.print_error("Unable to find a USB DFU device!")
Logger.print_error(e, prefix=False)
return []
def find_usb_rp2_boot_device() -> List[str]:
try:
output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
device_list = []
if output:
devices = output.splitlines()
device_list = [d.split(" ")[5] for d in devices if "RP2 Boot" in d]
return device_list
except CalledProcessError as e:
Logger.print_error("Unable to find a USB RP2 Boot device!")
Logger.print_error(e, prefix=False)
return []
def get_sd_flash_board_list() -> List[str]:
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
return []
@@ -104,6 +139,7 @@ def start_flash_process(flash_options: FlashOptions) -> None:
if flash_options.flash_method is FlashMethod.REGULAR:
cmd = [
"make",
f"KCONFIG_CONFIG={flash_options.selected_kconfig}",
flash_options.flash_command.value,
f"FLASH_DEVICE={flash_options.selected_mcu}",
]
@@ -131,17 +167,17 @@ def start_flash_process(flash_options: FlashOptions) -> None:
if rc != 0:
raise Exception(f"Flashing failed with returncode: {rc}")
else:
Logger.print_ok("Flashing successfull!", start="\n", end="\n\n")
Logger.print_ok("Flashing successful!", start="\n", end="\n\n")
except (Exception, CalledProcessError):
Logger.print_error("Flashing failed!", start="\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:
run(
"make clean",
f"make KCONFIG_CONFIG={kconfig} clean",
cwd=KLIPPER_DIR,
shell=True,
check=True,
@@ -151,10 +187,10 @@ def run_make_clean() -> None:
raise
def run_make_menuconfig() -> None:
def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try:
run(
"make PYTHON=python3 menuconfig",
f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig",
cwd=KLIPPER_DIR,
shell=True,
check=True,
@@ -164,10 +200,10 @@ def run_make_menuconfig() -> None:
raise
def run_make() -> None:
def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try:
run(
"make PYTHON=python3",
f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}",
cwd=KLIPPER_DIR,
shell=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 #
# https://github.com/dw-0/kiauh #
@@ -26,6 +26,7 @@ class FlashCommand(Enum):
class ConnectionType(Enum):
USB = "USB"
USB_DFU = "USB (DFU)"
USB_RP2040 = "USB (RP2040)"
UART = "UART"
@@ -38,6 +39,7 @@ class FlashOptions:
_selected_mcu: str = ""
_selected_board: str = ""
_selected_baudrate: int = 250000
_selected_kconfig: str = ".config"
def __new__(cls, *args, **kwargs):
if not cls._instance:
@@ -103,3 +105,11 @@ class FlashOptions:
@selected_baudrate.setter
def selected_baudrate(self, value: int) -> None:
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 #
# https://github.com/dw-0/kiauh #
@@ -9,18 +9,22 @@
from __future__ import annotations
import textwrap
from pathlib import Path
from shutil import copyfile
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 (
run_make,
run_make_clean,
run_make_menuconfig,
)
from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT
from core.logger import Logger
from components.klipper_firmware.flash_options import FlashOptions
from core.logger import DialogType, Logger
from core.menus import Option
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 (
check_package_install,
install_system_packages,
@@ -30,12 +34,25 @@ from utils.sys_utils import (
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperBuildFirmwareMenu(BaseMenu):
class KlipperKConfigMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "Firmware Config 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.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:
from core.menus.advanced_menu import AdvancedMenu
@@ -45,21 +62,107 @@ class KlipperBuildFirmwareMenu(BaseMenu):
)
def set_options(self) -> None:
if len(self.missing_deps) == 0:
self.input_label_txt = "Press ENTER to continue"
self.default_option = Option(method=self.start_build_process)
else:
self.input_label_txt = "Press ENTER to install dependencies"
self.default_option = Option(method=self.install_missing_deps)
if not Path(self.kconfigs_dirname).is_dir():
return
self.input_label_txt = "Select config or action to continue (default=N)"
self.default_option = Option(
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:
header = " [ Build Firmware Menu ] "
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
cfg_found_str = Color.apply(
"Previously saved firmware configs found!", Color.GREEN
)
menu = textwrap.dedent(
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: ║
║ ║
@@ -67,20 +170,14 @@ class KlipperBuildFirmwareMenu(BaseMenu):
)[1:]
for d in self.deps:
status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}"
status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}"
status_ok = Color.apply("*INSTALLED*", Color.GREEN)
status_missing = Color.apply("*MISSING*", Color.RED)
status = status_missing if d in self.missing_deps else status_ok
padding = 39 - len(d) + len(status) + (len(status_ok) - len(status))
d = f" {COLOR_CYAN}{d}{RESET_FORMAT}"
padding = 40 - len(d) + len(status) + (len(status_ok) - len(status))
d = Color.apply(f" {d}", Color.CYAN)
menu += f"{d}{status:>{padding}}\n"
menu += "║ ║\n"
if len(self.missing_deps) == 0:
line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}"
else:
line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}"
menu += f"{line:<62}\n"
menu += "╟───────────────────────────────────────────────────────╢\n"
print(menu, end="")
@@ -99,13 +196,16 @@ class KlipperBuildFirmwareMenu(BaseMenu):
def start_build_process(self, **kwargs) -> None:
try:
run_make_clean()
run_make_menuconfig()
run_make()
run_make_clean(self.kconfig)
run_make_menuconfig(self.kconfig)
run_make(self.kconfig)
Logger.print_ok("Firmware successfully built!")
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:
Logger.print_error(e)
Logger.print_error("Building Klipper Firmware failed!")
@@ -113,3 +213,62 @@ class KlipperBuildFirmwareMenu(BaseMenu):
finally:
if self.previous_menu is not None:
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 #
# https://github.com/dw-0/kiauh #
@@ -12,9 +12,9 @@ import textwrap
from typing import Type
from components.klipper_firmware.flash_options import FlashMethod, FlashOptions
from core.constants import COLOR_RED, RESET_FORMAT
from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu
from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.types.color import Color
# noinspection PyUnusedLocal
@@ -22,6 +22,9 @@ from core.menus.base_menu import BaseMenu
class KlipperNoFirmwareErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "!!! NO FIRMWARE FILE FOUND !!!"
self.title_color = Color.RED
self.title_style = MenuTitleStyle.PLAIN
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.flash_options = FlashOptions()
@@ -35,16 +38,11 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
self.default_option = Option(method=self.go_back)
def print_menu(self) -> None:
header = "!!! NO FIRMWARE FILE FOUND !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
line1 = "Unable to find a compiled firmware file!"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{line1:<62}
{Color.apply(line1, Color.RED):<62}
║ ║
║ Make sure, that: ║
║ ● the folder '~/klipper/out' and its content exist ║
@@ -71,6 +69,9 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
class KlipperNoBoardTypesErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "!!! ERROR GETTING BOARD LIST !!!"
self.title_color = Color.RED
self.title_style = MenuTitleStyle.PLAIN
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.footer_type = FooterType.BLANK
self.input_label_txt = "Press ENTER to go back to [Main Menu]"
@@ -82,16 +83,11 @@ class KlipperNoBoardTypesErrorMenu(BaseMenu):
self.default_option = Option(method=self.go_back)
def print_menu(self) -> None:
header = "!!! ERROR GETTING BOARD LIST !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
line1 = "Reading the list of supported boards failed!"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{line1:<62}
{Color.apply(line1, Color.RED):<62}
║ ║
║ Make sure, that: ║
║ ● the folder '~/klipper' and all its content exist ║

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -9,16 +9,21 @@
from __future__ import annotations
import textwrap
from typing import Type
from typing import Tuple, Type
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.menus.base_menu import BaseMenu
from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.types.color import Color
def __title_config__() -> Tuple[str, Color, MenuTitleStyle]:
return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN
# noinspection DuplicatedCode
class KlipperFlashMethodHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -34,15 +39,10 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
pass
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
subheader1 = Color.apply("Regular flashing method:", Color.CYAN)
subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{subheader1:<62}
║ The default method to flash controller boards which ║
@@ -77,6 +77,7 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
class KlipperFlashCommandHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -92,15 +93,10 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
pass
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
subheader1 = Color.apply("make flash:", Color.CYAN)
subheader2 = Color.apply("make serialflash:", Color.CYAN)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{subheader1:<62}
║ The default command to flash controller board, it ║
@@ -121,6 +117,7 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
class KlipperMcuConnectionHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -138,15 +135,12 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
pass
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
subheader1 = Color.apply("USB:", Color.CYAN)
subheader2 = Color.apply("UART:", Color.CYAN)
subheader3 = Color.apply("USB DFU:", Color.CYAN)
subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{subheader1:<62}
║ Selecting USB as the connection method will scan the ║
@@ -164,6 +158,19 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
║ port your controller board is connected to when using ║
║ this connection method. ║
║ ║
{subheader3:<62}
║ Selecting USB DFU as the connection method will scan ║
║ the USB ports for connected controller boards in ║
║ STM32 DFU mode, which is usually done by holding down ║
║ the BOOT button or setting a special jumper on the ║
║ board before powering up. ║
║ ║
{subheader4:<62}
║ Selecting USB RP2 Boot as the connection method will ║
║ scan the USB ports for connected RP2040 controller ║
║ boards in Boot mode, which is usually done by holding ║
║ down the BOOT button before powering up. ║
║ ║
╟───────────────────────────────────────────────────────╢
"""
)[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 #
# https://github.com/dw-0/kiauh #
@@ -10,6 +10,7 @@ from __future__ import annotations
import textwrap
import time
from pathlib import Path
from typing import Type
from components.klipper_firmware.firmware_utils import (
@@ -17,6 +18,7 @@ from components.klipper_firmware.firmware_utils import (
find_uart_device,
find_usb_device_by_id,
find_usb_dfu_device,
find_usb_rp2_boot_device,
get_sd_flash_board_list,
start_flash_process,
)
@@ -35,10 +37,10 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import (
KlipperFlashMethodHelpMenu,
KlipperMcuConnectionHelpMenu,
)
from core.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT
from core.logger import DialogType, Logger
from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu
from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.types.color import Color
from utils.input_utils import get_number_input
@@ -47,6 +49,8 @@ from utils.input_utils import get_number_input
class KlipperFlashMethodMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "MCU Flash Menu"
self.title_color = Color.CYAN
self.help_menu = KlipperFlashMethodHelpMenu
self.input_label_txt = "Select flash method"
self.footer_type = FooterType.BACK_HELP
@@ -66,17 +70,13 @@ class KlipperFlashMethodMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ MCU Flash Menu ] "
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}"
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}"
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}"
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
subheader = Color.apply("ATTENTION:", Color.YELLOW)
subline1 = Color.apply(
"Make sure to select the correct method for the MCU!", Color.YELLOW
)
subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Select the flash method for flashing the MCU. ║
║ ║
@@ -111,6 +111,9 @@ class KlipperFlashMethodMenu(BaseMenu):
class KlipperFlashCommandMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "Which flash command to use for flashing the MCU?"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.YELLOW
self.help_menu = KlipperFlashCommandHelpMenu
self.input_label_txt = "Select flash command"
self.footer_type = FooterType.BACK_HELP
@@ -131,8 +134,6 @@ class KlipperFlashCommandMenu(BaseMenu):
def print_menu(self) -> None:
menu = textwrap.dedent(
"""
╔═══════════════════════════════════════════════════════╗
║ Which flash command to use for flashing the MCU? ║
╟───────────────────────────────────────────────────────╢
║ 1) make flash (default) ║
║ 2) make serialflash (stm32flash) ║
@@ -160,6 +161,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False
):
super().__init__()
self.title = "Make sure that the controller board is connected now!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.__standalone = standalone
self.help_menu = KlipperMcuConnectionHelpMenu
@@ -177,22 +181,19 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
"1": Option(method=self.select_usb),
"2": Option(method=self.select_dfu),
"3": Option(method=self.select_usb_dfu),
"4": Option(method=self.select_usb_rp2040),
}
def print_menu(self) -> None:
header = "Make sure that the controller board is connected now!"
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
"""
╟───────────────────────────────────────────────────────╢
║ How is the controller board connected to the host? ║
╟───────────────────────────────────────────────────────╢
║ 1) USB ║
║ 2) UART ║
║ 3) USB (DFU mode) ║
║ 4) USB (RP2040 mode) ║
╟───────────────────────────┬───────────────────────────╢
"""
)[1:]
@@ -210,6 +211,10 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self.flash_options.connection_type = ConnectionType.USB_DFU
self.get_mcu_list()
def select_usb_rp2040(self, **kwargs):
self.flash_options.connection_type = ConnectionType.USB_RP2040
self.get_mcu_list()
def get_mcu_list(self, **kwargs):
conn_type = self.flash_options.connection_type
@@ -222,6 +227,11 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
elif conn_type is ConnectionType.USB_DFU:
Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
self.flash_options.mcu_list = find_usb_dfu_device()
elif conn_type is ConnectionType.USB_RP2040:
Logger.print_status(
"Identifying MCU connected via USB in RP2 Boot mode ..."
)
self.flash_options.mcu_list = find_usb_rp2_boot_device()
if len(self.flash_options.mcu_list) < 1:
Logger.print_warn("No MCUs found!")
@@ -231,7 +241,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
if self.__standalone and len(self.flash_options.mcu_list) > 0:
Logger.print_ok("The following MCUs were found:", prefix=False)
for i, mcu in enumerate(self.flash_options.mcu_list):
print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}")
print(f" ● MCU #{i}: {Color.CYAN}{mcu}{Color.RST}")
time.sleep(3)
return
@@ -246,6 +256,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
class KlipperSelectMcuIdMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions()
self.mcu_list = self.flash_options.mcu_list
self.input_label_txt = "Select MCU to flash"
@@ -264,14 +277,9 @@ class KlipperSelectMcuIdMenu(BaseMenu):
}
def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Make sure, to select the correct MCU! ║
║ ONLY flash a firmware created for the respective MCU! ║
@@ -283,7 +291,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
for i, mcu in enumerate(self.mcu_list):
mcu = mcu.split("/")[-1]
menu += f"{i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}\n"
menu += f"{i}) {Color.apply(f'{mcu:<51}', Color.CYAN)}\n"
menu += textwrap.dedent(
"""
@@ -338,7 +346,6 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
else:
menu = textwrap.dedent(
"""
╔═══════════════════════════════════════════════════════╗
║ Please select the type of board that corresponds to ║
║ the currently selected MCU ID you chose before. ║
║ ║
@@ -390,6 +397,9 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
class KlipperFlashOverviewMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions()
self.input_label_txt = "Perform action (default=Y)"
@@ -405,21 +415,17 @@ class KlipperFlashOverviewMenu(BaseMenu):
self.default_option = Option(self.execute_flash)
def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
method = self.flash_options.flash_method.value
command = self.flash_options.flash_command.value
conn_type = self.flash_options.connection_type.value
mcu = self.flash_options.selected_mcu.split("/")[-1]
board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
kconfig = Path(self.flash_options.selected_kconfig).name
color = Color.CYAN
subheader = f"[{Color.apply('Overview', color)}]"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Before contuining the flashing process, please check ║
║ if all parameters were set correctly! Once you made ║
@@ -433,18 +439,25 @@ class KlipperFlashOverviewMenu(BaseMenu):
menu += textwrap.dedent(
f"""
║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT}
║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT}
║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT}
║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT}
║ MCU: {Color.apply(f"{mcu:<48}", color)}
║ Connection: {Color.apply(f"{conn_type:<41}", color)}
║ Flash method: {Color.apply(f"{method:<39}", color)}
║ Flash command: {Color.apply(f"{command:<38}", color)}
"""
)[1:]
if self.flash_options.flash_method is FlashMethod.SD_CARD:
menu += textwrap.dedent(
f"""
║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT}
║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT}
║ Board type: {Color.apply(f"{board:<41}", color)}
║ Baudrate: {Color.apply(f"{baudrate:<43}", color)}
"""
)[1:]
if self.flash_options.flash_method is FlashMethod.REGULAR:
menu += textwrap.dedent(
f"""
║ Firmware config: {Color.apply(f"{kconfig:<36}", color)}
"""
)[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 #
# 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 #
# 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.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus
from core.types.component_status import ComponentStatus
from utils.common import (
check_install_dependencies,
get_install_status,

View File

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

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent
MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git"
# names
MOONRAKER_CFG_NAME = "moonraker.conf"
MOONRAKER_LOG_NAME = "moonraker.log"

View File

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

View File

@@ -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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -12,8 +12,8 @@ from typing import List
from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT
from core.menus.base_menu import print_back_footer
from core.types.color import Color
def print_moonraker_overview(
@@ -22,7 +22,7 @@ def print_moonraker_overview(
show_index=False,
show_select_all=False,
):
headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}"
headline = Color.apply("The following instances were found:", Color.GREEN)
dialog = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
@@ -32,7 +32,7 @@ def print_moonraker_overview(
)[1:]
if show_select_all:
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
select_all = Color.apply("a) Select all", Color.YELLOW)
dialog += f"{select_all:<63}\n"
dialog += "║ ║\n"
@@ -48,12 +48,16 @@ def print_moonraker_overview(
for i, k in enumerate(instance_map):
mr_name = instance_map.get(k)
m = f"<-> {mr_name}" if mr_name != "" else ""
line = f"{COLOR_CYAN}{f'{i+1})' if show_index else ''} {k} {m} {RESET_FORMAT}"
line = Color.apply(f"{f'{i+1})' if show_index else ''} {k} {m}", Color.CYAN)
dialog += f"{line:<63}\n"
warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}"
warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}"
warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}"
warn_l1 = Color.apply("PLEASE NOTE:", Color.YELLOW)
warn_l2 = Color.apply(
"If you select an instance with an existing Moonraker", Color.YELLOW
)
warn_l3 = Color.apply(
"instance, that Moonraker instance will be re-created!", Color.YELLOW
)
warning = textwrap.dedent(
f"""
║ ║

View File

@@ -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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -8,7 +8,6 @@
# ======================================================================= #
from __future__ import annotations
import json
import subprocess
from typing import List
@@ -28,9 +27,11 @@ from components.moonraker import (
)
from components.moonraker.moonraker import Moonraker
from components.moonraker.moonraker_dialogs import print_moonraker_overview
from components.moonraker.moonraker_utils import (
from components.moonraker.utils.sysdeps_parser import SysDepsParser
from components.moonraker.utils.utils import (
backup_moonraker_dir,
create_example_moonraker_conf,
load_sysdeps_json,
)
from components.webui_client.client_utils import (
enable_mainsail_remotemode,
@@ -154,16 +155,24 @@ def setup_moonraker_prerequesites() -> None:
def install_moonraker_packages() -> None:
moonraker_deps = []
Logger.print_status("Parsing Moonraker system dependencies ...")
moonraker_deps = []
if MOONRAKER_DEPS_JSON_FILE.exists():
with open(MOONRAKER_DEPS_JSON_FILE, "r") as deps:
moonraker_deps = json.load(deps).get("debian", [])
Logger.print_info(
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():
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)
if not moonraker_deps:
raise ValueError("Error reading Moonraker dependencies!")
raise ValueError("Error parsing Moonraker dependencies!")
check_install_dependencies({*moonraker_deps})

View File

@@ -0,0 +1,167 @@
# ======================================================================= #
# 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 #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import json
import shutil
from pathlib import Path
from typing import Dict, List, Optional
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 (
SimpleConfigParser,
)
from core.types import ComponentStatus
from core.types.component_status import ComponentStatus
from utils.common import get_install_status
from utils.instance_utils import get_instances
from utils.sys_utils import (
@@ -138,3 +139,12 @@ def backup_moonraker_db_dir() -> None:
bm.backup_directory(
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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -37,6 +37,7 @@ class BaseWebClient(ABC):
backup_dir: Path
repo_path: str
download_url: str
nginx_config: Path
nginx_access_log: Path
nginx_error_log: Path
client_config: BaseWebClientConfig

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -14,8 +14,11 @@ from components.klipper.klipper import Klipper
from components.moonraker.moonraker import Moonraker
from components.webui_client.base_data import BaseWebClientConfig
from core.logger import Logger
from core.services.message_service import Message
from core.types.color import Color
from utils.config_utils import remove_config_section
from utils.fs_utils import run_remove_routines
from utils.instance_type import InstanceType
from utils.instance_utils import get_instances
@@ -23,21 +26,66 @@ def run_client_config_removal(
client_config: BaseWebClientConfig,
kl_instances: List[Klipper],
mr_instances: List[Moonraker],
) -> None:
remove_client_config_dir(client_config)
remove_client_config_symlink(client_config)
remove_config_section(f"update_manager {client_config.name}", mr_instances)
remove_config_section(client_config.config_section, kl_instances)
def remove_client_config_dir(client_config: BaseWebClientConfig) -> None:
) -> Message:
completion_msg = Message(
title=f"{client_config.display_name} Removal Process completed",
color=Color.GREEN,
)
Logger.print_status(f"Removing {client_config.display_name} ...")
run_remove_routines(client_config.config_dir)
if run_remove_routines(client_config.config_dir):
completion_msg.text.append(f"{client_config.display_name} removed")
completion_msg = remove_moonraker_config_section(
completion_msg, client_config, mr_instances
)
completion_msg = remove_printer_config_section(
completion_msg, client_config, kl_instances
)
if completion_msg.text:
completion_msg.text.insert(0, "The following actions were performed:")
else:
completion_msg.color = Color.YELLOW
completion_msg.centered = True
completion_msg.text = ["Nothing to remove."]
return completion_msg
def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None:
def remove_cfg_symlink(client_config: BaseWebClientConfig, message: Message) -> Message:
instances: List[Klipper] = get_instances(Klipper)
kl_instances = []
for instance in instances:
run_remove_routines(
instance.base.cfg_dir.joinpath(client_config.config_filename)
)
cfg = instance.base.cfg_dir.joinpath(client_config.config_filename)
if run_remove_routines(cfg):
kl_instances.append(instance)
text = f"{client_config.display_name} removed from instance"
return update_msg(kl_instances, message, text)
def remove_printer_config_section(
message: Message, client_config: BaseWebClientConfig, kl_instances: List[Klipper]
) -> Message:
kl_section = client_config.config_section
kl_instances = remove_config_section(kl_section, kl_instances)
text = f"Klipper config section '{kl_section}' removed for instance"
return update_msg(kl_instances, message, text)
def remove_moonraker_config_section(
message: Message, client_config: BaseWebClientConfig, mr_instances: List[Moonraker]
) -> Message:
mr_section = f"update_manager {client_config.name}"
mr_instances = remove_config_section(mr_section, mr_instances)
text = f"Moonraker config section '{mr_section}' removed for instance"
return update_msg(mr_instances, message, text)
def update_msg(instances: List[InstanceType], message: Message, text: str) -> Message:
if not instances:
return message
instance_names = [i.service_file_path.stem for i in instances]
message.text.append(f"{text}: {', '.join(instance_names)}")
return message

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -34,7 +34,7 @@ from utils.input_utils import get_confirm
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
display_name = client_config.display_name
@@ -56,7 +56,8 @@ def install_client_config(client_data: BaseWebClient) -> None:
download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances)
backup_printer_config_dir()
if cfg_backup:
backup_printer_config_dir()
add_config_section(
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 #
# https://github.com/dw-0/kiauh #
@@ -13,15 +13,15 @@ from components.webui_client.base_data import BaseWebClient
from core.logger import DialogType, Logger
def print_moonraker_not_found_dialog() -> None:
def print_moonraker_not_found_dialog(name: str) -> None:
Logger.print_dialog(
DialogType.WARNING,
[
"No local Moonraker installation was found!",
"\n\n",
"It is possible to install Mainsail without a local Moonraker installation. "
f"It is possible to install {name} without a local Moonraker installation. "
"If you continue, you need to make sure, that Moonraker is installed on "
"another machine in your network. Otherwise Mainsail will NOT work "
f"another machine in your network. Otherwise {name} will NOT work "
"correctly.",
],
)
@@ -40,20 +40,25 @@ def print_client_already_installed_dialog(name: str) -> None:
def print_client_port_select_dialog(
name: str, port: int, ports_in_use: List[int]
) -> None:
Logger.print_dialog(
DialogType.CUSTOM,
[
f"Please select the port, {name} should be served on. If your are unsure "
f"what to select, hit Enter to apply the suggested value of: {port}",
"\n\n",
f"In case you need {name} to be served on a specific port, you can set it "
f"now. Make sure that the port is not already used by another application "
f"on your system!",
"\n\n",
"The following ports were found to be in use already:",
*[f"{port}" for port in ports_in_use],
],
)
dialog_content: List[str] = [
f"Please select the port, {name} should be served on. If your are unsure "
f"what to select, hit Enter to apply the suggested value of: {port}",
"\n\n",
f"In case you need {name} to be served on a specific port, you can set it "
f"now. Make sure that the port is not already used by another application "
f"on your system!",
]
if ports_in_use:
dialog_content.extend(
[
"\n\n",
"The following ports were found to be already in use:",
*[f"{p}" for p in ports_in_use if p != port],
]
)
Logger.print_dialog(DialogType.CUSTOM, dialog_content)
def print_install_client_config_dialog(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 #
# 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.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from core.logger import Logger
from core.services.message_service import Message
from core.types.color import Color
from utils.config_utils import remove_config_section
from utils.fs_utils import (
remove_with_sudo,
@@ -32,54 +34,79 @@ def run_client_removal(
remove_client: bool,
remove_client_cfg: bool,
backup_config: bool,
) -> None:
) -> Message:
completion_msg = Message(
title=f"{client.display_name} Removal Process completed",
color=Color.GREEN,
)
mr_instances: List[Moonraker] = get_instances(Moonraker)
kl_instances: List[Klipper] = get_instances(Klipper)
if backup_config:
bm = BackupManager()
bm.backup_file(client.config_file)
if bm.backup_file(client.config_file):
completion_msg.text.append(f"{client.config_file.name} backup created")
if remove_client:
client_name = client.name
remove_client_dir(client)
remove_client_nginx_config(client_name)
remove_client_nginx_logs(client, kl_instances)
if remove_client_dir(client):
completion_msg.text.append(f"{client.display_name} removed")
if remove_client_nginx_config(client_name):
completion_msg.text.append("● NGINX config removed")
if remove_client_nginx_logs(client, kl_instances):
completion_msg.text.append("● NGINX logs removed")
section = f"update_manager {client_name}"
remove_config_section(section, mr_instances)
handled_instances: List[Moonraker] = remove_config_section(
section, mr_instances
)
if handled_instances:
names = [i.service_file_path.stem for i in handled_instances]
completion_msg.text.append(
f"● Moonraker config section '{section}' removed for instance: {', '.join(names)}"
)
if remove_client_cfg:
run_client_config_removal(
cfg_completion_msg = run_client_config_removal(
client.client_config,
kl_instances,
mr_instances,
)
if cfg_completion_msg.color == Color.GREEN:
completion_msg.text.extend(cfg_completion_msg.text[1:])
if not completion_msg.text:
completion_msg.color = Color.YELLOW
completion_msg.centered = True
completion_msg.text.append("Nothing to remove.")
else:
completion_msg.text.insert(0, "The following actions were performed:")
return completion_msg
def remove_client_dir(client: BaseWebClient) -> None:
def remove_client_dir(client: BaseWebClient) -> bool:
Logger.print_status(f"Removing {client.display_name} ...")
run_remove_routines(client.client_dir)
return run_remove_routines(client.client_dir)
def remove_client_nginx_config(name: str) -> None:
def remove_client_nginx_config(name: str) -> bool:
Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
remove_with_sudo(NGINX_SITES_AVAILABLE.joinpath(name))
remove_with_sudo(NGINX_SITES_ENABLED.joinpath(name))
return remove_with_sudo(
[
NGINX_SITES_AVAILABLE.joinpath(name),
NGINX_SITES_ENABLED.joinpath(name),
]
)
def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> None:
def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> bool:
Logger.print_status(f"Removing NGINX logs for {client.display_name} ...")
remove_with_sudo(client.nginx_access_log)
remove_with_sudo(client.nginx_error_log)
files = [client.nginx_access_log, client.nginx_error_log]
if instances:
for instance in instances:
files.append(instance.base.log_dir.joinpath(client.nginx_access_log.name))
files.append(instance.base.log_dir.joinpath(client.nginx_error_log.name))
if not instances:
return
for instance in instances:
run_remove_routines(
instance.base.log_dir.joinpath(client.nginx_access_log.name)
)
run_remove_routines(instance.base.log_dir.joinpath(client.nginx_error_log.name))
return remove_with_sudo(files)

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -23,7 +23,6 @@ from components.webui_client.client_config.client_config_setup import (
install_client_config,
)
from components.webui_client.client_dialogs import (
print_client_port_select_dialog,
print_install_client_config_dialog,
print_moonraker_not_found_dialog,
)
@@ -33,18 +32,17 @@ from components.webui_client.client_utils import (
create_nginx_cfg,
detect_client_cfg_conflict,
enable_mainsail_remotemode,
get_next_free_port,
is_valid_port,
read_ports_from_nginx_configs,
get_client_port_selection,
symlink_webui_nginx_log,
)
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 utils.common import check_install_dependencies
from core.types.color import Color
from utils.common import backup_printer_config_dir, check_install_dependencies
from utils.config_utils import add_config_section
from utils.fs_utils import unzip
from utils.input_utils import get_confirm, get_number_input
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
from utils.sys_utils import (
cmd_sysctl_service,
@@ -53,21 +51,16 @@ from utils.sys_utils import (
)
def install_client(client: BaseWebClient) -> None:
if client is None:
raise ValueError("Missing parameter client_data!")
if client.client_dir.exists():
Logger.print_info(
f"{client.display_name} seems to be already installed! Skipped ..."
)
return
def install_client(
client: BaseWebClient,
settings: KiauhSettings,
reinstall: bool = False,
) -> None:
mr_instances: List[Moonraker] = get_instances(Moonraker)
enable_remotemode = False
if not mr_instances:
print_moonraker_not_found_dialog()
print_moonraker_not_found_dialog(client.display_name)
if not get_confirm(f"Continue {client.display_name} installation?"):
return
@@ -92,21 +85,10 @@ def install_client(client: BaseWebClient) -> None:
question = f"Download the recommended {client_config.display_name}?"
install_client_cfg = get_confirm(question, allow_go_back=False)
settings = KiauhSettings()
port: int = settings.get(client.name, "port")
ports_in_use: List[int] = read_ports_from_nginx_configs()
# check if configured port is a valid number and not in use already
valid_port = is_valid_port(port, ports_in_use)
while not valid_port:
next_port = get_next_free_port(ports_in_use)
print_client_port_select_dialog(client.display_name, next_port, ports_in_use)
port = get_number_input(
f"Configure {client.display_name} for port",
min_count=int(next_port),
default=next_port,
)
valid_port = is_valid_port(port, ports_in_use)
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"})
@@ -114,20 +96,22 @@ def install_client(client: BaseWebClient) -> None:
download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode()
if mr_instances:
add_config_section(
section=f"update_manager {client.name}",
instances=mr_instances,
options=[
("type", "web"),
("channel", "stable"),
("repo", str(client.repo_path)),
("path", str(client.client_dir)),
],
)
InstanceManager.restart_all(mr_instances)
backup_printer_config_dir()
add_config_section(
section=f"update_manager {client.name}",
instances=mr_instances,
options=[
("type", "web"),
("channel", "stable"),
("repo", str(client.repo_path)),
("path", str(client.client_dir)),
],
)
InstanceManager.restart_all(mr_instances)
if install_client_cfg and kl_instances:
install_client_config(client)
install_client_config(client, False)
copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg()
@@ -145,12 +129,24 @@ def install_client(client: BaseWebClient) -> None:
cmd_sysctl_service("nginx", "restart")
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
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}"
Logger.print_ok(f"{client.display_name} installation complete!", start="\n")
Logger.print_ok(log, prefix=False, end="\n\n")
# noinspection HttpUrlsUsage
Logger.print_dialog(
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:

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 #
# https://github.com/dw-0/kiauh #
@@ -21,29 +21,29 @@ from components.webui_client.base_data import (
BaseWebClient,
WebClientType,
)
from components.webui_client.client_dialogs import print_client_port_select_dialog
from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData
from core.backup_manager.backup_manager import BackupManager
from core.constants import (
COLOR_CYAN,
COLOR_YELLOW,
NGINX_CONFD,
NGINX_SITES_AVAILABLE,
NGINX_SITES_ENABLED,
RESET_FORMAT,
)
from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings
from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
from core.types import ComponentStatus
from core.types.color import Color
from core.types.component_status import ComponentStatus
from utils.common import get_install_status
from utils.fs_utils import create_symlink, remove_file
from utils.git_utils import (
get_latest_remote_tag,
get_latest_unstable_tag,
)
from utils.input_utils import get_number_input
from utils.instance_utils import get_instances
@@ -77,10 +77,10 @@ def get_current_client_config() -> str:
installed = [c for c in clients if c.client_config.config_dir.exists()]
if not installed:
return f"{COLOR_CYAN}-{RESET_FORMAT}"
return Color.apply("-", Color.CYAN)
elif len(installed) == 1:
cfg = installed[0].client_config
return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}"
return Color.apply(cfg.display_name, Color.CYAN)
# at this point, both client config folders exists, so we need to check
# which are actually included in the printer.cfg of all klipper instances
@@ -99,18 +99,18 @@ def get_current_client_config() -> str:
# if both are included in the same file, we have a potential conflict
if includes_mainsail and includes_fluidd:
return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}"
return Color.apply("Conflict", Color.YELLOW)
if not mainsail_includes and not fluidd_includes:
# there are no includes at all, even though the client config folders exist
return f"{COLOR_CYAN}-{RESET_FORMAT}"
return Color.apply("-", Color.CYAN)
elif len(fluidd_includes) > len(mainsail_includes):
# there are more instances that include fluidd than mainsail
return f"{COLOR_CYAN}{fluidd.client_config.display_name}{RESET_FORMAT}"
return Color.apply(fluidd.client_config.display_name, Color.CYAN)
else:
# there are the same amount of non-conflicting includes for each config
# or more instances include mainsail than fluidd
return f"{COLOR_CYAN}{mainsail.client_config.display_name}{RESET_FORMAT}"
return Color.apply(mainsail.client_config.display_name, Color.CYAN)
def enable_mainsail_remotemode() -> None:
@@ -336,34 +336,94 @@ def create_nginx_cfg(
raise
def get_nginx_config_list() -> List[Path]:
"""
Get a list of all NGINX config files in /etc/nginx/sites-enabled
:return: List of NGINX config files
"""
configs: List[Path] = []
for config in NGINX_SITES_ENABLED.iterdir():
if not config.is_file():
continue
configs.append(config)
return configs
def get_nginx_listen_port(config: Path) -> int | None:
"""
Get the listen port from an NGINX config file
:param config: The NGINX config file to read the port from
:return: The listen port as int or None if not found/parsable
"""
# noinspection HttpUrlsUsage
pattern = r"default_server|http://|https://|[;\[\]]"
port = ""
with open(config, "r") as cfg:
for line in cfg.readlines():
line = re.sub(pattern, "", line.strip())
if line.startswith("listen"):
if ":" not in line:
port = line.split()[-1]
else:
port = line.split(":")[-1]
try:
return int(port)
except ValueError:
Logger.print_error(
f"Unable to parse listen port {port} from {config.name}!"
)
return None
def read_ports_from_nginx_configs() -> List[int]:
"""
Helper function to iterate over all NGINX configs and read all ports defined for listen
Helper function to iterate over all NGINX configs
and read all ports defined for listen
:return: A sorted list of listen ports
"""
if not NGINX_SITES_ENABLED.exists():
return []
port_list = []
for config in NGINX_SITES_ENABLED.iterdir():
if not config.is_file():
continue
port_list: List[int] = []
for config in get_nginx_config_list():
port = get_nginx_listen_port(config)
if port is not None:
port_list.append(port)
with open(config, "r") as cfg:
lines = cfg.readlines()
for line in lines:
line = line.replace("default_server", "")
line = re.sub(r"[;:\[\]]", "", line.strip())
if line.startswith("listen") and line.split()[-1] not in port_list:
port_list.append(line.split()[-1])
ports_to_ints_list = [int(port) for port in port_list]
return sorted(ports_to_ints_list, key=lambda x: int(x))
return sorted(port_list, key=lambda x: int(x))
def is_valid_port(port: int, ports_in_use: List[int]) -> bool:
return port not in ports_in_use
def get_client_port_selection(
client: BaseWebClient,
settings: KiauhSettings,
reconfigure=False,
) -> int:
default_port: int = int(settings.get(client.name, "port"))
ports_in_use: List[int] = read_ports_from_nginx_configs()
next_free_port: int = get_next_free_port(ports_in_use)
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)
while True:
_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)
if port_input not in ports_in_use:
client_settings: WebUiSettings = settings[client.name]
client_settings.port = port_input
settings.save()
return port_input
Logger.print_error("This port is already in use. Please select another one.")
def get_next_free_port(ports_in_use: List[int]) -> int:
@@ -371,3 +431,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int:
used_ports = set(map(int, ports_in_use))
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 #
# https://github.com/dw-0/kiauh #
@@ -19,6 +19,7 @@ from components.webui_client.base_data import (
WebClientType,
)
from core.backup_manager import BACKUP_ROOT_DIR
from core.constants import NGINX_SITES_AVAILABLE
@dataclass()
@@ -44,6 +45,7 @@ class FluiddData(BaseWebClient):
config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
repo_path: str = "fluidd-core/fluidd"
nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd")
nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log")
nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log")
client_config: BaseWebClientConfig = None

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -19,6 +19,7 @@ from components.webui_client.base_data import (
WebClientType,
)
from core.backup_manager import BACKUP_ROOT_DIR
from core.constants import NGINX_SITES_AVAILABLE
@dataclass()
@@ -44,6 +45,7 @@ class MainsailData(BaseWebClient):
config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups")
repo_path: str = "mainsail-crew/mainsail"
nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail")
nginx_access_log: Path = Path("/var/log/nginx/mainsail-access.log")
nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log")
client_config: BaseWebClientConfig = None

View File

@@ -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 #
# 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.base_data import BaseWebClient
from core.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal
@@ -24,12 +24,14 @@ class ClientRemoveMenu(BaseMenu):
self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None
):
super().__init__()
self.title = f"Remove {client.display_name}"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.client: BaseWebClient = client
self.remove_client: bool = False
self.remove_client_cfg: bool = False
self.backup_config_json: bool = False
self.selection_state: bool = False
self.select_state: bool = False
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.remove_menu import RemoveMenu
@@ -50,23 +52,19 @@ class ClientRemoveMenu(BaseMenu):
client_config = self.client.client_config
client_config_name = client_config.display_name
header = f" [ Remove {client_name} ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
checked = f"[{Color.apply('x', Color.CYAN)}]"
unchecked = "[ ]"
o1 = checked if self.remove_client else unchecked
o2 = checked if self.remove_client_cfg else unchecked
o3 = checked if self.backup_config_json else unchecked
sel_state = f"{'Select'if not self.select_state else 'Deselect'} everything"
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Enter a number and hit enter to select / deselect ║
║ the specific option for removal. ║
╟───────────────────────────────────────────────────────╢
║ a) {self._get_selection_state_str():37}
║ a) {sel_state:49}
╟───────────────────────────────────────────────────────╢
║ 1) {o1} Remove {client_name:16}
║ 2) {o2} Remove {client_config_name:24}
@@ -79,10 +77,10 @@ class ClientRemoveMenu(BaseMenu):
print(menu, end="")
def toggle_all(self, **kwargs) -> None:
self.selection_state = not self.selection_state
self.remove_client = self.selection_state
self.remove_client_cfg = self.selection_state
self.backup_config_json = self.selection_state
self.select_state = not self.select_state
self.remove_client = self.select_state
self.remove_client_cfg = self.select_state
self.backup_config_json = self.select_state
def toggle_rm_client(self, **kwargs) -> None:
self.remove_client = not self.remove_client
@@ -99,28 +97,18 @@ class ClientRemoveMenu(BaseMenu):
and not self.remove_client_cfg
and not self.backup_config_json
):
error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}"
print(error)
print(Color.apply("Nothing selected ...", Color.RED))
return
client_remove.run_client_removal(
completion_msg = client_remove.run_client_removal(
client=self.client,
remove_client=self.remove_client,
remove_client_cfg=self.remove_client_cfg,
backup_config=self.backup_config_json,
)
self.message_service.set_message(completion_msg)
self.remove_client = False
self.remove_client_cfg = False
self.backup_config_json = False
self._go_back()
def _get_selection_state_str(self) -> str:
return (
"Select everything" if not self.selection_state else "Deselect everything"
)
def _go_back(self, **kwargs) -> None:
if self.previous_menu is not None:
self.previous_menu().run()
self.select_state = False

View File

@@ -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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -44,12 +44,14 @@ class BackupManager:
def ignore_folders(self, value: List[str]):
self._ignore_folders = value
def backup_file(self, file: Path, target: Path | None = None, custom_filename=None):
def backup_file(
self, file: Path, target: Path | None = None, custom_filename=None
) -> bool:
Logger.print_status(f"Creating backup of {file} ...")
if not file.exists():
Logger.print_info("File does not exist! Skipping ...")
return
return False
target = self.backup_root_dir if target is None else target
@@ -62,10 +64,13 @@ class BackupManager:
Path(target).mkdir(exist_ok=True)
shutil.copyfile(file, target.joinpath(filename))
Logger.print_ok("Backup successful!")
return True
except OSError as e:
Logger.print_error(f"Unable to backup '{file}':\n{e}")
return False
else:
Logger.print_info(f"File '{file}' not found ...")
return False
def backup_directory(
self, name: str, source: Path, target: Path | None = None
@@ -74,14 +79,14 @@ class BackupManager:
if source is None or not Path(source).exists():
Logger.print_info("Source directory does not exist! Skipping ...")
return
return None
target = self.backup_root_dir if target is None else target
try:
date = get_current_date().get("date")
time = get_current_date().get("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!")
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 #
# https://github.com/dw-0/kiauh #
@@ -13,15 +13,6 @@ from pathlib import Path
from core.backup_manager import BACKUP_ROOT_DIR
# text colors and formats
COLOR_WHITE = "\033[37m" # white
COLOR_MAGENTA = "\033[35m" # magenta
COLOR_GREEN = "\033[92m" # bright green
COLOR_YELLOW = "\033[93m" # bright yellow
COLOR_RED = "\033[91m" # bright red
COLOR_CYAN = "\033[96m" # bright cyan
RESET_FORMAT = "\033[0m" # reset format
# global dependencies
GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"]
@@ -33,7 +24,7 @@ CURRENT_USER = pwd.getpwuid(os.getuid())[0]
# dirs
SYSTEMD = Path("/etc/systemd/system")
PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups")
PRINTER_DATA_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-data-backups")
NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available")
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
NGINX_CONFD = Path("/etc/nginx/conf.d")

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 #
# 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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -12,78 +12,50 @@ import textwrap
from enum import Enum
from typing import List
from core.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_MAGENTA,
COLOR_RED,
COLOR_WHITE,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.types.color import Color
class DialogType(Enum):
INFO = ("INFO", COLOR_WHITE)
SUCCESS = ("SUCCESS", COLOR_GREEN)
ATTENTION = ("ATTENTION", COLOR_YELLOW)
WARNING = ("WARNING", COLOR_YELLOW)
ERROR = ("ERROR", COLOR_RED)
INFO = ("INFO", Color.WHITE)
SUCCESS = ("SUCCESS", Color.GREEN)
ATTENTION = ("ATTENTION", Color.YELLOW)
WARNING = ("WARNING", Color.YELLOW)
ERROR = ("ERROR", Color.RED)
CUSTOM = (None, None)
class DialogCustomColor(Enum):
WHITE = COLOR_WHITE
GREEN = COLOR_GREEN
YELLOW = COLOR_YELLOW
RED = COLOR_RED
CYAN = COLOR_CYAN
MAGENTA = COLOR_MAGENTA
LINE_WIDTH = 53
class Logger:
@staticmethod
def info(msg) -> None:
# log to kiauh.log
pass
@staticmethod
def warn(msg) -> None:
# log to kiauh.log
pass
@staticmethod
def error(msg) -> None:
# log to kiauh.log
pass
@staticmethod
def print_info(msg, prefix=True, start="", end="\n") -> None:
message = f"[INFO] {msg}" if prefix else msg
print(f"{COLOR_WHITE}{start}{message}{RESET_FORMAT}", end=end)
Logger.__print(Color.WHITE, start, message, end)
@staticmethod
def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None:
message = f"[OK] {msg}" if prefix else msg
print(f"{COLOR_GREEN}{start}{message}{RESET_FORMAT}", end=end)
Logger.__print(Color.GREEN, start, message, end)
@staticmethod
def print_warn(msg, prefix=True, start="", end="\n") -> None:
message = f"[WARN] {msg}" if prefix else msg
print(f"{COLOR_YELLOW}{start}{message}{RESET_FORMAT}", end=end)
Logger.__print(Color.YELLOW, start, message, end)
@staticmethod
def print_error(msg, prefix=True, start="", end="\n") -> None:
message = f"[ERROR] {msg}" if prefix else msg
print(f"{COLOR_RED}{start}{message}{RESET_FORMAT}", end=end)
Logger.__print(Color.RED, start, message, end)
@staticmethod
def print_status(msg, prefix=True, start="", end="\n") -> None:
message = f"\n###### {msg}" if prefix else msg
print(f"{COLOR_MAGENTA}{start}{message}{RESET_FORMAT}", end=end)
Logger.__print(Color.MAGENTA, start, message, end)
@staticmethod
def __print(color: Color, start: str, message: str, end: str) -> None:
print(Color.apply(f"{start}{message}", color), end=end)
@staticmethod
def print_dialog(
@@ -91,7 +63,7 @@ class Logger:
content: List[str],
center_content: bool = False,
custom_title: str | None = None,
custom_color: DialogCustomColor | None = None,
custom_color: Color | None = None,
margin_top: int = 0,
margin_bottom: int = 0,
) -> None:
@@ -111,10 +83,15 @@ class Logger:
"""
dialog_color = Logger._get_dialog_color(title, custom_color)
dialog_title = Logger._get_dialog_title(title, custom_title)
dialog_title_formatted = Logger._format_dialog_title(dialog_title)
dialog_content = Logger.format_content(content, LINE_WIDTH, center_content)
dialog_title_formatted = Logger._format_dialog_title(dialog_title, dialog_color)
dialog_content = Logger.format_content(
content,
LINE_WIDTH,
dialog_color,
center_content,
)
top = Logger._format_top_border(dialog_color)
bottom = Logger._format_bottom_border()
bottom = Logger._format_bottom_border(dialog_color)
print("\n" * margin_top)
print(
@@ -133,39 +110,45 @@ class Logger:
@staticmethod
def _get_dialog_color(
title: DialogType, custom_color: DialogCustomColor | None = None
) -> str:
title: DialogType, custom_color: Color | None = None
) -> Color:
if title == DialogType.CUSTOM and custom_color:
return str(custom_color.value)
return custom_color
color: str = title.value[1] if title.value[1] else DialogCustomColor.WHITE.value
color: Color = title.value[1] if title.value[1] else Color.WHITE
return color
@staticmethod
def _format_top_border(color: str) -> str:
return f"{color}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
@staticmethod
def _format_bottom_border() -> str:
return (
f"\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛{RESET_FORMAT}"
def _format_top_border(color: Color) -> str:
_border = Color.apply(
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", color
)
return _border
@staticmethod
def _format_dialog_title(title: str | None) -> str:
if title is not None:
return textwrap.dedent(f"""
{title:^{LINE_WIDTH}}
┠───────────────────────────────────────────────────────┨
""")
else:
return "\n"
def _format_bottom_border(color: Color) -> str:
_border = Color.apply(
"\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛", color
)
return _border
@staticmethod
def _format_dialog_title(title: str | None, color: Color) -> str:
if title is None:
return ""
_title = Color.apply(f"{title:^{LINE_WIDTH}}\n", color)
_title += Color.apply(
"┠───────────────────────────────────────────────────────┨\n", color
)
return _title
@staticmethod
def format_content(
content: List[str],
line_width: int,
color: Color = Color.WHITE,
center_content: bool = False,
border_left: str = "",
border_right: str = "",
@@ -184,11 +167,13 @@ class Logger:
if not center_content:
formatted_lines = [
f"{border_left} {line:<{line_width}} {border_right}" for line in lines
Color.apply(f"{border_left} {line:<{line_width}} {border_right}", color)
for line in lines
]
else:
formatted_lines = [
f"{border_left} {line:^{line_width}} {border_right}" for line in lines
Color.apply(f"{border_left} {line:^{line_width}} {border_right}", color)
for line in lines
]
return "\n".join(formatted_lines)

View File

@@ -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 #
# 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 #
# 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_firmware.menus.klipper_build_menu import (
KlipperBuildFirmwareMenu,
KlipperKConfigMenu,
)
from components.klipper_firmware.menus.klipper_flash_menu import (
KlipperFlashMethodMenu,
@@ -22,9 +23,9 @@ from components.klipper_firmware.menus.klipper_flash_menu import (
)
from components.moonraker import MOONRAKER_DIR
from components.moonraker.moonraker import Moonraker
from core.constants import COLOR_YELLOW, RESET_FORMAT
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
from procedures.system import change_system_hostname
from utils.git_utils import rollback_repository
@@ -34,6 +35,8 @@ from utils.git_utils import rollback_repository
class AdvancedMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.title = "Advanced Menu"
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -53,13 +56,8 @@ class AdvancedMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Advanced Menu ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
"""
╟───────────────────────────┬───────────────────────────╢
║ Klipper Firmware: │ Repository Rollback: ║
║ 1) [Build] │ 5) [Klipper] ║
@@ -79,12 +77,15 @@ class AdvancedMenu(BaseMenu):
rollback_repository(MOONRAKER_DIR, Moonraker)
def build(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=self.__class__).run()
def flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperFlashMethodMenu(previous_menu=self.__class__).run()
def build_flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).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 #
# 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.klipperscreen.klipperscreen import backup_klipperscreen_dir
from components.moonraker.moonraker_utils import (
from components.moonraker.utils.utils import (
backup_moonraker_db_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.mainsail_data import MainsailData
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
from utils.common import backup_printer_config_dir
@@ -34,6 +34,8 @@ from utils.common import backup_printer_config_dir
class BackupMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.title = "Backup Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -55,14 +57,11 @@ class BackupMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Backup Menu ] "
line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}"
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
line1 = Color.apply(
"INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW
)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{line1:^62}
╟───────────────────────────┬───────────────────────────╢

View File

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

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -15,13 +15,17 @@ from components.crowsnest.crowsnest import install_crowsnest
from components.klipper import klipper_setup
from components.klipperscreen.klipperscreen import install_klipperscreen
from components.moonraker import moonraker_setup
from components.webui_client import client_setup
from components.webui_client.client_config import client_config_setup
from components.webui_client.client_config.client_config_setup import (
install_client_config,
)
from components.webui_client.client_setup import install_client
from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData
from 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.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings
from core.types.color import Color
# noinspection PyUnusedLocal
@@ -29,6 +33,8 @@ from core.menus.base_menu import BaseMenu
class InstallMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.title = "Installation Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -49,13 +55,8 @@ class InstallMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Installation Menu ] "
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
"""
╟───────────────────────────┬───────────────────────────╢
║ Firmware & API: │ Touchscreen GUI: ║
║ 1) [Klipper] │ 7) [KlipperScreen] ║
@@ -80,16 +81,24 @@ class InstallMenu(BaseMenu):
moonraker_setup.install_moonraker()
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:
client_config_setup.install_client_config(MainsailData())
install_client_config(MainsailData())
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:
client_config_setup.install_client_config(FluiddData())
install_client_config(FluiddData())
def install_klipperscreen(self, **kwargs) -> None:
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 #
# 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.klipperscreen.klipperscreen import get_klipperscreen_status
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 (
get_client_status,
get_current_client_config,
)
from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData
from core.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_MAGENTA,
COLOR_RED,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.logger import Logger
from core.menus import FooterType
from core.menus.advanced_menu import AdvancedMenu
@@ -40,7 +32,8 @@ from core.menus.install_menu import InstallMenu
from core.menus.remove_menu import RemoveMenu
from core.menus.settings_menu import SettingsMenu
from core.menus.update_menu import UpdateMenu
from core.types import ComponentStatus, StatusMap, StatusText
from core.types.color import Color
from core.types.component_status import ComponentStatus, StatusMap, StatusText
from extensions.extensions_menu import ExtensionsMenu
from utils.common import get_kiauh_version, trunc_string
@@ -52,6 +45,8 @@ class MainMenu(BaseMenu):
super().__init__()
self.header: bool = True
self.title = "Main Menu"
self.title_color = Color.CYAN
self.footer_type: FooterType = FooterType.QUIT
self.version = ""
@@ -83,7 +78,7 @@ class MainMenu(BaseMenu):
setattr(
self,
f"{var}_status",
f"{COLOR_RED}Not installed{RESET_FORMAT}",
Color.apply("Not installed", Color.RED),
)
def _fetch_status(self) -> None:
@@ -109,34 +104,30 @@ class MainMenu(BaseMenu):
count_txt = f": {instance_count}"
setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt))
setattr(self, f"{name}_owner", f"{COLOR_CYAN}{owner}{RESET_FORMAT}")
setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}")
setattr(self, f"{name}_owner", Color.apply(owner, Color.CYAN))
setattr(self, f"{name}_repo", Color.apply(repo, Color.CYAN))
def _format_by_code(self, code: int, status: str, count: str) -> str:
color = COLOR_RED
color = Color.RED
if code == 0:
color = COLOR_RED
color = Color.RED
elif code == 1:
color = COLOR_YELLOW
color = Color.YELLOW
elif code == 2:
color = COLOR_GREEN
color = Color.GREEN
return f"{color}{status}{count}{RESET_FORMAT}"
return Color.apply(f"{status}{count}", color)
def print_menu(self) -> None:
self._fetch_status()
header = " [ Main Menu ] "
footer1 = f"{COLOR_CYAN}{self.version}{RESET_FORMAT}"
footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}"
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
footer1 = Color.apply(self.version, Color.CYAN)
link = Color.apply("https://git.io/JnmlX", Color.MAGENTA)
footer2 = f"Changelog: {link}"
pad1 = 32
pad2 = 26
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟──────────────────┬────────────────────────────────────╢
║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}}
║ │ Owner: {self.kl_owner:<{pad1}}

View File

@@ -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 #
# 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.mainsail_data import MainsailData
from components.webui_client.menus.client_remove_menu import ClientRemoveMenu
from core.constants import COLOR_RED, RESET_FORMAT
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal
@@ -30,6 +30,8 @@ from core.menus.base_menu import BaseMenu
class RemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.title = "Remove Menu"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -48,13 +50,8 @@ class RemoveMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Remove Menu ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
"""
╟───────────────────────────────────────────────────────╢
║ INFO: Configurations and/or any backups will be kept! ║
╟───────────────────────────┬───────────────────────────╢

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -9,13 +9,18 @@
from __future__ import annotations
import textwrap
from pathlib import Path
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.menus import Option
from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings, RepoSettings
from core.types.color import Color
from procedures.switch_repo import run_switch_repo_routine
from utils.input_utils import get_confirm, get_string_input
@@ -25,13 +30,16 @@ from utils.input_utils import get_confirm, get_string_input
class SettingsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.title = "Settings Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.klipper_repo: str | None = None
self.moonraker_repo: str | None = None
self.mainsail_unstable: bool | None = None
self.fluidd_unstable: bool | None = None
self.auto_backups_enabled: bool | None = None
self._load_settings()
print(self.klipper_status)
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu
@@ -48,32 +56,38 @@ class SettingsMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ KIAUH Settings ] "
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]"
color = Color.CYAN
checked = f"[{Color.apply('x', Color.GREEN)}]"
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
o2 = checked if self.fluidd_unstable else unchecked
o3 = checked if self.auto_backups_enabled else unchecked
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
║ Klipper source repository:
{self.klipper_repo:<67}
Moonraker source repository:
║ ● {self.moonraker_repo:<67}
Install unstable Webinterface releases:
║ Klipper:
● Repo: {kl_repo:51}
● Owner: {kl_owner:51}
● Branch: {kl_branch:51}
╟───────────────────────────────────────────────────────╢
Moonraker:
● Repo: {mr_repo:51}
║ ● Owner: {mr_owner:51}
║ ● Branch: {mr_branch:51}
╟───────────────────────────────────────────────────────╢
║ Install unstable releases: ║
{o1} Mainsail ║
{o2} Fluidd ║
║ ║
╟───────────────────────────────────────────────────────╢
║ Auto-Backup: ║
{o3} Automatic backup before update ║
║ ║
╟───────────────────────────────────────────────────────╢
║ 1) Set Klipper source repository ║
║ 2) Set Moonraker source repository ║
@@ -89,51 +103,55 @@ class SettingsMenu(BaseMenu):
def _load_settings(self) -> None:
self.settings = KiauhSettings()
self._format_repo_str("klipper")
self._format_repo_str("moonraker")
self.auto_backups_enabled = self.settings.kiauh.backup_before_update
self.mainsail_unstable = self.settings.mainsail.unstable_releases
self.fluidd_unstable = self.settings.fluidd.unstable_releases
def _format_repo_str(self, repo_name: Literal["klipper", "moonraker"]) -> None:
repo: RepoSettings = self.settings[repo_name]
repo_str = f"{'/'.join(repo.repo_url.rsplit('/', 2)[-2:])}"
branch_str = f"({COLOR_CYAN}@ {repo.branch}{RESET_FORMAT})"
# by default, we show the status of the installed repositories
self.klipper_status = get_klipper_status()
self.moonraker_status = get_moonraker_status()
# if the repository is not installed, we show the status of the settings from the config file
if self.klipper_status.repo == "-":
url_parts = self.settings.klipper.repo_url.split("/")
self.klipper_status.repo = url_parts[-1]
self.klipper_status.owner = url_parts[-2]
self.klipper_status.branch = self.settings.klipper.branch
if self.moonraker_status.repo == "-":
url_parts = self.settings.moonraker.repo_url.split("/")
self.moonraker_status.repo = url_parts[-1]
self.moonraker_status.owner = url_parts[-2]
self.moonraker_status.branch = self.settings.moonraker.branch
setattr(
self,
f"{repo_name}_repo",
f"{COLOR_CYAN}{repo_str}{RESET_FORMAT} {branch_str}",
)
def _gather_input(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> Tuple[str, str]:
warn_msg = [
"There is only basic input validation in place! "
"Make sure your the input is valid and has no typos or invalid characters!"]
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.",
if repo_dir.exists():
warn_msg.extend([
"For the change to take effect, the new repository will be cloned. "
"A backup of the old repository will be created.",
"\n\n",
"Make sure you don't have any ongoing prints running, as the services "
"will be restarted during this process! You will loose any ongoing print!",
],
)
"will be restarted during this process! You will loose any ongoing print!"])
Logger.print_dialog(DialogType.ATTENTION, warn_msg)
repo = get_string_input(
"Enter new repository URL",
allow_special_chars=True,
regex=r"^[\w/.:-]+$",
default=KLIPPER_REPO_URL if repo_name == "klipper" else MOONRAKER_REPO_URL,
)
branch = get_string_input(
"Enter new branch name",
allow_special_chars=True,
regex=r"^.+$",
default="master"
)
return repo, branch
def _set_repo(self, repo_name: Literal["klipper", "moonraker"]) -> None:
repo_url, branch = self._gather_input()
def _set_repo(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> None:
repo_url, branch = self._gather_input(repo_name, repo_dir)
display_name = repo_name.capitalize()
Logger.print_dialog(
DialogType.CUSTOM,
@@ -156,22 +174,26 @@ class SettingsMenu(BaseMenu):
Logger.print_ok("Changes saved!")
else:
Logger.print_info(
f"Skipping change of {display_name} source repository ..."
f"Changing of {display_name} source repository canceled ..."
)
return
Logger.print_status(f"Switching to {display_name}'s new source repository ...")
self._switch_repo(repo_name)
self._switch_repo(repo_name, repo_dir)
def _switch_repo(self, name: Literal["klipper", "moonraker"], repo_dir: Path ) -> None:
if not repo_dir.exists():
return
Logger.print_status(f"Switching to {name.capitalize()}'s new source repository ...")
def _switch_repo(self, name: Literal["klipper", "moonraker"]) -> None:
repo: RepoSettings = self.settings[name]
run_switch_repo_routine(name, repo)
def set_klipper_repo(self, **kwargs) -> None:
self._set_repo("klipper")
self._set_repo("klipper", KLIPPER_DIR)
def set_moonraker_repo(self, **kwargs) -> None:
self._set_repo("moonraker")
self._set_repo("moonraker", MOONRAKER_DIR)
def toggle_mainsail_release(self, **kwargs) -> None:
self.mainsail_unstable = not self.mainsail_unstable

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -21,7 +21,7 @@ from components.klipperscreen.klipperscreen import (
update_klipperscreen,
)
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 (
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.mainsail_data import MainsailData
from core.constants import (
COLOR_GREEN,
COLOR_RED,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.logger import DialogType, Logger
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.spinner import Spinner
from core.types import ComponentStatus
from core.types.color import Color
from core.types.component_status import ComponentStatus
from utils.input_utils import get_confirm
from utils.sys_utils import (
get_upgradable_packages,
@@ -56,6 +50,11 @@ from utils.sys_utils import (
class UpdateMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__()
self.loading_msg = "Loading update menu, please wait"
self.is_loading(True)
self.title = "Update Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.packages: List[str] = []
@@ -123,6 +122,9 @@ class UpdateMenu(BaseMenu):
},
}
self._fetch_update_status()
self.is_loading(False)
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu
@@ -143,29 +145,16 @@ class UpdateMenu(BaseMenu):
}
def print_menu(self) -> None:
spinner = Spinner("Loading update menu, please wait", color="green")
spinner.start()
self._fetch_update_status()
spinner.stop()
header = " [ Update Menu ] "
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
sysupgrades: str = "No upgrades available."
padding = 29
if self.package_count > 0:
sysupgrades = (
f"{COLOR_GREEN}{self.package_count} upgrades available!{RESET_FORMAT}"
sysupgrades = Color.apply(
f"{self.package_count} upgrades available!", Color.GREEN
)
padding = 38
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────┬───────────────┬───────────────╢
║ a) Update all │ │ ║
║ │ Current: │ Latest: ║
@@ -265,15 +254,15 @@ class UpdateMenu(BaseMenu):
self.package_count = len(self.packages)
def _format_local_status(self, local_version, remote_version) -> str:
color = COLOR_RED
color = Color.RED
if not local_version:
color = COLOR_RED
color = Color.RED
elif local_version == remote_version:
color = COLOR_GREEN
color = Color.GREEN
elif local_version != remote_version:
color = COLOR_YELLOW
color = Color.YELLOW
return f"{color}{local_version or '-'}{RESET_FORMAT}"
return Color.apply(local_version or "-", color)
def _set_status_data(self, name: str, status_fn: Callable, *args) -> None:
comp_status: ComponentStatus = status_fn(*args)
@@ -288,9 +277,9 @@ class UpdateMenu(BaseMenu):
local_status = self.status_data[name].get("local", None)
remote_status = self.status_data[name].get("remote", None)
color = COLOR_GREEN if remote_status else COLOR_RED
color = Color.GREEN if remote_status else Color.RED
local_txt = self._format_local_status(local_status, remote_status)
remote_txt = f"{color}{remote_status or '-'}{RESET_FORMAT}"
remote_txt = Color.apply(remote_status or "-", color)
setattr(self, f"{name}_local", local_txt)
setattr(self, f"{name}_remote", remote_txt)

View File

View File

@@ -0,0 +1,59 @@
# ======================================================================= #
# Copyright (C) 2020 - 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 dataclasses import dataclass, field
from typing import List
from core.logger import DialogType, Logger
from core.types.color import Color
@dataclass()
class Message:
title: str = field(default="")
text: List[str] = field(default_factory=list)
color: Color = field(default=Color.WHITE)
centered: bool = field(default=False)
class MessageService:
_instance = None
def __new__(cls) -> "MessageService":
if cls._instance is None:
cls._instance = super(MessageService, cls).__new__(cls)
return cls._instance
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
self.message = None
def set_message(self, message: Message) -> None:
self.message = message
def display_message(self) -> None:
if self.message is None:
return
Logger.print_dialog(
title=DialogType.CUSTOM,
content=self.message.text,
custom_title=self.message.title,
custom_color=self.message.color,
center_content=self.message.centered,
)
self.__clear_message()
def __clear_message(self) -> None:
self.message = None

View File

@@ -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 #
# https://github.com/dw-0/kiauh #
@@ -145,7 +145,8 @@ class KiauhSettings:
def _validate_str(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
v = self.config.getval(section, option)
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
if not v:
raise ValueError
def _apply_settings_from_file(self) -> None:

View File

@@ -3,13 +3,7 @@ import threading
import time
from typing import List, Literal
from core.constants import (
COLOR_GREEN,
COLOR_RED,
COLOR_WHITE,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.types.color import Color
SpinnerColor = Literal["white", "red", "green", "yellow"]
@@ -18,21 +12,18 @@ class Spinner:
def __init__(
self,
message: str = "Loading",
color: SpinnerColor = "white",
interval: float = 0.2,
) -> None:
self.message = f"{message} ..."
self.interval = interval
self._stop_event = threading.Event()
self._thread = threading.Thread(target=self._animate)
self._color = ""
self._set_color(color)
def _animate(self) -> None:
animation: List[str] = ["", "", "", "", "", "", "", "", "", ""]
while not self._stop_event.is_set():
for char in animation:
sys.stdout.write(f"\r{self._color}{char}{RESET_FORMAT} {self.message}")
sys.stdout.write(f"\r{Color.GREEN}{char}{Color.RST} {self.message}")
sys.stdout.flush()
time.sleep(self.interval)
if self._stop_event.is_set():
@@ -40,16 +31,6 @@ class Spinner:
sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r")
sys.stdout.flush()
def _set_color(self, color: SpinnerColor) -> None:
if color == "white":
self._color = COLOR_WHITE
elif color == "red":
self._color = COLOR_RED
elif color == "green":
self._color = COLOR_GREEN
elif color == "yellow":
self._color = COLOR_YELLOW
def start(self) -> None:
self._stop_event.clear()
if not self._thread.is_alive():

View File

@@ -192,11 +192,24 @@ class SimpleConfigParser:
self.config[section] = {"_raw": f"[{section}]\n"}
def _check_set_section_spacing(self):
prev_section = self.get_sections()[-1]
prev_section_content = self.config[prev_section]
last_item = list(prev_section_content.keys())[-1]
if last_item.startswith("#_") and last_item.keys()[-1] != "\n":
prev_section_content[last_item].append("\n")
prev_section_name: str = self.get_sections()[-1]
prev_section_content: Dict = self.config[prev_section_name]
last_option_name: str = list(prev_section_content.keys())[-1]
if last_option_name.startswith("#_"):
last_elem_value: str = prev_section_content[last_option_name][-1]
# if the last section is a collector, we first check if the last element
# in the collector ends with a newline. if it does not, we append a newline.
# this can happen if the config file does not end with a newline.
if not last_elem_value.endswith("\n"):
prev_section_content[last_option_name][-1] = f"{last_elem_value}\n"
# if the last item in a collector is not a newline, we append a newline, so
# that the new section is seperated from the options of the previous section
# by a newline
if last_elem_value != "\n":
prev_section_content[last_option_name].append("\n")
else:
prev_section_content[self._generate_rand_id()] = ["\n"]
@@ -298,7 +311,7 @@ class SimpleConfigParser:
"""Return the value of the given option in the given section as a converted value"""
try:
return conv(self.getval(section, option, fallback))
except ValueError as e:
except (ValueError, TypeError, AttributeError) as e:
if fallback is not _UNSET:
return fallback
raise ValueError(

View File

@@ -0,0 +1,33 @@
# a comment at the very top
# should be treated as the file header
# up to the first section, including all blank lines
[section_1]
option_1: value_1
option_1_1: True # this is a boolean
option_1_2: 5 ; this is an integer
option_1_3: 1.123 #;this is a float
[section_2] ; comment
option_2: value_2
; comment
[section_3]
option_3: value_3 # comment
[section_4]
# comment
option_4: value_4
[section number 5]
#option_5: value_5
option_5 = this.is.value-5
multi_option:
# these are multi-line values
value_5_1
value_5_2 ; here is a comment
value_5_3
option_5_1: value_5_1
# config ending with a comment

View File

@@ -0,0 +1,94 @@
# a comment at the very top
# should be treated as the file header
# up to the first section, including all blank lines
[section_1]
option_1: value_1
option_1_1: True # this is a boolean
option_1_2: 5 ; this is an integer
option_1_3: 1.123 #;this is a float
[section_2] ; comment
option_2: value_2
; comment
[section_3]
option_3: value_3 # comment
[section_4]
# comment
option_4: value_4
[section number 5]
#option_5: value_5
option_5 = this.is.value-5
multi_option:
# these are multi-line values
value_5_1
value_5_2 ; here is a comment
value_5_3
option_5_1: value_5_1
[gcode_macro M117]
rename_existing: M117.1
gcode:
{% if rawparams %}
{% set escaped_msg = rawparams.split(';', 1)[0].split('\x23', 1)[0]|replace('"', '\\"') %}
SET_DISPLAY_TEXT MSG="{escaped_msg}"
RESPOND TYPE=command MSG="{escaped_msg}"
{% else %}
SET_DISPLAY_TEXT
{% endif %}
# SDCard 'looping' (aka Marlin M808 commands) support
#
# Support SDCard looping
[sdcard_loop]
[gcode_macro M486]
gcode:
# Parameters known to M486 are as follows:
# [C<flag>] Cancel the current object
# [P<index>] Cancel the object with the given index
# [S<index>] Set the index of the current object.
# If the object with the given index has been canceled, this will cause
# the firmware to skip to the next object. The value -1 is used to
# indicate something that isnt an object and shouldnt be skipped.
# [T<count>] Reset the state and set the number of objects
# [U<index>] Un-cancel the object with the given index. This command will be
# ignored if the object has already been skipped
{% if 'exclude_object' not in printer %}
{action_raise_error("[exclude_object] is not enabled")}
{% endif %}
{% if 'T' in params %}
EXCLUDE_OBJECT RESET=1
{% for i in range(params.T | int) %}
EXCLUDE_OBJECT_DEFINE NAME={i}
{% endfor %}
{% endif %}
{% if 'C' in params %}
EXCLUDE_OBJECT CURRENT=1
{% endif %}
{% if 'P' in params %}
EXCLUDE_OBJECT NAME={params.P}
{% endif %}
{% if 'S' in params %}
{% if params.S == '-1' %}
{% if printer.exclude_object.current_object %}
EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object}
{% endif %}
{% else %}
EXCLUDE_OBJECT_START NAME={params.S}
{% endif %}
{% endif %}
{% if 'U' in params %}
EXCLUDE_OBJECT RESET=1 NAME={params.U}
{% endif %}

View File

@@ -32,7 +32,7 @@ def test_section_parsing(parser):
parser.config.keys()
), f"Expected keys: {expected_keys}, got: {parser.config.keys()}"
assert parser.in_option_block is False
assert parser.current_section == "section number 5"
assert parser.current_section == parser.get_sections()[-1]
assert parser.config["section_2"]["_raw"] == "[section_2] ; comment"

View File

@@ -0,0 +1,26 @@
# ======================================================================= #
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# https://github.com/dw-0/simple-config-parser #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg"]
@pytest.fixture(params=CONFIG_FILES)
def parser(request):
parser = SimpleConfigParser()
file_path = BASE_DIR.joinpath(request.param)
for line in load_testdata_from_file(file_path):
parser._parse_line(line) # noqa
return parser

View File

@@ -5,28 +5,13 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
NoOptionError,
NoSectionError,
SimpleConfigParser,
)
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_get_options(parser):
@@ -72,6 +57,7 @@ def test_getval(parser):
def test_getval_fallback(parser):
assert parser.getval("section_1", "option_128", "fallback") == "fallback"
assert parser.getval("section_1", "option_128", None) is None
def test_getval_exceptions(parser):
@@ -104,6 +90,7 @@ def test_getint_from_boolean(parser):
def test_getint_fallback(parser):
assert parser.getint("section_1", "option_128", 128) == 128
assert parser.getint("section_1", "option_128", None) is None
def test_getboolean(parser):
@@ -130,6 +117,7 @@ def test_getboolean_from_float(parser):
def test_getboolean_fallback(parser):
assert parser.getboolean("section_1", "option_128", True) is True
assert parser.getboolean("section_1", "option_128", False) is False
assert parser.getboolean("section_1", "option_128", None) is None
def test_getfloat(parser):
@@ -154,6 +142,7 @@ def test_getfloat_from_boolean(parser):
def test_getfloat_fallback(parser):
assert parser.getfloat("section_1", "option_128", 1.234) == 1.234
assert parser.getfloat("section_1", "option_128", None) is None
def test_set_existing_option(parser):

View File

@@ -7,8 +7,6 @@
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
@@ -17,12 +15,8 @@ BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
return SimpleConfigParser()
def test_read_file(parser):
def test_read_file():
parser = SimpleConfigParser()
parser.read_file(TEST_DATA_PATH)
assert parser.config is not None
assert parser.config.keys() is not None

View File

@@ -5,27 +5,12 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateSectionError,
SimpleConfigParser,
)
from tests.utils import load_testdata_from_file
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
@pytest.fixture
def parser():
parser = SimpleConfigParser()
for line in load_testdata_from_file(TEST_DATA_PATH):
parser._parse_line(line) # noqa
return parser
def test_get_sections(parser):

View File

@@ -25,50 +25,65 @@ def parser():
return parser
def test_get_conv(parser):
# Test conversion to int
def test_get_int_conv(parser):
should_be_int = parser._get_conv("section_1", "option_1_2", int)
assert isinstance(should_be_int, int)
# Test conversion to float
def test_get_float_conv(parser):
should_be_float = parser._get_conv("section_1", "option_1_3", float)
assert isinstance(should_be_float, float)
# Test conversion to boolean
def test_get_bool_conv(parser):
should_be_bool = parser._get_conv(
"section_1", "option_1_1", parser._convert_to_boolean
)
assert isinstance(should_be_bool, bool)
# Test fallback for int
def test_get_int_conv_fallback(parser):
should_be_fallback_int = parser._get_conv(
"section_1", "option_128", int, fallback=128
)
assert isinstance(should_be_fallback_int, int)
assert should_be_fallback_int == 128
assert parser._get_conv("section_1", "option_128", int, None) is None
# Test fallback for float
def test_get_float_conv_fallback(parser):
should_be_fallback_float = parser._get_conv(
"section_1", "option_128", float, fallback=1.234
)
assert isinstance(should_be_fallback_float, float)
assert should_be_fallback_float == 1.234
# Test fallback for boolean
assert parser._get_conv("section_1", "option_128", float, None) is None
def test_get_bool_conv_fallback(parser):
should_be_fallback_bool = parser._get_conv(
"section_1", "option_128", parser._convert_to_boolean, fallback=True
)
assert isinstance(should_be_fallback_bool, bool)
assert should_be_fallback_bool is True
# Test ValueError exception for invalid int conversion
assert (
parser._get_conv("section_1", "option_128", parser._convert_to_boolean, None)
is None
)
def test_get_int_conv_exception(parser):
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", int)
# Test ValueError exception for invalid float conversion
def test_get_float_conv_exception(parser):
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", float)
# Test ValueError exception for invalid boolean conversion
def test_get_bool_conv_exception(parser):
with pytest.raises(ValueError):
parser._get_conv("section_1", "option_1", parser._convert_to_boolean)

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 #
# https://github.com/dw-0/kiauh #
@@ -25,6 +25,7 @@ class ComponentStatus:
status: StatusCode
owner: str | None = None
repo: str | None = None
branch: str = ""
local: str | None = None
remote: str | 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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -15,10 +15,10 @@ import textwrap
from pathlib import Path
from typing import Dict, List, Type
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.logger import Logger
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
from extensions import EXTENSION_ROOT
from extensions.base_extension import BaseExtension
@@ -28,6 +28,8 @@ from extensions.base_extension import BaseExtension
class ExtensionsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__()
self.title = "Extensions Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu
self.extensions: Dict[str, BaseExtension] = self.discover_extensions()
@@ -58,12 +60,16 @@ class ExtensionsMenu(BaseMenu):
module_path = f"kiauh.extensions.{ext.name}.{module_name}"
# get the class name of the extension
ext_class: Type[BaseExtension] = inspect.getmembers(
importlib.import_module(module_path),
predicate=lambda o: inspect.isclass(o)
and issubclass(o, BaseExtension)
and o != BaseExtension,
)[0][1]
module = importlib.import_module(module_path)
def predicate(o):
return (
inspect.isclass(o)
and issubclass(o, BaseExtension)
and o != BaseExtension
)
ext_class: type = inspect.getmembers(module, predicate)[0][1]
# instantiate the extension with its metadata and add to dict
ext_instance: BaseExtension = ext_class(metadata)
@@ -72,20 +78,15 @@ class ExtensionsMenu(BaseMenu):
except (IOError, json.JSONDecodeError, ImportError) as e:
print(f"Failed loading extension {ext}: {e}")
return dict(sorted(ext_dict.items()))
return dict(sorted(ext_dict.items(), key=lambda x: int(x[0])))
def extension_submenu(self, **kwargs):
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()
def print_menu(self) -> None:
header = " [ Extensions Menu ] "
color = COLOR_CYAN
line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}"
count = 62 - len(color) - len(RESET_FORMAT)
line1 = Color.apply("Available Extensions:", Color.YELLOW)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{line1:<62}
║ ║
@@ -108,6 +109,8 @@ class ExtensionSubmenu(BaseMenu):
self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None
):
super().__init__()
self.title = extension.metadata.get("display_name")
self.title_color = Color.YELLOW
self.extension = extension
self.previous_menu: Type[BaseMenu] | None = previous_menu
@@ -125,9 +128,6 @@ class ExtensionSubmenu(BaseMenu):
self.options["2"] = Option(self.extension.remove_extension)
def print_menu(self) -> None:
header = f" [ {self.extension.metadata.get('display_name')} ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
line_width = 53
description: List[str] = self.extension.metadata.get("description", [])
description_text = Logger.format_content(
@@ -138,9 +138,7 @@ class ExtensionSubmenu(BaseMenu):
)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
"""
╟───────────────────────────────────────────────────────╢
"""
)[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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -20,10 +20,10 @@ from components.klipper.klipper_dialogs import (
DisplayType,
print_instance_overview,
)
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.logger import Logger
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
from extensions.base_extension import BaseExtension
from utils.git_utils import git_clone_wrapper
from utils.input_utils import get_selection_input
@@ -80,6 +80,8 @@ class MainsailThemeInstallMenu(BaseMenu):
def __init__(self, instances: List[Klipper]):
super().__init__()
self.title = "Mainsail Theme Installer"
self.title_color = Color.YELLOW
self.themes: List[ThemeData] = self.load_themes()
self.instances = instances
@@ -97,14 +99,11 @@ class MainsailThemeInstallMenu(BaseMenu):
}
def print_menu(self) -> None:
header = " [ Mainsail Theme Installer ] "
color = COLOR_YELLOW
line1 = f"{COLOR_CYAN}A preview of each Mainsail theme can be found here:{RESET_FORMAT}"
count = 62 - len(color) - len(RESET_FORMAT)
line1 = Color.apply(
"A preview of each Mainsail theme can be found here:", Color.YELLOW
)
menu = textwrap.dedent(
f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢
{line1:<62}
║ https://docs.mainsail.xyz/theming/themes ║

View File

@@ -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 #
# 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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #
@@ -132,7 +132,7 @@ class MoonrakerObico:
raise
env_file_content = env_template_file_content.replace(
"%CFG%",
f"{self.base.cfg_dir}/{self.cfg_file}",
f"{self.cfg_file}",
)
return env_file_content

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 #
# https://github.com/dw-0/kiauh #
@@ -12,6 +12,7 @@ from typing import List
from components.klipper.klipper import Klipper
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.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
@@ -308,7 +309,8 @@ class ObicoExtension(BaseExtension):
def _check_and_opt_link_instances(self) -> None:
Logger.print_status("Checking link status of Obico instances ...")
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico)
suffix_blacklist: List[str] = [suffix for suffix in SUFFIX_BLACKLIST if suffix != 'obico']
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico, suffix_blacklist=suffix_blacklist)
unlinked_instances: List[MoonrakerObico] = [
obico for obico in ob_instances if not obico.is_linked
]

View File

@@ -0,0 +1,28 @@
# ======================================================================= #
# 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 pathlib import Path
# repo
OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git"
# directories
OA_DIR = Path.home().joinpath("octoapp")
OA_ENV_DIR = Path.home().joinpath("octoapp-env")
# files
OA_REQ_FILE = OA_DIR.joinpath("requirements.txt")
OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json")
OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh")
OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh")
OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log")
# filenames
OA_CFG_NAME = "octoapp.conf"
OA_LOG_NAME = "octoapp.log"
OA_SYS_CFG_NAME = "octoapp-system.cfg"

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"index": 9,
"module": "octoapp_extension",
"maintained_by": "crysxd",
"display_name": "OctoApp for Klipper",
"description": [
"Your favorite 3D printing app for iOS & Android",
"- Print notifications on your phone & watch",
"- Control and start prints from your phone",
"- Live webcam view",
"- Live Gcode preview",
"- And much much more!"
],
"updates": true
}
}

View File

@@ -0,0 +1,75 @@
# ======================================================================= #
# 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 pathlib import Path
from subprocess import CalledProcessError, run
from components.moonraker import MOONRAKER_CFG_NAME
from components.moonraker.moonraker import Moonraker
from core.instance_manager.base_instance import BaseInstance
from core.logger import Logger
from extensions.octoapp import (
OA_CFG_NAME,
OA_DIR,
OA_ENV_DIR,
OA_INSTALL_SCRIPT,
OA_LOG_NAME,
OA_SYS_CFG_NAME,
OA_UPDATE_SCRIPT,
)
from utils.sys_utils import get_service_file_path
@dataclass
class Octoapp:
suffix: str
base: BaseInstance = field(init=False, repr=False)
service_file_path: Path = field(init=False)
log_file_name = OA_LOG_NAME
dir: Path = OA_DIR
env_dir: Path = OA_ENV_DIR
data_dir: Path = field(init=False)
store_dir: Path = field(init=False)
cfg_file: Path = field(init=False)
sys_cfg_file: Path = field(init=False)
def __post_init__(self):
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
self.base.log_file_name = self.log_file_name
self.service_file_path: Path = get_service_file_path(
Octoapp, self.suffix
)
self.store_dir = self.base.data_dir.joinpath("store")
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.data_dir = self.base.data_dir
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)
def create(self) -> None:
Logger.print_status("Creating OctoApp for Klipper Instance ...")
try:
cmd = f"{OA_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}"
run(cmd, check=True, shell=True)
except CalledProcessError as e:
Logger.print_error(f"Error creating instance: {e}")
raise
@staticmethod
def update() -> None:
try:
run(OA_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OA_DIR)
except CalledProcessError as e:
Logger.print_error(f"Error updating OctoApp for Klipper: {e}")
raise

View File

@@ -0,0 +1,208 @@
# ======================================================================= #
# 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 #
# ======================================================================= #
import json
from typing import List
from components.moonraker.moonraker import Moonraker
from components.klipper.klipper import Klipper
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from extensions.base_extension import BaseExtension
from extensions.octoapp import (
OA_DEPS_JSON_FILE,
OA_DIR,
OA_ENV_DIR,
OA_INSTALL_SCRIPT,
OA_INSTALLER_LOG_FILE,
OA_REPO,
OA_REQ_FILE,
OA_SYS_CFG_NAME,
)
from extensions.octoapp.octoapp import Octoapp
from utils.common import (
check_install_dependencies,
moonraker_exists,
)
from utils.config_utils import (
remove_config_section,
)
from utils.fs_utils import run_remove_routines
from utils.git_utils import git_clone_wrapper
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
from utils.sys_utils import (
install_python_requirements,
parse_packages_from_file,
)
# noinspection PyMethodMayBeStatic
class OctoappExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing OctoApp for Klipper ...")
# check if moonraker is installed. if not, notify the user and exit
if not moonraker_exists():
return
force_clone = False
OA_instances: List[Octoapp] = get_instances(Octoapp)
if OA_instances:
Logger.print_dialog(
DialogType.INFO,
[
"OctoApp is already installed!",
"It is safe to run the installer again to link your "
"printer or repair any issues.",
],
)
if not get_confirm("Re-run OctoApp installation?"):
Logger.print_info("Exiting OctoApp for Klipper installation ...")
return
else:
Logger.print_status("Re-Installing OctoApp for Klipper ...")
force_clone = True
mr_instances: List[Moonraker] = get_instances(Moonraker)
mr_names = [f"{moonraker.data_dir.name}" for moonraker in mr_instances]
if len(mr_names) > 1:
Logger.print_dialog(
DialogType.INFO,
[
"The following Moonraker instances were found:",
*mr_names,
"\n\n",
"The setup will apply the same names to OctoApp!",
],
)
if not get_confirm(
"Continue OctoApp for Klipper installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting OctoApp for Klipper installation ...")
return
try:
git_clone_wrapper(OA_REPO, OA_DIR, force=force_clone)
for moonraker in mr_instances:
instance = Octoapp(suffix=moonraker.suffix)
instance.create()
InstanceManager.restart_all(mr_instances)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully installed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(
f"Error during OctoApp for Klipper installation:\n{e}"
)
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating OctoApp for Klipper ...")
try:
Octoapp.update()
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully updated!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoApp for Klipper update:\n{e}")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing OctoApp for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
ob_instances: List[Octoapp] = get_instances(Octoapp)
try:
self._remove_OA_instances(ob_instances)
self._remove_OA_store_dirs()
self._remove_OA_dir()
self._remove_OA_env()
remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances)
run_remove_routines(OA_INSTALLER_LOG_FILE)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoApp for Klipper successfully removed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoApp for Klipper removal:\n{e}")
def _install_OA_dependencies(self) -> None:
OA_deps = []
if OA_DEPS_JSON_FILE.exists():
with open(OA_DEPS_JSON_FILE, "r") as deps:
OA_deps = json.load(deps).get("debian", [])
elif OA_INSTALL_SCRIPT.exists():
OA_deps = parse_packages_from_file(OA_INSTALL_SCRIPT)
if not OA_deps:
raise ValueError("Error reading OctoApp dependencies!")
check_install_dependencies({*OA_deps})
install_python_requirements(OA_ENV_DIR, OA_REQ_FILE)
def _remove_OA_instances(
self,
instance_list: List[Octoapp],
) -> None:
if not instance_list:
Logger.print_info("No OctoApp instances found. Skipped ...")
return
for instance in instance_list:
Logger.print_status(
f"Removing instance {instance.service_file_path.stem} ..."
)
InstanceManager.remove(instance)
def _remove_OA_dir(self) -> None:
Logger.print_status("Removing OctoApp for Klipper directory ...")
if not OA_DIR.exists():
Logger.print_info(f"'{OA_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OA_DIR)
def _remove_OA_store_dirs(self) -> None:
Logger.print_status("Removing OctoApp for Klipper store directory ...")
klipper_instances: List[Moonraker] = get_instances(Klipper)
for instance in klipper_instances:
store_dir = instance.data_dir.joinpath("octoapp-store")
if not store_dir.exists():
Logger.print_info(f"'{store_dir}' does not exist. Skipped ...")
return
run_remove_routines(store_dir)
def _remove_OA_env(self) -> None:
Logger.print_status("Removing OctoApp for Klipper environment ...")
if not OA_ENV_DIR.exists():
Logger.print_info(f"'{OA_ENV_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OA_ENV_DIR)

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 #
# 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 #
# 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 #
# 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 #
# https://github.com/dw-0/kiauh #

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