Compare commits

...

59 Commits

Author SHA1 Message Date
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
dw-0
ec3f93eeda Release v6.0.0-alpha.4
Merge develop into master (v6.0.0-alpha.4)
2024-09-22 09:43:04 +02:00
dw-0
afeb2bf02e feat: implement update all feature (#541)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-22 09:38:15 +02:00
dw-0
4b17c68454 fix: trunc owner and repo name if they would overflow (#540)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-22 08:58:44 +02:00
dw-0
df414ce37e fix: run umask 022 at launch (#538)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-21 21:01:19 +02:00
dw-0
975629f097 refactor: rework client config conflict detection (#537)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-21 18:42:19 +02:00
dw-0
fd2910ba67 fix: remove klipper.env and moonraker.env during removal (#536)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-21 15:10:30 +02:00
dw-0
6b6607c5ab fix: update scp integration for more robust config handling (#535)
* chore: remove scp

* Squashed 'kiauh/core/submodules/simple_config_parser/' content from commit abee21c

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: abee21c08658be4529028844304df60650c09afa

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from abee21c..aa0302b

aa0302b fix: fix missing newline chars in raw strings

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: aa0302b02b56b252ed88fd2db88ee878a5bb7b5b

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from aa0302b..ef52958

ef52958 refactor: conditionally add empty line when adding new section

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: ef529580f469ef020135cb03e250fcd4e0d70acf

* fix: update scp integration for more robust cfg modification

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-21 13:55:30 +02:00
CODeRUS
b604d93d0c fix: RP2040 firmware detection (#533)
Co-authored-by: dw-0 <th33xitus@gmail.com>
2024-09-21 12:10:20 +02:00
dw-0
7e87f8af32 refactor: implement Mobileraker and OctoEverywhere as community extensions (#532)
* refactor: move mobileraker to extensions

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

* refactor: move octoeverywhere to extensions

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-20 12:05:29 +02:00
dw-0
29b5ab00cd fix: correctly point to printers config dir (#531)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-15 08:36:49 +02:00
dw-0
4cf523a758 Merge pull request #524 from dw-0/develop
Merge develop into master
2024-09-08 19:04:19 +02:00
dw-0
694a4c20c5 fix: typo in "origin" and "managed_services" (#520)
* fix: typo in "origin" and "managed_services" for klipperscreen update manager config

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

* fix: typo in "origin" for moonraker telegram bot update manager config

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-08 18:58:07 +02:00
dw-0
a54514c400 fix: fix switching of repositories (#519)
* fix: fix repo switching

Extend the functionality of repo switching by creating a backup before the switch. Also implement a rollback mechanic in case of an error.

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

* refactor: fail when installing requirements fails

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

* refactor: display owner and repo in main menu on separate lines

long owner and repo names would case the menu to be too wide

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

---------

Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-05 20:31:38 +02:00
145 changed files with 5864 additions and 2606 deletions

4
.gitignore vendored
View File

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

130
README.md
View File

@@ -28,12 +28,12 @@
### 📋 Prerequisites ### 📋 Prerequisites
KIAUH is a script that assists you in installing Klipper on a Linux operating system that has KIAUH is a script that assists you in installing Klipper on a Linux operating system that has
already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure
that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image
if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/) if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/)
is the simplest way to flash an image like this to an SD card. is the simplest way to flash an image like this to an SD card.
* Once you have downloaded, installed and launched the Raspberry Pi Imager, * Once you have downloaded, installed and launched the Raspberry Pi Imager,
select `Choose OS -> Raspberry Pi OS (other)`: \ select `Choose OS -> Raspberry Pi OS (other)`: \
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350"> <img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350">
@@ -44,7 +44,7 @@ select `Choose OS -> Raspberry Pi OS (other)`: \
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350"> <img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
</p> </p>
* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which * Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which
you want to flash the image. you want to flash the image.
* Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu) * Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu)
@@ -52,9 +52,9 @@ and enable SSH and configure Wi-Fi.
* If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html). * If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html).
These steps **only** apply if you are actually using a Raspberry Pi. In case you want These steps **only** apply if you are actually using a Raspberry Pi. In case you want
to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed
to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run
and operate on the Linux Distribution you are going to flash. You likely will have the most success with and operate on the Linux Distribution you are going to flash. You likely will have the most success with
distributions based on Debian 11 Bullseye. Read the notes further down below in this document. distributions based on Debian 11 Bullseye. Read the notes further down below in this document.
@@ -82,8 +82,8 @@ Finally, start KIAUH by running the next command:
``` ```
* **Step 4:** \ * **Step 4:** \
You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending
on what you want to do. To choose an action, simply type the corresponding number into the "Perform action" on what you want to do. To choose an action, simply type the corresponding number into the "Perform action"
prompt and confirm by hitting ENTER. prompt and confirm by hitting ENTER.
<hr> <hr>
@@ -101,77 +101,83 @@ prompt and confirm by hitting ENTER.
<h2 align="center">🌐 Sources & Further Information</h2> <h2 align="center">🌐 Sources & Further Information</h2>
<table> <table align="center">
<tr> <tr>
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th> <th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th>
<th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th> <th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th>
<th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th> <th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th>
</tr> </tr>
<tr> <tr>
<th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th> <th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th> <th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th> <th>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
<th>by <a href="https://github.com/Arksine">Arksine</a></th> <th>by <a href="https://github.com/Arksine">Arksine</a></th>
<th>by <a href="https://github.com/mainsail-crew">mainsail-crew</a></th> <th>by <a href="https://github.com/mainsail-crew">mainsail-crew</a></th>
</tr>
<tr>
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
</tr> </tr>
<tr> <tr>
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th> <th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th> <th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th> <th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
</tr> </tr>
<tr> <tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th> <th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th> <th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th> <th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
</tr>
<tr>
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
</tr>
<tr>
<th>by <a href="https://github.com/nlef">nlef</a></th>
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/nlef">nlef</a></th> <th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th> <th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th> <th><h3><a href="https://github.com/crysxd/OctoApp-Plugin">OctoApp For Klipper</a></h3></th>
</tr>
<tr>
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="Mobileraker Logo" height="64"></a></th>
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th>
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
</tr>
<tr>
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
</tr> </tr>
<tr> <tr>
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th> <th><h3><a href="https://github.com/staubgeborener/klipper-backup">Klipper-Backup</a></h3></th>
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th> <th><h3><a href="https://simplyprint.io/">SimplyPrint for Klipper</a></h3></th>
<th><h3><a href="https://github.com/crysxd/OctoApp-Plugin">OctoApp For Klipper</a></h3></th>
<th><h3></h3></th>
</tr> </tr>
<tr> <tr>
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="OctoEverywhere Logo" height="64"></a></th> <th><a href="https://github.com/staubgeborener/klipper-backup"><img src="https://avatars.githubusercontent.com/u/28908603?v=4" alt="Staubgeroner Avatar" height="64"></a></th>
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th> <th><a href="https://github.com/SimplyPrint"><img src="https://avatars.githubusercontent.com/u/64896552?s=200&v=4" alt="" height="64"></a></th>
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
</tr> </tr>
<tr> <tr>
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th> <th>by <a href="https://github.com/Staubgeborener">Staubgeborener</a></th>
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th> <th>by <a href="https://github.com/SimplyPrint">SimplyPrint</a></th>
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
<th></th>
</tr> </tr>
</table> </table>
<hr> <hr>
@@ -186,6 +192,12 @@ prompt and confirm by hitting ENTER.
<hr> <hr>
<div align="center">
<img src="https://repobeats.axiom.co/api/embed/a1afbda9190c04a90cf4bd3061e5573bc836cb05.svg" alt="Repobeats analytics image"/>
</div>
<hr>
<h2 align="center">✨ Credits ✨</h2> <h2 align="center">✨ Credits ✨</h2>
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo! * A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!

View File

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

View File

@@ -10,7 +10,10 @@
#=======================================================================# #=======================================================================#
set -e set -e
clear clear -x
# make sure we have the correct permissions while running the script
umask 022
### sourcing all additional scripts ### sourcing all additional scripts
KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
@@ -107,7 +110,7 @@ function launch_kiauh_v6() {
export PYTHONPATH="${entrypoint}" export PYTHONPATH="${entrypoint}"
clear clear -x
python3 "${entrypoint}/kiauh.py" python3 "${entrypoint}/kiauh.py"
} }

View File

@@ -30,7 +30,7 @@ from core.backup_manager.backup_manager import BackupManager
from core.constants import CURRENT_USER from core.constants import CURRENT_USER
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import ( from utils.common import (
check_install_dependencies, check_install_dependencies,
get_install_status, get_install_status,

View File

@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent MODULE_PATH = Path(__file__).resolve().parent
KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git"
# names # names
KLIPPER_LOG_NAME = "klippy.log" KLIPPER_LOG_NAME = "klippy.log"
KLIPPER_CFG_NAME = "printer.cfg" KLIPPER_CFG_NAME = "printer.cfg"

View File

@@ -41,6 +41,7 @@ class Klipper:
env_dir: Path = KLIPPER_ENV_DIR env_dir: Path = KLIPPER_ENV_DIR
data_dir: Path = field(init=False) data_dir: Path = field(init=False)
cfg_file: Path = field(init=False) cfg_file: Path = field(init=False)
env_file: Path = field(init=False)
serial: Path = field(init=False) serial: Path = field(init=False)
uds: Path = field(init=False) uds: Path = field(init=False)
@@ -51,6 +52,7 @@ class Klipper:
self.service_file_path: Path = get_service_file_path(Klipper, self.suffix) self.service_file_path: Path = get_service_file_path(Klipper, self.suffix)
self.data_dir: Path = get_data_dir(Klipper, self.suffix) self.data_dir: Path = get_data_dir(Klipper, self.suffix)
self.cfg_file: Path = self.base.cfg_dir.joinpath(KLIPPER_CFG_NAME) self.cfg_file: Path = self.base.cfg_dir.joinpath(KLIPPER_CFG_NAME)
self.env_file: Path = self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME)
self.serial: Path = self.base.comms_dir.joinpath(KLIPPER_SERIAL_NAME) self.serial: Path = self.base.comms_dir.joinpath(KLIPPER_SERIAL_NAME)
self.uds: Path = self.base.comms_dir.joinpath(KLIPPER_UDS_NAME) self.uds: Path = self.base.comms_dir.joinpath(KLIPPER_UDS_NAME)

View File

@@ -11,14 +11,9 @@ import textwrap
from enum import Enum, unique from enum import Enum, unique
from typing import List from typing import List
from core.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.instance_type import InstanceType
from core.menus.base_menu import print_back_footer from core.menus.base_menu import print_back_footer
from core.types.color import Color
from utils.instance_type import InstanceType
@unique @unique
@@ -42,12 +37,12 @@ def print_instance_overview(
if display_type is DisplayType.SERVICE_NAME if display_type is DisplayType.SERVICE_NAME
else "printer directories" else "printer directories"
) )
headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}" headline = Color.apply(f"The following {d_type} were found:", Color.GREEN)
dialog += f"{headline:^64}\n" dialog += f"{headline:^64}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n" dialog += "╟───────────────────────────────────────────────────────╢\n"
if show_select_all: if show_select_all:
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}" select_all = Color.apply("a) Select all", Color.YELLOW)
dialog += f"{select_all:<63}\n" dialog += f"{select_all:<63}\n"
dialog += "║ ║\n" dialog += "║ ║\n"
@@ -56,7 +51,9 @@ def print_instance_overview(
name = s.service_file_path.stem name = s.service_file_path.stem
else: else:
name = s.data_dir name = s.data_dir
line = f"{COLOR_CYAN}{f'{i + start_index})' if show_index else ''} {name}{RESET_FORMAT}" line = Color.apply(
f"{f'{i + start_index})' if show_index else ''} {name}", Color.CYAN
)
dialog += f"{line:<63}\n" dialog += f"{line:<63}\n"
dialog += "╟───────────────────────────────────────────────────────╢\n" dialog += "╟───────────────────────────────────────────────────────╢\n"
@@ -65,8 +62,10 @@ def print_instance_overview(
def print_select_instance_count_dialog() -> None: def print_select_instance_count_dialog() -> None:
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}" line1 = Color.apply("WARNING:", Color.YELLOW)
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}" line2 = Color.apply(
"Setting up too many instances may crash your system.", Color.YELLOW
)
dialog = textwrap.dedent( dialog = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
@@ -85,8 +84,8 @@ def print_select_instance_count_dialog() -> None:
def print_select_custom_name_dialog() -> None: def print_select_custom_name_dialog() -> None:
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}" line1 = Color.apply("INFO:", Color.YELLOW)
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}" line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW)
dialog = textwrap.dedent( dialog = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗

View File

@@ -15,6 +15,8 @@ from components.klipper.klipper import Klipper
from components.klipper.klipper_dialogs import print_instance_overview from components.klipper.klipper_dialogs import print_instance_overview
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import Logger
from core.services.message_service import Message
from core.types.color import Color
from utils.fs_utils import run_remove_routines from utils.fs_utils import run_remove_routines
from utils.input_utils import get_selection_input from utils.input_utils import get_selection_input
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
@@ -25,7 +27,11 @@ def run_klipper_removal(
remove_service: bool, remove_service: bool,
remove_dir: bool, remove_dir: bool,
remove_env: bool, remove_env: bool,
) -> None: ) -> Message:
completion_msg = Message(
title="Klipper Removal Process completed",
color=Color.GREEN,
)
klipper_instances: List[Klipper] = get_instances(Klipper) klipper_instances: List[Klipper] = get_instances(Klipper)
if remove_service: if remove_service:
@@ -33,20 +39,36 @@ def run_klipper_removal(
if klipper_instances: if klipper_instances:
instances_to_remove = select_instances_to_remove(klipper_instances) instances_to_remove = select_instances_to_remove(klipper_instances)
remove_instances(instances_to_remove) 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: else:
Logger.print_info("No Klipper Services installed! Skipped ...") Logger.print_info("No Klipper Services installed! Skipped ...")
if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"): if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"):
Logger.print_info("There are still other Klipper services installed:") completion_msg.text = [
Logger.print_info(f"'{KLIPPER_DIR}' was not removed.", prefix=False) "Some Klipper services are still installed:",
Logger.print_info(f"'{KLIPPER_ENV_DIR}' was not removed.", prefix=False) f"'{KLIPPER_DIR}' was not removed, even though selected for removal.",
f"'{KLIPPER_ENV_DIR}' was not removed, even though selected for removal.",
]
else: else:
if remove_dir: if remove_dir:
Logger.print_status("Removing Klipper local repository ...") 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: if remove_env:
Logger.print_status("Removing Klipper Python environment ...") 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: def select_instances_to_remove(instances: List[Klipper]) -> List[Klipper] | None:
@@ -83,16 +105,13 @@ def remove_instances(
for instance in instance_list: for instance in instance_list:
Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...") Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...")
InstanceManager.remove(instance) InstanceManager.remove(instance)
delete_klipper_env_file(instance)
def delete_klipper_logs(instances: List[Klipper]) -> None: def delete_klipper_env_file(instance: Klipper):
all_logfiles = [] Logger.print_status(f"Remove '{instance.env_file}'")
for instance in instances: if not instance.env_file.exists():
all_logfiles = list(instance.base.log_dir.glob("klippy.log*")) msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
if not all_logfiles: Logger.print_info(msg)
Logger.print_info("No Klipper logs found. Skipped ...")
return return
run_remove_routines(instance.env_file)
for log in all_logfiles:
Logger.print_status(f"Remove '{log}'")
run_remove_routines(log)

View File

@@ -36,7 +36,7 @@ from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
) )
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import get_install_status from utils.common import get_install_status
from utils.input_utils import get_confirm, get_number_input, get_string_input from utils.input_utils import get_confirm, get_number_input, get_string_input
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
@@ -174,8 +174,8 @@ def create_example_printer_cfg(
return return
scp = SimpleConfigParser() scp = SimpleConfigParser()
scp.read(target) scp.read_file(target)
scp.set("virtual_sdcard", "path", str(instance.base.gcodes_dir)) scp.set_option("virtual_sdcard", "path", str(instance.base.gcodes_dir))
# include existing client configs in the example config # include existing client configs in the example config
if clients is not None and len(clients) > 0: if clients is not None and len(clients) > 0:
@@ -185,7 +185,7 @@ def create_example_printer_cfg(
scp.add_section(section=section) scp.add_section(section=section)
create_client_config_symlink(client_config, [instance]) create_client_config_symlink(client_config, [instance])
scp.write(target) scp.write_file(target)
Logger.print_ok(f"Example printer.cfg created in '{instance.base.cfg_dir}'") Logger.print_ok(f"Example printer.cfg created in '{instance.base.cfg_dir}'")

View File

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

View File

@@ -6,8 +6,16 @@
# # # #
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import re
from subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run from subprocess import (
DEVNULL,
PIPE,
STDOUT,
CalledProcessError,
Popen,
check_output,
run,
)
from typing import List from typing import List
from components.klipper import KLIPPER_DIR from components.klipper import KLIPPER_DIR
@@ -30,17 +38,20 @@ def find_firmware_file() -> bool:
f1 = "klipper.elf.hex" f1 = "klipper.elf.hex"
f2 = "klipper.elf" f2 = "klipper.elf"
f3 = "klipper.bin" f3 = "klipper.bin"
f4 = "klipper.uf2"
fw_file_exists: bool = ( fw_file_exists: bool = (
target.joinpath(f1).exists() and target.joinpath(f2).exists() (target.joinpath(f1).exists() and target.joinpath(f2).exists())
) or target.joinpath(f3).exists() or target.joinpath(f3).exists()
or target.joinpath(f4).exists()
)
return target_exists and fw_file_exists return target_exists and fw_file_exists
def find_usb_device_by_id() -> List[str]: def find_usb_device_by_id() -> List[str]:
try: try:
command = "find /dev/serial/by-id/* 2>/dev/null" command = "find /dev/serial/by-id/*"
output = check_output(command, shell=True, text=True) output = check_output(command, shell=True, text=True, stderr=DEVNULL)
return output.splitlines() return output.splitlines()
except CalledProcessError as e: except CalledProcessError as e:
Logger.print_error("Unable to find a USB device!") Logger.print_error("Unable to find a USB device!")
@@ -50,9 +61,14 @@ def find_usb_device_by_id() -> List[str]:
def find_uart_device() -> List[str]: def find_uart_device() -> List[str]:
try: try:
command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"' cmd = "find /dev -maxdepth 1"
output = check_output(command, shell=True, text=True) output = check_output(cmd, shell=True, text=True, stderr=DEVNULL)
return output.splitlines() 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: except CalledProcessError as e:
Logger.print_error("Unable to find a UART device!") Logger.print_error("Unable to find a UART device!")
Logger.print_error(e, prefix=False) Logger.print_error(e, prefix=False)
@@ -61,15 +77,34 @@ def find_uart_device() -> List[str]:
def find_usb_dfu_device() -> List[str]: def find_usb_dfu_device() -> List[str]:
try: try:
command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"' output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
output = check_output(command, shell=True, text=True) device_list = []
return output.splitlines() 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: except CalledProcessError as e:
Logger.print_error("Unable to find a USB DFU device!") Logger.print_error("Unable to find a USB DFU device!")
Logger.print_error(e, prefix=False) Logger.print_error(e, prefix=False)
return [] 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]: def get_sd_flash_board_list() -> List[str]:
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
return [] return []

View File

@@ -26,6 +26,7 @@ class FlashCommand(Enum):
class ConnectionType(Enum): class ConnectionType(Enum):
USB = "USB" USB = "USB"
USB_DFU = "USB (DFU)" USB_DFU = "USB (DFU)"
USB_RP2040 = "USB (RP2040)"
UART = "UART" UART = "UART"

View File

@@ -17,10 +17,10 @@ from components.klipper_firmware.firmware_utils import (
run_make_clean, run_make_clean,
run_make_menuconfig, run_make_menuconfig,
) )
from core.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT
from core.logger import Logger from core.logger import Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from utils.sys_utils import ( from utils.sys_utils import (
check_package_install, check_package_install,
install_system_packages, install_system_packages,
@@ -33,6 +33,8 @@ from utils.sys_utils import (
class KlipperBuildFirmwareMenu(BaseMenu): class KlipperBuildFirmwareMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Build Firmware Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"}
self.missing_deps: List[str] = check_package_install(self.deps) self.missing_deps: List[str] = check_package_install(self.deps)
@@ -53,13 +55,8 @@ class KlipperBuildFirmwareMenu(BaseMenu):
self.default_option = Option(method=self.install_missing_deps) self.default_option = Option(method=self.install_missing_deps)
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Build Firmware Menu ] "
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ The following dependencies are required: ║ ║ The following dependencies are required: ║
║ ║ ║ ║
@@ -67,20 +64,22 @@ class KlipperBuildFirmwareMenu(BaseMenu):
)[1:] )[1:]
for d in self.deps: for d in self.deps:
status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}" status_ok = Color.apply("*INSTALLED*", Color.GREEN)
status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}" status_missing = Color.apply("*MISSING*", Color.RED)
status = status_missing if d in self.missing_deps else status_ok status = status_missing if d in self.missing_deps else status_ok
padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) padding = 39 - len(d) + len(status) + (len(status_ok) - len(status))
d = f" {COLOR_CYAN}{d}{RESET_FORMAT}" d = Color.apply(f" {d}", Color.CYAN)
menu += f"{d}{status:>{padding}}\n" menu += f"{d}{status:>{padding}}\n"
menu += "║ ║\n" menu += "║ ║\n"
if len(self.missing_deps) == 0: color = Color.GREEN if len(self.missing_deps) == 0 else Color.RED
line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}" txt = (
else: "All dependencies are met!"
line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}" if len(self.missing_deps) == 0
else "Dependencies are missing!"
)
menu += f"{line:<62}\n" menu += f"{Color.apply(txt, color):<62}\n"
menu += "╟───────────────────────────────────────────────────────╢\n" menu += "╟───────────────────────────────────────────────────────╢\n"
print(menu, end="") print(menu, end="")

View File

@@ -12,9 +12,9 @@ import textwrap
from typing import Type from typing import Type
from components.klipper_firmware.flash_options import FlashMethod, FlashOptions from components.klipper_firmware.flash_options import FlashMethod, FlashOptions
from core.constants import COLOR_RED, RESET_FORMAT
from core.menus import FooterType, Option from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -22,6 +22,9 @@ from core.menus.base_menu import BaseMenu
class KlipperNoFirmwareErrorMenu(BaseMenu): class KlipperNoFirmwareErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! NO FIRMWARE FILE FOUND !!!"
self.title_color = Color.RED
self.title_style = MenuTitleStyle.PLAIN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
@@ -35,16 +38,11 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
self.default_option = Option(method=self.go_back) self.default_option = Option(method=self.go_back)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! NO FIRMWARE FILE FOUND !!!" line1 = "Unable to find a compiled firmware file!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {Color.apply(line1, Color.RED):<62}
║ ║ ║ ║
║ Make sure, that: ║ ║ Make sure, that: ║
║ ● the folder '~/klipper/out' and its content exist ║ ║ ● the folder '~/klipper/out' and its content exist ║
@@ -71,6 +69,9 @@ class KlipperNoFirmwareErrorMenu(BaseMenu):
class KlipperNoBoardTypesErrorMenu(BaseMenu): class KlipperNoBoardTypesErrorMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! ERROR GETTING BOARD LIST !!!"
self.title_color = Color.RED
self.title_style = MenuTitleStyle.PLAIN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.footer_type = FooterType.BLANK self.footer_type = FooterType.BLANK
self.input_label_txt = "Press ENTER to go back to [Main Menu]" self.input_label_txt = "Press ENTER to go back to [Main Menu]"
@@ -82,16 +83,11 @@ class KlipperNoBoardTypesErrorMenu(BaseMenu):
self.default_option = Option(method=self.go_back) self.default_option = Option(method=self.go_back)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ERROR GETTING BOARD LIST !!!" line1 = "Reading the list of supported boards failed!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:<62} {Color.apply(line1, Color.RED):<62}
║ ║ ║ ║
║ Make sure, that: ║ ║ Make sure, that: ║
║ ● the folder '~/klipper' and all its content exist ║ ║ ● the folder '~/klipper' and all its content exist ║

View File

@@ -9,16 +9,21 @@
from __future__ import annotations from __future__ import annotations
import textwrap import textwrap
from typing import Type from typing import Tuple, Type
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.menus.base_menu import BaseMenu from core.types.color import Color
def __title_config__() -> Tuple[str, Color, MenuTitleStyle]:
return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN
# noinspection DuplicatedCode # noinspection DuplicatedCode
class KlipperFlashMethodHelpMenu(BaseMenu): class KlipperFlashMethodHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -34,15 +39,10 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("Regular flashing method:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN)
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ The default method to flash controller boards which ║ ║ The default method to flash controller boards which ║
@@ -77,6 +77,7 @@ class KlipperFlashMethodHelpMenu(BaseMenu):
class KlipperFlashCommandHelpMenu(BaseMenu): class KlipperFlashCommandHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -92,15 +93,10 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("make flash:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("make serialflash:", Color.CYAN)
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ The default command to flash controller board, it ║ ║ The default command to flash controller board, it ║
@@ -121,6 +117,7 @@ class KlipperFlashCommandHelpMenu(BaseMenu):
class KlipperMcuConnectionHelpMenu(BaseMenu): class KlipperMcuConnectionHelpMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title, self.title_color, self.title_style = __title_config__()
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -138,15 +135,12 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
pass pass
def print_menu(self) -> None: def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > " subheader1 = Color.apply("USB:", Color.CYAN)
color = COLOR_YELLOW subheader2 = Color.apply("UART:", Color.CYAN)
count = 62 - len(color) - len(RESET_FORMAT) subheader3 = Color.apply("USB DFU:", Color.CYAN)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}" subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN)
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{subheader1:<62} {subheader1:<62}
║ Selecting USB as the connection method will scan the ║ ║ Selecting USB as the connection method will scan the ║
@@ -164,6 +158,19 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
║ port your controller board is connected to when using ║ ║ port your controller board is connected to when using ║
║ this connection method. ║ ║ 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:] )[1:]

View File

@@ -17,6 +17,7 @@ from components.klipper_firmware.firmware_utils import (
find_uart_device, find_uart_device,
find_usb_device_by_id, find_usb_device_by_id,
find_usb_dfu_device, find_usb_dfu_device,
find_usb_rp2_boot_device,
get_sd_flash_board_list, get_sd_flash_board_list,
start_flash_process, start_flash_process,
) )
@@ -35,10 +36,10 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import (
KlipperFlashMethodHelpMenu, KlipperFlashMethodHelpMenu,
KlipperMcuConnectionHelpMenu, KlipperMcuConnectionHelpMenu,
) )
from core.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import FooterType, Option from core.menus import FooterType, Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu, MenuTitleStyle
from core.types.color import Color
from utils.input_utils import get_number_input from utils.input_utils import get_number_input
@@ -47,6 +48,8 @@ from utils.input_utils import get_number_input
class KlipperFlashMethodMenu(BaseMenu): class KlipperFlashMethodMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "MCU Flash Menu"
self.title_color = Color.CYAN
self.help_menu = KlipperFlashMethodHelpMenu self.help_menu = KlipperFlashMethodHelpMenu
self.input_label_txt = "Select flash method" self.input_label_txt = "Select flash method"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -66,17 +69,13 @@ class KlipperFlashMethodMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ MCU Flash Menu ] " subheader = Color.apply("ATTENTION:", Color.YELLOW)
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}" subline1 = Color.apply(
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}" "Make sure to select the correct method for the MCU!", Color.YELLOW
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}" )
subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW)
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Select the flash method for flashing the MCU. ║ ║ Select the flash method for flashing the MCU. ║
║ ║ ║ ║
@@ -111,6 +110,9 @@ class KlipperFlashMethodMenu(BaseMenu):
class KlipperFlashCommandMenu(BaseMenu): class KlipperFlashCommandMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "Which flash command to use for flashing the MCU?"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.YELLOW
self.help_menu = KlipperFlashCommandHelpMenu self.help_menu = KlipperFlashCommandHelpMenu
self.input_label_txt = "Select flash command" self.input_label_txt = "Select flash command"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -131,8 +133,6 @@ class KlipperFlashCommandMenu(BaseMenu):
def print_menu(self) -> None: def print_menu(self) -> None:
menu = textwrap.dedent( menu = textwrap.dedent(
""" """
╔═══════════════════════════════════════════════════════╗
║ Which flash command to use for flashing the MCU? ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) make flash (default) ║ ║ 1) make flash (default) ║
║ 2) make serialflash (stm32flash) ║ ║ 2) make serialflash (stm32flash) ║
@@ -160,6 +160,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False
): ):
super().__init__() super().__init__()
self.title = "Make sure that the controller board is connected now!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.__standalone = standalone self.__standalone = standalone
self.help_menu = KlipperMcuConnectionHelpMenu self.help_menu = KlipperMcuConnectionHelpMenu
@@ -177,22 +180,19 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
"1": Option(method=self.select_usb), "1": Option(method=self.select_usb),
"2": Option(method=self.select_dfu), "2": Option(method=self.select_dfu),
"3": Option(method=self.select_usb_dfu), "3": Option(method=self.select_usb_dfu),
"4": Option(method=self.select_usb_rp2040),
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = "Make sure that the controller board is connected now!"
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ How is the controller board connected to the host? ║ ║ How is the controller board connected to the host? ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) USB ║ ║ 1) USB ║
║ 2) UART ║ ║ 2) UART ║
║ 3) USB (DFU mode) ║ ║ 3) USB (DFU mode) ║
║ 4) USB (RP2040 mode) ║
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
""" """
)[1:] )[1:]
@@ -210,6 +210,10 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self.flash_options.connection_type = ConnectionType.USB_DFU self.flash_options.connection_type = ConnectionType.USB_DFU
self.get_mcu_list() 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): def get_mcu_list(self, **kwargs):
conn_type = self.flash_options.connection_type conn_type = self.flash_options.connection_type
@@ -222,6 +226,11 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
elif conn_type is ConnectionType.USB_DFU: elif conn_type is ConnectionType.USB_DFU:
Logger.print_status("Identifying MCU connected via USB in DFU mode ...") Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
self.flash_options.mcu_list = find_usb_dfu_device() self.flash_options.mcu_list = find_usb_dfu_device()
elif conn_type is ConnectionType.USB_RP2040:
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: if len(self.flash_options.mcu_list) < 1:
Logger.print_warn("No MCUs found!") Logger.print_warn("No MCUs found!")
@@ -231,7 +240,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
if self.__standalone and len(self.flash_options.mcu_list) > 0: if self.__standalone and len(self.flash_options.mcu_list) > 0:
Logger.print_ok("The following MCUs were found:", prefix=False) Logger.print_ok("The following MCUs were found:", prefix=False)
for i, mcu in enumerate(self.flash_options.mcu_list): for i, mcu in enumerate(self.flash_options.mcu_list):
print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}") print(f" ● MCU #{i}: {Color.CYAN}{mcu}{Color.RST}")
time.sleep(3) time.sleep(3)
return return
@@ -246,6 +255,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
class KlipperSelectMcuIdMenu(BaseMenu): class KlipperSelectMcuIdMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
self.mcu_list = self.flash_options.mcu_list self.mcu_list = self.flash_options.mcu_list
self.input_label_txt = "Select MCU to flash" self.input_label_txt = "Select MCU to flash"
@@ -264,14 +276,9 @@ class KlipperSelectMcuIdMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ATTENTION !!!" header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]"
header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Make sure, to select the correct MCU! ║ ║ Make sure, to select the correct MCU! ║
║ ONLY flash a firmware created for the respective MCU! ║ ║ ONLY flash a firmware created for the respective MCU! ║
@@ -283,7 +290,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
for i, mcu in enumerate(self.mcu_list): for i, mcu in enumerate(self.mcu_list):
mcu = mcu.split("/")[-1] mcu = mcu.split("/")[-1]
menu += f"{i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}\n" menu += f"{i}) {Color.apply(f'{mcu:<51}', Color.CYAN)}\n"
menu += textwrap.dedent( menu += textwrap.dedent(
""" """
@@ -338,7 +345,6 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
else: else:
menu = textwrap.dedent( menu = textwrap.dedent(
""" """
╔═══════════════════════════════════════════════════════╗
║ Please select the type of board that corresponds to ║ ║ Please select the type of board that corresponds to ║
║ the currently selected MCU ID you chose before. ║ ║ the currently selected MCU ID you chose before. ║
║ ║ ║ ║
@@ -390,6 +396,9 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
class KlipperFlashOverviewMenu(BaseMenu): class KlipperFlashOverviewMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): def __init__(self, previous_menu: Type[BaseMenu] | None = None):
super().__init__() super().__init__()
self.title = "!!! ATTENTION !!!"
self.title_style = MenuTitleStyle.PLAIN
self.title_color = Color.RED
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
self.input_label_txt = "Perform action (default=Y)" self.input_label_txt = "Perform action (default=Y)"
@@ -405,21 +414,16 @@ class KlipperFlashOverviewMenu(BaseMenu):
self.default_option = Option(self.execute_flash) self.default_option = Option(self.execute_flash)
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
method = self.flash_options.flash_method.value method = self.flash_options.flash_method.value
command = self.flash_options.flash_command.value command = self.flash_options.flash_command.value
conn_type = self.flash_options.connection_type.value conn_type = self.flash_options.connection_type.value
mcu = self.flash_options.selected_mcu.split("/")[-1] mcu = self.flash_options.selected_mcu.split("/")[-1]
board = self.flash_options.selected_board board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" color = Color.CYAN
subheader = f"[{Color.apply('Overview', color)}]"
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Before contuining the flashing process, please check ║ ║ Before contuining the flashing process, please check ║
║ if all parameters were set correctly! Once you made ║ ║ if all parameters were set correctly! Once you made ║
@@ -433,18 +437,18 @@ class KlipperFlashOverviewMenu(BaseMenu):
menu += textwrap.dedent( menu += textwrap.dedent(
f""" f"""
║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT} ║ MCU: {Color.apply(f"{mcu:<48}", color)}
║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT} ║ Connection: {Color.apply(f"{conn_type:<41}", color)}
║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT} ║ Flash method: {Color.apply(f"{method:<39}", color)}
║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT} ║ Flash command: {Color.apply(f"{command:<38}", color)}
""" """
)[1:] )[1:]
if self.flash_options.flash_method is FlashMethod.SD_CARD: if self.flash_options.flash_method is FlashMethod.SD_CARD:
menu += textwrap.dedent( menu += textwrap.dedent(
f""" f"""
║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT} ║ Board type: {Color.apply(f"{board:<41}", color)}
║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT} ║ Baudrate: {Color.apply(f"{baudrate:<43}", color)}
""" """
)[1:] )[1:]

View File

@@ -30,7 +30,7 @@ from core.constants import SYSTEMD
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import ( from utils.common import (
check_install_dependencies, check_install_dependencies,
get_install_status, get_install_status,
@@ -103,8 +103,8 @@ def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None:
options=[ options=[
("type", "git_repo"), ("type", "git_repo"),
("path", KLIPPERSCREEN_DIR.as_posix()), ("path", KLIPPERSCREEN_DIR.as_posix()),
("orgin", KLIPPERSCREEN_REPO), ("origin", KLIPPERSCREEN_REPO),
("manages_servcies", "KlipperScreen"), ("managed_services", "KlipperScreen"),
("env", f"{KLIPPERSCREEN_ENV_DIR}/bin/python"), ("env", f"{KLIPPERSCREEN_ENV_DIR}/bin/python"),
("requirements", KLIPPERSCREEN_REQ_FILE.as_posix()), ("requirements", KLIPPERSCREEN_REQ_FILE.as_posix()),
("install_script", KLIPPERSCREEN_INSTALL_SCRIPT.as_posix()), ("install_script", KLIPPERSCREEN_INSTALL_SCRIPT.as_posix()),

View File

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

View File

@@ -1,201 +0,0 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import shutil
from pathlib import Path
from subprocess import CalledProcessError, run
from typing import List
from components.klipper.klipper import Klipper
from components.mobileraker import (
MOBILERAKER_BACKUP_DIR,
MOBILERAKER_DIR,
MOBILERAKER_ENV_DIR,
MOBILERAKER_INSTALL_SCRIPT,
MOBILERAKER_LOG_NAME,
MOBILERAKER_REPO,
MOBILERAKER_REQ_FILE,
MOBILERAKER_SERVICE_FILE,
MOBILERAKER_SERVICE_NAME,
MOBILERAKER_UPDATER_SECTION_NAME,
)
from components.moonraker.moonraker import Moonraker
from core.backup_manager.backup_manager import BackupManager
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 utils.common import check_install_dependencies, get_install_status
from utils.config_utils import add_config_section, remove_config_section
from utils.git_utils import (
git_clone_wrapper,
git_pull_wrapper,
)
from utils.input_utils import get_confirm
from utils.instance_utils import get_instances
from utils.sys_utils import (
check_python_version,
cmd_sysctl_service,
install_python_requirements,
remove_system_service,
)
def install_mobileraker() -> None:
Logger.print_status("Installing Mobileraker's companion ...")
if not check_python_version(3, 7):
return
mr_instances = get_instances(Moonraker)
if not mr_instances:
Logger.print_dialog(
DialogType.WARNING,
[
"Moonraker not found! Mobileraker's companion will not properly work "
"without a working Moonraker installation.",
"Mobileraker's companion's update manager configuration for Moonraker "
"will not be added to any moonraker.conf.",
],
)
if not get_confirm(
"Continue Mobileraker's companion installation?",
default_choice=False,
allow_go_back=True,
):
return
check_install_dependencies()
git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR)
try:
run(MOBILERAKER_INSTALL_SCRIPT.as_posix(), shell=True, check=True)
if mr_instances:
patch_mobileraker_update_manager(mr_instances)
InstanceManager.restart_all(mr_instances)
else:
Logger.print_info(
"Moonraker is not installed! Cannot add Mobileraker's "
"companion to update manager!"
)
Logger.print_ok("Mobileraker's companion successfully installed!")
except CalledProcessError as e:
Logger.print_error(f"Error installing Mobileraker's companion:\n{e}")
return
def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None:
add_config_section(
section=MOBILERAKER_UPDATER_SECTION_NAME,
instances=instances,
options=[
("type", "git_repo"),
("path", MOBILERAKER_DIR.as_posix()),
("origin", MOBILERAKER_REPO),
("primary_branch", "main"),
("managed_services", "mobileraker"),
("env", f"{MOBILERAKER_ENV_DIR}/bin/python"),
("requirements", MOBILERAKER_REQ_FILE.as_posix()),
("install_script", MOBILERAKER_INSTALL_SCRIPT.as_posix()),
],
)
def update_mobileraker() -> None:
try:
if not MOBILERAKER_DIR.exists():
Logger.print_info(
"Mobileraker's companion does not seem to be installed! Skipping ..."
)
return
Logger.print_status("Updating Mobileraker's companion ...")
cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "stop")
settings = KiauhSettings()
if settings.kiauh.backup_before_update:
backup_mobileraker_dir()
git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR)
install_python_requirements(MOBILERAKER_ENV_DIR, MOBILERAKER_REQ_FILE)
cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "start")
Logger.print_ok("Mobileraker's companion updated successfully.", end="\n\n")
except CalledProcessError as e:
Logger.print_error(f"Error updating Mobileraker's companion:\n{e}")
return
def get_mobileraker_status() -> ComponentStatus:
return get_install_status(
MOBILERAKER_DIR,
MOBILERAKER_ENV_DIR,
files=[MOBILERAKER_SERVICE_FILE],
)
def remove_mobileraker() -> None:
Logger.print_status("Removing Mobileraker's companion ...")
try:
if MOBILERAKER_DIR.exists():
Logger.print_status("Removing Mobileraker's companion directory ...")
shutil.rmtree(MOBILERAKER_DIR)
Logger.print_ok("Mobileraker's companion directory successfully removed!")
else:
Logger.print_warn("Mobileraker's companion directory not found!")
if MOBILERAKER_ENV_DIR.exists():
Logger.print_status("Removing Mobileraker's companion environment ...")
shutil.rmtree(MOBILERAKER_ENV_DIR)
Logger.print_ok("Mobileraker's companion environment successfully removed!")
else:
Logger.print_warn("Mobileraker's companion environment not found!")
if MOBILERAKER_SERVICE_FILE.exists():
remove_system_service(MOBILERAKER_SERVICE_NAME)
kl_instances: List[Klipper] = get_instances(Klipper)
for instance in kl_instances:
logfile = instance.base.log_dir.joinpath(MOBILERAKER_LOG_NAME)
if logfile.exists():
Logger.print_status(f"Removing {logfile} ...")
Path(logfile).unlink()
Logger.print_ok(f"{logfile} successfully removed!")
mr_instances: List[Moonraker] = get_instances(Moonraker)
if mr_instances:
Logger.print_status(
"Removing Mobileraker's companion from update manager ..."
)
remove_config_section(MOBILERAKER_UPDATER_SECTION_NAME, mr_instances)
Logger.print_ok(
"Mobileraker's companion successfully removed from update manager!"
)
Logger.print_ok("Mobileraker's companion successfully removed!")
except Exception as e:
Logger.print_error(f"Error removing Mobileraker's companion:\n{e}")
def backup_mobileraker_dir() -> None:
bm = BackupManager()
bm.backup_directory(
MOBILERAKER_DIR.name,
source=MOBILERAKER_DIR,
target=MOBILERAKER_BACKUP_DIR,
)
bm.backup_directory(
MOBILERAKER_ENV_DIR.name,
source=MOBILERAKER_ENV_DIR,
target=MOBILERAKER_BACKUP_DIR,
)

View File

@@ -13,6 +13,8 @@ from core.backup_manager import BACKUP_ROOT_DIR
MODULE_PATH = Path(__file__).resolve().parent MODULE_PATH = Path(__file__).resolve().parent
MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git"
# names # names
MOONRAKER_CFG_NAME = "moonraker.conf" MOONRAKER_CFG_NAME = "moonraker.conf"
MOONRAKER_LOG_NAME = "moonraker.log" MOONRAKER_LOG_NAME = "moonraker.log"

View File

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

View File

@@ -43,6 +43,7 @@ class Moonraker:
env_dir: Path = MOONRAKER_ENV_DIR env_dir: Path = MOONRAKER_ENV_DIR
data_dir: Path = field(init=False) data_dir: Path = field(init=False)
cfg_file: Path = field(init=False) cfg_file: Path = field(init=False)
env_file: Path = field(init=False)
backup_dir: Path = field(init=False) backup_dir: Path = field(init=False)
certs_dir: Path = field(init=False) certs_dir: Path = field(init=False)
db_dir: Path = field(init=False) db_dir: Path = field(init=False)
@@ -55,6 +56,7 @@ class Moonraker:
self.service_file_path: Path = get_service_file_path(Moonraker, self.suffix) self.service_file_path: Path = get_service_file_path(Moonraker, self.suffix)
self.data_dir: Path = self.base.data_dir self.data_dir: Path = self.base.data_dir
self.cfg_file: Path = self.base.cfg_dir.joinpath(MOONRAKER_CFG_NAME) self.cfg_file: Path = self.base.cfg_dir.joinpath(MOONRAKER_CFG_NAME)
self.env_file: Path = self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME)
self.backup_dir: Path = self.base.data_dir.joinpath("backup") self.backup_dir: Path = self.base.data_dir.joinpath("backup")
self.certs_dir: Path = self.base.data_dir.joinpath("certs") self.certs_dir: Path = self.base.data_dir.joinpath("certs")
self.db_dir: Path = self.base.data_dir.joinpath("database") self.db_dir: Path = self.base.data_dir.joinpath("database")
@@ -138,7 +140,7 @@ class Moonraker:
return None return None
scp = SimpleConfigParser() scp = SimpleConfigParser()
scp.read(self.cfg_file) scp.read_file(self.cfg_file)
port: int | None = scp.getint("server", "port", fallback=None) port: int | None = scp.getint("server", "port", fallback=None)
return port return port

View File

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

View File

@@ -94,6 +94,7 @@ def remove_instances(
for instance in instance_list: for instance in instance_list:
Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...") Logger.print_status(f"Removing instance {instance.service_file_path.stem} ...")
InstanceManager.remove(instance) InstanceManager.remove(instance)
delete_moonraker_env_file(instance)
def remove_polkit_rules() -> None: def remove_polkit_rules() -> None:
@@ -111,14 +112,10 @@ def remove_polkit_rules() -> None:
Logger.print_ok("Policykit rules successfully removed!") Logger.print_ok("Policykit rules successfully removed!")
def delete_moonraker_logs(instances: List[Moonraker]) -> None: def delete_moonraker_env_file(instance: Moonraker):
all_logfiles = [] Logger.print_status(f"Remove '{instance.env_file}'")
for instance in instances: if not instance.env_file.exists():
all_logfiles = list(instance.base.log_dir.glob("moonraker.log*")) msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..."
if not all_logfiles: Logger.print_info(msg)
Logger.print_info("No Moonraker logs found. Skipped ...")
return return
run_remove_routines(instance.env_file)
for log in all_logfiles:
Logger.print_status(f"Remove '{log}'")
run_remove_routines(log)

View File

@@ -25,7 +25,7 @@ from core.logger import Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser, SimpleConfigParser,
) )
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.common import get_install_status from utils.common import get_install_status
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import ( from utils.sys_utils import (
@@ -77,20 +77,15 @@ def create_example_moonraker_conf(
uds = instance.base.comms_dir.joinpath("klippy.sock") uds = instance.base.comms_dir.joinpath("klippy.sock")
scp = SimpleConfigParser() scp = SimpleConfigParser()
scp.read(target) scp.read_file(target)
trusted_clients: List[str] = [ trusted_clients: List[str] = [
".".join(ip), f" {'.'.join(ip)}\n",
*scp.get("authorization", "trusted_clients"), *scp.getval("authorization", "trusted_clients"),
] ]
scp.set("server", "port", str(port)) scp.set_option("server", "port", str(port))
scp.set("server", "klippy_uds_address", str(uds)) scp.set_option("server", "klippy_uds_address", str(uds))
scp.set( scp.set_option("authorization", "trusted_clients", trusted_clients)
"authorization",
"trusted_clients",
"\n".join(trusted_clients),
True,
)
# add existing client and client configs in the update section # add existing client and client configs in the update section
if clients is not None and len(clients) > 0: if clients is not None and len(clients) > 0:
@@ -105,7 +100,7 @@ def create_example_moonraker_conf(
] ]
scp.add_section(section=c_section) scp.add_section(section=c_section)
for option in c_options: for option in c_options:
scp.set(c_section, option[0], option[1]) scp.set_option(c_section, option[0], option[1])
# client config part # client config part
c_config = c.client_config c_config = c.client_config
@@ -120,9 +115,9 @@ def create_example_moonraker_conf(
] ]
scp.add_section(section=c_config_section) scp.add_section(section=c_config_section)
for option in c_config_options: for option in c_config_options:
scp.set(c_config_section, option[0], option[1]) scp.set_option(c_config_section, option[0], option[1])
scp.write(target) scp.write_file(target)
Logger.print_ok(f"Example moonraker.conf created in '{instance.base.cfg_dir}'") Logger.print_ok(f"Example moonraker.conf created in '{instance.base.cfg_dir}'")

View File

@@ -1,197 +0,0 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
import json
from typing import List
from components.moonraker.moonraker import Moonraker
from components.octoeverywhere import (
OE_DEPS_JSON_FILE,
OE_DIR,
OE_ENV_DIR,
OE_INSTALL_SCRIPT,
OE_INSTALLER_LOG_FILE,
OE_REPO,
OE_REQ_FILE,
OE_SYS_CFG_NAME,
)
from components.octoeverywhere.octoeverywhere import Octoeverywhere
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from core.types import ComponentStatus
from utils.common import (
check_install_dependencies,
get_install_status,
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,
)
def get_octoeverywhere_status() -> ComponentStatus:
return get_install_status(OE_DIR, OE_ENV_DIR, Octoeverywhere)
def install_octoeverywhere() -> None:
Logger.print_status("Installing OctoEverywhere for Klipper ...")
# check if moonraker is installed. if not, notify the user and exit
if not moonraker_exists():
return
force_clone = False
oe_instances: List[Octoeverywhere] = get_instances(Octoeverywhere)
if oe_instances:
Logger.print_dialog(
DialogType.INFO,
[
"OctoEverywhere 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 OctoEverywhere installation?"):
Logger.print_info("Exiting OctoEverywhere for Klipper installation ...")
return
else:
Logger.print_status("Re-Installing OctoEverywhere 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 OctoEverywhere!",
],
)
if not get_confirm(
"Continue OctoEverywhere for Klipper installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting OctoEverywhere for Klipper installation ...")
return
try:
git_clone_wrapper(OE_REPO, OE_DIR, force=force_clone)
for moonraker in mr_instances:
instance = Octoeverywhere(suffix=moonraker.suffix)
instance.create()
InstanceManager.restart_all(mr_instances)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoEverywhere for Klipper successfully installed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(
f"Error during OctoEverywhere for Klipper installation:\n{e}"
)
def update_octoeverywhere() -> None:
Logger.print_status("Updating OctoEverywhere for Klipper ...")
try:
Octoeverywhere.update()
Logger.print_dialog(
DialogType.SUCCESS,
["OctoEverywhere for Klipper successfully updated!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoEverywhere for Klipper update:\n{e}")
def remove_octoeverywhere() -> None:
Logger.print_status("Removing OctoEverywhere for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
ob_instances: List[Octoeverywhere] = get_instances(Octoeverywhere)
try:
remove_oe_instances(ob_instances)
remove_oe_dir()
remove_oe_env()
remove_config_section(f"include {OE_SYS_CFG_NAME}", mr_instances)
run_remove_routines(OE_INSTALLER_LOG_FILE)
Logger.print_dialog(
DialogType.SUCCESS,
["OctoEverywhere for Klipper successfully removed!"],
center_content=True,
)
except Exception as e:
Logger.print_error(f"Error during OctoEverywhere for Klipper removal:\n{e}")
def install_oe_dependencies() -> None:
oe_deps = []
if OE_DEPS_JSON_FILE.exists():
with open(OE_DEPS_JSON_FILE, "r") as deps:
oe_deps = json.load(deps).get("debian", [])
elif OE_INSTALL_SCRIPT.exists():
oe_deps = parse_packages_from_file(OE_INSTALL_SCRIPT)
if not oe_deps:
raise ValueError("Error reading OctoEverywhere dependencies!")
check_install_dependencies({*oe_deps})
install_python_requirements(OE_ENV_DIR, OE_REQ_FILE)
def remove_oe_instances(
instance_list: List[Octoeverywhere],
) -> None:
if not instance_list:
Logger.print_info("No OctoEverywhere 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_oe_dir() -> None:
Logger.print_status("Removing OctoEverywhere for Klipper directory ...")
if not OE_DIR.exists():
Logger.print_info(f"'{OE_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OE_DIR)
def remove_oe_env() -> None:
Logger.print_status("Removing OctoEverywhere for Klipper environment ...")
if not OE_ENV_DIR.exists():
Logger.print_info(f"'{OE_ENV_DIR}' does not exist. Skipped ...")
return
run_remove_routines(OE_ENV_DIR)

View File

@@ -37,6 +37,7 @@ class BaseWebClient(ABC):
backup_dir: Path backup_dir: Path
repo_path: str repo_path: str
download_url: str download_url: str
nginx_config: Path
nginx_access_log: Path nginx_access_log: Path
nginx_error_log: Path nginx_error_log: Path
client_config: BaseWebClientConfig client_config: BaseWebClientConfig

View File

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

View File

@@ -34,7 +34,7 @@ from utils.input_utils import get_confirm
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
def install_client_config(client_data: BaseWebClient) -> None: def install_client_config(client_data: BaseWebClient, cfg_backup=True) -> None:
client_config: BaseWebClientConfig = client_data.client_config client_config: BaseWebClientConfig = client_data.client_config
display_name = client_config.display_name display_name = client_config.display_name
@@ -56,7 +56,8 @@ def install_client_config(client_data: BaseWebClient) -> None:
download_client_config(client_config) download_client_config(client_config)
create_client_config_symlink(client_config, kl_instances) create_client_config_symlink(client_config, kl_instances)
backup_printer_config_dir() if cfg_backup:
backup_printer_config_dir()
add_config_section( add_config_section(
section=f"update_manager {client_config.name}", section=f"update_manager {client_config.name}",

View File

@@ -13,15 +13,15 @@ from components.webui_client.base_data import BaseWebClient
from core.logger import DialogType, Logger 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( Logger.print_dialog(
DialogType.WARNING, DialogType.WARNING,
[ [
"No local Moonraker installation was found!", "No local Moonraker installation was found!",
"\n\n", "\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 " "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.", "correctly.",
], ],
) )
@@ -40,20 +40,25 @@ def print_client_already_installed_dialog(name: str) -> None:
def print_client_port_select_dialog( def print_client_port_select_dialog(
name: str, port: int, ports_in_use: List[int] name: str, port: int, ports_in_use: List[int]
) -> None: ) -> None:
Logger.print_dialog( dialog_content: List[str] = [
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}",
f"Please select the port, {name} should be served on. If your are unsure " "\n\n",
f"what to select, hit Enter to apply the suggested value of: {port}", f"In case you need {name} to be served on a specific port, you can set it "
"\n\n", f"now. Make sure that the port is not already used by another application "
f"In case you need {name} to be served on a specific port, you can set it " f"on your system!",
f"now. Make sure that the port is not already used by another application " ]
f"on your system!",
"\n\n", if ports_in_use:
"The following ports were found to be in use already:", dialog_content.extend(
*[f"{port}" for port in ports_in_use], [
], "\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: def print_install_client_config_dialog(client: BaseWebClient) -> None:

View File

@@ -19,6 +19,8 @@ from components.webui_client.client_config.client_config_remove import (
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED
from core.logger import Logger from core.logger import Logger
from core.services.message_service import Message
from core.types.color import Color
from utils.config_utils import remove_config_section from utils.config_utils import remove_config_section
from utils.fs_utils import ( from utils.fs_utils import (
remove_with_sudo, remove_with_sudo,
@@ -32,54 +34,79 @@ def run_client_removal(
remove_client: bool, remove_client: bool,
remove_client_cfg: bool, remove_client_cfg: bool,
backup_config: bool, backup_config: bool,
) -> None: ) -> Message:
completion_msg = Message(
title=f"{client.display_name} Removal Process completed",
color=Color.GREEN,
)
mr_instances: List[Moonraker] = get_instances(Moonraker) mr_instances: List[Moonraker] = get_instances(Moonraker)
kl_instances: List[Klipper] = get_instances(Klipper) kl_instances: List[Klipper] = get_instances(Klipper)
if backup_config: if backup_config:
bm = BackupManager() bm = BackupManager()
bm.backup_file(client.config_file) if bm.backup_file(client.config_file):
completion_msg.text.append(f"{client.config_file.name} backup created")
if remove_client: if remove_client:
client_name = client.name client_name = client.name
remove_client_dir(client) if remove_client_dir(client):
remove_client_nginx_config(client_name) completion_msg.text.append(f"{client.display_name} removed")
remove_client_nginx_logs(client, kl_instances) if remove_client_nginx_config(client_name):
completion_msg.text.append("● NGINX config removed")
if remove_client_nginx_logs(client, kl_instances):
completion_msg.text.append("● NGINX logs removed")
section = f"update_manager {client_name}" section = f"update_manager {client_name}"
remove_config_section(section, mr_instances) handled_instances: List[Moonraker] = remove_config_section(
section, mr_instances
)
if handled_instances:
names = [i.service_file_path.stem for i in handled_instances]
completion_msg.text.append(
f"● Moonraker config section '{section}' removed for instance: {', '.join(names)}"
)
if remove_client_cfg: if remove_client_cfg:
run_client_config_removal( cfg_completion_msg = run_client_config_removal(
client.client_config, client.client_config,
kl_instances, kl_instances,
mr_instances, mr_instances,
) )
if cfg_completion_msg.color == Color.GREEN:
completion_msg.text.extend(cfg_completion_msg.text[1:])
if not completion_msg.text:
completion_msg.color = Color.YELLOW
completion_msg.centered = True
completion_msg.text.append("Nothing to remove.")
else:
completion_msg.text.insert(0, "The following actions were performed:")
return completion_msg
def remove_client_dir(client: BaseWebClient) -> None: def remove_client_dir(client: BaseWebClient) -> bool:
Logger.print_status(f"Removing {client.display_name} ...") Logger.print_status(f"Removing {client.display_name} ...")
run_remove_routines(client.client_dir) return run_remove_routines(client.client_dir)
def remove_client_nginx_config(name: str) -> None: def remove_client_nginx_config(name: str) -> bool:
Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...")
return remove_with_sudo(
remove_with_sudo(NGINX_SITES_AVAILABLE.joinpath(name)) [
remove_with_sudo(NGINX_SITES_ENABLED.joinpath(name)) NGINX_SITES_AVAILABLE.joinpath(name),
NGINX_SITES_ENABLED.joinpath(name),
]
)
def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> None: def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> bool:
Logger.print_status(f"Removing NGINX logs for {client.display_name} ...") Logger.print_status(f"Removing NGINX logs for {client.display_name} ...")
remove_with_sudo(client.nginx_access_log) files = [client.nginx_access_log, client.nginx_error_log]
remove_with_sudo(client.nginx_error_log) if instances:
for instance in instances:
files.append(instance.base.log_dir.joinpath(client.nginx_access_log.name))
files.append(instance.base.log_dir.joinpath(client.nginx_error_log.name))
if not instances: return remove_with_sudo(files)
return
for instance in instances:
run_remove_routines(
instance.base.log_dir.joinpath(client.nginx_access_log.name)
)
run_remove_routines(instance.base.log_dir.joinpath(client.nginx_error_log.name))

View File

@@ -23,7 +23,6 @@ from components.webui_client.client_config.client_config_setup import (
install_client_config, install_client_config,
) )
from components.webui_client.client_dialogs import ( from components.webui_client.client_dialogs import (
print_client_port_select_dialog,
print_install_client_config_dialog, print_install_client_config_dialog,
print_moonraker_not_found_dialog, print_moonraker_not_found_dialog,
) )
@@ -33,18 +32,17 @@ from components.webui_client.client_utils import (
create_nginx_cfg, create_nginx_cfg,
detect_client_cfg_conflict, detect_client_cfg_conflict,
enable_mainsail_remotemode, enable_mainsail_remotemode,
get_next_free_port, get_client_port_selection,
is_valid_port,
read_ports_from_nginx_configs,
symlink_webui_nginx_log, symlink_webui_nginx_log,
) )
from core.instance_manager.instance_manager import InstanceManager from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger from core.logger import DialogType, Logger
from core.settings.kiauh_settings import KiauhSettings 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.config_utils import add_config_section
from utils.fs_utils import unzip from utils.fs_utils import unzip
from utils.input_utils import get_confirm, get_number_input from utils.input_utils import get_confirm
from utils.instance_utils import get_instances from utils.instance_utils import get_instances
from utils.sys_utils import ( from utils.sys_utils import (
cmd_sysctl_service, cmd_sysctl_service,
@@ -53,21 +51,16 @@ from utils.sys_utils import (
) )
def install_client(client: BaseWebClient) -> None: def install_client(
if client is None: client: BaseWebClient,
raise ValueError("Missing parameter client_data!") settings: KiauhSettings,
reinstall: bool = False,
if client.client_dir.exists(): ) -> None:
Logger.print_info(
f"{client.display_name} seems to be already installed! Skipped ..."
)
return
mr_instances: List[Moonraker] = get_instances(Moonraker) mr_instances: List[Moonraker] = get_instances(Moonraker)
enable_remotemode = False enable_remotemode = False
if not mr_instances: 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?"): if not get_confirm(f"Continue {client.display_name} installation?"):
return return
@@ -92,21 +85,10 @@ def install_client(client: BaseWebClient) -> None:
question = f"Download the recommended {client_config.display_name}?" question = f"Download the recommended {client_config.display_name}?"
install_client_cfg = get_confirm(question, allow_go_back=False) install_client_cfg = get_confirm(question, allow_go_back=False)
settings = KiauhSettings() default_port: int = int(settings.get(client.name, "port"))
port: int = settings.get(client.name, "port") port: int = (
ports_in_use: List[int] = read_ports_from_nginx_configs() default_port if reinstall else get_client_port_selection(client, settings)
)
# 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)
check_install_dependencies({"nginx"}) check_install_dependencies({"nginx"})
@@ -114,20 +96,22 @@ def install_client(client: BaseWebClient) -> None:
download_client(client) download_client(client)
if enable_remotemode and client.client == WebClientType.MAINSAIL: if enable_remotemode and client.client == WebClientType.MAINSAIL:
enable_mainsail_remotemode() enable_mainsail_remotemode()
if mr_instances:
add_config_section( backup_printer_config_dir()
section=f"update_manager {client.name}", add_config_section(
instances=mr_instances, section=f"update_manager {client.name}",
options=[ instances=mr_instances,
("type", "web"), options=[
("channel", "stable"), ("type", "web"),
("repo", str(client.repo_path)), ("channel", "stable"),
("path", str(client.client_dir)), ("repo", str(client.repo_path)),
], ("path", str(client.client_dir)),
) ],
InstanceManager.restart_all(mr_instances) )
InstanceManager.restart_all(mr_instances)
if install_client_cfg and kl_instances: if install_client_cfg and kl_instances:
install_client_config(client) install_client_config(client, False)
copy_upstream_nginx_cfg() copy_upstream_nginx_cfg()
copy_common_vars_nginx_cfg() copy_common_vars_nginx_cfg()
@@ -145,12 +129,24 @@ def install_client(client: BaseWebClient) -> None:
cmd_sysctl_service("nginx", "restart") cmd_sysctl_service("nginx", "restart")
except Exception as e: except Exception as e:
Logger.print_error(f"{client.display_name} installation failed!\n{e}") Logger.print_error(e)
Logger.print_dialog(
DialogType.ERROR,
center_content=True,
content=[f"{client.display_name} installation failed!"],
)
return return
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}" # noinspection HttpUrlsUsage
Logger.print_ok(f"{client.display_name} installation complete!", start="\n") Logger.print_dialog(
Logger.print_ok(log, prefix=False, end="\n\n") DialogType.CUSTOM,
custom_title=f"{client.display_name} installation complete!",
custom_color=Color.GREEN,
center_content=True,
content=[
f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}",
],
)
def download_client(client: BaseWebClient) -> None: def download_client(client: BaseWebClient) -> None:

View File

@@ -21,26 +21,30 @@ from components.webui_client.base_data import (
BaseWebClient, BaseWebClient,
WebClientType, 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.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.backup_manager.backup_manager import BackupManager from core.backup_manager.backup_manager import BackupManager
from core.constants import ( from core.constants import (
COLOR_CYAN,
COLOR_YELLOW,
NGINX_CONFD, NGINX_CONFD,
NGINX_SITES_AVAILABLE, NGINX_SITES_AVAILABLE,
NGINX_SITES_ENABLED, NGINX_SITES_ENABLED,
RESET_FORMAT,
) )
from core.logger import Logger from core.logger import Logger
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
from core.types import ComponentStatus from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
from core.types.color import Color
from core.types.component_status import ComponentStatus
from utils.common import get_install_status from utils.common import get_install_status
from utils.fs_utils import create_symlink, remove_file from utils.fs_utils import create_symlink, remove_file
from utils.git_utils import ( from utils.git_utils import (
get_latest_remote_tag, get_latest_remote_tag,
get_latest_unstable_tag, get_latest_unstable_tag,
) )
from utils.input_utils import get_number_input
from utils.instance_utils import get_instances
def get_client_status( def get_client_status(
@@ -67,20 +71,46 @@ def get_client_config_status(client: BaseWebClient) -> ComponentStatus:
return get_install_status(client.client_config.config_dir) return get_install_status(client.client_config.config_dir)
def get_current_client_config(clients: List[BaseWebClient]) -> str: def get_current_client_config() -> str:
installed = [] mainsail, fluidd = MainsailData(), FluiddData()
for client in clients: clients: List[BaseWebClient] = [mainsail, fluidd]
client_config = client.client_config installed = [c for c in clients if c.client_config.config_dir.exists()]
if client_config.config_dir.exists():
installed.append(client)
if len(installed) > 1: if not installed:
return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}" return Color.apply("-", Color.CYAN)
elif len(installed) == 1: elif len(installed) == 1:
cfg = installed[0].client_config cfg = installed[0].client_config
return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}" return Color.apply(cfg.display_name, Color.CYAN)
return f"{COLOR_CYAN}-{RESET_FORMAT}" # 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
mainsail_includes, fluidd_includes = [], []
klipper_instances: List[Klipper] = get_instances(Klipper)
for instance in klipper_instances:
scp = SimpleConfigParser()
scp.read_file(instance.cfg_file)
includes_mainsail = scp.has_section(mainsail.client_config.config_section)
includes_fluidd = scp.has_section(fluidd.client_config.config_section)
if includes_mainsail:
mainsail_includes.append(instance)
if includes_fluidd:
fluidd_includes.append(instance)
# if both are included in the same file, we have a potential conflict
if includes_mainsail and includes_fluidd:
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 Color.apply("-", Color.CYAN)
elif len(fluidd_includes) > len(mainsail_includes):
# there are more instances that include fluidd than mainsail
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 Color.apply(mainsail.client_config.display_name, Color.CYAN)
def enable_mainsail_remotemode() -> None: def enable_mainsail_remotemode() -> None:
@@ -306,34 +336,94 @@ def create_nginx_cfg(
raise raise
def get_nginx_config_list() -> List[Path]:
"""
Get a list of all NGINX config files in /etc/nginx/sites-enabled
:return: List of NGINX config files
"""
configs: List[Path] = []
for config in NGINX_SITES_ENABLED.iterdir():
if not config.is_file():
continue
configs.append(config)
return configs
def get_nginx_listen_port(config: Path) -> int | None:
"""
Get the listen port from an NGINX config file
:param config: The NGINX config file to read the port from
:return: The listen port as int or None if not found/parsable
"""
# noinspection HttpUrlsUsage
pattern = r"default_server|http://|https://|[;\[\]]"
port = ""
with open(config, "r") as cfg:
for line in cfg.readlines():
line = re.sub(pattern, "", line.strip())
if line.startswith("listen"):
if ":" not in line:
port = line.split()[-1]
else:
port = line.split(":")[-1]
try:
return int(port)
except ValueError:
Logger.print_error(
f"Unable to parse listen port {port} from {config.name}!"
)
return None
def read_ports_from_nginx_configs() -> List[int]: def read_ports_from_nginx_configs() -> List[int]:
""" """
Helper function to iterate over all NGINX configs and read all ports defined for listen Helper function to iterate over all NGINX configs
and read all ports defined for listen
:return: A sorted list of listen ports :return: A sorted list of listen ports
""" """
if not NGINX_SITES_ENABLED.exists(): if not NGINX_SITES_ENABLED.exists():
return [] return []
port_list = [] port_list: List[int] = []
for config in NGINX_SITES_ENABLED.iterdir(): for config in get_nginx_config_list():
if not config.is_file(): port = get_nginx_listen_port(config)
continue if port is not None:
port_list.append(port)
with open(config, "r") as cfg: return sorted(port_list, key=lambda x: int(x))
lines = cfg.readlines()
for line in lines:
line = 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))
def is_valid_port(port: int, ports_in_use: List[int]) -> bool: def get_client_port_selection(
return port not in ports_in_use 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: def get_next_free_port(ports_in_use: List[int]) -> int:
@@ -341,3 +431,23 @@ def get_next_free_port(ports_in_use: List[int]) -> int:
used_ports = set(map(int, ports_in_use)) used_ports = set(map(int, ports_in_use))
return min(valid_ports - used_ports) return min(valid_ports - used_ports)
def set_listen_port(client: BaseWebClient, curr_port: int, new_port: int) -> None:
"""
Set the port the client should listen on in the NGINX config
:param curr_port: The current port the client listens on
:param new_port: The new port to set
:param client: The client to set the port for
:return: None
"""
config = NGINX_SITES_AVAILABLE.joinpath(client.name)
with open(config, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if "listen" in line:
lines[i] = line.replace(str(curr_port), str(new_port))
with open(config, "w") as f:
f.writelines(lines)

View File

@@ -19,6 +19,7 @@ from components.webui_client.base_data import (
WebClientType, WebClientType,
) )
from core.backup_manager import BACKUP_ROOT_DIR from core.backup_manager import BACKUP_ROOT_DIR
from core.constants import NGINX_SITES_AVAILABLE
@dataclass() @dataclass()
@@ -44,6 +45,7 @@ class FluiddData(BaseWebClient):
config_file: Path = client_dir.joinpath("config.json") config_file: Path = client_dir.joinpath("config.json")
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups") backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
repo_path: str = "fluidd-core/fluidd" repo_path: str = "fluidd-core/fluidd"
nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd")
nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log") nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log")
nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log") nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log")
client_config: BaseWebClientConfig = None client_config: BaseWebClientConfig = None

View File

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

View File

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

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

View File

@@ -17,6 +17,10 @@ from core.logger import Logger
from utils.common import get_current_date from utils.common import get_current_date
class BackupManagerException(Exception):
pass
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
class BackupManager: class BackupManager:
@@ -40,12 +44,14 @@ class BackupManager:
def ignore_folders(self, value: List[str]): def ignore_folders(self, value: List[str]):
self._ignore_folders = value self._ignore_folders = value
def backup_file(self, file: Path, target: Path | None = None, custom_filename=None): def backup_file(
self, file: Path, target: Path | None = None, custom_filename=None
) -> bool:
Logger.print_status(f"Creating backup of {file} ...") Logger.print_status(f"Creating backup of {file} ...")
if not file.exists(): if not file.exists():
Logger.print_info("File does not exist! Skipping ...") Logger.print_info("File does not exist! Skipping ...")
return return False
target = self.backup_root_dir if target is None else target target = self.backup_root_dir if target is None else target
@@ -58,14 +64,17 @@ class BackupManager:
Path(target).mkdir(exist_ok=True) Path(target).mkdir(exist_ok=True)
shutil.copyfile(file, target.joinpath(filename)) shutil.copyfile(file, target.joinpath(filename))
Logger.print_ok("Backup successful!") Logger.print_ok("Backup successful!")
return True
except OSError as e: except OSError as e:
Logger.print_error(f"Unable to backup '{file}':\n{e}") Logger.print_error(f"Unable to backup '{file}':\n{e}")
return False
else: else:
Logger.print_info(f"File '{file}' not found ...") Logger.print_info(f"File '{file}' not found ...")
return False
def backup_directory( def backup_directory(
self, name: str, source: Path, target: Path | None = None self, name: str, source: Path, target: Path | None = None
) -> None: ) -> Path | None:
Logger.print_status(f"Creating backup of {name} in {target} ...") Logger.print_status(f"Creating backup of {name} in {target} ...")
if source is None or not Path(source).exists(): if source is None or not Path(source).exists():
@@ -76,15 +85,15 @@ class BackupManager:
try: try:
date = get_current_date().get("date") date = get_current_date().get("date")
time = get_current_date().get("time") time = get_current_date().get("time")
shutil.copytree( backup_target = target.joinpath(f"{name.lower()}-{date}-{time}")
source, shutil.copytree(source, backup_target, ignore=self.ignore_folders_func)
target.joinpath(f"{name.lower()}-{date}-{time}"),
ignore=self.ignore_folders_func,
)
Logger.print_ok("Backup successful!") Logger.print_ok("Backup successful!")
return backup_target
except OSError as e: except OSError as e:
Logger.print_error(f"Unable to backup directory '{source}':\n{e}") Logger.print_error(f"Unable to backup directory '{source}':\n{e}")
return raise BackupManagerException(f"Unable to backup directory '{source}':\n{e}")
def ignore_folders_func(self, dirpath, filenames) -> List[str]: def ignore_folders_func(self, dirpath, filenames) -> List[str]:
return ( return (

View File

@@ -13,15 +13,6 @@ from pathlib import Path
from core.backup_manager import BACKUP_ROOT_DIR from core.backup_manager import BACKUP_ROOT_DIR
# text colors and formats
COLOR_WHITE = "\033[37m" # white
COLOR_MAGENTA = "\033[35m" # magenta
COLOR_GREEN = "\033[92m" # bright green
COLOR_YELLOW = "\033[93m" # bright yellow
COLOR_RED = "\033[91m" # bright red
COLOR_CYAN = "\033[96m" # bright cyan
RESET_FORMAT = "\033[0m" # reset format
# global dependencies # global dependencies
GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"]
@@ -33,7 +24,7 @@ CURRENT_USER = pwd.getpwuid(os.getuid())[0]
# dirs # dirs
SYSTEMD = Path("/etc/systemd/system") 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_AVAILABLE = Path("/etc/nginx/sites-available")
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
NGINX_CONFD = Path("/etc/nginx/conf.d") NGINX_CONFD = Path("/etc/nginx/conf.d")

View File

@@ -12,8 +12,8 @@ from pathlib import Path
from subprocess import CalledProcessError from subprocess import CalledProcessError
from typing import List from typing import List
from core.instance_type import InstanceType
from core.logger import Logger from core.logger import Logger
from utils.instance_type import InstanceType
from utils.sys_utils import cmd_sysctl_service from utils.sys_utils import cmd_sysctl_service

View File

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

View File

@@ -22,9 +22,9 @@ from components.klipper_firmware.menus.klipper_flash_menu import (
) )
from components.moonraker import MOONRAKER_DIR from components.moonraker import MOONRAKER_DIR
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker import Moonraker
from core.constants import COLOR_YELLOW, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from procedures.system import change_system_hostname from procedures.system import change_system_hostname
from utils.git_utils import rollback_repository from utils.git_utils import rollback_repository
@@ -34,6 +34,8 @@ from utils.git_utils import rollback_repository
class AdvancedMenu(BaseMenu): class AdvancedMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Advanced Menu"
self.title_color = Color.YELLOW
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -53,13 +55,8 @@ class AdvancedMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Advanced Menu ] "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
║ Klipper Firmware: │ Repository Rollback: ║ ║ Klipper Firmware: │ Repository Rollback: ║
║ 1) [Build] │ 5) [Klipper] ║ ║ 1) [Build] │ 5) [Klipper] ║

View File

@@ -23,9 +23,9 @@ from components.webui_client.client_utils import (
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
from utils.common import backup_printer_config_dir from utils.common import backup_printer_config_dir
@@ -34,6 +34,8 @@ from utils.common import backup_printer_config_dir
class BackupMenu(BaseMenu): class BackupMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Backup Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -55,14 +57,11 @@ class BackupMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Backup Menu ] " line1 = Color.apply(
line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}" "INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW
color = COLOR_CYAN )
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
{line1:^62} {line1:^62}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢

View File

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

View File

@@ -14,16 +14,18 @@ from typing import Type
from components.crowsnest.crowsnest import install_crowsnest from components.crowsnest.crowsnest import install_crowsnest
from components.klipper import klipper_setup from components.klipper import klipper_setup
from components.klipperscreen.klipperscreen import install_klipperscreen from components.klipperscreen.klipperscreen import install_klipperscreen
from components.mobileraker.mobileraker import install_mobileraker
from components.moonraker import moonraker_setup from components.moonraker import moonraker_setup
from components.octoeverywhere.octoeverywhere_setup import install_octoeverywhere from components.webui_client.client_config.client_config_setup import (
from components.webui_client import client_setup install_client_config,
from components.webui_client.client_config import client_config_setup )
from components.webui_client.client_setup import install_client
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.constants import COLOR_GREEN, RESET_FORMAT from components.webui_client.menus.client_install_menu import ClientInstallMenu
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -31,6 +33,8 @@ from core.menus.base_menu import BaseMenu
class InstallMenu(BaseMenu): class InstallMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Installation Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -47,32 +51,24 @@ class InstallMenu(BaseMenu):
"5": Option(method=self.install_mainsail_config), "5": Option(method=self.install_mainsail_config),
"6": Option(method=self.install_fluidd_config), "6": Option(method=self.install_fluidd_config),
"7": Option(method=self.install_klipperscreen), "7": Option(method=self.install_klipperscreen),
"8": Option(method=self.install_mobileraker), "8": Option(method=self.install_crowsnest),
"9": Option(method=self.install_crowsnest),
"10": Option(method=self.install_octoeverywhere),
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Installation Menu ] "
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
║ Firmware & API: │ Touchscreen GUI: ║ ║ Firmware & API: │ Touchscreen GUI: ║
║ 1) [Klipper] │ 7) [KlipperScreen] ║ ║ 1) [Klipper] │ 7) [KlipperScreen] ║
║ 2) [Moonraker] │ ║ ║ 2) [Moonraker] │ ║
║ │ Android / iOS: ║ │ Webcam Streamer:
║ Webinterface: │ 8) [Mobileraker] ║ Webinterface: │ 8) [Crowsnest]
║ 3) [Mainsail] │ ║ ║ 3) [Mainsail] │ ║
║ 4) [Fluidd] │ Webcam Streamer: ║ 4) [Fluidd] │
║ │ 9) [Crowsnest] ║
║ Client-Config: │ ║
║ 5) [Mainsail-Config] │ Remote Access: ║
║ 6) [Fluidd-Config] │ 10) [OctoEverywhere] ║
║ │ ║ ║ │ ║
║ Client-Config: │ ║
║ 5) [Mainsail-Config] │ ║
║ 6) [Fluidd-Config] │ ║
╟───────────────────────────┴───────────────────────────╢ ╟───────────────────────────┴───────────────────────────╢
""" """
)[1:] )[1:]
@@ -85,25 +81,27 @@ class InstallMenu(BaseMenu):
moonraker_setup.install_moonraker() moonraker_setup.install_moonraker()
def install_mainsail(self, **kwargs) -> None: def install_mainsail(self, **kwargs) -> None:
client_setup.install_client(MainsailData()) client: MainsailData = MainsailData()
if client.client_dir.exists():
ClientInstallMenu(client, self.__class__).run()
else:
install_client(client, settings=KiauhSettings())
def install_mainsail_config(self, **kwargs) -> None: def install_mainsail_config(self, **kwargs) -> None:
client_config_setup.install_client_config(MainsailData()) install_client_config(MainsailData())
def install_fluidd(self, **kwargs) -> None: def install_fluidd(self, **kwargs) -> None:
client_setup.install_client(FluiddData()) client: FluiddData = FluiddData()
if client.client_dir.exists():
ClientInstallMenu(client, self.__class__).run()
else:
install_client(client, settings=KiauhSettings())
def install_fluidd_config(self, **kwargs) -> None: def install_fluidd_config(self, **kwargs) -> None:
client_config_setup.install_client_config(FluiddData()) install_client_config(FluiddData())
def install_klipperscreen(self, **kwargs) -> None: def install_klipperscreen(self, **kwargs) -> None:
install_klipperscreen() install_klipperscreen()
def install_mobileraker(self, **kwargs) -> None:
install_mobileraker()
def install_crowsnest(self, **kwargs) -> None: def install_crowsnest(self, **kwargs) -> None:
install_crowsnest() install_crowsnest()
def install_octoeverywhere(self, **kwargs) -> None:
install_octoeverywhere()

View File

@@ -16,23 +16,13 @@ from components.crowsnest.crowsnest import get_crowsnest_status
from components.klipper.klipper_utils import get_klipper_status from components.klipper.klipper_utils import get_klipper_status
from components.klipperscreen.klipperscreen import get_klipperscreen_status from components.klipperscreen.klipperscreen import get_klipperscreen_status
from components.log_uploads.menus.log_upload_menu import LogUploadMenu from components.log_uploads.menus.log_upload_menu import LogUploadMenu
from components.mobileraker.mobileraker import get_mobileraker_status
from components.moonraker.moonraker_utils import get_moonraker_status from components.moonraker.moonraker_utils import get_moonraker_status
from components.octoeverywhere.octoeverywhere_setup import get_octoeverywhere_status
from components.webui_client.client_utils import ( from components.webui_client.client_utils import (
get_client_status, get_client_status,
get_current_client_config, get_current_client_config,
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.constants import (
COLOR_CYAN,
COLOR_GREEN,
COLOR_MAGENTA,
COLOR_RED,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.logger import Logger from core.logger import Logger
from core.menus import FooterType from core.menus import FooterType
from core.menus.advanced_menu import AdvancedMenu from core.menus.advanced_menu import AdvancedMenu
@@ -42,9 +32,10 @@ from core.menus.install_menu import InstallMenu
from core.menus.remove_menu import RemoveMenu from core.menus.remove_menu import RemoveMenu
from core.menus.settings_menu import SettingsMenu from core.menus.settings_menu import SettingsMenu
from core.menus.update_menu import UpdateMenu from core.menus.update_menu import UpdateMenu
from core.types import ComponentStatus, StatusMap, StatusText from core.types.color import Color
from core.types.component_status import ComponentStatus, StatusMap, StatusText
from extensions.extensions_menu import ExtensionsMenu from extensions.extensions_menu import ExtensionsMenu
from utils.common import get_kiauh_version from utils.common import get_kiauh_version, trunc_string
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -54,12 +45,15 @@ class MainMenu(BaseMenu):
super().__init__() super().__init__()
self.header: bool = True self.header: bool = True
self.title = "Main Menu"
self.title_color = Color.CYAN
self.footer_type: FooterType = FooterType.QUIT self.footer_type: FooterType = FooterType.QUIT
self.version = "" self.version = ""
self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = "" self.kl_status, self.kl_owner, self.kl_repo = "", "", ""
self.ms_status = self.fl_status = self.ks_status = self.mb_status = "" self.mr_status, self.mr_owner, self.mr_repo = "", "", ""
self.cn_status = self.cc_status = self.oe_status = "" self.ms_status, self.fl_status, self.ks_status = "", "", ""
self.cn_status, self.cc_status = "", ""
self._init_status() self._init_status()
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -79,12 +73,12 @@ class MainMenu(BaseMenu):
} }
def _init_status(self) -> None: def _init_status(self) -> None:
status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn", "oe"] status_vars = ["kl", "mr", "ms", "fl", "ks", "cn"]
for var in status_vars: for var in status_vars:
setattr( setattr(
self, self,
f"{var}_status", f"{var}_status",
f"{COLOR_RED}Not installed{RESET_FORMAT}", Color.apply("Not installed", Color.RED),
) )
def _fetch_status(self) -> None: def _fetch_status(self) -> None:
@@ -93,17 +87,16 @@ class MainMenu(BaseMenu):
self._get_component_status("mr", get_moonraker_status) self._get_component_status("mr", get_moonraker_status)
self._get_component_status("ms", get_client_status, MainsailData()) self._get_component_status("ms", get_client_status, MainsailData())
self._get_component_status("fl", get_client_status, FluiddData()) self._get_component_status("fl", get_client_status, FluiddData())
self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
self._get_component_status("ks", get_klipperscreen_status) self._get_component_status("ks", get_klipperscreen_status)
self._get_component_status("mb", get_mobileraker_status)
self._get_component_status("cn", get_crowsnest_status) self._get_component_status("cn", get_crowsnest_status)
self._get_component_status("oe", get_octoeverywhere_status) self.cc_status = get_current_client_config()
def _get_component_status(self, name: str, status_fn: Callable, *args) -> None: def _get_component_status(self, name: str, status_fn: Callable, *args) -> None:
status_data: ComponentStatus = status_fn(*args) status_data: ComponentStatus = status_fn(*args)
code: int = status_data.status code: int = status_data.status
status: StatusText = StatusMap[code] status: StatusText = StatusMap[code]
repo: str = status_data.repo owner: str = trunc_string(status_data.owner, 23)
repo: str = trunc_string(status_data.repo, 23)
instance_count: int = status_data.instances instance_count: int = status_data.instances
count_txt: str = "" count_txt: str = ""
@@ -111,47 +104,44 @@ class MainMenu(BaseMenu):
count_txt = f": {instance_count}" count_txt = f": {instance_count}"
setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt))
setattr(self, f"{name}_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: def _format_by_code(self, code: int, status: str, count: str) -> str:
color = COLOR_RED color = Color.RED
if code == 0: if code == 0:
color = COLOR_RED color = Color.RED
elif code == 1: elif code == 1:
color = COLOR_YELLOW color = Color.YELLOW
elif code == 2: elif code == 2:
color = COLOR_GREEN color = Color.GREEN
return f"{color}{status}{count}{RESET_FORMAT}" return Color.apply(f"{status}{count}", color)
def print_menu(self) -> None: def print_menu(self) -> None:
self._fetch_status() self._fetch_status()
header = " [ Main Menu ] " footer1 = Color.apply(self.version, Color.CYAN)
footer1 = f"{COLOR_CYAN}{self.version}{RESET_FORMAT}" link = Color.apply("https://git.io/JnmlX", Color.MAGENTA)
footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}" footer2 = f"Changelog: {link}"
color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT)
pad1 = 32 pad1 = 32
pad2 = 26 pad2 = 26
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟──────────────────┬────────────────────────────────────╢ ╟──────────────────┬────────────────────────────────────╢
║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}} ║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}}
║ │ Repo: {self.kl_repo:<{pad1}} ║ │ Owner: {self.kl_owner:<{pad1}}
║ 1) [Install] ├────────────────────────────────────╢ ║ 1) [Install] │ Repo: {self.kl_repo:<{pad1}}
║ 2) [Update] │ Moonraker: {self.mr_status:<{pad1}} ║ 2) [Update] ├────────────────────────────────────╢
║ 3) [Remove] │ Repo: {self.mr_repo:<{pad1}} ║ 3) [Remove] │ Moonraker: {self.mr_status:<{pad1}}
║ 4) [Advanced] ├────────────────────────────────────╢ ║ 4) [Advanced] │ Owner: {self.mr_owner:<{pad1}}
║ 5) [Backup] │ Mainsail: {self.ms_status:<{pad2}} ║ 5) [Backup] │ Repo: {self.mr_repo:<{pad1}}
║ ├────────────────────────────────────╢
║ S) [Settings] │ Mainsail: {self.ms_status:<{pad2}}
║ │ Fluidd: {self.fl_status:<{pad2}} ║ │ Fluidd: {self.fl_status:<{pad2}}
S) [Settings] │ Client-Config: {self.cc_status:<{pad2}} Community: │ Client-Config: {self.cc_status:<{pad2}}
│ ║ E) [Extensions] │ ║
Community: │ KlipperScreen: {self.ks_status:<{pad2}} │ KlipperScreen: {self.ks_status:<{pad2}}
║ E) [Extensions] │ Mobileraker: {self.mb_status:<{pad2}}
║ │ OctoEverywhere: {self.oe_status:<{pad2}}
║ │ Crowsnest: {self.cn_status:<{pad2}} ║ │ Crowsnest: {self.cn_status:<{pad2}}
╟──────────────────┼────────────────────────────────────╢ ╟──────────────────┼────────────────────────────────────╢
{footer1:^25}{footer2:^43} {footer1:^25}{footer2:^43}

View File

@@ -14,17 +14,15 @@ from typing import Type
from components.crowsnest.crowsnest import remove_crowsnest from components.crowsnest.crowsnest import remove_crowsnest
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
from components.klipperscreen.klipperscreen import remove_klipperscreen from components.klipperscreen.klipperscreen import remove_klipperscreen
from components.mobileraker.mobileraker import remove_mobileraker
from components.moonraker.menus.moonraker_remove_menu import ( from components.moonraker.menus.moonraker_remove_menu import (
MoonrakerRemoveMenu, MoonrakerRemoveMenu,
) )
from components.octoeverywhere.octoeverywhere_setup import remove_octoeverywhere
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from components.webui_client.menus.client_remove_menu import ClientRemoveMenu from components.webui_client.menus.client_remove_menu import ClientRemoveMenu
from core.constants import COLOR_RED, RESET_FORMAT
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -32,6 +30,8 @@ from core.menus.base_menu import BaseMenu
class RemoveMenu(BaseMenu): class RemoveMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Remove Menu"
self.title_color = Color.RED
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
@@ -46,32 +46,22 @@ class RemoveMenu(BaseMenu):
"3": Option(method=self.remove_mainsail), "3": Option(method=self.remove_mainsail),
"4": Option(method=self.remove_fluidd), "4": Option(method=self.remove_fluidd),
"5": Option(method=self.remove_klipperscreen), "5": Option(method=self.remove_klipperscreen),
"6": Option(method=self.remove_mobileraker), "6": Option(method=self.remove_crowsnest),
"7": Option(method=self.remove_crowsnest),
"8": Option(method=self.remove_octoeverywhere),
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Remove Menu ] "
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
f""" """
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ INFO: Configurations and/or any backups will be kept! ║ ║ INFO: Configurations and/or any backups will be kept! ║
╟───────────────────────────┬───────────────────────────╢ ╟───────────────────────────┬───────────────────────────╢
║ Firmware & API: │ Android / iOS: ║ Firmware & API: │ Touchscreen GUI:
║ 1) [Klipper] │ 6) [Mobileraker] ║ 1) [Klipper] │ 5) [KlipperScreen]
║ 2) [Moonraker] │ ║ ║ 2) [Moonraker] │ ║
║ │ Webcam Streamer: ║ ║ │ Webcam Streamer: ║
║ Klipper Webinterface: │ 7) [Crowsnest] ║ ║ Klipper Webinterface: │ 6) [Crowsnest] ║
║ 3) [Mainsail] │ ║ ║ 3) [Mainsail] │ ║
║ 4) [Fluidd] │ Remote Access: ║ 4) [Fluidd] │
║ │ 8) [OctoEverywhere] ║
║ Touchscreen GUI: │ ║
║ 5) [KlipperScreen] │ ║
╟───────────────────────────┴───────────────────────────╢ ╟───────────────────────────┴───────────────────────────╢
""" """
)[1:] )[1:]
@@ -92,11 +82,5 @@ class RemoveMenu(BaseMenu):
def remove_klipperscreen(self, **kwargs) -> None: def remove_klipperscreen(self, **kwargs) -> None:
remove_klipperscreen() remove_klipperscreen()
def remove_mobileraker(self, **kwargs) -> None:
remove_mobileraker()
def remove_crowsnest(self, **kwargs) -> None: def remove_crowsnest(self, **kwargs) -> None:
remove_crowsnest() remove_crowsnest()
def remove_octoeverywhere(self, **kwargs) -> None:
remove_octoeverywhere()

View File

@@ -8,24 +8,23 @@
# ======================================================================= # # ======================================================================= #
from __future__ import annotations from __future__ import annotations
import shutil
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from typing import Tuple, Type from typing import Literal, Tuple, Type
from components.klipper import KLIPPER_DIR from components.klipper import KLIPPER_DIR, KLIPPER_REPO_URL
from components.klipper.klipper import Klipper from components.klipper.klipper_utils import get_klipper_status
from components.moonraker import MOONRAKER_DIR from components.moonraker import MOONRAKER_DIR, MOONRAKER_REPO_URL
from components.moonraker.moonraker import Moonraker from components.moonraker.moonraker_utils import get_moonraker_status
from core.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.settings.kiauh_settings import KiauhSettings from core.settings.kiauh_settings import KiauhSettings, RepoSettings
from utils.git_utils import git_clone_wrapper from core.types.color import Color
from core.types.component_status import ComponentStatus
from procedures.switch_repo import run_switch_repo_routine
from utils.git_utils import get_repo_name
from utils.input_utils import get_confirm, get_string_input from utils.input_utils import get_confirm, get_string_input
from utils.instance_utils import get_instances
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -33,13 +32,16 @@ from utils.instance_utils import get_instances
class SettingsMenu(BaseMenu): class SettingsMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.title = "Settings Menu"
self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.klipper_repo: str | None = None
self.moonraker_repo: str | None = None
self.mainsail_unstable: bool | None = None self.mainsail_unstable: bool | None = None
self.fluidd_unstable: bool | None = None self.fluidd_unstable: bool | None = None
self.auto_backups_enabled: bool | None = None self.auto_backups_enabled: bool | None = None
self._load_settings() self._load_settings()
print(self.klipper_status)
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
@@ -56,32 +58,38 @@ class SettingsMenu(BaseMenu):
} }
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ KIAUH Settings ] " color = Color.CYAN
color = COLOR_CYAN checked = f"[{Color.apply('x', Color.GREEN)}]"
count = 62 - len(color) - len(RESET_FORMAT)
checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]"
unchecked = "[ ]" unchecked = "[ ]"
kl_repo: str = Color.apply(self.klipper_status.repo, color)
kl_branch: str = Color.apply(self.klipper_status.branch, color)
kl_owner: str = Color.apply(self.klipper_status.owner, color)
mr_repo: str = Color.apply(self.moonraker_status.repo, color)
mr_branch: str = Color.apply(self.moonraker_status.branch, color)
mr_owner: str = Color.apply(self.moonraker_status.owner, color)
o1 = checked if self.mainsail_unstable else unchecked o1 = checked if self.mainsail_unstable else unchecked
o2 = checked if self.fluidd_unstable else unchecked o2 = checked if self.fluidd_unstable else unchecked
o3 = checked if self.auto_backups_enabled else unchecked o3 = checked if self.auto_backups_enabled else unchecked
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Klipper source repository: ║ Klipper:
{self.klipper_repo:<67} ● Repo: {kl_repo:51}
● Owner: {kl_owner:51}
Moonraker source repository: ● Branch: {kl_branch:51}
║ ● {self.moonraker_repo:<67} ╟───────────────────────────────────────────────────────╢
Moonraker:
Install unstable Webinterface releases: ● Repo: {mr_repo:51}
║ ● Owner: {mr_owner:51}
║ ● Branch: {mr_branch:51}
╟───────────────────────────────────────────────────────╢
║ Install unstable releases: ║
{o1} Mainsail ║ {o1} Mainsail ║
{o2} Fluidd ║ {o2} Fluidd ║
║ ║ ╟───────────────────────────────────────────────────────╢
║ Auto-Backup: ║ ║ Auto-Backup: ║
{o3} Automatic backup before update ║ {o3} Automatic backup before update ║
║ ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ 1) Set Klipper source repository ║ ║ 1) Set Klipper source repository ║
║ 2) Set Moonraker source repository ║ ║ 2) Set Moonraker source repository ║
@@ -97,45 +105,55 @@ class SettingsMenu(BaseMenu):
def _load_settings(self) -> None: def _load_settings(self) -> None:
self.settings = KiauhSettings() self.settings = KiauhSettings()
self._format_repo_str("klipper")
self._format_repo_str("moonraker")
self.auto_backups_enabled = self.settings.kiauh.backup_before_update self.auto_backups_enabled = self.settings.kiauh.backup_before_update
self.mainsail_unstable = self.settings.mainsail.unstable_releases self.mainsail_unstable = self.settings.mainsail.unstable_releases
self.fluidd_unstable = self.settings.fluidd.unstable_releases self.fluidd_unstable = self.settings.fluidd.unstable_releases
def _format_repo_str(self, repo_name: str) -> None: # by default, we show the status of the installed repositories
repo = self.settings.get(repo_name, "repo_url") self.klipper_status = get_klipper_status()
repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}" self.moonraker_status = get_moonraker_status()
branch = self.settings.get(repo_name, "branch") # if the repository is not installed, we show the status of the settings from the config file
branch = f"({COLOR_CYAN}@ {branch}{RESET_FORMAT})" if self.klipper_status.repo == "-":
setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}") url_parts = self.settings.klipper.repo_url.split("/")
self.klipper_status.repo = url_parts[-1]
self.klipper_status.owner = url_parts[-2]
self.klipper_status.branch = self.settings.klipper.branch
if self.moonraker_status.repo == "-":
url_parts = self.settings.moonraker.repo_url.split("/")
self.moonraker_status.repo = url_parts[-1]
self.moonraker_status.owner = url_parts[-2]
self.moonraker_status.branch = self.settings.moonraker.branch
def _gather_input(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> Tuple[str, str]:
warn_msg = [
"There is only basic input validation in place! "
"Make sure your the input is valid and has no typos or invalid characters!"]
if repo_dir.exists():
warn_msg.extend([
"For the change to take effect, the new repository will be cloned. "
"A backup of the old repository will be created.",
"\n\n",
"Make sure you don't have any ongoing prints running, as the services "
"will be restarted during this process! You will loose any ongoing print!"])
Logger.print_dialog(DialogType.ATTENTION, warn_msg)
def _gather_input(self) -> Tuple[str, str]:
Logger.print_dialog(
DialogType.ATTENTION,
[
"There is no input validation in place! Make sure your"
" input is valid and has no typos! For any change to"
" take effect, the repository must be cloned again. "
"Make sure you don't have any ongoing prints running, "
"as the services will be restarted!"
],
)
repo = get_string_input( repo = get_string_input(
"Enter new repository URL", "Enter new repository URL",
allow_special_chars=True, regex="^[\w/.:-]+$",
default=KLIPPER_REPO_URL if repo_name == "klipper" else MOONRAKER_REPO_URL,
) )
branch = get_string_input( branch = get_string_input(
"Enter new branch name", "Enter new branch name",
allow_special_chars=True, regex="^.+$",
default="master"
) )
return repo, branch return repo, branch
def _set_repo(self, repo_name: str) -> None: def _set_repo(self, repo_name: Literal["klipper", "moonraker"], repo_dir: Path) -> None:
repo_url, branch = self._gather_input() repo_url, branch = self._gather_input(repo_name, repo_dir)
display_name = repo_name.capitalize() display_name = repo_name.capitalize()
Logger.print_dialog( Logger.print_dialog(
DialogType.CUSTOM, DialogType.CUSTOM,
@@ -148,50 +166,36 @@ class SettingsMenu(BaseMenu):
) )
if get_confirm("Apply changes?", allow_go_back=True): if get_confirm("Apply changes?", allow_go_back=True):
self.settings.set(repo_name, "repo_url", repo_url) repo: RepoSettings = self.settings[repo_name]
self.settings.set(repo_name, "branch", branch) repo.repo_url = repo_url
repo.branch = branch
self.settings.save() self.settings.save()
self._load_settings() self._load_settings()
Logger.print_ok("Changes saved!") Logger.print_ok("Changes saved!")
else: else:
Logger.print_info( Logger.print_info(
f"Skipping change of {display_name} source repository ..." f"Changing of {display_name} source repository canceled ..."
) )
return return
Logger.print_status(f"Switching to {display_name}'s new source repository ...") self._switch_repo(repo_name, repo_dir)
self._switch_repo(repo_name)
Logger.print_ok(f"Switched to {repo_url} at branch {branch}!")
def _switch_repo(self, name: str) -> None: def _switch_repo(self, name: Literal["klipper", "moonraker"], repo_dir: Path ) -> None:
target_dir: Path if not repo_dir.exists():
if name == "klipper":
target_dir = KLIPPER_DIR
_type = Klipper
elif name == "moonraker":
target_dir = MOONRAKER_DIR
_type = Moonraker
else:
Logger.print_error("Invalid repository name!")
return return
if target_dir.exists(): Logger.print_status(f"Switching to {name.capitalize()}'s new source repository ...")
shutil.rmtree(target_dir)
instances = get_instances(_type) repo: RepoSettings = self.settings[name]
InstanceManager.stop_all(instances) run_switch_repo_routine(name, repo)
repo = self.settings.get(name, "repo_url")
branch = self.settings.get(name, "branch")
git_clone_wrapper(repo, target_dir, branch)
InstanceManager.start_all(instances)
def set_klipper_repo(self, **kwargs) -> None: def set_klipper_repo(self, **kwargs) -> None:
self._set_repo("klipper") self._set_repo("klipper", KLIPPER_DIR)
def set_moonraker_repo(self, **kwargs) -> None: def set_moonraker_repo(self, **kwargs) -> None:
self._set_repo("moonraker") self._set_repo("moonraker", MOONRAKER_DIR)
def toggle_mainsail_release(self, **kwargs) -> None: def toggle_mainsail_release(self, **kwargs) -> None:
self.mainsail_unstable = not self.mainsail_unstable self.mainsail_unstable = not self.mainsail_unstable

View File

@@ -20,16 +20,8 @@ from components.klipperscreen.klipperscreen import (
get_klipperscreen_status, get_klipperscreen_status,
update_klipperscreen, update_klipperscreen,
) )
from components.mobileraker.mobileraker import (
get_mobileraker_status,
update_mobileraker,
)
from components.moonraker.moonraker_setup import update_moonraker from components.moonraker.moonraker_setup import update_moonraker
from components.moonraker.moonraker_utils import get_moonraker_status from components.moonraker.moonraker_utils import get_moonraker_status
from components.octoeverywhere.octoeverywhere_setup import (
get_octoeverywhere_status,
update_octoeverywhere,
)
from components.webui_client.client_config.client_config_setup import ( from components.webui_client.client_config.client_config_setup import (
update_client_config, update_client_config,
) )
@@ -40,17 +32,11 @@ from components.webui_client.client_utils import (
) )
from components.webui_client.fluidd_data import FluiddData from components.webui_client.fluidd_data import FluiddData
from components.webui_client.mainsail_data import MainsailData from components.webui_client.mainsail_data import MainsailData
from core.constants import (
COLOR_GREEN,
COLOR_RED,
COLOR_YELLOW,
RESET_FORMAT,
)
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.menus import Option from core.menus import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.spinner import Spinner from core.types.color import Color
from core.types import ComponentStatus from core.types.component_status import ComponentStatus
from utils.input_utils import get_confirm from utils.input_utils import get_confirm
from utils.sys_utils import ( from utils.sys_utils import (
get_upgradable_packages, get_upgradable_packages,
@@ -64,6 +50,11 @@ from utils.sys_utils import (
class UpdateMenu(BaseMenu): class UpdateMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None:
super().__init__() super().__init__()
self.loading_msg = "Loading update menu, please wait"
self.is_loading(True)
self.title = "Update Menu"
self.title_color = Color.GREEN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.packages: List[str] = [] self.packages: List[str] = []
@@ -76,25 +67,64 @@ class UpdateMenu(BaseMenu):
self.fluidd_local = self.fluidd_remote = "" self.fluidd_local = self.fluidd_remote = ""
self.fluidd_config_local = self.fluidd_config_remote = "" self.fluidd_config_local = self.fluidd_config_remote = ""
self.klipperscreen_local = self.klipperscreen_remote = "" self.klipperscreen_local = self.klipperscreen_remote = ""
self.mobileraker_local = self.mobileraker_remote = ""
self.crowsnest_local = self.crowsnest_remote = "" self.crowsnest_local = self.crowsnest_remote = ""
self.octoeverywhere_local = self.octoeverywhere_remote = ""
self.mainsail_data = MainsailData() self.mainsail_data = MainsailData()
self.fluidd_data = FluiddData() self.fluidd_data = FluiddData()
self.status_data = { self.status_data = {
"klipper": {"installed": False, "local": None, "remote": None}, "klipper": {
"moonraker": {"installed": False, "local": None, "remote": None}, "display_name": "Klipper",
"mainsail": {"installed": False, "local": None, "remote": None}, "installed": False,
"mainsail_config": {"installed": False, "local": None, "remote": None}, "local": None,
"fluidd": {"installed": False, "local": None, "remote": None}, "remote": None,
"fluidd_config": {"installed": False, "local": None, "remote": None}, },
"mobileraker": {"installed": False, "local": None, "remote": None}, "moonraker": {
"klipperscreen": {"installed": False, "local": None, "remote": None}, "display_name": "Moonraker",
"crowsnest": {"installed": False, "local": None, "remote": None}, "installed": False,
"octoeverywhere": {"installed": False, "local": None, "remote": None}, "local": None,
"remote": None,
},
"mainsail": {
"display_name": "Mainsail",
"installed": False,
"local": None,
"remote": None,
},
"mainsail_config": {
"display_name": "Mainsail-Config",
"installed": False,
"local": None,
"remote": None,
},
"fluidd": {
"display_name": "Fluidd",
"installed": False,
"local": None,
"remote": None,
},
"fluidd_config": {
"display_name": "Fluidd-Config",
"installed": False,
"local": None,
"remote": None,
},
"klipperscreen": {
"display_name": "KlipperScreen",
"installed": False,
"local": None,
"remote": None,
},
"crowsnest": {
"display_name": "Crowsnest",
"installed": False,
"local": None,
"remote": None,
},
} }
self._fetch_update_status()
self.is_loading(False)
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu from core.menus.main_menu import MainMenu
@@ -110,36 +140,21 @@ class UpdateMenu(BaseMenu):
"5": Option(self.update_mainsail_config), "5": Option(self.update_mainsail_config),
"6": Option(self.update_fluidd_config), "6": Option(self.update_fluidd_config),
"7": Option(self.update_klipperscreen), "7": Option(self.update_klipperscreen),
"8": Option(self.update_mobileraker), "8": Option(self.update_crowsnest),
"9": Option(self.update_crowsnest), "9": Option(self.upgrade_system_packages),
"10": Option(self.update_octoeverywhere),
"11": Option(self.upgrade_system_packages),
} }
def print_menu(self) -> None: def print_menu(self) -> None:
spinner = Spinner("Loading update menu, please wait", color="green")
spinner.start()
self._fetch_update_status()
spinner.stop()
header = " [ Update Menu ] "
color = COLOR_GREEN
count = 62 - len(color) - len(RESET_FORMAT)
sysupgrades: str = "No upgrades available." sysupgrades: str = "No upgrades available."
padding = 29 padding = 29
if self.package_count > 0: if self.package_count > 0:
sysupgrades = ( sysupgrades = Color.apply(
f"{COLOR_GREEN}{self.package_count} upgrades available!{RESET_FORMAT}" f"{self.package_count} upgrades available!", Color.GREEN
) )
padding = 38 padding = 38
menu = textwrap.dedent( menu = textwrap.dedent(
f""" f"""
╔═══════════════════════════════════════════════════════╗
{color}{header:~^{count}}{RESET_FORMAT}
╟───────────────────────┬───────────────┬───────────────╢ ╟───────────────────────┬───────────────┬───────────────╢
║ a) Update all │ │ ║ ║ a) Update all │ │ ║
║ │ Current: │ Latest: ║ ║ │ Current: │ Latest: ║
@@ -157,58 +172,65 @@ class UpdateMenu(BaseMenu):
║ │ │ ║ ║ │ │ ║
║ Other: ├───────────────┼───────────────╢ ║ Other: ├───────────────┼───────────────╢
║ 7) KlipperScreen │ {self.klipperscreen_local:<22}{self.klipperscreen_remote:<22} ║ 7) KlipperScreen │ {self.klipperscreen_local:<22}{self.klipperscreen_remote:<22}
║ 8) Mobileraker{self.mobileraker_local:<22}{self.mobileraker_remote:<22} ║ 8) Crowsnest {self.crowsnest_local:<22}{self.crowsnest_remote:<22}
║ 9) Crowsnest │ {self.crowsnest_local:<22}{self.crowsnest_remote:<22}
║ 10) OctoEverywhere │ {self.octoeverywhere_local:<22}{self.octoeverywhere_remote:<22}
║ ├───────────────┴───────────────╢ ║ ├───────────────┴───────────────╢
11) System │ {sysupgrades:^{padding}} 9) System │ {sysupgrades:^{padding}}
╟───────────────────────┴───────────────────────────────╢ ╟───────────────────────┴───────────────────────────────╢
""" """
)[1:] )[1:]
print(menu, end="") print(menu, end="")
def update_all(self, **kwargs) -> None: def update_all(self, **kwargs) -> None:
print("update_all") Logger.print_status("Updating all components ...")
self.update_klipper()
self.update_moonraker()
self.update_mainsail()
self.update_mainsail_config()
self.update_fluidd()
self.update_fluidd_config()
self.update_klipperscreen()
self.update_crowsnest()
self.upgrade_system_packages()
def update_klipper(self, **kwargs) -> None: def update_klipper(self, **kwargs) -> None:
if self._check_is_installed("klipper"): self._run_update_routine("klipper", update_klipper)
update_klipper()
def update_moonraker(self, **kwargs) -> None: def update_moonraker(self, **kwargs) -> None:
if self._check_is_installed("moonraker"): self._run_update_routine("moonraker", update_moonraker)
update_moonraker()
def update_mainsail(self, **kwargs) -> None: def update_mainsail(self, **kwargs) -> None:
if self._check_is_installed("mainsail"): self._run_update_routine(
update_client(self.mainsail_data) "mainsail",
update_client,
self.mainsail_data,
)
def update_mainsail_config(self, **kwargs) -> None: def update_mainsail_config(self, **kwargs) -> None:
if self._check_is_installed("mainsail_config"): self._run_update_routine(
update_client_config(self.mainsail_data) "mainsail_config",
update_client_config,
self.mainsail_data,
)
def update_fluidd(self, **kwargs) -> None: def update_fluidd(self, **kwargs) -> None:
if self._check_is_installed("fluidd"): self._run_update_routine(
update_client(self.fluidd_data) "fluidd",
update_client,
self.fluidd_data,
)
def update_fluidd_config(self, **kwargs) -> None: def update_fluidd_config(self, **kwargs) -> None:
if self._check_is_installed("fluidd_config"): self._run_update_routine(
update_client_config(self.fluidd_data) "fluidd_config",
update_client_config,
self.fluidd_data,
)
def update_klipperscreen(self, **kwargs) -> None: def update_klipperscreen(self, **kwargs) -> None:
if self._check_is_installed("klipperscreen"): self._run_update_routine("klipperscreen", update_klipperscreen)
update_klipperscreen()
def update_mobileraker(self, **kwargs) -> None:
if self._check_is_installed("mobileraker"):
update_mobileraker()
def update_crowsnest(self, **kwargs) -> None: def update_crowsnest(self, **kwargs) -> None:
if self._check_is_installed("crowsnest"): self._run_update_routine("crowsnest", update_crowsnest)
update_crowsnest()
def update_octoeverywhere(self, **kwargs) -> None:
if self._check_is_installed("octoeverywhere"):
update_octoeverywhere()
def upgrade_system_packages(self, **kwargs) -> None: def upgrade_system_packages(self, **kwargs) -> None:
self._run_system_updates() self._run_system_updates()
@@ -225,24 +247,22 @@ class UpdateMenu(BaseMenu):
"fluidd_config", get_client_config_status, self.fluidd_data "fluidd_config", get_client_config_status, self.fluidd_data
) )
self._set_status_data("klipperscreen", get_klipperscreen_status) self._set_status_data("klipperscreen", get_klipperscreen_status)
self._set_status_data("mobileraker", get_mobileraker_status)
self._set_status_data("crowsnest", get_crowsnest_status) self._set_status_data("crowsnest", get_crowsnest_status)
self._set_status_data("octoeverywhere", get_octoeverywhere_status)
update_system_package_lists(silent=True) update_system_package_lists(silent=True)
self.packages = get_upgradable_packages() self.packages = get_upgradable_packages()
self.package_count = len(self.packages) self.package_count = len(self.packages)
def _format_local_status(self, local_version, remote_version) -> str: def _format_local_status(self, local_version, remote_version) -> str:
color = COLOR_RED color = Color.RED
if not local_version: if not local_version:
color = COLOR_RED color = Color.RED
elif local_version == remote_version: elif local_version == remote_version:
color = COLOR_GREEN color = Color.GREEN
elif local_version != remote_version: elif local_version != remote_version:
color = COLOR_YELLOW color = Color.YELLOW
return f"{color}{local_version or '-'}{RESET_FORMAT}" return Color.apply(local_version or "-", color)
def _set_status_data(self, name: str, status_fn: Callable, *args) -> None: def _set_status_data(self, name: str, status_fn: Callable, *args) -> None:
comp_status: ComponentStatus = status_fn(*args) comp_status: ComponentStatus = status_fn(*args)
@@ -257,18 +277,32 @@ class UpdateMenu(BaseMenu):
local_status = self.status_data[name].get("local", None) local_status = self.status_data[name].get("local", None)
remote_status = self.status_data[name].get("remote", None) remote_status = self.status_data[name].get("remote", None)
color = COLOR_GREEN if remote_status else COLOR_RED color = Color.GREEN if remote_status else Color.RED
local_txt = self._format_local_status(local_status, remote_status) local_txt = self._format_local_status(local_status, remote_status)
remote_txt = f"{color}{remote_status or '-'}{RESET_FORMAT}" remote_txt = Color.apply(remote_status or "-", color)
setattr(self, f"{name}_local", local_txt) setattr(self, f"{name}_local", local_txt)
setattr(self, f"{name}_remote", remote_txt) setattr(self, f"{name}_remote", remote_txt)
def _check_is_installed(self, name: str) -> bool: def _check_is_installed(self, name: str) -> bool:
if not self.status_data[name]["installed"]: return self.status_data[name]["installed"]
Logger.print_info(f"{name.capitalize()} is not installed! Skipped ...")
return False def _is_update_available(self, name: str) -> bool:
return True return self.status_data[name]["local"] != self.status_data[name]["remote"]
def _run_update_routine(self, name: str, update_fn: Callable, *args) -> None:
display_name = self.status_data[name]["display_name"]
is_installed = self._check_is_installed(name)
is_update_available = self._is_update_available(name)
if not is_installed:
Logger.print_info(f"{display_name} is not installed! Skipped ...")
return
elif not is_update_available:
Logger.print_info(f"{display_name} is already up to date! Skipped ...")
return
update_fn(*args)
def _run_system_updates(self) -> None: def _run_system_updates(self) -> None:
if not self.packages: if not self.packages:

View File

View File

@@ -0,0 +1,59 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from dataclasses import dataclass, field
from typing import List
from core.logger import DialogType, Logger
from core.types.color import Color
@dataclass()
class Message:
title: str = field(default="")
text: List[str] = field(default_factory=list)
color: Color = field(default=Color.WHITE)
centered: bool = field(default=False)
class MessageService:
_instance = None
def __new__(cls) -> "MessageService":
if cls._instance is None:
cls._instance = super(MessageService, cls).__new__(cls)
return cls._instance
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
self.message = None
def set_message(self, message: Message) -> None:
self.message = message
def display_message(self) -> None:
if self.message is None:
return
Logger.print_dialog(
title=DialogType.CUSTOM,
content=self.message.text,
custom_title=self.message.title,
custom_color=self.message.color,
center_content=self.message.centered,
)
self.__clear_message()
def __clear_message(self) -> None:
self.message = None

View File

@@ -8,6 +8,9 @@
# ======================================================================= # # ======================================================================= #
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from core.logger import DialogType, Logger from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
NoOptionError, NoOptionError,
@@ -22,33 +25,21 @@ DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
@dataclass
class AppSettings: class AppSettings:
def __init__(self) -> None: backup_before_update: bool | None = field(default=None)
self.backup_before_update = None
class KlipperSettings: @dataclass
def __init__(self) -> None: class RepoSettings:
self.repo_url = None repo_url: str | None = field(default=None)
self.branch = None branch: str | None = field(default=None)
class MoonrakerSettings: @dataclass
def __init__(self) -> None: class WebUiSettings:
self.repo_url = None port: str | None = field(default=None)
self.branch = None unstable_releases: bool | None = field(default=None)
class MainsailSettings:
def __init__(self) -> None:
self.port = None
self.unstable_releases = None
class FluiddSettings:
def __init__(self) -> None:
self.port = None
self.unstable_releases = None
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -61,6 +52,16 @@ class KiauhSettings:
cls._instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs) cls._instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs)
return cls._instance return cls._instance
def __repr__(self) -> str:
return (
f"KiauhSettings(kiauh={self.kiauh}, klipper={self.klipper},"
f" moonraker={self.moonraker}, mainsail={self.mainsail},"
f" fluidd={self.fluidd})"
)
def __getitem__(self, item: str) -> Any:
return getattr(self, item)
def __init__(self) -> None: def __init__(self) -> None:
if not hasattr(self, "__initialized"): if not hasattr(self, "__initialized"):
self.__initialized = False self.__initialized = False
@@ -69,27 +70,17 @@ class KiauhSettings:
self.__initialized = True self.__initialized = True
self.config = SimpleConfigParser() self.config = SimpleConfigParser()
self.kiauh = AppSettings() self.kiauh = AppSettings()
self.klipper = KlipperSettings() self.klipper = RepoSettings()
self.moonraker = MoonrakerSettings() self.moonraker = RepoSettings()
self.mainsail = MainsailSettings() self.mainsail = WebUiSettings()
self.fluidd = FluiddSettings() self.fluidd = WebUiSettings()
self.kiauh.backup_before_update = None
self.klipper.repo_url = None
self.klipper.branch = None
self.moonraker.repo_url = None
self.moonraker.branch = None
self.mainsail.port = None
self.mainsail.unstable_releases = None
self.fluidd.port = None
self.fluidd.unstable_releases = None
self._load_config() self._load_config()
def get(self, section: str, option: str) -> str | int | bool: def get(self, section: str, option: str) -> str | int | bool:
""" """
Get a value from the settings state by providing the section and option name as strings. Get a value from the settings state by providing the section and option name as
Prefer direct access to the properties, as it is usually safer! strings. Prefer direct access to the properties, as it is usually safer!
:param section: The section name as string. :param section: The section name as string.
:param option: The option name as string. :param option: The option name as string.
:return: The value of the option as string, int or bool. :return: The value of the option as string, int or bool.
@@ -102,23 +93,9 @@ class KiauhSettings:
except AttributeError: except AttributeError:
raise raise
def set(self, section: str, option: str, value: str | int | bool) -> None:
"""
Set a value in the settings state by providing the section and option name as strings.
Prefer direct access to the properties, as it is usually safer!
:param section: The section name as string.
:param option: The option name as string.
:param value: The value to set as string, int or bool.
"""
try:
section = getattr(self, section)
section.option = value # type: ignore
except AttributeError:
raise
def save(self) -> None: def save(self) -> None:
self._set_config_options() self._set_config_options_state()
self.config.write(CUSTOM_CFG) self.config.write_file(CUSTOM_CFG)
self._load_config() self._load_config()
def _load_config(self) -> None: def _load_config(self) -> None:
@@ -126,10 +103,10 @@ class KiauhSettings:
self._kill() self._kill()
cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG
self.config.read(cfg) self.config.read_file(cfg)
self._validate_cfg() self._validate_cfg()
self._read_settings() self._apply_settings_from_file()
def _validate_cfg(self) -> None: def _validate_cfg(self) -> None:
try: try:
@@ -159,7 +136,7 @@ class KiauhSettings:
def _validate_bool(self, section: str, option: str) -> None: def _validate_bool(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option) self._v_section, self._v_option = (section, option)
bool(self.config.getboolean(section, option)) (bool(self.config.getboolean(section, option)))
def _validate_int(self, section: str, option: str) -> None: def _validate_int(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option) self._v_section, self._v_option = (section, option)
@@ -167,18 +144,19 @@ class KiauhSettings:
def _validate_str(self, section: str, option: str) -> None: def _validate_str(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option) self._v_section, self._v_option = (section, option)
v = self.config.get(section, option) v = self.config.getval(section, option)
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
if not v:
raise ValueError raise ValueError
def _read_settings(self) -> None: def _apply_settings_from_file(self) -> None:
self.kiauh.backup_before_update = self.config.getboolean( self.kiauh.backup_before_update = self.config.getboolean(
"kiauh", "backup_before_update" "kiauh", "backup_before_update"
) )
self.klipper.repo_url = self.config.get("klipper", "repo_url") self.klipper.repo_url = self.config.getval("klipper", "repo_url")
self.klipper.branch = self.config.get("klipper", "branch") self.klipper.branch = self.config.getval("klipper", "branch")
self.moonraker.repo_url = self.config.get("moonraker", "repo_url") self.moonraker.repo_url = self.config.getval("moonraker", "repo_url")
self.moonraker.branch = self.config.get("moonraker", "branch") self.moonraker.branch = self.config.getval("moonraker", "branch")
self.mainsail.port = self.config.getint("mainsail", "port") self.mainsail.port = self.config.getint("mainsail", "port")
self.mainsail.unstable_releases = self.config.getboolean( self.mainsail.unstable_releases = self.config.getboolean(
"mainsail", "unstable_releases" "mainsail", "unstable_releases"
@@ -188,24 +166,24 @@ class KiauhSettings:
"fluidd", "unstable_releases" "fluidd", "unstable_releases"
) )
def _set_config_options(self) -> None: def _set_config_options_state(self) -> None:
self.config.set( self.config.set_option(
"kiauh", "kiauh",
"backup_before_update", "backup_before_update",
str(self.kiauh.backup_before_update), str(self.kiauh.backup_before_update),
) )
self.config.set("klipper", "repo_url", self.klipper.repo_url) self.config.set_option("klipper", "repo_url", self.klipper.repo_url)
self.config.set("klipper", "branch", self.klipper.branch) self.config.set_option("klipper", "branch", self.klipper.branch)
self.config.set("moonraker", "repo_url", self.moonraker.repo_url) self.config.set_option("moonraker", "repo_url", self.moonraker.repo_url)
self.config.set("moonraker", "branch", self.moonraker.branch) self.config.set_option("moonraker", "branch", self.moonraker.branch)
self.config.set("mainsail", "port", str(self.mainsail.port)) self.config.set_option("mainsail", "port", str(self.mainsail.port))
self.config.set( self.config.set_option(
"mainsail", "mainsail",
"unstable_releases", "unstable_releases",
str(self.mainsail.unstable_releases), str(self.mainsail.unstable_releases),
) )
self.config.set("fluidd", "port", str(self.fluidd.port)) self.config.set_option("fluidd", "port", str(self.fluidd.port))
self.config.set( self.config.set_option(
"fluidd", "unstable_releases", str(self.fluidd.unstable_releases) "fluidd", "unstable_releases", str(self.fluidd.unstable_releases)
) )

View File

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

View File

@@ -0,0 +1,62 @@
# ======================================================================= #
# 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 #
# ======================================================================= #
import re
# definition of section line:
# - then line MUST start with an opening square bracket - it is the first section marker
# - the section marker MUST be followed by at least one character - it is the section name
# - the section name MUST be followed by a closing square bracket - it is the second section marker
# - the second section marker MAY be followed by any amount of whitespace characters
# - the second section marker MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
SECTION_RE = re.compile(r"^\[(\S.*\S|\S)]\s*([#;].*)?$")
# definition of option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the value MAY be of any length and character
# - the value MAY contain any amount of trailing whitespaces
# - the value MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
OPTION_RE = re.compile(r"^([^;#:=\s]+)\s?[:=]\s*([^;#:=\s][^;#]*?)\s*([#;].*)?$")
# definition of options block start line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST NOT be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the separator MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
OPTIONS_BLOCK_START_RE = re.compile(r"^([^;#:=\s]+)\s*[:=]\s*([#;].*)?$")
# definition of comment line:
# - the line MAY start with any amount of whitespace characters
# - the line MUST contain a # or ; - it is the comment marker
# - the comment marker MAY be followed by any amount of whitespace characters
# - the comment MAY be of any length and character
LINE_COMMENT_RE = re.compile(r"^\s*[#;].*")
# definition of empty line:
# - the line MUST contain only whitespace characters
EMPTY_LINE_RE = re.compile(r"^\s*$")
BOOLEAN_STATES = {
"1": True,
"yes": True,
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
HEADER_IDENT = "#_header"

View File

@@ -1,5 +1,5 @@
# ======================================================================= # # ======================================================================= #
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> # # Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
# # # #
# https://github.com/dw-0/simple-config-parser # # https://github.com/dw-0/simple-config-parser #
# # # #
@@ -8,47 +8,24 @@
from __future__ import annotations from __future__ import annotations
import re import secrets
import string
from pathlib import Path from pathlib import Path
from typing import Callable, Dict, List, Match, Tuple, TypedDict from typing import Callable, Dict, List
from ..simple_config_parser.constants import (
BOOLEAN_STATES,
EMPTY_LINE_RE,
HEADER_IDENT,
LINE_COMMENT_RE,
OPTION_RE,
OPTIONS_BLOCK_START_RE,
SECTION_RE,
)
_UNSET = object() _UNSET = object()
class Section(TypedDict):
"""
A single section in the config file
- _raw: The raw representation of the section name
- options: A list of options in the section
"""
_raw: str
options: List[Option]
class Option(TypedDict, total=False):
"""
A single option in a section in the config file
- is_multiline: Whether the option is a multiline option
- option: The name of the option
- value: The value of the option
- _raw: The raw representation of the option
- _raw_value: The raw value of the option
A multinline option is an option that contains multiple lines of text following
the option name in the next line. The value of a multiline option is a list of
strings, where each string represents a single line of text.
"""
is_multiline: bool
option: str
value: str | List[str]
_raw: str
_raw_value: str | List[str]
class NoSectionError(Exception): class NoSectionError(Exception):
"""Raised when a section is not defined""" """Raised when a section is not defined"""
@@ -57,14 +34,6 @@ class NoSectionError(Exception):
super().__init__(msg) super().__init__(msg)
class NoOptionError(Exception):
"""Raised when an option is not defined in a section"""
def __init__(self, option: str, section: str):
msg = f"Option '{option}' in section '{section}' is not defined"
super().__init__(msg)
class DuplicateSectionError(Exception): class DuplicateSectionError(Exception):
"""Raised when a section is defined more than once""" """Raised when a section is defined more than once"""
@@ -73,11 +42,11 @@ class DuplicateSectionError(Exception):
super().__init__(msg) super().__init__(msg)
class DuplicateOptionError(Exception): class NoOptionError(Exception):
"""Raised when an option is defined more than once""" """Raised when an option is not defined in a section"""
def __init__(self, option: str, section: str): def __init__(self, option: str, section: str):
msg = f"Option '{option}' in section '{section}' is defined more than once" msg = f"Option '{option}' in section '{section}' is not defined"
super().__init__(msg) super().__init__(msg)
@@ -85,159 +54,208 @@ class DuplicateOptionError(Exception):
class SimpleConfigParser: class SimpleConfigParser:
"""A customized config parser targeted at handling Klipper style config files""" """A customized config parser targeted at handling Klipper style config files"""
# definition of section line: def __init__(self) -> None:
# - then line MUST start with an opening square bracket - it is the first section marker self.header: List[str] = []
# - the section marker MUST be followed by at least one character - it is the section name self.config: Dict = {}
# - the section name MUST be followed by a closing square bracket - it is the second section marker self.current_section: str | None = None
# - the second section marker MAY be followed by any amount of whitespace characters self.current_opt_block: str | None = None
# - the second section marker MAY be followed by a # or ; - it is the comment marker self.current_collector: str | None = None
# - the inline comment MAY be of any length and character self.in_option_block: bool = False
_SECTION_RE = re.compile(r"\[(.+)]\s*([#;].*)?$")
# definition of option line: def _match_section(self, line: str) -> bool:
# - the line MUST start with a word - it is the option name """Wheter or not the given line matches the definition of a section"""
# - the option name MUST be followed by a colon or an equal sign - it is the separator return SECTION_RE.match(line) is not None
# - the separator MUST be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the value MAY be of any length and character
# - the value MAY contain any amount of trailing whitespaces
# - the value MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_OPTION_RE = re.compile(r"^([^:=\s]+)\s?[:=]\s*([^=:].*)\s*([#;].*)?$")
# definition of multiline option line: def _match_option(self, line: str) -> bool:
# - the line MUST start with a word - it is the option name """Wheter or not the given line matches the definition of an option"""
# - the option name MUST be followed by a colon or an equal sign - it is the separator return OPTION_RE.match(line) is not None
# - the separator MUST NOT be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the separator MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_MLOPTION_RE = re.compile(r"^([^:=\s]+)\s*[:=]\s*([#;].*)?$")
# definition of comment line: def _match_options_block_start(self, line: str) -> bool:
# - the line MAY start with any amount of whitespace characters """Wheter or not the given line matches the definition of a multiline option"""
# - the line MUST contain a # or ; - it is the comment marker return OPTIONS_BLOCK_START_RE.match(line) is not None
# - the comment marker MAY be followed by any amount of whitespace characters
# - the comment MAY be of any length and character
_COMMENT_RE = re.compile(r"^\s*([#;].*)?$")
# definition of empty line: def _match_line_comment(self, line: str) -> bool:
# - the line MUST contain only whitespace characters """Wheter or not the given line matches the definition of a comment"""
_EMPTY_LINE_RE = re.compile(r"^\s*$") return LINE_COMMENT_RE.match(line) is not None
BOOLEAN_STATES = { def _match_empty_line(self, line: str) -> bool:
"1": True, """Wheter or not the given line matches the definition of an empty line"""
"yes": True, return EMPTY_LINE_RE.match(line) is not None
"true": True,
"on": True,
"0": False,
"no": False,
"false": False,
"off": False,
}
def __init__(self): def _parse_line(self, line: str) -> None:
self._config: Dict = {} """Parses a line and determines its type"""
self._header: List[str] = [] if self._match_section(line):
self._all_sections: List[str] = [] self.current_collector = None
self._all_options: Dict = {} self.current_opt_block = None
self.section_name: str = "" self.current_section = SECTION_RE.match(line).group(1)
self.in_option_block: bool = False # whether we are in a multiline option block self.config[self.current_section] = {"_raw": line}
def read(self, file: Path) -> None: elif self._match_option(line):
""" self.current_collector = None
Read the given file and store the result in the internal state. self.current_opt_block = None
Call this method before using any other methods. Calling this method option = OPTION_RE.match(line).group(1)
multiple times will reset the internal state on each call. value = OPTION_RE.match(line).group(2)
""" self.config[self.current_section][option] = {"_raw": line, "value": value}
self._reset_state() elif self._match_options_block_start(line):
self.current_collector = None
option = OPTIONS_BLOCK_START_RE.match(line).group(1)
self.current_opt_block = option
self.config[self.current_section][option] = {"_raw": line, "value": []}
try: elif self.current_opt_block is not None:
with open(file, "r") as f: self.config[self.current_section][self.current_opt_block]["value"].append(
self._parse_config(f.readlines()) line
)
except OSError: elif self._match_empty_line(line) or self._match_line_comment(line):
raise self.current_opt_block = None
def _reset_state(self): # if current_section is None, we are at the beginning of the file,
"""Reset the internal state.""" # so we consider the part up to the first section as the file header
if not self.current_section:
self.config.setdefault(HEADER_IDENT, []).append(line)
else:
section = self.config[self.current_section]
self._config.clear() # set the current collector to a new value, so that continuous
self._header.clear() # empty lines or comments are collected into the same collector
self._all_sections.clear() if not self.current_collector:
self._all_options.clear() self.current_collector = self._generate_rand_id()
self.section_name = "" section[self.current_collector] = []
self.in_option_block = False
def write(self, filename): section[self.current_collector].append(line)
"""Write the internal state to the given file"""
content = self._construct_content() def read_file(self, file: Path) -> None:
"""Read and parse a config file"""
with open(file, "r") as file:
for line in file:
self._parse_line(line)
with open(filename, "w") as f: # print(json.dumps(self.config, indent=4))
f.write(content)
def _construct_content(self) -> str: def write_file(self, file: Path) -> None:
""" """Write the current config to the config file"""
Constructs the content of the configuration file based on the internal state of if not file:
the _config object by iterating over the sections and their options. It starts raise ValueError("No config file specified")
by checking if a header is present and extends the content list with its elements.
Then, for each section, it appends the raw representation of the section to the
content list. If the section has a body, it iterates over its options and extends
the content list with their raw representations. If an option is multiline, it
also extends the content list with its raw value. Finally, the content list is
joined into a single string and returned.
:return: The content of the configuration file as a string with open(file, "w") as file:
""" self._write_header(file)
content: List[str] = [] self._write_sections(file)
if self._header is not None:
content.extend(self._header)
for section in self._config:
content.append(self._config[section]["_raw"])
if (sec_body := self._config[section].get("body")) is not None: def _write_header(self, file) -> None:
for option in sec_body: """Write the header to the config file"""
content.extend(option["_raw"]) for line in self.config.get(HEADER_IDENT, []):
if option["is_multiline"]: file.write(line)
content.extend(option["_raw_value"])
content: str = "".join(content)
return content def _write_sections(self, file) -> None:
"""Write the sections to the config file"""
for section in self.get_sections():
for key, value in self.config[section].items():
self._write_section_content(file, key, value)
def sections(self) -> List[str]: def _write_section_content(self, file, key, value) -> None:
"""Return a list of section names""" """Write the content of a section to the config file"""
if key == "_raw":
file.write(value)
elif key.startswith("#_"):
for line in value:
file.write(line)
elif isinstance(value["value"], list):
file.write(value["_raw"])
for line in value["value"]:
file.write(line)
else:
file.write(value["_raw"])
return self._all_sections def get_sections(self) -> List[str]:
"""Return a list of all section names, but exclude any section starting with '#_'"""
return list(
filter(
lambda section: not section.startswith("#_"),
self.config.keys(),
)
)
def has_section(self, section: str) -> bool:
"""Check if a section exists"""
return section in self.get_sections()
def add_section(self, section: str) -> None: def add_section(self, section: str) -> None:
"""Add a new section to the internal state""" """Add a new section to the config"""
if section in self.get_sections():
if section in self._all_sections:
raise DuplicateSectionError(section) raise DuplicateSectionError(section)
self._all_sections.append(section)
self._all_options[section] = {} if len(self.get_sections()) >= 1:
self._config[section] = {"_raw": f"\n[{section}]\n", "body": []} self._check_set_section_spacing()
self.config[section] = {"_raw": f"[{section}]\n"}
def _check_set_section_spacing(self):
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"]
def remove_section(self, section: str) -> None: def remove_section(self, section: str) -> None:
"""Remove the given section""" """Remove a section from the config"""
self.config.pop(section, None)
if section not in self._all_sections: def get_options(self, section: str) -> List[str]:
raise NoSectionError(section) """Return a list of all option names for a given section"""
return list(
filter(
lambda option: option != "_raw" and not option.startswith("#_"),
self.config[section].keys(),
)
)
self._all_sections.pop(self._all_sections.index(section)) def has_option(self, section: str, option: str) -> bool:
self._all_options.pop(section) """Check if an option exists in a section"""
self._config.pop(section) return self.has_section(section) and option in self.get_options(section)
def options(self, section) -> List[str]: def set_option(self, section: str, option: str, value: str | List[str]) -> None:
"""Return a list of option names for the given section name""" """
Set the value of an option in a section. If the section does not exist,
it is created. If the option does not exist, it is created.
"""
if not self.has_section(section):
self.add_section(section)
return self._all_options.get(section) if not self.has_option(section, option):
self.config[section][option] = {
"_raw": f"{option}:\n"
if isinstance(value, list)
else f"{option}: {value}\n",
"value": value,
}
else:
opt = self.config[section][option]
if not isinstance(value, list):
opt["_raw"] = opt["_raw"].replace(opt["value"], value)
opt["value"] = value
def get( def remove_option(self, section: str, option: str) -> None:
"""Remove an option from a section"""
self.config[section].pop(option, None)
def getval(
self, section: str, option: str, fallback: str | _UNSET = _UNSET self, section: str, option: str, fallback: str | _UNSET = _UNSET
) -> str | List[str]: ) -> str | List[str]:
""" """
@@ -246,15 +264,12 @@ class SimpleConfigParser:
If the key is not found and 'fallback' is provided, it is used as If the key is not found and 'fallback' is provided, it is used as
a fallback value. a fallback value.
""" """
try: try:
if section not in self._all_sections: if section not in self.get_sections():
raise NoSectionError(section) raise NoSectionError(section)
if option not in self.get_options(section):
if option not in self._all_options.get(section):
raise NoOptionError(option, section) raise NoOptionError(option, section)
return self.config[section][option]["value"]
return self._all_options[section][option]
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if fallback is _UNSET: if fallback is _UNSET:
raise raise
@@ -262,25 +277,29 @@ class SimpleConfigParser:
def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int: def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int:
"""Return the value of the given option in the given section as an int""" """Return the value of the given option in the given section as an int"""
return self._get_conv(section, option, int, fallback=fallback) return self._get_conv(section, option, int, fallback=fallback)
def getfloat( def getfloat(
self, section: str, option: str, fallback: float | _UNSET = _UNSET self, section: str, option: str, fallback: float | _UNSET = _UNSET
) -> float: ) -> float:
"""Return the value of the given option in the given section as a float"""
return self._get_conv(section, option, float, fallback=fallback) return self._get_conv(section, option, float, fallback=fallback)
def getboolean( def getboolean(
self, section: str, option: str, fallback: bool | _UNSET = _UNSET self, section: str, option: str, fallback: bool | _UNSET = _UNSET
) -> bool: ) -> bool:
"""Return the value of the given option in the given section as a boolean"""
return self._get_conv( return self._get_conv(
section, option, self._convert_to_boolean, fallback=fallback section, option, self._convert_to_boolean, fallback=fallback
) )
def _convert_to_boolean(self, value) -> bool: def _convert_to_boolean(self, value: str) -> bool:
if value.lower() not in self.BOOLEAN_STATES: """Convert a string to a boolean"""
if isinstance(value, bool):
return value
if value.lower() not in BOOLEAN_STATES:
raise ValueError("Not a boolean: %s" % value) raise ValueError("Not a boolean: %s" % value)
return self.BOOLEAN_STATES[value.lower()] return BOOLEAN_STATES[value.lower()]
def _get_conv( def _get_conv(
self, self,
@@ -289,300 +308,18 @@ class SimpleConfigParser:
conv: Callable[[str], int | float | bool], conv: Callable[[str], int | float | bool],
fallback: _UNSET = _UNSET, fallback: _UNSET = _UNSET,
) -> int | float | bool: ) -> int | float | bool:
"""Return the value of the given option in the given section as a converted value"""
try: try:
return conv(self.get(section, option, fallback)) return conv(self.getval(section, option, fallback))
except: except (ValueError, TypeError, AttributeError) as e:
if fallback is not _UNSET: if fallback is not _UNSET:
return fallback return fallback
raise raise ValueError(
f"Cannot convert {self.getval(section, option)} to {conv.__name__}"
def items(self, section: str) -> List[Tuple[str, str]]: ) from e
"""Return a list of (option, value) tuples for a specific section"""
def _generate_rand_id(self) -> str:
if section not in self._all_sections: """Generate a random id with 6 characters"""
raise NoSectionError(section) chars = string.ascii_letters + string.digits
rand_string = "".join(secrets.choice(chars) for _ in range(12))
result = [] return f"#_{rand_string}"
for _option in self._all_options[section]:
result.append((_option, self._all_options[section][_option]))
return result
def set(
self,
section: str,
option: str,
value: str,
multiline: bool = False,
indent: int = 4,
) -> None:
"""Set the given option to the given value in the given section
If the option is already defined, it will be overwritten. If the option
is not defined yet, it will be added to the section body.
The multiline parameter can be used to specify whether the value is
multiline or not. If it is not specified, the value will be considered
as multiline if it contains a newline character. The value will then be split
into multiple lines. If the value does not contain a newline character, it
will be considered as a single line value. The indent parameter can be used
to specify the indentation of the multiline value. Indentations are with spaces.
:param section: The section to set the option in
:param option: The option to set
:param value: The value to set
:param multiline: Whether the value is multiline or not
:param indent: The indentation for multiline values
"""
if section not in self._all_sections:
raise NoSectionError(section)
# prepare the options value and raw value depending on the multiline flag
_raw_value: List[str] | None = None
if multiline or "\n" in value:
_multiline = True
_raw: str = f"{option}:\n"
_value: List[str] = value.split("\n")
_raw_value: List[str] = [f"{' ' * indent}{v}\n" for v in _value]
else:
_multiline = False
_raw: str = f"{option}: {value}\n"
_value: str = value
# the option does not exist yet
if option not in self._all_options.get(section):
_option: Option = {
"is_multiline": _multiline,
"option": option,
"value": _value,
"_raw": _raw,
}
if _raw_value is not None:
_option["_raw_value"] = _raw_value
self._config[section]["body"].insert(0, _option)
# the option exists and we need to update it
else:
for _option in self._config[section]["body"]:
if _option["option"] == option:
if multiline:
_option["_raw"] = _raw
else:
# we preserve inline comments by replacing the old value with the new one
_option["_raw"] = _option["_raw"].replace(
_option["value"], _value
)
_option["value"] = _value
if _raw_value is not None:
_option["_raw_value"] = _raw_value
break
self._all_options[section][option] = _value
def remove_option(self, section: str, option: str) -> None:
"""Remove the given option from the given section"""
if section not in self._all_sections:
raise NoSectionError(section)
if option not in self._all_options.get(section):
raise NoOptionError(option, section)
for _option in self._config[section]["body"]:
if _option["option"] == option:
del self._all_options[section][option]
self._config[section]["body"].remove(_option)
break
def has_section(self, section: str) -> bool:
"""Return True if the given section exists, False otherwise"""
return section in self._all_sections
def has_option(self, section: str, option: str) -> bool:
"""Return True if the given option exists in the given section, False otherwise"""
return option in self._all_options.get(section)
def _is_section(self, line: str) -> bool:
"""Check if the given line contains a section definition"""
return self._SECTION_RE.match(line) is not None
def _is_option(self, line: str) -> bool:
"""Check if the given line contains an option definition"""
match: Match[str] | None = self._OPTION_RE.match(line)
if not match:
return False
# if there is no value, it's not a regular option but a multiline option
if match.group(2).strip() == "":
return False
if not match.group(1).strip() == "":
return True
return False
def _is_comment(self, line: str) -> bool:
"""Check if the given line is a comment"""
return self._COMMENT_RE.match(line) is not None
def _is_empty_line(self, line: str) -> bool:
"""Check if the given line is an empty line"""
return self._EMPTY_LINE_RE.match(line) is not None
def _is_multiline_option(self, line: str) -> bool:
"""Check if the given line starts a multiline option block"""
match: Match[str] | None = self._MLOPTION_RE.match(line)
if not match:
return False
return True
def _parse_config(self, content: List[str]) -> None:
"""Parse the given content and store the result in the internal state"""
_curr_multi_opt = ""
# THE ORDER MATTERS, DO NOT REORDER THE CONDITIONS!
for line in content:
if self._is_section(line):
self._parse_section(line)
elif self._is_option(line):
self._parse_option(line)
# if it's not a regular option with the value inline,
# it might be a might be a multiline option block
elif self._is_multiline_option(line):
self.in_option_block = True
_curr_multi_opt = self._OPTION_RE.match(line).group(1).strip()
self._add_option_to_section_body(_curr_multi_opt, "", line)
elif self.in_option_block:
self._parse_multiline_option(_curr_multi_opt, line)
# if it's nothing from above, it's probably a comment or an empty line
elif self._is_comment(line) or self._is_empty_line(line):
self._parse_comment(line)
def _parse_section(self, line: str) -> None:
"""Parse a section line and store the result in the internal state"""
match: Match[str] | None = self._SECTION_RE.match(line)
if not match:
return
self.in_option_block = False
section_name: str = match.group(1).strip()
self._store_internal_state_section(section_name, line)
def _store_internal_state_section(self, section: str, raw_value: str) -> None:
"""Store the given section and its raw value in the internal state"""
if section in self._all_sections:
raise DuplicateSectionError(section)
self.section_name = section
self._all_sections.append(section)
self._all_options[section] = {}
self._config[section]: Section = {"_raw": raw_value, "body": []}
def _parse_option(self, line: str) -> None:
"""Parse an option line and store the result in the internal state"""
self.in_option_block = False
match: Match[str] | None = self._OPTION_RE.match(line)
if not match:
return
option: str = match.group(1).strip()
value: str = match.group(2).strip()
if ";" in value:
i = value.index(";")
value = value[:i].strip()
elif "#" in value:
i = value.index("#")
value = value[:i].strip()
self._store_internal_state_option(option, value, line)
def _store_internal_state_option(
self, option: str, value: str, raw_value: str
) -> None:
"""Store the given option and its raw value in the internal state"""
section_options = self._all_options.setdefault(self.section_name, {})
if option in section_options:
raise DuplicateOptionError(option, self.section_name)
section_options[option] = value
self._add_option_to_section_body(option, value, raw_value)
def _parse_multiline_option(self, curr_ml_opt: str, line: str) -> None:
"""Parse a multiline option line and store the result in the internal state"""
section_options = self._all_options.setdefault(self.section_name, {})
multiline_options = section_options.setdefault(curr_ml_opt, [])
_cleaned_line = line.strip().strip("\n")
if _cleaned_line and not self._is_comment(line):
multiline_options.append(_cleaned_line)
# add the option to the internal multiline option value state
self._ensure_section_body_exists()
for _option in self._config[self.section_name]["body"]:
if _option.get("option") == curr_ml_opt:
_option.update(
is_multiline=True,
_raw_value=_option.get("_raw_value", []) + [line],
value=multiline_options,
)
def _parse_comment(self, line: str) -> None:
"""
Parse a comment line and store the result in the internal state
If the there was no previous section parsed, the lines are handled as
the file header and added to the internal header list as it means, that
we are at the very top of the file.
"""
self.in_option_block = False
if not self.section_name:
self._header.append(line)
else:
self._add_option_to_section_body("", "", line)
def _ensure_section_body_exists(self) -> None:
"""
Ensure that the section body exists in the internal state.
If the section body does not exist, it is created as an empty list
"""
if self.section_name not in self._config:
self._config.setdefault(self.section_name, {}).setdefault("body", [])
def _add_option_to_section_body(
self, option: str, value: str, line: str, is_multiline: bool = False
) -> None:
"""Add a raw option line to the internal state"""
self._ensure_section_body_exists()
new_option: Option = {
"is_multiline": is_multiline,
"option": option,
"value": value,
"_raw": line,
}
option_body = self._config[self.section_name]["body"]
option_body.append(new_option)

View File

@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test SimpleConfigParser" type="tests" factoryName="py.test">
<module name="simple-config-parser" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.8 (simple-config-parser)" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;-s -vv&quot;" />
<option name="_new_target" value="&quot;&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
# 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

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

@@ -1,95 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
@pytest.fixture
def parser():
parser = SimpleConfigParser()
parser._header = ["header1\n", "header2\n"]
parser._config = {
"section1": {
"_raw": "[section1]\n",
"body": [
{
"_raw": "option1: value1\n",
"_raw_value": "value1\n",
"is_multiline": False,
"option": "option1",
"value": "value1",
},
{
"_raw": "option2: value2\n",
"_raw_value": "value2\n",
"is_multiline": False,
"option": "option2",
"value": "value2",
},
],
},
"section2": {
"_raw": "[section2]\n",
"body": [
{
"_raw": "option3: value3\n",
"_raw_value": "value3\n",
"is_multiline": False,
"option": "option3",
"value": "value3",
},
],
},
"section3": {
"_raw": "[section3]\n",
"body": [
{
"_raw": "option4:\n",
"_raw_value": [" value4\n", " value5\n", " value6\n"],
"is_multiline": True,
"option": "option4",
"value": ["value4", "value5", "value6"],
},
],
},
}
return parser
def test_construct_content(parser):
content = parser._construct_content()
assert (
content == "header1\nheader2\n"
"[section1]\n"
"option1: value1\n"
"option2: value2\n"
"[section2]\n"
"option3: value3\n"
"[section3]\n"
"option4:\n"
" value4\n"
" value5\n"
" value6\n"
)
def test_construct_content_no_header(parser):
parser._header = None
content = parser._construct_content()
assert (
content == "[section1]\n"
"option1: value1\n"
"option2: value2\n"
"[section2]\n"
"option3: value3\n"
"[section3]\n"
"option4:\n"
" value4\n"
" value5\n"
" value6\n"
)
def test_construct_content_no_sections(parser):
parser._config = {}
content = parser._construct_content()
assert content == "".join(parser._header)

View File

@@ -1,84 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateOptionError,
DuplicateSectionError,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestInternalStateChanges:
@pytest.mark.parametrize(
"given", ["dummy_section", "dummy_section 2", "another_section"]
)
def test_ensure_section_body_exists(self, parser, given):
parser._config = {}
parser.section_name = given
parser._ensure_section_body_exists()
assert parser._config[given] is not None
assert parser._config[given]["body"] == []
def test_add_option_to_section_body(self):
pass
@pytest.mark.parametrize(
"given", ["dummy_section", "dummy_section 2", "another_section\n"]
)
def test_store_internal_state_section(self, parser, given):
parser._store_internal_state_section(given, given)
assert parser._all_sections == [given]
assert parser._all_options[given] == {}
assert parser._config[given]["body"] == []
assert parser._config[given]["_raw"] == given
def test_duplicate_section_error(self, parser):
section_name = "dummy_section"
parser._all_sections = [section_name]
with pytest.raises(DuplicateSectionError) as excinfo:
parser._store_internal_state_section(section_name, section_name)
message = f"Section '{section_name}' is defined more than once"
assert message in str(excinfo.value)
# Check that the internal state of the parser is correct
assert parser.in_option_block is False
assert parser.section_name == ""
assert parser._all_sections == [section_name]
@pytest.mark.parametrize(
"given_name, given_value, given_raw_value",
[("dummyoption", "dummyvalue", "dummyvalue\n")],
)
def test_store_internal_state_option(
self, parser, given_name, given_value, given_raw_value
):
parser.section_name = "dummy_section"
parser._store_internal_state_option(given_name, given_value, given_raw_value)
assert parser._all_options[parser.section_name] == {given_name: given_value}
new_option = {
"is_multiline": False,
"option": given_name,
"value": given_value,
"_raw": given_raw_value,
}
assert parser._config[parser.section_name]["body"] == [new_option]
def test_duplicate_option_error(self, parser):
option_name = "dummyoption"
value = "dummyvalue"
parser.section_name = "dummy_section"
parser._all_options = {parser.section_name: {option_name: value}}
with pytest.raises(DuplicateOptionError) as excinfo:
parser._store_internal_state_option(option_name, value, value)
message = f"Option '{option_name}' in section '{parser.section_name}' is defined more than once"
assert message in str(excinfo.value)

View File

@@ -1,6 +0,0 @@
testcases = [
"# comment # 1",
"; comment # 2",
" ; indented comment",
" # another indented comment",
]

View File

@@ -1,28 +0,0 @@
testcases = [
("option: value", "option", "value"),
("option : value", "option", "value"),
("option :value", "option", "value"),
("option= value", "option", "value"),
("option = value", "option", "value"),
("option =value", "option", "value"),
("option: value\n", "option", "value"),
("option: value # inline comment", "option", "value"),
("option: value # inline comment\n", "option", "value"),
(
"description: Helper: park toolhead used in PAUSE and CANCEL_PRINT",
"description",
"Helper: park toolhead used in PAUSE and CANCEL_PRINT",
),
("description: homing!", "description", "homing!"),
("description: inline macro :-)", "description", "inline macro :-)"),
("path: %GCODES_DIR%", "path", "%GCODES_DIR%"),
(
"serial = /dev/serial/by-id/<your-mcu-id>",
"serial",
"/dev/serial/by-id/<your-mcu-id>",
),
("parameter_temperature_(°C): 155", "parameter_temperature_(°C)", "155"),
("parameter_humidity_(%_RH): 45", "parameter_humidity_(%_RH)", "45"),
("parameter_spool_weight_(%): 10", "parameter_spool_weight_(%)", "10"),
("path: /dev/shm/drying_box.json", "path", "/dev/shm/drying_box.json"),
]

View File

@@ -1,8 +0,0 @@
testcases = [
("[test_section]", "test_section"),
("[test_section two]", "test_section two"),
("[section1] # inline comment", "section1"),
("[section2] ; second comment", "section2"),
("[include moonraker-obico-update.cfg]", "include moonraker-obico-update.cfg"),
("[include moonraker_obico_macros.cfg]", "include moonraker_obico_macros.cfg"),
]

View File

@@ -1,92 +0,0 @@
import pytest
from data.case_parse_comment import testcases as case_parse_comment
from data.case_parse_option import testcases as case_parse_option
from data.case_parse_section import testcases as case_parse_section
from src.simple_config_parser.simple_config_parser import (
Option,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestSingleLineParsing:
@pytest.mark.parametrize("given, expected", [*case_parse_section])
def test_parse_section(self, parser, given, expected):
parser._parse_section(given)
# Check that the internal state of the parser is correct
assert parser.section_name == expected
assert parser.in_option_block is False
assert parser._all_sections == [expected]
assert parser._config[expected]["_raw"] == given
assert parser._config[expected]["body"] == []
@pytest.mark.parametrize(
"given, expected_option, expected_value", [*case_parse_option]
)
def test_parse_option(self, parser, given, expected_option, expected_value):
section_name = "test_section"
parser.section_name = section_name
parser._parse_option(given)
# Check that the internal state of the parser is correct
assert parser.section_name == section_name
assert parser.in_option_block is False
assert parser._all_options[section_name][expected_option] == expected_value
section_option = parser._config[section_name]["body"][0]
assert section_option["option"] == expected_option
assert section_option["value"] == expected_value
assert section_option["_raw"] == given
@pytest.mark.parametrize(
"option, next_line",
[("gcode", "next line"), ("gcode", " {{% some jinja template %}}")],
)
def test_parse_multiline_option(self, parser, option, next_line):
parser.section_name = "dummy_section"
parser.in_option_block = True
parser._add_option_to_section_body(option, "", option)
parser._parse_multiline_option(option, next_line)
cleaned_next_line = next_line.strip().strip("\n")
assert parser._all_options[parser.section_name] is not None
assert parser._all_options[parser.section_name][option] == [cleaned_next_line]
expected_option: Option = {
"is_multiline": True,
"option": option,
"value": [cleaned_next_line],
"_raw": option,
"_raw_value": [next_line],
}
assert parser._config[parser.section_name]["body"] == [expected_option]
@pytest.mark.parametrize("given", [*case_parse_comment])
def test_parse_comment(self, parser, given):
parser.section_name = "dummy_section"
parser._parse_comment(given)
# internal state checks after parsing
assert parser.in_option_block is False
expected_option = {
"is_multiline": False,
"_raw": given,
"option": "",
"value": "",
}
assert parser._config[parser.section_name]["body"] == [expected_option]
@pytest.mark.parametrize("given", ["# header line", "; another header line"])
def test_parse_header_comment(self, parser, given):
parser.section_name = ""
parser._parse_comment(given)
assert parser.in_option_block is False
assert parser._header == [given]

View File

@@ -1,9 +0,0 @@
testcases = [
("# an arbitrary comment", True),
("; another arbitrary comment", True),
(" ; indented comment", True),
(" # indented comment", True),
("not_a: comment", False),
("also_not_a= comment", False),
("[definitely_not_a_comment]", False),
]

View File

@@ -1,9 +0,0 @@
testcases = [
("", True),
(" ", True),
("not empty", False),
(" # indented comment", False),
("not: empty", False),
("also_not= empty", False),
("[definitely_not_empty]", False),
]

View File

@@ -1,27 +0,0 @@
testcases = [
("valid_option:", True),
("valid_option:\n", True),
("valid_option: ; inline comment", True),
("valid_option: # inline comment", True),
("valid_option :", True),
("valid_option=", True),
("valid_option= ", True),
("valid_option =", True),
("valid_option = ", True),
("invalid_option ==", False),
("invalid_option :=", False),
("not_a_valid_option", False),
("", False),
("# that's a comment", False),
("; that's a comment", False),
("parameter_humidity_(%_RH):", True),
("parameter_spool_weight_(%):", True),
("parameter_temperature_(°C):", True),
("parameter_humidity_(%_RH): 18.123", False),
("parameter_spool_weight_(%): 150", False),
("parameter_temperature_(°C): 30,5", False),
("trusted_clients:", True),
("trusted_clients: 192.168.1.0/24", False),
("cors_domains:", True),
("cors_domains: http://*.lan", False),
]

View File

@@ -1,31 +0,0 @@
testcases = [
("valid_option: value", True),
("valid_option: value\n", True),
("valid_option: value ; inline comment", True),
("valid_option: value # inline comment", True),
("valid_option: value # inline comment\n", True),
("valid_option : value", True),
("valid_option :value", True),
("valid_option= value", True),
("valid_option = value", True),
("valid_option =value", True),
("invalid_option:", False),
("invalid_option=", False),
("invalid_option:: value", False),
("invalid_option :: value", False),
("invalid_option ::value", False),
("invalid_option== value", False),
("invalid_option == value", False),
("invalid_option ==value", False),
("invalid_option:= value", False),
("invalid_option := value", False),
("invalid_option :=value", False),
("[that_is_a_section]", False),
("[that_is_section two]", False),
("not_a_valid_option", False),
("description: homing!", True),
("description: inline macro :-)", True),
("path: %GCODES_DIR%", True),
("path: /dev/shm/drying_box.json", True),
("serial = /dev/serial/by-id/<your-mcu-id>", True),
]

View File

@@ -1,12 +0,0 @@
testcases = [
("[example_section]", True),
("[gcode_macro CANCEL_PRINT]", True),
("[gcode_macro SET_PAUSE_NEXT_LAYER]", True),
("[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]", True),
("[update_manager moonraker-obico]", True),
("[include moonraker_obico_macros.cfg]", True),
("[include moonraker-obico-update.cfg]", True),
("[example_section two]", True),
("not_a_valid_section", False),
("section: invalid", False),
]

View File

@@ -1,37 +0,0 @@
import pytest
from data.case_line_is_comment import testcases as case_line_is_comment
from data.case_line_is_empty import testcases as case_line_is_empty
from data.case_line_is_multiline_option import (
testcases as case_line_is_multiline_option,
)
from data.case_line_is_option import testcases as case_line_is_option
from data.case_line_is_section import testcases as case_line_is_section
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestLineTypeDetection:
@pytest.mark.parametrize("given, expected", [*case_line_is_section])
def test_line_is_section(self, parser, given, expected):
assert parser._is_section(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_option])
def test_line_is_option(self, parser, given, expected):
assert parser._is_option(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_multiline_option])
def test_line_is_multiline_option(self, parser, given, expected):
assert parser._is_multiline_option(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_comment])
def test_line_is_comment(self, parser, given, expected):
assert parser._is_comment(given) is expected
@pytest.mark.parametrize("given, expected", [*case_line_is_empty])
def test_line_is_empty(self, parser, given, expected):
assert parser._is_empty_line(given) is expected

View File

@@ -1,196 +0,0 @@
import pytest
from src.simple_config_parser.simple_config_parser import (
DuplicateSectionError,
NoOptionError,
NoSectionError,
SimpleConfigParser,
)
@pytest.fixture
def parser():
return SimpleConfigParser()
class TestPublicAPI:
def test_has_section(self, parser):
parser._all_sections = ["section1"]
assert parser.has_section("section1") is True
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_add_section(self, parser, section):
parser.add_section(section)
assert section in parser._all_sections
assert parser._all_options[section] == {}
cfg_section = {"_raw": f"\n[{section}]\n", "body": []}
assert parser._config[section] == cfg_section
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_add_existing_section(self, parser, section):
parser._all_sections = [section]
with pytest.raises(DuplicateSectionError):
parser.add_section(section)
assert parser._all_sections == [section]
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_remove_section(self, parser, section):
parser.add_section(section)
parser.remove_section(section)
assert section not in parser._all_sections
assert section not in parser._all_options
assert section not in parser._config
@pytest.mark.parametrize("section", ["section1", "section2", "section three"])
def test_remove_non_existing_section(self, parser, section):
with pytest.raises(NoSectionError):
parser.remove_section(section)
def test_get_all_sections(self, parser):
parser.add_section("section1")
parser.add_section("section2")
parser.add_section("section three")
assert parser.sections() == ["section1", "section2", "section three"]
def test_has_option(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "value1")
assert parser.has_option("section1", "option1") is True
@pytest.mark.parametrize(
"section, option, value",
[
("section1", "option1", "value1"),
("section2", "option2", "value2"),
("section three", "option3", "value three"),
],
)
def test_set_new_option(self, parser, section, option, value):
parser.add_section(section)
parser.set(section, option, value)
assert section in parser._all_sections
assert option in parser._all_options[section]
assert parser._all_options[section][option] == value
assert parser._config[section]["body"][0]["is_multiline"] is False
assert parser._config[section]["body"][0]["option"] == option
assert parser._config[section]["body"][0]["value"] == value
assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value}\n"
def test_set_existing_option(self, parser):
section, option, value1, value2 = "section1", "option1", "value1", "value2"
parser.add_section(section)
parser.set(section, option, value1)
parser.set(section, option, value2)
assert parser._all_options[section][option] == value2
assert parser._config[section]["body"][0]["is_multiline"] is False
assert parser._config[section]["body"][0]["option"] == option
assert parser._config[section]["body"][0]["value"] == value2
assert parser._config[section]["body"][0]["_raw"] == f"{option}: {value2}\n"
def test_set_new_multiline_option(self, parser):
section, option, value = "section1", "option1", "value1\nvalue2\nvalue3"
parser.add_section(section)
parser.set(section, option, value)
assert parser._config[section]["body"][0]["is_multiline"] is True
assert parser._config[section]["body"][0]["option"] == option
values = ["value1", "value2", "value3"]
raw_values = [" value1\n", " value2\n", " value3\n"]
assert parser._config[section]["body"][0]["value"] == values
assert parser._config[section]["body"][0]["_raw"] == f"{option}:\n"
assert parser._config[section]["body"][0]["_raw_value"] == raw_values
assert parser._all_options[section][option] == values
def test_set_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.set("section1", "option1", "value1")
def test_remove_option(self, parser):
section, option, value = "section1", "option1", "value1"
parser.add_section(section)
parser.set(section, option, value)
parser.remove_option(section, option)
assert option not in parser._all_options[section]
assert option not in parser._config[section]["body"]
def test_remove_non_existing_option(self, parser):
parser.add_section("section1")
with pytest.raises(NoOptionError):
parser.remove_option("section1", "option1")
def test_remove_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.remove_option("section1", "option1")
def test_get_option(self, parser):
parser.add_section("section1")
parser.add_section("section2")
parser.set("section1", "option1", "value1")
parser.set("section2", "option2", "value2")
parser.set("section2", "option3", "value two")
assert parser.get("section1", "option1") == "value1"
assert parser.get("section2", "option2") == "value2"
assert parser.get("section2", "option3") == "value two"
def test_get_option_of_non_existing_section(self, parser):
with pytest.raises(NoSectionError):
parser.get("section1", "option1")
def test_get_option_of_non_existing_option(self, parser):
parser.add_section("section1")
with pytest.raises(NoOptionError):
parser.get("section1", "option1")
def test_get_option_fallback(self, parser):
parser.add_section("section1")
assert parser.get("section1", "option1", "fallback_value") == "fallback_value"
def test_get_options(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "value1")
parser.set("section1", "option2", "value2")
parser.set("section1", "option3", "value3")
options = {"option1": "value1", "option2": "value2", "option3": "value3"}
assert parser.options("section1") == options
def test_get_option_as_int(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "1")
option = parser.getint("section1", "option1")
assert isinstance(option, int) is True
def test_get_option_as_float(self, parser):
parser.add_section("section1")
parser.set("section1", "option1", "1.234")
option = parser.getfloat("section1", "option1")
assert isinstance(option, float) is True
@pytest.mark.parametrize(
"value",
["True", "true", "on", "1", "yes", "False", "false", "off", "0", "no"],
)
def test_get_option_as_boolean(self, parser, value):
parser.add_section("section1")
parser.set("section1", "option1", value)
option = parser.getboolean("section1", "option1")
assert isinstance(option, bool) is True

View File

@@ -0,0 +1,7 @@
not_empty
[also_not_empty]
#
;
;
#
option: value

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_line_comment(parser, line):
"""Test that a line matches the definition of a line comment"""
assert (
parser._match_empty_line(line) is True
), f"Expected line '{line}' to match line comment definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_line_comment(parser, line):
"""Test that a line does not match the definition of a line comment"""
assert (
parser._match_empty_line(line) is False
), f"Expected line '{line}' to not match line comment definition!"

View File

@@ -0,0 +1,28 @@
;[example_section]
#[example_section]
# [example_section]
; [example_section]
;[gcode_macro CANCEL_PRINT]
#[gcode_macro CANCEL_PRINT]
# [gcode_macro CANCEL_PRINT]
; [gcode_macro CANCEL_PRINT]
;[gcode_macro SET_PAUSE_NEXT_LAYER]
#[gcode_macro SET_PAUSE_NEXT_LAYER]
# [gcode_macro SET_PAUSE_NEXT_LAYER]
; [gcode_macro SET_PAUSE_NEXT_LAYER]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]

View File

@@ -0,0 +1,5 @@
not_a_comment: nono
[also not a comment]
not_a_comment: ; comment
not_a_comment: # comment

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_line_comment(parser, line):
"""Test that a line matches the definition of a line comment"""
assert (
parser._match_line_comment(line) is True
), f"Expected line '{line}' to match line comment definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_line_comment(parser, line):
"""Test that a line does not match the definition of a line comment"""
assert (
parser._match_line_comment(line) is False
), f"Expected line '{line}' to not match line comment definition!"

View File

@@ -0,0 +1,461 @@
baud: 250000
minimum_cruise_ratio: 0.5
square_corner_velocity: 5.0
full_steps_per_rotation: 200
position_min: 0
homing_speed: 5.0
homing_retract_dist: 5.0
kinematics: cartesian
kinematics: delta
minimum_z_position: 0
speed: 50
horizontal_move_z: 5
kinematics: deltesian
minimum_z_position: 0
min_angle: 5
slow_ratio: 3
kinematics: corexy
kinematics: corexz
kinematics: hybrid_corexy
kinematics: hybrid_corexz
kinematics: polar
kinematics: rotary_delta
minimum_z_position: 0
speed: 50
horizontal_move_z: 5
kinematics: winch
kinematics: none
max_velocity: 1
max_accel: 1
instantaneous_corner_velocity: 1.000
max_extrude_only_distance: 50.0
pressure_advance: 0.0
pressure_advance_smooth_time: 0.040
max_power: 1.0
pullup_resistor: 4700
smooth_time: 1.0
max_delta: 2.0
pwm_cycle_time: 0.100
min_extrude_temp: 170
speed: 50
horizontal_move_z: 5
probe_count: 3, 3
round_probe_count: 5
fade_start: 1.0
fade_end: 0.0
split_delta_z: .025
move_check_distance: 5.0
mesh_pps: 2, 2
algorithm: lagrange
bicubic_tension: .2
x_adjust: 0
y_adjust: 0
z_adjust: 0
speed: 50
horizontal_move_z: 5
horizontal_move_z: 5
probe_height: 0
speed: 50
probe_speed: 5
speed: 50
horizontal_move_z: 5
screw_thread: CW-M3
speed: 50
horizontal_move_z: 5
retries: 0
retry_tolerance: 0
speed: 50
horizontal_move_z: 5
max_adjust: 4
retries: 0
retry_tolerance: 0
speed: 50.0
z_hop_speed: 15.0
move_to_previous: False
axes: xyz
endstop_align_zero: False
description: G-Code macro
initial_duration: 0.0
timeout: 600
enable_force_move: False
recover_velocity: 50.
retract_length: 0
retract_speed: 20
unretract_extra_length: 0
unretract_speed: 10
resolution: 1.0
default_type: echo
default_prefix: echo:
shaper_freq_x: 0
shaper_freq_y: 0
shaper_type: mzv
damping_ratio_x: 0.1
damping_ratio_y: 0.1
spi_speed: 5000000
axes_map: x, y, z
rate: 3200
spi_speed: 5000000
axes_map: x, y, z
i2c_speed: 400000
axes_map: x, y, z
min_freq: 5
max_freq: 133.33
accel_per_hz: 75
hz_per_sec: 1
mcu: mcu
deactivate_on_each_sample: True
x_offset: 0.0
y_offset: 0.0
speed: 5.0
samples: 1
sampleretract_dist: 2.0
samples_result: average
samples_tolerance: 0.100
samples_toleranceretries: 0
pin_move_time: 0.680
stow_on_each_sample: True
probe_with_touch_mode: False
pin_up_reports_not_triggered: True
pin_up_touch_modereports_triggered: True
recovery_time: 0.4
sensor_type: ldc1612
speed: 50
horizontal_move_z: 5
calibrate_start_x: 20
calibrate_end_x: 200
calibrate_y: 112.5
max_error: 120
hysteresis: 5
heating_gain: 2
extruder_heating_z: 50.
max_validation_temp: 60.
pullup_resistor: 4700
inlineresistor: 0
adc_voltage: 5.0
voltage_offset: 0
sensor_type: PT1000
pullup_resistor: 4700
spi_speed: 4000000
tc_type: K
tc_use_50Hz_filter: False
tc_averaging_count: 1
rtd_nominal_r: 100
rtd_referencer: 430
rtd_num_of_wires: 2
rtd_use_50Hz_filter: False
sensor_type: BME280
sensor_type: AHT10
sensor_type: temperature_mcu
sensor_mcu: mcu
sensor_type: temperature_host
sensor_type: DS18B20
sensor_type: temperature_combined
max_power: 1.0
shutdown_speed: 0
cycle_time: 0.010
hardware_pwm: False
kick_start_time: 0.100
off_below: 0.0
tachometer_ppr: 2
tachometer_poll_interval: 0.0015
heater: extruder
heater_temp: 50.0
fan_speed: 1.0
fan_speed: 1.0
pid_deriv_time: 2.0
target_temp: 40.0
max_speed: 1.0
min_speed: 0.3
cycle_time: 0.010
hardware_pwm: False
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
color_order: GRB
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
i2c_address: 98
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
i2c_address: 98
color_order: RGBW
initial_RED: 0.0
initial_GREEN: 0.0
initial_BLUE: 0.0
initial_WHITE: 0.0
maximum_servo_angle: 180
minimum_pulse_width: 0.001
maximum_pulse_width: 0.002
pwm: False
cycle_time: 0.100
hardware_pwm: False
cycle_time: 0.100
hardware_pwm: False
cycle_time: 0.100
interpolate: True
senseresistor: 0.110
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 0
driver_TBL: 1
driver_TOFF: 4
driver_HEND: 7
driver_HSTRT: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_PWM_AUTOSCALE: True
driver_PWM_FREQ: 1
driver_PWM_GRAD: 4
driver_PWM_AMPL: 128
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
interpolate: True
sense_resistor: 0.110
stealthchop_threshold: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 20
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 0
driver_HSTRT: 5
driver_PWM_AUTOGRAD: True
driver_PWM_AUTOSCALE: True
driver_PWM_LIM: 12
driver_PWM_REG: 8
driver_PWM_FREQ: 1
driver_PWM_GRAD: 14
driver_PWM_OFS: 36
interpolate: True
sense_resistor: 0.110
stealthchop_threshold: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 8
driver_TPOWERDOWN: 20
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 0
driver_HSTRT: 5
driver_PWM_AUTOGRAD: True
driver_PWM_AUTOSCALE: True
driver_PWM_LIM: 12
driver_PWM_REG: 8
driver_PWM_FREQ: 1
driver_PWM_GRAD: 14
driver_PWM_OFS: 36
driver_SGTHRS: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
spi_speed: 4000000
interpolate: True
idle_current_percent: 100
driver_TBL: 2
driver_RNDTF: 0
driver_HDEC: 0
driver_CHM: 0
driver_HEND: 3
driver_HSTRT: 3
driver_TOFF: 4
driver_SEIMIN: 0
driver_SEDN: 0
driver_SEMAX: 0
driver_SEUP: 0
driver_SEMIN: 0
driver_SFILT: 0
driver_SGT: 0
driver_SLPH: 0
driver_SLPL: 0
driver_DISS2G: 0
driver_TS2G: 3
interpolate: True
rref: 12000
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_OFFSET_SIN90: 0
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 6
driver_IRUNDELAY: 4
driver_TPOWERDOWN: 10
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 2
driver_HSTRT: 5
driver_FD3: 0
driver_TPFD: 4
driver_CHM: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_DISS2G: 0
driver_DISS2VS: 0
driver_PWM_AUTOSCALE: True
driver_PWM_AUTOGRAD: True
driver_PWM_FREQ: 0
driver_FREEWHEEL: 0
driver_PWM_GRAD: 0
driver_PWM_OFS: 29
driver_PWM_REG: 4
driver_PWM_LIM: 12
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
driver_SG4_ANGLE_OFFSET: 1
interpolate: True
sense_resistor: 0.075
stealthchop_threshold: 0
driver_MSLUT0: 2863314260
driver_MSLUT1: 1251300522
driver_MSLUT2: 608774441
driver_MSLUT3: 269500962
driver_MSLUT4: 4227858431
driver_MSLUT5: 3048961917
driver_MSLUT6: 1227445590
driver_MSLUT7: 4211234
driver_W0: 2
driver_W1: 1
driver_W2: 1
driver_W3: 1
driver_X1: 128
driver_X2: 255
driver_X3: 255
driver_START_SIN: 0
driver_START_SIN90: 247
driver_MULTISTEP_FILT: True
driver_IHOLDDELAY: 6
driver_TPOWERDOWN: 10
driver_TBL: 2
driver_TOFF: 3
driver_HEND: 2
driver_HSTRT: 5
driver_FD3: 0
driver_TPFD: 4
driver_CHM: 0
driver_VHIGHFS: 0
driver_VHIGHCHM: 0
driver_DISS2G: 0
driver_DISS2VS: 0
driver_PWM_AUTOSCALE: True
driver_PWM_AUTOGRAD: True
driver_PWM_FREQ: 0
driver_FREEWHEEL: 0
driver_PWM_GRAD: 0
driver_PWM_OFS: 30
driver_PWM_REG: 4
driver_PWM_LIM: 12
driver_SGT: 0
driver_SEMIN: 0
driver_SEUP: 0
driver_SEMAX: 0
driver_SEDN: 0
driver_SEIMIN: 0
driver_SFILT: 0
driver_DRVSTRENGTH: 0
driver_BBMCLKS: 4
driver_BBMTIME: 0
driver_FILT_ISENSE: 0
i2c_address: 96
analog_pullup_resistor: 4700
lcd_type: hd44780
hd44780_protocol_init: True
lcd_type: hd44780_spi
hd44780_protocol_init: True
lcd_type: st7920
lcd_type: emulated_st7920
lcd_type: uc1701
vcomh: 0
invert: False
x_offset: 0
type: disabled
type: list
type: command
type: input
pause_on_runout: True
event_delay: 3.0
pause_delay: 0.5
detection_length: 7.0
default_nominal_filament_diameter: 1.75
max_difference: 0.2
measurement_delay: 100
cal_dia1: 1.50
cal_dia2: 2.00
raw_dia1: 9500
raw_dia2: 10500
default_nominal_filament_diameter: 1.75
max_difference: 0.200
measurement_delay: 70
enable: False
measurement_interval: 10
logging: False
min_diameter: 1.0
use_current_dia_while_delay: False
sensor_type: hx711
gain: A-128
sample_rate: 80
sensor_type: hx717
gain: A-128
sample_rate: 320
sensor_type: ads1220
spi_speed: 512000
gain: 128
sample_rate: 660
smooth_time: 2.0
enable_pin: !gpio0_20
standstill_power_down: False
baud: 115200
feedrate_splice: 0.8
feedrate_normal: 1.0
auto_load_speed: 2
auto_cancel_variation: 0.1
sample_period: 0.000400

View File

@@ -0,0 +1,37 @@
[section]
[section with spaces]
[section with spaces and comments] ; comment 1
[section with spaces and comments] # comment 2
indented_option: value
option_with_no_value:
another_option_with_no_value:
indented_option_with_no_value:
# position_min: 0
# homing_speed: 5.0
### this is a comment
; this is also a comment
# [section]
# [section with spaces]
# [section with spaces and comments] ; comment 1
;[section]
;[section with spaces]
;[section with spaces and comments] ; comment 1
# commented_option: value
#commented_option: value
;commented_option: value
; commented_option: value
#
;
option_1 :: value
option_1:: value
option_1 ::value
option_2 == value
option_2== value
option_2 ==value
option_1 := value
option_1:= value
option_1 :=value
option_2 := value
option_2:= value
option_2 :=value

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_option(parser, line):
"""Test that a line matches the definition of an option"""
assert (
parser._match_option(line) is True
), f"Expected line '{line}' to match option definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_option(parser, line):
"""Test that a line does not match the definition of an option"""
assert (
parser._match_option(line) is False
), f"Expected line '{line}' to not match option definition!"

View File

@@ -0,0 +1,15 @@
trusted_clients:
gcode:
cors_domains:
an_options_block_start_with_comment: ; this is a comment
an_options_block_start_with_comment: # this is a comment
options_block_start_with_comment:;this is a comment
options_block_start_with_comment :;this is a comment
options_block_start_with_comment:#this is a comment
options_block_start_with_comment :#this is a comment
parameter_temperature_(°C):
parameter_temperature_(°C)=
parameter_humidity_(%_RH):
parameter_humidity_(%_RH) :
parameter_spool_weight_(%):
parameter_spool_weight_(%) =

View File

@@ -0,0 +1,31 @@
type: jsonfile
path: /dev/shm/drying_box.json
baud: 250000
minimum_cruise_ratio: 0.5
square_corner_velocity: 5.0
full_steps_per_rotation: 200
position_min: 0
homing_speed: 5.0
# baud: 250000
# minimum_cruise_ratio: 0.5
# square_corner_velocity: 5.0
# full_steps_per_rotation: 200
# position_min: 0
# homing_speed: 5.0
### this is a comment
; this is also a comment
;
#
homing_speed::
homing_speed::
homing_speed ::
homing_speed ::
homing_speed==
homing_speed==
homing_speed ==
homing_speed ==
homing_speed :=
homing_speed :=
homing_speed =:
homing_speed =:

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_options_block_start(parser, line):
"""Test that a line matches the definition of an options block start"""
assert (
parser._match_options_block_start(line) is True
), f"Expected line '{line}' to match options block start definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_options_block_start(parser, line):
"""Test that a line does not match the definition of an options block start"""
assert (
parser._match_options_block_start(line) is False
), f"Expected line '{line}' to not match options block start definition!"

View File

@@ -0,0 +1,127 @@
[example_section]
[gcode_macro CANCEL_PRINT]
[gcode_macro SET_PAUSE_NEXT_LAYER]
[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
[update_manager moonraker-obico]
[include moonraker_obico_macros.cfg]
[include moonraker-obico-update.cfg]
[example_section two]
[valid_content]
[valid content]
[content123]
[a]
[valid_content] # comment
[something];comment
[mcu]
[printer]
[printer]
[stepper_x]
[stepper_y]
[stepper_z]
[printer]
[stepper_a]
[stepper_b]
[stepper_c]
[delta_calibrate]
[printer]
[stepper_left]
[stepper_right]
[stepper_bed]
[stepper_arm]
[delta_calibrate]
[extruder]
[heater_bed]
[bed_mesh]
[bed_tilt]
[bed_screws]
[screws_tilt_adjust]
[z_tilt]
[quad_gantry_level]
[skew_correction]
[z_thermal_adjust]
[safe_z_home]
[homing_override]
[endstop_phase stepper_z]
[gcode_macro my_cmd]
[delayed_gcode my_delayed_gcode]
[save_variables]
[idle_timeout]
[virtual_sdcard]
[sdcard_loop]
[force_move]
[pause_resume]
[firmware_retraction]
[gcode_arcs]
[respond]
[exclude_object]
[input_shaper]
[adxl345]
[lis2dw]
[mpu9250 my_accelerometer]
[resonance_tester]
[board_pins my_aliases]
[duplicate_pin_override]
[probe]
[bltouch]
[smart_effector]
[probe_eddy_current my_eddy_probe]
[axis_twist_compensation]
[stepper_z1]
[extruder1]
[dual_carriage]
[extruder_stepper my_extra_stepper]
[manual_stepper my_stepper]
[verify_heater heater_config_name]
[homing_heaters]
[thermistor my_thermistor]
[adc_temperature my_sensor]
[heater_generic my_generic_heater]
[temperature_sensor my_sensor]
[temperature_probe my_probe]
[fan]
[heater_fan heatbreak_cooling_fan]
[controller_fan my_controller_fan]
[temperature_fan my_temp_fan]
[fan_generic extruder_partfan]
[led my_led]
[neopixel my_neopixel]
[dotstar my_dotstar]
[pca9533 my_pca9533]
[pca9632 my_pca9632]
[servo my_servo]
[gcode_button my_gcode_button]
[output_pin my_pin]
[pwm_tool my_tool]
[pwm_cycle_time my_pin]
[static_digital_output my_output_pins]
[multi_pin my_multi_pin]
[tmc2130 stepper_x]
[tmc2208 stepper_x]
[tmc2209 stepper_x]
[tmc2660 stepper_x]
[tmc2240 stepper_x]
[tmc5160 stepper_x]
[ad5206 my_digipot]
[mcp4451 my_digipot]
[mcp4728 my_dac]
[mcp4018 my_digipot]
[display]
[display_data my_group_name my_data_name]
[display_template my_template_name]
[display_glyph my_display_glyph]
[menu __some_list __some_name]
[menu some_name]
[menu some_list]
[menu some_list some_command]
[menu some_list some_input]
[filament_switch_sensor my_sensor]
[filament_motion_sensor my_sensor]
[tsl1401cl_filament_width_sensor]
[hall_filament_width_sensor]
[load_cell]
[sx1509 my_sx1509]
[samd_sercom my_sercom]
[adc_scaled my_name]
[replicape]
[palette2]
[angle my_angle_sensor]

View File

@@ -0,0 +1,19 @@
section: invalid
not_a_valid_section
[missing_square_bracket
missing_square_bracket]
[]
[ ]
[indented_section]
[indented_section] # comment
[indented_section] ; comment
;[commented_section]
#[another_commented_section]
; [commented_section]
# [another_commented_section]
this_is_an_option: 123
this_is_an_indented_option: 123
this_is_an_option_block_start:
#
;

View File

@@ -0,0 +1,39 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
def test_match_section(parser, line):
"""Test that a line matches the definition of a section"""
assert (
parser._match_section(line) is True
), f"Expected line '{line}' to match section definition!"
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
def test_non_matching_section(parser, line):
"""Test that a line does not match the definition of a section"""
assert (
parser._match_section(line) is False
), f"Expected line '{line}' to not match section definition!"

View File

@@ -0,0 +1,62 @@
# ======================================================================= #
# 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.constants import HEADER_IDENT
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")
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_section_parsing(parser):
expected_keys = {"section_1", "section_2", "section_3", "section_4"}
assert expected_keys.issubset(
parser.config.keys()
), f"Expected keys: {expected_keys}, got: {parser.config.keys()}"
assert parser.in_option_block is False
assert parser.current_section == parser.get_sections()[-1]
assert parser.config["section_2"]["_raw"] == "[section_2] ; comment"
def test_option_parsing(parser):
assert parser.config["section_1"]["option_1"]["value"] == "value_1"
assert parser.config["section_1"]["option_1"]["_raw"] == "option_1: value_1"
assert parser.config["section_3"]["option_3"]["value"] == "value_3"
assert (
parser.config["section_3"]["option_3"]["_raw"] == "option_3: value_3 # comment"
)
def test_header_parsing(parser):
header = parser.config[HEADER_IDENT]
assert isinstance(header, list)
assert len(header) > 0
def test_collector_parsing(parser):
section = "section_2"
section_content = list(parser.config[section].keys())
coll_name = [name for name in section_content if name.startswith("#_")][0]
collector = parser.config[section][coll_name]
assert collector is not None
assert isinstance(collector, list)
assert len(collector) > 0
assert "; comment" in collector

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

@@ -0,0 +1,178 @@
# ======================================================================= #
# 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 #
# ======================================================================= #
import pytest
from src.simple_config_parser.simple_config_parser import (
NoOptionError,
NoSectionError,
)
def test_get_options(parser):
expected_options = {
"section_1": {"option_1"},
"section_2": {"option_2"},
"section_3": {"option_3"},
"section_4": {"option_4"},
"section number 5": {"option_5", "multi_option", "option_5_1"},
}
for section, options in expected_options.items():
assert options.issubset(
parser.get_options(section)
), f"Expected options: {options} in section: {section}, got: {parser.get_options(section)}"
assert "_raw" not in parser.get_options(section)
assert all(
not option.startswith("#_") for option in parser.get_options(section)
)
def test_has_option(parser):
assert parser.has_option("section_1", "option_1") is True
assert parser.has_option("section_1", "option_128") is False
# section does not exist:
assert parser.has_option("section_128", "option_1") is False
def test_getval(parser):
# test regular option values
assert parser.getval("section_1", "option_1") == "value_1"
assert parser.getval("section_3", "option_3") == "value_3"
assert parser.getval("section_4", "option_4") == "value_4"
assert parser.getval("section number 5", "option_5") == "this.is.value-5"
assert parser.getval("section number 5", "option_5_1") == "value_5_1"
assert parser.getval("section_2", "option_2") == "value_2"
# test multiline option values
ml_val = parser.getval("section number 5", "multi_option")
assert isinstance(ml_val, list)
assert len(ml_val) > 0
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):
with pytest.raises(NoSectionError):
parser.getval("section_128", "option_1")
with pytest.raises(NoOptionError):
parser.getval("section_1", "option_128")
def test_getint(parser):
value = parser.getint("section_1", "option_1_2")
assert isinstance(value, int)
def test_getint_from_val(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1")
def test_getint_from_float(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1_3")
def test_getint_from_boolean(parser):
with pytest.raises(ValueError):
parser.getint("section_1", "option_1_1")
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):
value = parser.getboolean("section_1", "option_1_1")
assert isinstance(value, bool)
assert value is True or value is False
def test_getboolean_from_val(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1")
def test_getboolean_from_int(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1_2")
def test_getboolean_from_float(parser):
with pytest.raises(ValueError):
parser.getboolean("section_1", "option_1_3")
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):
value = parser.getfloat("section_1", "option_1_3")
assert isinstance(value, float)
def test_getfloat_from_val(parser):
with pytest.raises(ValueError):
parser.getfloat("section_1", "option_1")
def test_getfloat_from_int(parser):
value = parser.getfloat("section_1", "option_1_2")
assert isinstance(value, float)
def test_getfloat_from_boolean(parser):
with pytest.raises(ValueError):
parser.getfloat("section_1", "option_1_1")
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):
parser.set_option("section_1", "new_option", "new_value")
assert parser.getval("section_1", "new_option") == "new_value"
assert parser.config["section_1"]["new_option"]["_raw"] == "new_option: new_value\n"
parser.set_option("section_1", "new_option", "new_value_2")
assert parser.getval("section_1", "new_option") == "new_value_2"
assert (
parser.config["section_1"]["new_option"]["_raw"] == "new_option: new_value_2\n"
)
def test_set_new_option(parser):
parser.set_option("new_section", "very_new_option", "very_new_value")
assert (
parser.has_section("new_section") is True
), f"Expected 'new_section' in {parser.get_sections()}"
assert parser.getval("new_section", "very_new_option") == "very_new_value"
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
assert parser.getval("section_2", "array_option") == [
"value_1",
"value_2",
"value_3",
]
assert parser.config["section_2"]["array_option"]["_raw"] == "array_option:\n"
def test_remove_option(parser):
parser.remove_option("section_1", "option_1")
assert parser.has_option("section_1", "option_1") is False

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