diff --git a/.editorconfig b/.editorconfig index b3230cc..bb9afe1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,5 @@ end_of_line = lf [*.py] max_line_length = 88 -[*.{sh,yml,yaml,json}] +[*.{sh,yml,yaml,json,md}] indent_size = 2 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 3892969..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Bug report -description: Create a report to help us improve -labels: ["bug"] -body: - - type: markdown - attributes: - value: | - This issue form is for reporting bugs only! - If you have a feature request, please use [feature_request](/new?template=feature_request.yml) - - type: textarea - id: distro - attributes: - label: Linux Distribution - description: >- - The linux distribution the issue occured on - validations: - required: true - - type: textarea - id: what-happened - attributes: - label: What happened - description: >- - A clear and concise description of what the bug is. - validations: - required: true - - type: textarea - id: expected-behavior - attributes: - label: What did you expect to happen - description: >- - A clear and concise description of what you expected to happen. - validations: - required: true - - type: textarea - id: repro-steps - attributes: - label: How to reproduce - description: >- - Minimal and precise steps to reproduce this bug. - validations: - required: true - - type: textarea - id: additional-info - attributes: - label: Additional information - description: | - If you have any additional information for us, use the field below. - - Please note, you can attach screenshots or screen recordings here, by - dragging and dropping files in the field below. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 000afb0..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Klipper Discord - url: https://discord.klipper3d.org/ - about: Quickest way to get in contact diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 64d6647..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Feature request -description: Suggest an idea for this project -labels: ["feature request"] -body: - - type: markdown - attributes: - value: | - This issue form is for feature requests only! - If you've found a bug, please use [bug_report](/new?template=bug_report.yml) - - type: textarea - id: problem-description - attributes: - label: Is your feature request related to a problem? Please describe - description: >- - A clear and concise description of what the problem is. - validations: - required: true - - type: textarea - id: solution-description - attributes: - label: Describe the solution you'd like - description: >- - A clear and concise description of what you want to happen. - validations: - required: true - - type: textarea - id: possible-alternatives - attributes: - label: Describe alternatives you've considered - description: >- - A clear and concise description of any alternative solutions or features you've considered. - - type: textarea - id: additional-info - attributes: - label: Additional information - description: | - If you have any additional information for us, use the field below. - - Please note, you can attach screenshots or screen recordings here, by - dragging and dropping files in the field below. diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..4a9a0d9 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,31 @@ +name: Deploy Documentation +on: + workflow_dispatch: + push: + branches: + - docs +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - name: Install dependencies + run: pip install -r requirements.txt + - name: Build and deploy documentation + run: mkdocs gh-deploy --force diff --git a/.github/workflows/release-ff-and-tag.yml b/.github/workflows/release-ff-and-tag.yml deleted file mode 100644 index 04effb1..0000000 --- a/.github/workflows/release-ff-and-tag.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Release - Fast-Forward and Tag -on: - workflow_dispatch: - inputs: - tag_name: - description: 'Provide a tag name (e.g. v1.0.0)' - required: true - type: string - -jobs: - ff-and-tag: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: 'master' - - name: Merge Fast Forward - uses: MaximeHeckel/github-action-merge-fast-forward@v1.1.0 - with: - branchtomerge: origin/develop - branch: master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create and Push Tag - run: | - git tag ${{ inputs.tag_name }} - git push origin ${{ inputs.tag_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.shellcheckrc b/.shellcheckrc deleted file mode 100644 index faf9529..0000000 --- a/.shellcheckrc +++ /dev/null @@ -1,15 +0,0 @@ -source=scripts - -enable=avoid-nullary-conditions -enable=deprecate-which -enable=quote-safe-variables -enable=require-variable-braces -enable=require-double-brackets - -# SC2162: `read` without `-r` will mangle backslashes. -# https://github.com/koalaman/shellcheck/wiki/SC2162 -disable=SC2162 - -# SC2164: Use `cd ... || exit` in case `cd` fails -# https://github.com/koalaman/shellcheck/wiki/SC2164 -disable=SC2164 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b9e998a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM squidfunk/mkdocs-material:latest + +# Install additional plugins required by our mkdocs configuration +RUN pip install \ + mkdocs-git-revision-date-localized-plugin \ + mkdocstrings[python] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md deleted file mode 100644 index 72ece02..0000000 --- a/README.md +++ /dev/null @@ -1,214 +0,0 @@ -

- - KIAUH logo -

Klipper Installation And Update Helper

- -

- -

- A handy installation script that makes installing Klipper (and more) a breeze! -

- -

- - - - - -
- - -

- -
- -

- πŸ“„οΈ Instructions πŸ“„ -

- -### πŸ“‹ Prerequisites -KIAUH is a script that assists you in installing Klipper on a Linux operating system that has -already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure -that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image -if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/) -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, -select `Choose OS -> Raspberry Pi OS (other)`: \ -

- KIAUH logo -

- -* Then select `Raspberry Pi OS Lite (32bit)` (or 64bit if you want to use that instead): -

- KIAUH logo -

- -* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which -you want to flash the image. - -* Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu) -and enable SSH and configure Wi-Fi. - -* If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html). - -These steps **only** apply if you are actually using a Raspberry Pi. In case you want -to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed -to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run -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. - -### πŸ’Ύ Download and use KIAUH -**πŸ“’ Disclaimer: Usage of this script happens at your own risk!** - -* **Step 1:** \ -To download this script, it is necessary to have git installed. If you don't have git already installed, or if you are unsure, run the following command: -```shell -sudo apt-get update && sudo apt-get install git -y -``` - -* **Step 2:** \ -Once git is installed, use the following command to download KIAUH into your home-directory: - -```shell -cd ~ && git clone https://github.com/dw-0/kiauh.git -``` - -* **Step 3:** \ -Finally, start KIAUH by running the next command: - -```shell -./kiauh/kiauh.sh -``` - -* **Step 4:** \ -You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending -on what you want to do. To choose an action, simply type the corresponding number into the "Perform action" -prompt and confirm by hitting ENTER. - -
- -

❗ Notes ❗

- -### **πŸ“‹ Please see the [Changelog](docs/changelog.md) for possible important changes!** - -- Mainly tested on Raspberry Pi OS Lite (Debian 10 Buster / Debian 11 Bullseye) - - Other Debian based distributions (like Ubuntu 20 to 22) likely work too - - Reported to work on Armbian as well but not tested in detail -- During the use of this script you will be asked for your sudo password. There are several functions involved which need sudo privileges. - -
- -

🌐 Sources & Further Information

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Klipper

Moonraker

Mainsail

Klipper LogoArksine avatarMainsail Logo
by KevinOConnorby Arksineby mainsail-crew

Fluidd

KlipperScreen

OctoPrint

Fluidd Logojordanruthe avatarOctoPrint Logo
by fluidd-coreby jordanrutheby OctoPrint

Moonraker-Telegram-Bot

PrettyGCode for Klipper

Obico for Klipper

nlef avatarKragrathea avatarObico logo
by nlefby Kragratheaby Obico

Mobileraker's Companion

OctoEverywhere For Klipper

OctoApp For Klipper

Mobileraker LogoOctoEverywhere LogoOctoApp Logo
by Patrick Schmidtby Quinn Damerellby Christian WΓΌrthner

Klipper-Backup

SimplyPrint for Klipper

Staubgeroner Avatar
by Staubgeborenerby SimplyPrint
- -
- -

πŸŽ–οΈ Contributors πŸŽ–οΈ

- - - -
- -
- Repobeats analytics image -
- -
- -

✨ Credits ✨

- -* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo! -* Also, a big thank you to everyone who supported my work with a [Ko-fi](https://ko-fi.com/dw__0) ! -* Last but not least: Thank you to all contributors and members of the Klipper Community who like and share this project! - -
- -

A special thank you to JetBrains for sponsoring this project with their incredible software!

-

- - JetBrains Logo (Main) logo. - -

diff --git a/default.kiauh.cfg b/default.kiauh.cfg deleted file mode 100644 index d3ac149..0000000 --- a/default.kiauh.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[kiauh] -backup_before_update: False - -[klipper] -# add custom repositories here, if at least one is given, the first in the list will be used by default -# otherwise the official repository is used -# -# format: https://github.com/username/repository, branch -# example: https://github.com/Klipper3d/klipper, master -# -# branch is optional, if given, it must be preceded by a comma, if not given, 'master' is used -repositories: - https://github.com/Klipper3d/klipper - -[moonraker] -# Moonraker supports two optional Python packages that can be used to reduce its CPU load -# If set to true, those packages will be installed during the Moonraker installation -optional_speedups: True - -# add custom repositories here, if at least one is given, the first in the list will be used by default -# otherwise the official repository is used -# -# format: https://github.com/username/repository, branch -# example: https://github.com/Arksine/moonraker, master -# -# branch is optional, if given, it must be preceded by a comma, if not given, 'master' is used -repositories: - https://github.com/Arksine/moonraker - -[mainsail] -port: 80 -unstable_releases: False - -[fluidd] -port: 80 -unstable_releases: False diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de98507 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + mkdocs: + build: . + ports: + - "8000:8000" + volumes: + - ./:/docs + command: serve --dev-addr=0.0.0.0:8000 diff --git a/kiauh/components/__init__.py b/docs/assets/configuration.md similarity index 100% rename from kiauh/components/__init__.py rename to docs/assets/configuration.md diff --git a/docs/assets/logo-large.png b/docs/assets/logo-large.png new file mode 100644 index 0000000..0135a87 Binary files /dev/null and b/docs/assets/logo-large.png differ diff --git a/resources/screenshots/kiauh.png b/docs/assets/logo.png similarity index 100% rename from resources/screenshots/kiauh.png rename to docs/assets/logo.png diff --git a/docs/changelog.md b/docs/development/changelog.md similarity index 99% rename from docs/changelog.md rename to docs/development/changelog.md index 0699150..b6b2976 100644 --- a/docs/changelog.md +++ b/docs/development/changelog.md @@ -276,7 +276,7 @@ Each service gets its corresponding instance added to the service filename. * The user can now choose to install Klipper as a systemd service. -* The Shell Command extension and `shell_command.py` got renamed to G-Code Shell Command extension and `gcode_shell_command.py`. In case the [pending PR](https://github.com/KevinOConnor/klipper/pull/2173) will be merged in the future, this was an early attempt to dodge possible incompatibilities. The [G-Code Shell Command docs](gcode_shell_command.md) has been updated accordingly. +* The Shell Command extension and `shell_command.py` got renamed to G-Code Shell Command extension and `gcode_shell_command.py`. In case the [pending PR](https://github.com/KevinOConnor/klipper/pull/2173) will be merged in the future, this was an early attempt to dodge possible incompatibilities. The [G-Code Shell Command docs](extensions/gcode-shell-command) has been updated accordingly. * The way how KIAUH interacts and writes to the users printer.cfg got changed. Usually KIAUH wrote everything directly into the printer.cfg. The way it will work from now on is, that a new file called `kiauh.cfg` will be created if there is something that needs to be written to the printer.cfg and everything gets written to `kiauh.cfg` instead. The only thing which then gets written to the users printer.cfg is `[include kiauh.cfg]`. This line will be located at the very top of the existing printer.cfg with a little comment as a note. The user can then decide to either keep the `kiauh.cfg` or take its content, places it into the printer.cfg directly and remove the `[include kiauh.cfg]`. diff --git a/kiauh/components/klipper/menus/__init__.py b/docs/development/contributing.md similarity index 100% rename from kiauh/components/klipper/menus/__init__.py rename to docs/development/contributing.md diff --git a/docs/gcode_shell_command.md b/docs/extensions/gcode-shell-command.md similarity index 100% rename from docs/gcode_shell_command.md rename to docs/extensions/gcode-shell-command.md diff --git a/kiauh/components/klipper/services/__init__.py b/docs/extensions/index.md similarity index 100% rename from kiauh/components/klipper/services/__init__.py rename to docs/extensions/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..94e7824 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +

+ KIAUH - Klipper Installation And Update Helper +

+ +

+ KIAUH logo +

+

+ A handy installation script that makes installing Klipper (and more) a breeze! +

+ + +## Features + +- Easy installation of Klipper and related components +- Support for multiple instances +- Extension system for additional functionality +- Configuration management +- And more! + diff --git a/kiauh/components/moonraker/menus/__init__.py b/docs/installation.md similarity index 100% rename from kiauh/components/moonraker/menus/__init__.py rename to docs/installation.md diff --git a/kiauh.py b/kiauh.py deleted file mode 100644 index 6f99540..0000000 --- a/kiauh.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 kiauh.main import main - -if __name__ == "__main__": - main() diff --git a/kiauh.sh b/kiauh.sh deleted file mode 100755 index 7aec82d..0000000 --- a/kiauh.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e -clear -x - -# make sure we have the correct permissions while running the script -umask 022 - -### sourcing all additional scripts -KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" -for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done -for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done - -#===================================================# -#=================== UPDATE KIAUH ==================# -#===================================================# - -function update_kiauh() { - status_msg "Updating KIAUH ..." - - cd "${KIAUH_SRCDIR}" - git reset --hard && git pull - - ok_msg "Update complete! Please restart KIAUH." - exit 0 -} - -#===================================================# -#=================== KIAUH STATUS ==================# -#===================================================# - -function kiauh_update_avail() { - [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return - local origin head - - cd "${KIAUH_SRCDIR}" - - ### abort if not on master branch - ! git branch -a | grep -q "\* master" && return - - ### compare commit hash - git fetch -q - origin=$(git rev-parse --short=8 origin/master) - head=$(git rev-parse --short=8 HEAD) - - if [[ ${origin} != "${head}" ]]; then - echo "true" - fi -} - -function save_startup_version() { - local launch_version - - echo "${1}" - - sed -i "/^version_to_launch=/d" "${INI_FILE}" - sed -i '$a'"version_to_launch=${1}" "${INI_FILE}" -} - -function kiauh_update_dialog() { - [[ ! $(kiauh_update_avail) == "true" ]] && return - top_border - echo -e "|${green} New KIAUH update available! ${white}|" - hr - echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|" - blank_line - echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|" - echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|" - echo -e "|${yellow} features. Please consider updating! ${white}|" - bottom_border - - local yn - read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn - while true; do - case "${yn}" in - Y|y|Yes|yes|"") - do_action "update_kiauh" - break;; - N|n|No|no) - break;; - *) - deny_action "kiauh_update_dialog";; - esac - done -} - -function launch_kiauh_v5() { - main_menu -} - -function launch_kiauh_v6() { - local entrypoint - - if ! command -v python3 &>/dev/null || [[ $(python3 -V | cut -d " " -f2 | cut -d "." -f2) -lt 8 ]]; then - echo "Python 3.8 or higher is not installed!" - echo "Please install Python 3.8 or higher and try again." - exit 1 - fi - - entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") - - export PYTHONPATH="${entrypoint}" - - clear -x - python3 "${entrypoint}/kiauh.py" -} - -function main() { - read_kiauh_ini "${FUNCNAME[0]}" - - if [[ ${version_to_launch} -eq 5 ]]; then - launch_kiauh_v5 - elif [[ ${version_to_launch} -eq 6 ]]; then - launch_kiauh_v6 - else - top_border - echo -e "| ${green}KIAUH v6.0.0-alpha1 is available now!${white} |" - hr - echo -e "| View Changelog: ${magenta}https://git.io/JnmlX${white} |" - blank_line - echo -e "| KIAUH v6 was completely rewritten from the ground up. |" - echo -e "| It's based on Python 3.8 and has many improvements. |" - blank_line - echo -e "| ${yellow}NOTE: Version 6 is still in alpha, so bugs may occur!${white} |" - echo -e "| ${yellow}Yet, your feedback and bug reports are very much${white} |" - echo -e "| ${yellow}appreciated and will help finalize the release.${white} |" - hr - echo -e "| Would you like to try out KIAUH v6? |" - echo -e "| 1) Yes |" - echo -e "| 2) No |" - echo -e "| 3) Yes, remember my choice for next time |" - echo -e "| 4) No, remember my choice for next time |" - quit_footer - while true; do - read -p "${cyan}###### Select action:${white} " -e input - case "${input}" in - 1) - launch_kiauh_v6 - break;; - 2) - launch_kiauh_v5 - break;; - 3) - save_startup_version 6 - launch_kiauh_v6 - break;; - 4) - save_startup_version 5 - launch_kiauh_v5 - break;; - Q|q) - echo -e "${green}###### Happy printing! ######${white}"; echo - exit 0;; - *) - error_msg "Invalid Input!\n";; - esac - done && input="" - fi -} - -check_if_ratos -check_euid -init_logfile -set_globals -kiauh_update_dialog -read_kiauh_ini -init_ini -main diff --git a/kiauh/__init__.py b/kiauh/__init__.py deleted file mode 100644 index bbf131c..0000000 --- a/kiauh/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 sys -from pathlib import Path - -PROJECT_ROOT = Path(__file__).resolve().parent.parent -APPLICATION_ROOT = Path(__file__).resolve().parent -sys.path.append(str(APPLICATION_ROOT)) diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py deleted file mode 100644 index 01a0230..0000000 --- a/kiauh/components/crowsnest/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR -from core.constants import SYSTEMD - -# repo -CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" - -# names -CROWSNEST_SERVICE_NAME = "crowsnest.service" - -# directories -CROWSNEST_DIR = Path.home().joinpath("crowsnest") -CROWSNEST_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("crowsnest-backups") - -# files -CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") -CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh") -CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest") -CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest") -CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py deleted file mode 100644 index 516f63f..0000000 --- a/kiauh/components/crowsnest/crowsnest.py +++ /dev/null @@ -1,177 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -import time -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import List - -from components.crowsnest import ( - CROWSNEST_BACKUP_DIR, - CROWSNEST_BIN_FILE, - CROWSNEST_DIR, - CROWSNEST_INSTALL_SCRIPT, - CROWSNEST_LOGROTATE_FILE, - CROWSNEST_MULTI_CONFIG, - CROWSNEST_REPO, - CROWSNEST_SERVICE_FILE, - CROWSNEST_SERVICE_NAME, -) -from components.klipper.klipper import Klipper -from core.backup_manager.backup_manager import BackupManager -from core.logger import DialogType, Logger -from core.settings.kiauh_settings import KiauhSettings -from core.types.component_status import ComponentStatus -from utils.common import ( - check_install_dependencies, - get_install_status, -) -from utils.git_utils import ( - git_clone_wrapper, - git_pull_wrapper, -) -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - parse_packages_from_file, -) - - -def install_crowsnest() -> None: - # Step 1: Clone crowsnest repo - git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") - - # Step 2: Install dependencies - check_install_dependencies({"make"}) - - # Step 3: Check for Multi Instance - instances: List[Klipper] = get_instances(Klipper) - - if len(instances) > 1: - print_multi_instance_warning(instances) - - if not get_confirm("Do you want to continue with the installation?"): - Logger.print_info("Crowsnest installation aborted!") - return - - Logger.print_status("Launching crowsnest's install configurator ...") - time.sleep(3) - configure_multi_instance() - - # Step 4: Launch crowsnest installer - Logger.print_status("Launching crowsnest installer ...") - Logger.print_info("Installer will prompt you for sudo password!") - try: - run( - "sudo make install", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - -def print_multi_instance_warning(instances: List[Klipper]) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Multi instance install detected!", - "\n\n", - "Crowsnest is NOT designed to support multi instances. A workaround " - "for this is to choose the most used instance as a 'master' and use " - "this instance to set up your 'crowsnest.conf' and steering it's service.", - "\n\n", - "The following instances were found:", - *[f"● {instance.data_dir.name}" for instance in instances], - ], - ) - - -def configure_multi_instance() -> None: - try: - run( - "make config", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - if CROWSNEST_MULTI_CONFIG.exists(): - Path.unlink(CROWSNEST_MULTI_CONFIG) - return - - if not CROWSNEST_MULTI_CONFIG.exists(): - Logger.print_error("Generating .config failed, installation aborted") - - -def update_crowsnest() -> None: - try: - cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "stop") - - if not CROWSNEST_DIR.exists(): - git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") - else: - Logger.print_status("Updating Crowsnest ...") - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - bm = BackupManager() - bm.backup_directory( - CROWSNEST_DIR.name, - source=CROWSNEST_DIR, - target=CROWSNEST_BACKUP_DIR, - ) - - git_pull_wrapper(CROWSNEST_DIR) - - deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT) - check_install_dependencies({*deps}) - - cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart") - - Logger.print_ok("Crowsnest updated successfully.", end="\n\n") - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - -def get_crowsnest_status() -> ComponentStatus: - files = [ - CROWSNEST_BIN_FILE, - CROWSNEST_LOGROTATE_FILE, - CROWSNEST_SERVICE_FILE, - ] - return get_install_status(CROWSNEST_DIR, files=files) - - -def remove_crowsnest() -> None: - if not CROWSNEST_DIR.exists(): - Logger.print_info("Crowsnest does not seem to be installed! Skipping ...") - return - - try: - run( - "make uninstall", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - Logger.print_status("Removing crowsnest directory ...") - shutil.rmtree(CROWSNEST_DIR) - Logger.print_ok("Directory removed!") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py deleted file mode 100644 index 22efb95..0000000 --- a/kiauh/components/klipper/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent - -KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" - -# names -KLIPPER_LOG_NAME = "klippy.log" -KLIPPER_CFG_NAME = "printer.cfg" -KLIPPER_SERIAL_NAME = "klippy.serial" -KLIPPER_UDS_NAME = "klippy.sock" -KLIPPER_ENV_FILE_NAME = "klipper.env" -KLIPPER_SERVICE_NAME = "klipper.service" - -# directories -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") -KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") -KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") - -# files -KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") -KLIPPER_INSTALL_SCRIPT = KLIPPER_DIR.joinpath("scripts/install-ubuntu-22.04.sh") -KLIPPER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_SERVICE_NAME}") -KLIPPER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_ENV_FILE_NAME}") - - -EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/components/klipper/assets/klipper.env b/kiauh/components/klipper/assets/klipper.env deleted file mode 100644 index b56553e..0000000 --- a/kiauh/components/klipper/assets/klipper.env +++ /dev/null @@ -1 +0,0 @@ -KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%" diff --git a/kiauh/components/klipper/assets/klipper.service b/kiauh/components/klipper/assets/klipper.service deleted file mode 100644 index b41788f..0000000 --- a/kiauh/components/klipper/assets/klipper.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Klipper 3D Printer Firmware SV1 -Documentation=https://www.klipper3d.org/ -After=network-online.target -Wants=udev.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -RemainAfterExit=yes -WorkingDirectory=%KLIPPER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $KLIPPER_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/components/klipper/assets/printer.cfg b/kiauh/components/klipper/assets/printer.cfg deleted file mode 100644 index 88fe7df..0000000 --- a/kiauh/components/klipper/assets/printer.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[mcu] -serial: /dev/serial/by-id/ - -[virtual_sdcard] -path: %GCODES_DIR% -on_error_gcode: CANCEL_PRINT - -[printer] -kinematics: none -max_velocity: 1000 -max_accel: 1000 diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py deleted file mode 100644 index 8615186..0000000 --- a/kiauh/components/klipper/klipper.py +++ /dev/null @@ -1,142 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.klipper import ( - KLIPPER_CFG_NAME, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_ENV_FILE_NAME, - KLIPPER_ENV_FILE_TEMPLATE, - KLIPPER_LOG_NAME, - KLIPPER_SERIAL_NAME, - KLIPPER_SERVICE_TEMPLATE, - KLIPPER_UDS_NAME, -) -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from utils.fs_utils import create_folders, get_data_dir -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class Klipper: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = KLIPPER_LOG_NAME - klipper_dir: Path = KLIPPER_DIR - env_dir: Path = KLIPPER_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - env_file: Path = field(init=False) - serial: Path = field(init=False) - uds: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Klipper, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(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.env_file: Path = self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) - self.serial: Path = self.base.comms_dir.joinpath(KLIPPER_SERIAL_NAME) - self.uds: Path = self.base.comms_dir.joinpath(KLIPPER_UDS_NAME) - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Klipper Instance ...") - - try: - create_folders(self.base.base_folders) - - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - - create_env_file( - path=self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = KLIPPER_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%KLIPPER_DIR%", - self.klipper_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = KLIPPER_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%KLIPPER_DIR%", self.klipper_dir.as_posix() - ) - env_file_content = env_file_content.replace( - "%CFG%", - f"{self.base.cfg_dir}/{KLIPPER_CFG_NAME}", - ) - env_file_content = env_file_content.replace( - "%SERIAL%", - self.serial.as_posix() if self.serial else "", - ) - env_file_content = env_file_content.replace( - "%LOG%", - self.base.log_dir.joinpath(self.log_file_name).as_posix(), - ) - env_file_content = env_file_content.replace( - "%UDS%", - self.uds.as_posix() if self.uds else "", - ) - - return env_file_content diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py deleted file mode 100644 index 87ccefa..0000000 --- a/kiauh/components/klipper/klipper_dialogs.py +++ /dev/null @@ -1,113 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 textwrap -from enum import Enum, unique -from typing import List - -from core.menus.base_menu import print_back_footer -from core.types.color import Color -from utils.instance_type import InstanceType - - -@unique -class DisplayType(Enum): - SERVICE_NAME = "SERVICE_NAME" - PRINTER_NAME = "PRINTER_NAME" - - -def print_instance_overview( - instances: List[InstanceType], - display_type: DisplayType = DisplayType.SERVICE_NAME, - show_headline=True, - show_index=False, - start_index=0, - show_select_all=False, -) -> None: - dialog = "╔═══════════════════════════════════════════════════════╗\n" - if show_headline: - d_type = ( - "Klipper instances" - if display_type is DisplayType.SERVICE_NAME - else "printer directories" - ) - headline = Color.apply(f"The following {d_type} were found:", Color.GREEN) - dialog += f"β•‘{headline:^64}β•‘\n" - dialog += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - if show_select_all: - select_all = Color.apply("a) Select all", Color.YELLOW) - dialog += f"β•‘ {select_all:<63}β•‘\n" - dialog += "β•‘ β•‘\n" - - for i, s in enumerate(instances): - if display_type is DisplayType.SERVICE_NAME: - name = s.service_file_path.stem - else: - name = s.data_dir - line = Color.apply( - f"{f'{i + start_index})' if show_index else '●'} {name}", Color.CYAN - ) - dialog += f"β•‘ {line:<63}β•‘\n" - dialog += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - print(dialog, end="") - print_back_footer() - - -def print_select_instance_count_dialog() -> None: - line1 = Color.apply("WARNING:", Color.YELLOW) - line2 = Color.apply( - "Setting up too many instances may crash your system.", Color.YELLOW - ) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - β•‘ Please select the number of Klipper instances to set β•‘ - β•‘ up. The number of Klipper instances will determine β•‘ - β•‘ the amount of printers you can run from this host. β•‘ - β•‘ β•‘ - β•‘ {line1:<63}β•‘ - β•‘ {line2:<63}β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - - print(dialog, end="") - print_back_footer() - - -def print_select_custom_name_dialog() -> None: - line1 = Color.apply("INFO:", Color.YELLOW) - line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - β•‘ Do you want to assign a custom name to each instance? β•‘ - β•‘ β•‘ - β•‘ Assigning a custom name will create a Klipper service β•‘ - β•‘ and a printer directory with the chosen name. β•‘ - β•‘ β•‘ - β•‘ Example for custom name 'kiauh': β•‘ - β•‘ ● Klipper service: klipper-kiauh.service β•‘ - β•‘ ● Printer directory: printer_kiauh_data β•‘ - β•‘ β•‘ - β•‘ If skipped, each instance will get an index assigned β•‘ - β•‘ in ascending order, starting at '1' in case of a new β•‘ - β•‘ installation. Otherwise, the index will be derived β•‘ - β•‘ from amount of already existing instances. β•‘ - β•‘ β•‘ - β•‘ {line1:<63}β•‘ - β•‘ {line2:<63}β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - - print(dialog, end="") - print_back_footer() diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py deleted file mode 100644 index 6c75c68..0000000 --- a/kiauh/components/klipper/klipper_utils.py +++ /dev/null @@ -1,256 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 grp -import os -import shutil -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import Dict, List - -from components.klipper import ( - KLIPPER_BACKUP_DIR, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_INSTALL_SCRIPT, - MODULE_PATH, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - print_instance_overview, - print_select_instance_count_dialog, -) -from components.webui_client.base_data import BaseWebClient -from components.webui_client.client_config.client_config_setup import ( - create_client_config_symlink, -) -from core.backup_manager.backup_manager import BackupManager -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import SUFFIX_BLACKLIST -from core.logger import DialogType, Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from core.types.component_status import ComponentStatus -from utils.common import check_install_dependencies, get_install_status -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm, get_number_input, get_string_input -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - install_python_packages, - parse_packages_from_file, -) - - -def get_klipper_status() -> ComponentStatus: - return get_install_status(KLIPPER_DIR, KLIPPER_ENV_DIR, Klipper) - - -def add_to_existing() -> bool | None: - kl_instances: List[Klipper] = get_instances(Klipper) - print_instance_overview(kl_instances) - _input: bool | None = get_confirm("Add new instances?", allow_go_back=True) - return _input - - -def get_install_count() -> int | None: - """ - Print a dialog for selecting the amount of Klipper instances - to set up with an option to navigate back. Returns None if the - user selected to go back, otherwise an integer greater or equal than 1 | - :return: Integer >= 1 or None - """ - kl_instances = get_instances(Klipper) - print_select_instance_count_dialog() - question = ( - f"Number of" - f"{' additional' if len(kl_instances) > 0 else ''} " - f"Klipper instances to set up" - ) - _input: int | None = get_number_input(question, 1, default=1, allow_go_back=True) - return _input - - -def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None: - existing_names = [] - existing_names.extend(SUFFIX_BLACKLIST) - existing_names.extend(name_dict[n] for n in name_dict) - pattern = r"^[a-zA-Z0-9]+$" - - question = f"Enter name for instance {key}" - name_dict[key] = get_string_input(question, exclude=existing_names, regex=pattern) - - -def check_user_groups() -> None: - user_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] - missing_groups = [g for g in ["tty", "dialout"] if g not in user_groups] - - if not missing_groups: - return - - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Your current user is not in group:", - *[f"● {g}" for g in missing_groups], - "\n\n", - "It is possible that you won't be able to successfully connect and/or " - "flash the controller board without your user being a member of that " - "group. If you want to add the current user to the group(s) listed above, " - "answer with 'Y'. Else skip with 'n'.", - "\n\n", - "INFO:", - "Relog required for group assignments to take effect!", - ], - ) - - if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"): - log = "Skipped adding user to required groups. You might encounter issues." - Logger.warn(log) - return - - try: - for group in missing_groups: - Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...") - command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] - run(command, check=True) - Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") - except CalledProcessError as e: - Logger.print_error(f"Unable to add user to usergroups: {e}") - raise - - log = "Remember to relog/restart this machine for the group(s) to be applied!" - Logger.print_warn(log) - - -def handle_disruptive_system_packages() -> None: - services = [] - - command = ["systemctl", "is-enabled", "brltty"] - brltty_status = run(command, capture_output=True, text=True) - - command = ["systemctl", "is-enabled", "brltty-udev"] - brltty_udev_status = run(command, capture_output=True, text=True) - - command = ["systemctl", "is-enabled", "ModemManager"] - modem_manager_status = run(command, capture_output=True, text=True) - - if "enabled" in brltty_status.stdout: - services.append("brltty") - if "enabled" in brltty_udev_status.stdout: - services.append("brltty-udev") - if "enabled" in modem_manager_status.stdout: - services.append("ModemManager") - - for service in services if services else []: - try: - cmd_sysctl_service(service, "mask") - except CalledProcessError: - Logger.print_dialog( - DialogType.WARNING, - [ - f"KIAUH was unable to mask the {service} system service. " - "Please fix the problem manually. Otherwise, this may have " - "undesirable effects on the operation of Klipper." - ], - ) - - -def create_example_printer_cfg( - instance: Klipper, clients: List[BaseWebClient] | None = None -) -> None: - Logger.print_status(f"Creating example printer.cfg in '{instance.base.cfg_dir}'") - if instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' already exists.") - return - - source = MODULE_PATH.joinpath("assets/printer.cfg") - target = instance.cfg_file - try: - shutil.copy(source, target) - except OSError as e: - Logger.print_error(f"Unable to create example printer.cfg:\n{e}") - return - - scp = SimpleConfigParser() - scp.read_file(target) - scp.set_option("virtual_sdcard", "path", str(instance.base.gcodes_dir)) - - # include existing client configs in the example config - if clients is not None and len(clients) > 0: - for c in clients: - client_config = c.client_config - section = client_config.config_section - scp.add_section(section=section) - create_client_config_symlink(client_config, [instance]) - - scp.write_file(target) - - Logger.print_ok(f"Example printer.cfg created in '{instance.base.cfg_dir}'") - - -def backup_klipper_dir() -> None: - bm = BackupManager() - bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR) - bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR) - - -def install_klipper_packages() -> None: - script = KLIPPER_INSTALL_SCRIPT - packages = parse_packages_from_file(script) - - # Add pkg-config for rp2040 build - packages.append("pkg-config") - - # Add dbus requirement for DietPi distro - if check_file_exist(Path("/boot/dietpi/.version")): - packages.append("dbus") - - check_install_dependencies({*packages}) - - -def install_input_shaper_deps() -> None: - if not KLIPPER_ENV_DIR.exists(): - Logger.print_warn("Required Klipper python environment not found!") - return - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Resonance measurements and shaper auto-calibration require additional " - "software dependencies which are not installed by default. " - "If you agree, the following additional system packages will be installed:", - "● python3-numpy", - "● python3-matplotlib", - "● libatlas-base-dev", - "● libopenblas-dev", - "\n\n", - "Also, the following Python package will be installed:", - "● numpy", - ], - custom_title="Install Input Shaper Dependencies", - ) - if not get_confirm( - "Do you want to install the required packages?", default_choice=False - ): - return - - apt_deps = ( - "python3-numpy", - "python3-matplotlib", - "libatlas-base-dev", - "libopenblas-dev", - ) - check_install_dependencies({*apt_deps}) - - py_deps = ("numpy",) - - install_python_packages(KLIPPER_ENV_DIR, {*py_deps}) diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py deleted file mode 100644 index 8ec3bcd..0000000 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ /dev/null @@ -1,102 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper.services.klipper_setup_service import KlipperSetupService -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class KlipperRemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - - self.title = "Remove Klipper" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BACK - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.select_state = False - - self.klsvc = KlipperSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_remove_klipper_service), - "2": Option(method=self.toggle_remove_klipper_dir), - "3": Option(method=self.toggle_remove_klipper_env), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.rm_svc else unchecked - o2 = checked if self.rm_dir else unchecked - o3 = checked if self.rm_env else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Enter a number and hit enter to select / deselect β•‘ - β•‘ the specific option for removal. β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ a) {sel_state:49} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) {o1} Remove Service β•‘ - β•‘ 2) {o2} Remove Local Repository β•‘ - β•‘ 3) {o3} Remove Python Environment β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ C) Continue β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.rm_svc = self.select_state - self.rm_dir = self.select_state - self.rm_env = self.select_state - - def toggle_remove_klipper_service(self, **kwargs) -> None: - self.rm_svc = not self.rm_svc - - def toggle_remove_klipper_dir(self, **kwargs) -> None: - self.rm_dir = not self.rm_dir - - def toggle_remove_klipper_env(self, **kwargs) -> None: - self.rm_env = not self.rm_env - - def run_removal_process(self, **kwargs) -> None: - if not self.rm_svc and not self.rm_dir and not self.rm_env: - msg = "Nothing selected! Select options to remove first." - print(Color.apply(msg, Color.RED)) - return - - self.klsvc.remove(self.rm_svc, self.rm_dir, self.rm_env) - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.select_state = False diff --git a/kiauh/components/klipper/services/klipper_instance_service.py b/kiauh/components/klipper/services/klipper_instance_service.py deleted file mode 100644 index 593e173..0000000 --- a/kiauh/components/klipper/services/klipper_instance_service.py +++ /dev/null @@ -1,46 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import List - -from components.klipper.klipper import Klipper -from utils.instance_utils import get_instances - - -class KlipperInstanceService: - __cls_instance = None - __instances: List[Klipper] = [] - - def __new__(cls) -> "KlipperInstanceService": - if cls.__cls_instance is None: - cls.__cls_instance = super(KlipperInstanceService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - - def load_instances(self) -> None: - self.__instances = get_instances(Klipper) - - def create_new_instance(self, suffix: str) -> Klipper: - instance = Klipper(suffix) - self.__instances.append(instance) - return instance - - def get_all_instances(self) -> List[Klipper]: - return self.__instances - - def get_instance_by_suffix(self, suffix: str) -> Klipper | None: - instances: List[Klipper] = [i for i in self.__instances if i.suffix == suffix] - return instances[0] if instances else None diff --git a/kiauh/components/klipper/services/klipper_setup_service.py b/kiauh/components/klipper/services/klipper_setup_service.py deleted file mode 100644 index 2a80b94..0000000 --- a/kiauh/components/klipper/services/klipper_setup_service.py +++ /dev/null @@ -1,366 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from copy import copy -from typing import Dict, List, Tuple - -from components.klipper import ( - EXIT_KLIPPER_SETUP, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_REPO_URL, - KLIPPER_REQ_FILE, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - print_instance_overview, - print_select_custom_name_dialog, -) -from components.klipper.klipper_utils import ( - assign_custom_name, - backup_klipper_dir, - check_user_groups, - create_example_printer_cfg, - get_install_count, - handle_disruptive_system_packages, - install_klipper_packages, -) -from components.klipper.services.klipper_instance_service import KlipperInstanceService -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from components.webui_client.client_utils import ( - get_existing_clients, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.message_service import Message, MessageService -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm, get_selection_input -from utils.sys_utils import ( - cmd_sysctl_manage, - create_python_venv, - install_python_requirements, - unit_file_exists, -) - - -# noinspection PyMethodMayBeStatic -class KlipperSetupService: - __cls_instance = None - - kisvc: KlipperInstanceService - misvc: MoonrakerInstanceService - msgsvc = MessageService - - settings: KiauhSettings - klipper_list: List[Klipper] - moonraker_list: List[Moonraker] - - def __new__(cls) -> "KlipperSetupService": - if cls.__cls_instance is None: - cls.__cls_instance = super(KlipperSetupService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__init_state() - - def __init_state(self) -> None: - self.settings = KiauhSettings() - - self.kisvc = KlipperInstanceService() - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc = MoonrakerInstanceService() - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - self.msgsvc = MessageService() - - def __refresh_state(self) -> None: - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - def install(self) -> None: - self.__refresh_state() - - Logger.print_status("Installing Klipper ...") - - match_moonraker: bool = False - - # if there are more moonraker instances than klipper instances, ask the user to - # match the klipper instance count to the count of moonraker instances with the same suffix - if len(self.moonraker_list) > len(self.klipper_list): - is_confirmed = self.__display_moonraker_info() - if not is_confirmed: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - match_moonraker = True - - install_count, name_dict = self.__get_install_count_and_name_dict() - - if install_count == 0: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - - is_multi_install = install_count > 1 or ( - len(name_dict) >= 1 and install_count >= 1 - ) - if not name_dict and install_count == 1: - name_dict = {0: ""} - elif is_multi_install and not match_moonraker: - custom_names = self.__use_custom_names_or_go_back() - if custom_names is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - - self.__handle_instance_names(install_count, name_dict, custom_names) - - create_example_cfg = get_confirm("Create example printer.cfg?") - # run the actual installation - try: - self.__run_setup(name_dict, create_example_cfg) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Klipper installation failed!") - return - - def update(self) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Do NOT continue if there are ongoing prints running!", - "All Klipper instances will be restarted during the update process and " - "ongoing prints WILL FAIL.", - ], - ) - - if not get_confirm("Update Klipper now?"): - return - - self.__refresh_state() - - if self.settings.kiauh.backup_before_update: - backup_klipper_dir() - - InstanceManager.stop_all(self.klipper_list) - git_pull_wrapper(KLIPPER_DIR) - install_klipper_packages() - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) - InstanceManager.start_all(self.klipper_list) - - def remove( - self, - remove_service: bool, - remove_dir: bool, - remove_env: bool, - ) -> None: - self.__refresh_state() - - completion_msg = Message( - title="Klipper Removal Process completed", - color=Color.GREEN, - ) - - if remove_service: - Logger.print_status("Removing Klipper instances ...") - if self.klipper_list: - instances_to_remove = self.__get_instances_to_remove() - self.__remove_instances(instances_to_remove) - if instances_to_remove: - instance_names = [ - i.service_file_path.stem for i in instances_to_remove - ] - txt = f"● Klipper instances removed: {', '.join(instance_names)}" - completion_msg.text.append(txt) - else: - Logger.print_info("No Klipper Services installed! Skipped ...") - - if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"): - completion_msg.text = [ - "Some Klipper services are still installed:", - f"● '{KLIPPER_DIR}' was not removed, even though selected for removal.", - f"● '{KLIPPER_ENV_DIR}' was not removed, even though selected for removal.", - ] - else: - if remove_dir: - Logger.print_status("Removing Klipper local repository ...") - if run_remove_routines(KLIPPER_DIR): - completion_msg.text.append("● Klipper local repository removed") - if remove_env: - Logger.print_status("Removing Klipper Python environment ...") - if run_remove_routines(KLIPPER_ENV_DIR): - completion_msg.text.append("● Klipper Python environment removed") - - if completion_msg.text: - completion_msg.text.insert(0, "The following actions were performed:") - else: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text = ["Nothing to remove."] - - self.msgsvc.set_message(completion_msg) - - def __get_install_count_and_name_dict(self) -> Tuple[int, Dict[int, str]]: - install_count: int | None - if len(self.moonraker_list) > len(self.klipper_list): - install_count = len(self.moonraker_list) - name_dict = { - i: moonraker.suffix for i, moonraker in enumerate(self.moonraker_list) - } - else: - install_count = get_install_count() - name_dict = { - i: klipper.suffix for i, klipper in enumerate(self.klipper_list) - } - - if install_count is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return 0, {} - - return install_count, name_dict - - def __run_setup(self, name_dict: Dict[int, str], create_example_cfg: bool) -> None: - if not self.klipper_list: - self.__install_deps() - - for i in name_dict: - # skip this iteration if there is already an instance with the name - if name_dict[i] in [n.suffix for n in self.klipper_list]: - continue - - instance = Klipper(suffix=name_dict[i]) - instance.create() - InstanceManager.enable(instance) - - if create_example_cfg: - # if a client-config is installed, include it in the new example cfg - clients = get_existing_clients() - create_example_printer_cfg(instance, clients) - - InstanceManager.start(instance) - - cmd_sysctl_manage("daemon-reload") - - # step 4: check/handle conflicting packages/services - handle_disruptive_system_packages() - - # step 5: check for required group membership - check_user_groups() - - def __install_deps(self) -> None: - default_repo = (KLIPPER_REPO_URL, "master") - repo = self.settings.klipper.repositories - # pull the first repo defined in kiauh.cfg or fallback to the official Klipper repo - repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo - git_clone_wrapper(repo, KLIPPER_DIR, branch) - - try: - install_klipper_packages() - if create_python_venv(KLIPPER_ENV_DIR, False, False, self.settings.klipper.use_python_binary): - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) - except Exception: - Logger.print_error("Error during installation of Klipper requirements!") - raise - - def __display_moonraker_info(self) -> bool: - # todo: only show the klipper instances that are not already installed - Logger.print_dialog( - DialogType.INFO, - [ - "Existing Moonraker instances detected:", - *[f"● {m.service_file_path.stem}" for m in self.moonraker_list], - "\n\n", - "The following Klipper instances will be installed:", - *[f"● klipper-{m.suffix}" for m in self.moonraker_list], - ], - ) - _input: bool = get_confirm("Proceed with installation?") - return _input - - def __handle_instance_names( - self, install_count: int, name_dict: Dict[int, str], custom_names: bool - ) -> None: - for i in range(install_count): # 3 - key: int = len(name_dict.keys()) + 1 - if custom_names: - assign_custom_name(key, name_dict) - else: - name_dict[key] = str(len(name_dict) + 1) - - def __use_custom_names_or_go_back(self) -> bool | None: - print_select_custom_name_dialog() - _input: bool | None = get_confirm( - "Assign custom names?", - False, - allow_go_back=True, - ) - return _input - - def __get_instances_to_remove(self) -> List[Klipper] | None: - start_index = 1 - curr_instances: List[Klipper] = self.klipper_list - instance_count = len(curr_instances) - - options = [str(i + start_index) for i in range(instance_count)] - options.extend(["a", "b"]) - instance_map = {options[i]: self.klipper_list[i] for i in range(instance_count)} - - print_instance_overview( - self.klipper_list, - start_index=start_index, - show_index=True, - show_select_all=True, - ) - selection = get_selection_input("Select Klipper instance to remove", options) - - if selection == "b": - return None - elif selection == "a": - return copy(self.klipper_list) - - return [instance_map[selection]] - - def __remove_instances( - self, - instance_list: List[Klipper] | None, - ) -> None: - if not instance_list: - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - self.__delete_klipper_env_file(instance) - - self.__refresh_state() - - def __delete_klipper_env_file(self, instance: Klipper): - Logger.print_status(f"Remove '{instance.env_file}'") - if not instance.env_file.exists(): - msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." - Logger.print_info(msg) - return - run_remove_routines(instance.env_file) diff --git a/kiauh/components/klipper_firmware/__init__.py b/kiauh/components/klipper_firmware/__init__.py deleted file mode 100644 index 3a39f20..0000000 --- a/kiauh/components/klipper_firmware/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 components.klipper import KLIPPER_DIR - -SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh") diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py deleted file mode 100644 index ad35247..0000000 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ /dev/null @@ -1,213 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from pathlib import Path -from subprocess import ( - DEVNULL, - PIPE, - STDOUT, - CalledProcessError, - Popen, - check_output, - run, -) -from typing import List - -from components.klipper import KLIPPER_DIR -from components.klipper.klipper import Klipper -from components.klipper_firmware import SD_FLASH_SCRIPT -from components.klipper_firmware.flash_options import ( - FlashMethod, - FlashOptions, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.instance_utils import get_instances -from utils.sys_utils import log_process - - -def find_firmware_file() -> bool: - target = KLIPPER_DIR.joinpath("out") - target_exists: bool = target.exists() - - f1 = "klipper.elf.hex" - f2 = "klipper.elf" - f3 = "klipper.bin" - f4 = "klipper.uf2" - fw_file_exists: bool = ( - (target.joinpath(f1).exists() and target.joinpath(f2).exists()) - or target.joinpath(f3).exists() - or target.joinpath(f4).exists() - ) - - return target_exists and fw_file_exists - - -def find_usb_device_by_id() -> List[str]: - try: - command = "find /dev/serial/by-id/*" - output = check_output(command, shell=True, text=True, stderr=DEVNULL) - return output.splitlines() - except CalledProcessError as e: - Logger.print_error("Unable to find a USB device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_uart_device() -> List[str]: - try: - cmd = "find /dev -maxdepth 1" - output = check_output(cmd, shell=True, text=True, stderr=DEVNULL) - device_list = [] - if output: - pattern = r"^/dev/tty(AMA0|S0)$" - devices = output.splitlines() - device_list = [d for d in devices if re.search(pattern, d)] - return device_list - except CalledProcessError as e: - Logger.print_error("Unable to find a UART device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_usb_dfu_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 "DFU" in d] - return device_list - - except CalledProcessError as e: - Logger.print_error("Unable to find a USB DFU device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_usb_rp2_boot_device() -> List[str]: - try: - output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL) - device_list = [] - if output: - devices = output.splitlines() - device_list = [d.split(" ")[5] for d in devices if "RP2 Boot" in d] - return device_list - - except CalledProcessError as e: - Logger.print_error("Unable to find a USB RP2 Boot device!") - Logger.print_error(e, prefix=False) - return [] - - -def get_sd_flash_board_list() -> List[str]: - if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): - return [] - - try: - cmd = f"{SD_FLASH_SCRIPT} -l" - blist: List[str] = check_output(cmd, shell=True, text=True).splitlines()[1:] - return blist - except CalledProcessError as e: - Logger.print_error(f"An unexpected error occured:\n{e}") - return [] - - -def start_flash_process(flash_options: FlashOptions) -> None: - Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...") - try: - if not flash_options.flash_method: - raise Exception("Missing value for flash_method!") - if not flash_options.flash_command: - raise Exception("Missing value for flash_command!") - if not flash_options.selected_mcu: - raise Exception("Missing value for selected_mcu!") - if not flash_options.connection_type: - raise Exception("Missing value for connection_type!") - if ( - flash_options.flash_method == FlashMethod.SD_CARD - and not flash_options.selected_board - ): - raise Exception("Missing value for selected_board!") - - if flash_options.flash_method is FlashMethod.REGULAR: - cmd = [ - "make", - f"KCONFIG_CONFIG={flash_options.selected_kconfig}", - flash_options.flash_command.value, - f"FLASH_DEVICE={flash_options.selected_mcu}", - ] - elif flash_options.flash_method is FlashMethod.SD_CARD: - if not SD_FLASH_SCRIPT.exists(): - raise Exception("Unable to find Klippers sdcard flash script!") - cmd = [ - SD_FLASH_SCRIPT.as_posix(), - f"-b {flash_options.selected_baudrate}", - flash_options.selected_mcu, - flash_options.selected_board, - ] - else: - raise Exception("Invalid value for flash_method!") - - instances = get_instances(Klipper) - InstanceManager.stop_all(instances) - - process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) - log_process(process) - - InstanceManager.start_all(instances) - - rc = process.returncode - if rc != 0: - raise Exception(f"Flashing failed with returncode: {rc}") - else: - Logger.print_ok("Flashing successful!", start="\n", end="\n\n") - - except (Exception, CalledProcessError): - Logger.print_error("Flashing failed!", start="\n") - Logger.print_error("See the console output above!", end="\n\n") - - -def run_make_clean(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make KCONFIG_CONFIG={kconfig} clean", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise - - -def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise - - -def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py deleted file mode 100644 index d547681..0000000 --- a/kiauh/components/klipper_firmware/flash_options.py +++ /dev/null @@ -1,115 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import field -from enum import Enum -from typing import List - - -class FlashMethod(Enum): - REGULAR = "Regular" - SD_CARD = "SD Card" - - -class FlashCommand(Enum): - FLASH = "flash" - SERIAL_FLASH = "serialflash" - - -class ConnectionType(Enum): - USB = "USB" - USB_DFU = "USB (DFU)" - USB_RP2040 = "USB (RP2040)" - UART = "UART" - - -class FlashOptions: - _instance = None - _flash_method: FlashMethod | None = None - _flash_command: FlashCommand | None = None - _connection_type: ConnectionType | None = None - _mcu_list: List[str] = field(default_factory=list) - _selected_mcu: str = "" - _selected_board: str = "" - _selected_baudrate: int = 250000 - _selected_kconfig: str = ".config" - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(FlashOptions, cls).__new__(cls, *args, **kwargs) - return cls._instance - - @classmethod - def destroy(cls) -> None: - cls._instance = None - - @property - def flash_method(self) -> FlashMethod | None: - return self._flash_method - - @flash_method.setter - def flash_method(self, value: FlashMethod | None): - self._flash_method = value - - @property - def flash_command(self) -> FlashCommand | None: - return self._flash_command - - @flash_command.setter - def flash_command(self, value: FlashCommand | None): - self._flash_command = value - - @property - def connection_type(self) -> ConnectionType | None: - return self._connection_type - - @connection_type.setter - def connection_type(self, value: ConnectionType | None): - self._connection_type = value - - @property - def mcu_list(self) -> List[str]: - return self._mcu_list - - @mcu_list.setter - def mcu_list(self, value: List[str]) -> None: - self._mcu_list = value - - @property - def selected_mcu(self) -> str: - return self._selected_mcu - - @selected_mcu.setter - def selected_mcu(self, value: str) -> None: - self._selected_mcu = value - - @property - def selected_board(self) -> str: - return self._selected_board - - @selected_board.setter - def selected_board(self, value: str) -> None: - self._selected_board = value - - @property - def selected_baudrate(self) -> int: - return self._selected_baudrate - - @selected_baudrate.setter - def selected_baudrate(self, value: int) -> None: - self._selected_baudrate = value - - @property - def selected_kconfig(self) -> str: - return self._selected_kconfig - - @selected_kconfig.setter - def selected_kconfig(self, value: str) -> None: - self._selected_kconfig = value diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py deleted file mode 100644 index d0acf26..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ /dev/null @@ -1,274 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 pathlib import Path -from shutil import copyfile -from typing import List, Set, Type - -from components.klipper import KLIPPER_DIR, KLIPPER_KCONFIGS_DIR -from components.klipper_firmware.firmware_utils import ( - run_make, - run_make_clean, - run_make_menuconfig, -) -from components.klipper_firmware.flash_options import FlashOptions -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from utils.input_utils import get_confirm, get_string_input -from utils.sys_utils import ( - check_package_install, - install_system_packages, - update_system_package_lists, -) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperKConfigMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Firmware Config Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.flash_options = FlashOptions() - self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR - self.kconfig_default = KLIPPER_DIR.joinpath(".config") - self.configs: List[Path] = [] - self.kconfig = ( - self.kconfig_default if not Path(self.kconfigs_dirname).is_dir() else None - ) - - def run(self) -> None: - if not self.kconfig: - super().run() - else: - self.flash_options.selected_kconfig = self.kconfig - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - if not Path(self.kconfigs_dirname).is_dir(): - return - - self.input_label_txt = "Select config or action to continue (default=N)" - self.default_option = Option( - method=self.select_config, opt_data=self.kconfig_default - ) - - option_index = 1 - for kconfig in Path(self.kconfigs_dirname).iterdir(): - if not kconfig.name.endswith(".config"): - continue - kconfig_path = self.kconfigs_dirname.joinpath(kconfig) - if Path(kconfig_path).is_file(): - self.configs += [kconfig] - self.options[str(option_index)] = Option( - method=self.select_config, opt_data=kconfig_path - ) - option_index += 1 - self.options["n"] = Option( - method=self.select_config, opt_data=self.kconfig_default - ) - - def print_menu(self) -> None: - cfg_found_str = Color.apply( - "Previously saved firmware configs found!", Color.GREEN - ) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {cfg_found_str:^62} β•‘ - β•‘ β•‘ - β•‘ Select an existing config or create a new one. β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Available firmware configs: β•‘ - """ - )[1:] - - start_index = 1 - for i, s in enumerate(self.configs): - line = f"{start_index + i}) {s.name}" - menu += f"β•‘ {line:<54}β•‘\n" - - new_config = Color.apply("N) Create new firmware config", Color.GREEN) - menu += "β•‘ β•‘\n" - menu += f"β•‘ {new_config:<62} β•‘\n" - - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - print(menu, end="") - - def select_config(self, **kwargs) -> None: - selection: str | None = kwargs.get("opt_data", None) - if selection is None: - raise Exception("opt_data is None") - if not Path(selection).is_file() and selection != self.kconfig_default: - raise Exception("opt_data does not exists") - self.kconfig = selection - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperBuildFirmwareMenu(BaseMenu): - def __init__( - self, kconfig: str | None = None, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = "Build Firmware Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} - self.missing_deps: List[str] = check_package_install(self.deps) - self.flash_options = FlashOptions() - self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR - self.kconfig_default = KLIPPER_DIR.joinpath(".config") - self.kconfig = self.flash_options.selected_kconfig - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - self.input_label_txt = "Press ENTER to install dependencies" - self.default_option = Option(method=self.install_missing_deps) - - def run(self): - # immediately start the build process if all dependencies are met - if len(self.missing_deps) == 0: - self.start_build_process() - else: - super().run() - - def print_menu(self) -> None: - txt = Color.apply("Dependencies are missing!", Color.RED) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {txt:^62} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ The following dependencies are required: β•‘ - β•‘ β•‘ - """ - )[1:] - - for d in self.deps: - status_ok = Color.apply("*INSTALLED*", Color.GREEN) - status_missing = Color.apply("*MISSING*", Color.RED) - status = status_missing if d in self.missing_deps else status_ok - padding = 40 - len(d) + len(status) + (len(status_ok) - len(status)) - d = Color.apply(f"● {d}", Color.CYAN) - menu += f"β•‘ {d}{status:>{padding}} β•‘\n" - - menu += "β•‘ β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - print(menu, end="") - - def install_missing_deps(self, **kwargs) -> None: - try: - update_system_package_lists(silent=False) - Logger.print_status("Installing system packages...") - install_system_packages(self.missing_deps) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Installing dependencies failed!") - finally: - # restart this menu - KlipperBuildFirmwareMenu().run() - - def start_build_process(self, **kwargs) -> None: - try: - run_make_clean(self.kconfig) - run_make_menuconfig(self.kconfig) - run_make(self.kconfig) - - Logger.print_ok("Firmware successfully built!") - Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") - - if self.kconfig == self.kconfig_default: - self.save_firmware_config() - - except Exception as e: - Logger.print_error(e) - Logger.print_error("Building Klipper Firmware failed!") - - finally: - if self.previous_menu is not None: - self.previous_menu().run() - - def save_firmware_config(self) -> None: - Logger.print_dialog( - DialogType.CUSTOM, - [ - "You can save the firmware build configs for multiple MCUs," - " and use them to update the firmware after a Klipper version upgrade" - ], - custom_title="Save firmware config", - ) - if not get_confirm( - "Do you want to save firmware config?", default_choice=False - ): - return - - filename = self.kconfig_default - while True: - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Allowed characters: a-z, 0-9 and '-'", - "The name must not contain the following:", - "\n\n", - "● Any special characters", - "● No leading or trailing '-'", - ], - ) - input_name = get_string_input( - "Enter the new firmware config name", - regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$", - ) - filename = self.kconfigs_dirname.joinpath(f"{input_name}.config") - - if Path(filename).is_file(): - if get_confirm( - f"Firmware config {input_name} already exists, overwrite?", - default_choice=False, - ): - break - - if Path(filename).is_dir(): - Logger.print_error(f"Path {filename} exists and it's a directory") - - if not Path(filename).exists(): - break - - if not get_confirm( - f"Save firmware config to '{filename}'?", default_choice=True - ): - Logger.print_info("Aborted saving firmware config ...") - return - - if not Path(self.kconfigs_dirname).exists(): - Path(self.kconfigs_dirname).mkdir() - - copyfile(self.kconfig_default, filename) - - Logger.print_ok() - Logger.print_ok(f"Firmware config successfully saved to {filename}") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py deleted file mode 100644 index 42f513c..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ /dev/null @@ -1,107 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper_firmware.flash_options import FlashMethod, FlashOptions -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperNoFirmwareErrorMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! NO FIRMWARE FILE FOUND !!!" - self.title_color = Color.RED - self.title_style = MenuTitleStyle.PLAIN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.flash_options = FlashOptions() - self.footer_type = FooterType.BLANK - self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = previous_menu - - def set_options(self) -> None: - self.default_option = Option(method=self.go_back) - - def print_menu(self) -> None: - line1 = "Unable to find a compiled firmware file!" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {Color.apply(line1, Color.RED):<62} β•‘ - β•‘ β•‘ - β•‘ Make sure, that: β•‘ - β•‘ ● the folder '~/klipper/out' and its content exist β•‘ - β•‘ ● the folder contains the following file: β•‘ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.REGULAR: - menu += "β•‘ ● 'klipper.elf' β•‘\n" - menu += "β•‘ ● 'klipper.elf.hex' β•‘\n" - else: - menu += "β•‘ ● 'klipper.bin' β•‘\n" - - print(menu, end="") - - def go_back(self, **kwargs) -> None: - from core.menus.advanced_menu import AdvancedMenu - - AdvancedMenu().run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperNoBoardTypesErrorMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ERROR GETTING BOARD LIST !!!" - self.title_color = Color.RED - self.title_style = MenuTitleStyle.PLAIN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BLANK - self.input_label_txt = "Press ENTER to go back to [Main Menu]" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = previous_menu - - def set_options(self) -> None: - self.default_option = Option(method=self.go_back) - - def print_menu(self) -> None: - line1 = "Reading the list of supported boards failed!" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {Color.apply(line1, Color.RED):<62} β•‘ - β•‘ β•‘ - β•‘ Make sure, that: β•‘ - β•‘ ● the folder '~/klipper' and all its content exist β•‘ - β•‘ ● the content of folder '~/klipper' is not currupted β•‘ - β•‘ ● the file '~/klipper/scripts/flash-sd.py' exist β•‘ - β•‘ ● your current user has access to those files/folders β•‘ - β•‘ β•‘ - β•‘ If in doubt or this process continues to fail, please β•‘ - β•‘ consider to download Klipper again. β•‘ - """ - )[1:] - print(menu, end="") - - def go_back(self, **kwargs) -> None: - from core.menus.main_menu import MainMenu - - MainMenu().run() diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py deleted file mode 100644 index 165117e..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ /dev/null @@ -1,177 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 Tuple, Type - -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color - - -def __title_config__() -> Tuple[str, Color, MenuTitleStyle]: - return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN - - -# noinspection DuplicatedCode -class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashMethodMenu, - ) - - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashMethodMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("Regular flashing method:", Color.CYAN) - subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {subheader1:<62} β•‘ - β•‘ The default method to flash controller boards which β•‘ - β•‘ are connected and updated over USB and not by placing β•‘ - β•‘ a compiled firmware file onto an internal SD-Card. β•‘ - β•‘ β•‘ - β•‘ Common controllers that get flashed that way are: β•‘ - β•‘ - Arduino Mega 2560 β•‘ - β•‘ - Fysetc F6 / S6 (used without a Display + SD-Slot) β•‘ - β•‘ β•‘ - β•‘ {subheader2:<62} β•‘ - β•‘ Many popular controller boards ship with a bootloader β•‘ - β•‘ capable of updating the firmware via SD-Card. β•‘ - β•‘ Choose this method if your controller board supports β•‘ - β•‘ this way of updating. This method ONLY works for up- β•‘ - β•‘ grading firmware. The initial flashing procedure must β•‘ - β•‘ be done manually per the instructions that apply to β•‘ - β•‘ your controller board. β•‘ - β•‘ β•‘ - β•‘ Common controllers that can be flashed that way are: β•‘ - β•‘ - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 β•‘ - β•‘ - Fysetc F6 / S6 (used with a Display + SD-Slot) β•‘ - β•‘ - Fysetc Spider β•‘ - β•‘ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - -# noinspection DuplicatedCode -class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashCommandMenu, - ) - - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashCommandMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("make flash:", Color.CYAN) - subheader2 = Color.apply("make serialflash:", Color.CYAN) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {subheader1:<62} β•‘ - β•‘ The default command to flash controller board, it β•‘ - β•‘ will detect selected microcontroller and use suitable β•‘ - β•‘ tool for flashing it. β•‘ - β•‘ β•‘ - β•‘ {subheader2:<62} β•‘ - β•‘ Special command to flash STM32 microcontrollers in β•‘ - β•‘ DFU mode but connected via serial. stm32flash command β•‘ - β•‘ will be used internally. β•‘ - β•‘ β•‘ - """ - )[1:] - print(menu, end="") - - -# noinspection DuplicatedCode -class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperSelectMcuConnectionMenu, - ) - - self.previous_menu = ( - previous_menu - if previous_menu is not None - else KlipperSelectMcuConnectionMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("USB:", Color.CYAN) - subheader2 = Color.apply("UART:", Color.CYAN) - subheader3 = Color.apply("USB DFU:", Color.CYAN) - subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {subheader1:<62} β•‘ - β•‘ Selecting USB as the connection method will scan the β•‘ - β•‘ USB ports for connected controller boards. This will β•‘ - β•‘ be similar to the 'ls /dev/serial/by-id/*' command β•‘ - β•‘ suggested by the official Klipper documentation for β•‘ - β•‘ determining successfull USB connections! β•‘ - β•‘ β•‘ - β•‘ {subheader2:<62} β•‘ - β•‘ Selecting UART as the connection method will list all β•‘ - β•‘ possible UART serial ports. Note: This method ALWAYS β•‘ - β•‘ returns something as it seems impossible to determine β•‘ - β•‘ if a valid Klipper controller board is connected or β•‘ - β•‘ not. Because of that, you MUST know which UART serial β•‘ - β•‘ port your controller board is connected to when using β•‘ - β•‘ this connection method. β•‘ - β•‘ β•‘ - β•‘ {subheader3:<62} β•‘ - β•‘ Selecting USB DFU as the connection method will scan β•‘ - β•‘ the USB ports for connected controller boards in β•‘ - β•‘ STM32 DFU mode, which is usually done by holding down β•‘ - β•‘ the BOOT button or setting a special jumper on the β•‘ - β•‘ board before powering up. β•‘ - β•‘ β•‘ - β•‘ {subheader4:<62} β•‘ - β•‘ Selecting USB RP2 Boot as the connection method will β•‘ - β•‘ scan the USB ports for connected RP2040 controller β•‘ - β•‘ boards in Boot mode, which is usually done by holding β•‘ - β•‘ down the BOOT button before powering up. β•‘ - β•‘ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py deleted file mode 100644 index a9d1fe8..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ /dev/null @@ -1,484 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 -import time -from pathlib import Path -from typing import Type - -from components.klipper_firmware.firmware_utils import ( - find_firmware_file, - find_uart_device, - find_usb_device_by_id, - find_usb_dfu_device, - find_usb_rp2_boot_device, - get_sd_flash_board_list, - start_flash_process, -) -from components.klipper_firmware.flash_options import ( - ConnectionType, - FlashCommand, - FlashMethod, - FlashOptions, -) -from components.klipper_firmware.menus.klipper_flash_error_menu import ( - KlipperNoBoardTypesErrorMenu, - KlipperNoFirmwareErrorMenu, -) -from components.klipper_firmware.menus.klipper_flash_help_menu import ( - KlipperFlashCommandHelpMenu, - KlipperFlashMethodHelpMenu, - KlipperMcuConnectionHelpMenu, -) -from core.logger import DialogType, Logger -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color -from utils.input_utils import get_number_input - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashMethodMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "MCU Flash Menu" - self.title_color = Color.CYAN - self.help_menu = KlipperFlashMethodHelpMenu - self.input_label_txt = "Select flash method" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(self.select_regular), - "2": Option(self.select_sdcard), - } - - def print_menu(self) -> None: - subheader = Color.apply("ATTENTION:", Color.YELLOW) - subline1 = Color.apply( - "Make sure to select the correct method for the MCU!", Color.YELLOW - ) - subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Select the flash method for flashing the MCU. β•‘ - β•‘ β•‘ - β•‘ {subheader:<62} β•‘ - β•‘ {subline1:<62} β•‘ - β•‘ {subline2:<62} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) Regular flashing method β•‘ - β•‘ 2) Updating via SD-Card Update β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def select_regular(self, **kwargs): - self.flash_options.flash_method = FlashMethod.REGULAR - self.goto_next_menu() - - def select_sdcard(self, **kwargs): - self.flash_options.flash_method = FlashMethod.SD_CARD - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - if find_firmware_file(): - KlipperFlashCommandMenu(previous_menu=self.__class__).run() - else: - KlipperNoFirmwareErrorMenu().run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashCommandMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Which flash command to use for flashing the MCU?" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.YELLOW - self.help_menu = KlipperFlashCommandHelpMenu - self.input_label_txt = "Select flash command" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashMethodMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(self.select_flash), - "2": Option(self.select_serialflash), - } - self.default_option = Option(self.select_flash) - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) make flash (default) β•‘ - β•‘ 2) make serialflash (stm32flash) β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def select_flash(self, **kwargs): - self.flash_options.flash_command = FlashCommand.FLASH - self.goto_next_menu() - - def select_serialflash(self, **kwargs): - self.flash_options.flash_command = FlashCommand.SERIAL_FLASH - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - KlipperSelectMcuConnectionMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectMcuConnectionMenu(BaseMenu): - def __init__( - self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False - ): - super().__init__() - self.title = "Make sure that the controller board is connected now!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.__standalone = standalone - self.help_menu = KlipperMcuConnectionHelpMenu - self.input_label_txt = "Select connection type" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashCommandMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.select_usb), - "2": Option(method=self.select_dfu), - "3": Option(method=self.select_usb_dfu), - "4": Option(method=self.select_usb_rp2040), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ How is the controller board connected to the host? β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) USB β•‘ - β•‘ 2) UART β•‘ - β•‘ 3) USB (DFU mode) β•‘ - β•‘ 4) USB (RP2040 mode) β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def select_usb(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB - self.get_mcu_list() - - def select_dfu(self, **kwargs): - self.flash_options.connection_type = ConnectionType.UART - self.get_mcu_list() - - def select_usb_dfu(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB_DFU - self.get_mcu_list() - - def select_usb_rp2040(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB_RP2040 - self.get_mcu_list() - - def get_mcu_list(self, **kwargs): - conn_type = self.flash_options.connection_type - - if conn_type is ConnectionType.USB: - Logger.print_status("Identifying MCU connected via USB ...") - self.flash_options.mcu_list = find_usb_device_by_id() - elif conn_type is ConnectionType.UART: - Logger.print_status("Identifying MCU possibly connected via UART ...") - self.flash_options.mcu_list = find_uart_device() - elif conn_type is ConnectionType.USB_DFU: - Logger.print_status("Identifying MCU connected via USB in DFU mode ...") - self.flash_options.mcu_list = find_usb_dfu_device() - elif conn_type is ConnectionType.USB_RP2040: - Logger.print_status( - "Identifying MCU connected via USB in RP2 Boot mode ..." - ) - self.flash_options.mcu_list = find_usb_rp2_boot_device() - - if len(self.flash_options.mcu_list) < 1: - Logger.print_warn("No MCUs found!") - Logger.print_warn("Make sure they are connected and repeat this step.") - - # if standalone is True, we only display the MCUs to the user and return - if self.__standalone and len(self.flash_options.mcu_list) > 0: - Logger.print_ok("The following MCUs were found:", prefix=False) - for i, mcu in enumerate(self.flash_options.mcu_list): - print(f" ● MCU #{i}: {Color.CYAN}{mcu}{Color.RST}") - time.sleep(3) - return - - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - KlipperSelectMcuIdMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectMcuIdMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ATTENTION !!!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.RED - self.flash_options = FlashOptions() - self.mcu_list = self.flash_options.mcu_list - self.input_label_txt = "Select MCU to flash" - self.footer_type = FooterType.BACK - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu - if previous_menu is not None - else KlipperSelectMcuConnectionMenu - ) - - def set_options(self) -> None: - self.options = { - f"{i}": Option(self.flash_mcu, f"{i}") for i in range(len(self.mcu_list)) - } - - def print_menu(self) -> None: - header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Make sure, to select the correct MCU! β•‘ - β•‘ ONLY flash a firmware created for the respective MCU! β•‘ - β•‘ β•‘ - β•Ÿ{header2:─^64}β•’ - β•‘ β•‘ - """ - )[1:] - - for i, mcu in enumerate(self.mcu_list): - mcu = mcu.split("/")[-1] - menu += f"β•‘ {i}) {Color.apply(f'{mcu:<51}', Color.CYAN)}β•‘\n" - - menu += textwrap.dedent( - """ - β•‘ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def flash_mcu(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - selected_mcu = self.mcu_list[index] - self.flash_options.selected_mcu = selected_mcu - - if self.flash_options.flash_method == FlashMethod.SD_CARD: - KlipperSelectSDFlashBoardMenu(previous_menu=self.__class__).run() - elif self.flash_options.flash_method == FlashMethod.REGULAR: - KlipperFlashOverviewMenu(previous_menu=self.__class__).run() - except Exception as e: - Logger.print_error(e) - Logger.print_error("Flashing failed!") - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectSDFlashBoardMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.flash_options = FlashOptions() - self.available_boards = get_sd_flash_board_list() - self.input_label_txt = "Select board type" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperSelectMcuIdMenu - ) - - def set_options(self) -> None: - self.options = { - f"{i}": Option(self.board_select, f"{i}") - for i in range(len(self.available_boards)) - } - - def print_menu(self) -> None: - if len(self.available_boards) < 1: - KlipperNoBoardTypesErrorMenu().run() - else: - menu = textwrap.dedent( - """ - β•‘ Please select the type of board that corresponds to β•‘ - β•‘ the currently selected MCU ID you chose before. β•‘ - β•‘ β•‘ - β•‘ The following boards are currently supported: β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - - for i, board in enumerate(self.available_boards): - line = f" {i}) {board}" - menu += f"β•‘{line:<55}β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’" - print(menu, end="") - - def board_select(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - self.flash_options.selected_board = self.available_boards[index] - self.baudrate_select() - except Exception as e: - Logger.print_error(e) - Logger.print_error("Board selection failed!") - - def baudrate_select(self, **kwargs): - Logger.print_dialog( - DialogType.CUSTOM, - [ - "If your board is flashed with firmware that connects " - "at a custom baud rate, please change it now.", - "\n\n", - "If you are unsure, stick to the default 250000!", - ], - ) - self.flash_options.selected_baudrate = get_number_input( - question="Please set the baud rate", - default=250000, - min_value=0, - allow_go_back=True, - ) - KlipperFlashOverviewMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashOverviewMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ATTENTION !!!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.RED - self.flash_options = FlashOptions() - self.input_label_txt = "Perform action (default=Y)" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_options(self) -> None: - self.options = { - "y": Option(self.execute_flash), - "n": Option(self.abort_process), - } - - self.default_option = Option(self.execute_flash) - - def print_menu(self) -> None: - method = self.flash_options.flash_method.value - command = self.flash_options.flash_command.value - conn_type = self.flash_options.connection_type.value - mcu = self.flash_options.selected_mcu.split("/")[-1] - board = self.flash_options.selected_board - baudrate = self.flash_options.selected_baudrate - kconfig = Path(self.flash_options.selected_kconfig).name - color = Color.CYAN - subheader = f"[{Color.apply('Overview', color)}]" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Before contuining the flashing process, please check β•‘ - β•‘ if all parameters were set correctly! Once you made β•‘ - β•‘ sure everything is correct, start the process. If any β•‘ - β•‘ parameter needs to be changed, you can go back (B) β•‘ - β•‘ step by step or abort and start from the beginning. β•‘ - β•‘{subheader:─^64}β•‘ - β•‘ β•‘ - """ - )[1:] - - menu += textwrap.dedent( - f""" - β•‘ MCU: {Color.apply(f"{mcu:<48}", color)} β•‘ - β•‘ Connection: {Color.apply(f"{conn_type:<41}", color)} β•‘ - β•‘ Flash method: {Color.apply(f"{method:<39}", color)} β•‘ - β•‘ Flash command: {Color.apply(f"{command:<38}", color)} β•‘ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.SD_CARD: - menu += textwrap.dedent( - f""" - β•‘ Board type: {Color.apply(f"{board:<41}", color)} β•‘ - β•‘ Baudrate: {Color.apply(f"{baudrate:<43}", color)} β•‘ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.REGULAR: - menu += textwrap.dedent( - f""" - β•‘ Firmware config: {Color.apply(f"{kconfig:<36}", color)} β•‘ - """ - )[1:] - - menu += textwrap.dedent( - """ - β•‘ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Y) Start flash process β•‘ - β•‘ N) Abort - Return to Advanced Menu β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def execute_flash(self, **kwargs): - start_flash_process(self.flash_options) - Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...") - time.sleep(5) - KlipperFlashMethodMenu().run() - - def abort_process(self, **kwargs): - from core.menus.advanced_menu import AdvancedMenu - - AdvancedMenu().run() diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py deleted file mode 100644 index c8333bb..0000000 --- a/kiauh/components/klipperscreen/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR -from core.constants import SYSTEMD - -# repo -KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" - -# names -KLIPPERSCREEN_SERVICE_NAME = "KlipperScreen.service" -KLIPPERSCREEN_UPDATER_SECTION_NAME = "update_manager KlipperScreen" -KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" - -# directories -KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") -KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") -KLIPPERSCREEN_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipperscreen-backups") - -# files -KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( - "scripts/KlipperScreen-requirements.txt" -) -KLIPPERSCREEN_INSTALL_SCRIPT = KLIPPERSCREEN_DIR.joinpath( - "scripts/KlipperScreen-install.sh" -) -KLIPPERSCREEN_SERVICE_FILE = SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py deleted file mode 100644 index a73f87a..0000000 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ /dev/null @@ -1,206 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipperscreen import ( - KLIPPERSCREEN_BACKUP_DIR, - KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV_DIR, - KLIPPERSCREEN_INSTALL_SCRIPT, - KLIPPERSCREEN_LOG_NAME, - KLIPPERSCREEN_REPO, - KLIPPERSCREEN_REQ_FILE, - KLIPPERSCREEN_SERVICE_FILE, - KLIPPERSCREEN_SERVICE_NAME, - KLIPPERSCREEN_UPDATER_SECTION_NAME, -) -from components.moonraker.moonraker import Moonraker -from core.backup_manager.backup_manager import BackupManager -from core.constants import SYSTEMD -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.settings.kiauh_settings import KiauhSettings -from core.types.component_status 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.fs_utils import remove_with_sudo -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_klipperscreen() -> None: - Logger.print_status("Installing KlipperScreen ...") - - 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! KlipperScreen will not properly work " - "without a working Moonraker installation.", - "\n\n", - "KlipperScreens update manager configuration for Moonraker " - "will not be added to any moonraker.conf.", - ], - ) - if not get_confirm( - "Continue KlipperScreen installation?", - default_choice=False, - allow_go_back=True, - ): - return - - check_install_dependencies() - - git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) - - try: - run(KLIPPERSCREEN_INSTALL_SCRIPT.as_posix(), shell=True, check=True) - if mr_instances: - patch_klipperscreen_update_manager(mr_instances) - InstanceManager.restart_all(mr_instances) - else: - Logger.print_info( - "Moonraker is not installed! Cannot add " - "KlipperScreen to update manager!" - ) - Logger.print_ok("KlipperScreen successfully installed!") - except CalledProcessError as e: - Logger.print_error(f"Error installing KlipperScreen:\n{e}") - return - - -def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None: - add_config_section( - section=KLIPPERSCREEN_UPDATER_SECTION_NAME, - instances=instances, - options=[ - ("type", "git_repo"), - ("path", KLIPPERSCREEN_DIR.as_posix()), - ("origin", KLIPPERSCREEN_REPO), - ("managed_services", "KlipperScreen"), - ("env", f"{KLIPPERSCREEN_ENV_DIR}/bin/python"), - ("requirements", KLIPPERSCREEN_REQ_FILE.as_posix()), - ("install_script", KLIPPERSCREEN_INSTALL_SCRIPT.as_posix()), - ], - ) - - -def update_klipperscreen() -> None: - if not KLIPPERSCREEN_DIR.exists(): - Logger.print_info("KlipperScreen does not seem to be installed! Skipping ...") - return - - try: - Logger.print_status("Updating KlipperScreen ...") - - cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "stop") - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - backup_klipperscreen_dir() - - git_pull_wrapper(KLIPPERSCREEN_DIR) - - install_python_requirements(KLIPPERSCREEN_ENV_DIR, KLIPPERSCREEN_REQ_FILE) - - cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "start") - - Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") - except CalledProcessError as e: - Logger.print_error(f"Error updating KlipperScreen:\n{e}") - return - - -def get_klipperscreen_status() -> ComponentStatus: - return get_install_status( - KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV_DIR, - files=[SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME)], - ) - - -def remove_klipperscreen() -> None: - Logger.print_status("Removing KlipperScreen ...") - try: - if KLIPPERSCREEN_DIR.exists(): - Logger.print_status("Removing KlipperScreen directory ...") - shutil.rmtree(KLIPPERSCREEN_DIR) - Logger.print_ok("KlipperScreen directory successfully removed!") - else: - Logger.print_warn("KlipperScreen directory not found!") - - if KLIPPERSCREEN_ENV_DIR.exists(): - Logger.print_status("Removing KlipperScreen environment ...") - shutil.rmtree(KLIPPERSCREEN_ENV_DIR) - Logger.print_ok("KlipperScreen environment successfully removed!") - else: - Logger.print_warn("KlipperScreen environment not found!") - - if KLIPPERSCREEN_SERVICE_FILE.exists(): - remove_system_service(KLIPPERSCREEN_SERVICE_NAME) - - logfile = Path(f"/tmp/{KLIPPERSCREEN_LOG_NAME}") - if logfile.exists(): - Logger.print_status("Removing KlipperScreen log file ...") - remove_with_sudo(logfile) - Logger.print_ok("KlipperScreen log file successfully removed!") - - kl_instances: List[Klipper] = get_instances(Klipper) - for instance in kl_instances: - logfile = instance.base.log_dir.joinpath(KLIPPERSCREEN_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 KlipperScreen from update manager ...") - remove_config_section("update_manager KlipperScreen", mr_instances) - Logger.print_ok("KlipperScreen successfully removed from update manager!") - - Logger.print_ok("KlipperScreen successfully removed!") - - except Exception as e: - Logger.print_error(f"Error removing KlipperScreen:\n{e}") - - -def backup_klipperscreen_dir() -> None: - bm = BackupManager() - bm.backup_directory( - KLIPPERSCREEN_DIR.name, - source=KLIPPERSCREEN_DIR, - target=KLIPPERSCREEN_BACKUP_DIR, - ) - bm.backup_directory( - KLIPPERSCREEN_ENV_DIR.name, - source=KLIPPERSCREEN_ENV_DIR, - target=KLIPPERSCREEN_BACKUP_DIR, - ) diff --git a/kiauh/components/log_uploads/__init__.py b/kiauh/components/log_uploads/__init__.py deleted file mode 100644 index 2d4b133..0000000 --- a/kiauh/components/log_uploads/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path -from typing import Dict, Literal, Union - -FileKey = Literal["filepath", "display_name"] -LogFile = Dict[FileKey, Union[str, Path]] diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py deleted file mode 100644 index 3047a11..0000000 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ /dev/null @@ -1,55 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 urllib.request -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.log_uploads import LogFile -from core.logger import Logger -from utils.instance_utils import get_instances - - -def get_logfile_list() -> List[LogFile]: - log_dirs: List[Path] = [ - instance.base.log_dir for instance in get_instances(Klipper) - ] - - logfiles: List[LogFile] = [] - for _dir in log_dirs: - for f in _dir.iterdir(): - logfiles.append({"filepath": f, "display_name": get_display_name(f)}) - - return logfiles - - -def get_display_name(filepath: Path) -> str: - printer = " ".join(filepath.parts[-3].split("_")[:-1]) - name = filepath.name - - return f"{printer}: {name}" - - -def upload_logfile(logfile: LogFile) -> None: - file = logfile.get("filepath") - name = logfile.get("display_name") - Logger.print_status(f"Uploading the following logfile from {name} ...") - - with open(file, "rb") as f: - headers = {"x-random": ""} - req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f) - try: - response = urllib.request.urlopen(req) - link = response.read().decode("utf-8") - Logger.print_ok("Upload successful! Access it via the following link:") - Logger.print_ok(f">>>> {link}", False) - except Exception as e: - Logger.print_error("Uploading logfile failed!") - Logger.print_error(str(e)) diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py deleted file mode 100644 index 6c131fb..0000000 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ /dev/null @@ -1,67 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.log_uploads.log_upload_utils import get_logfile_list, upload_logfile -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyMethodMayBeStatic -class LogUploadMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Log Upload" - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.logfile_list = get_logfile_list() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - f"{index}": Option(self.upload, opt_index=f"{index}") - for index in range(len(self.logfile_list)) - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ You can select the following logfiles for uploading: β•‘ - β•‘ β•‘ - """ - )[1:] - - for logfile in enumerate(self.logfile_list): - line = f"{logfile[0]}) {logfile[1].get('display_name')}" - menu += f"β•‘ {line:<54}β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - print(menu, end="") - - def upload(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - upload_logfile(self.logfile_list[index]) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Log upload failed!") diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py deleted file mode 100644 index 79bdf99..0000000 --- a/kiauh/components/moonraker/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR - -MODULE_PATH = Path(__file__).resolve().parent - -MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" - -# names -MOONRAKER_CFG_NAME = "moonraker.conf" -MOONRAKER_LOG_NAME = "moonraker.log" -MOONRAKER_SERVICE_NAME = "moonraker.service" -MOONRAKER_DEFAULT_PORT = 7125 -MOONRAKER_ENV_FILE_NAME = "moonraker.env" - -# directories -MOONRAKER_DIR = Path.home().joinpath("moonraker") -MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") -MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups") -MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups") - -# files -MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") -MOONRAKER_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-requirements.txt") -MOONRAKER_SPEEDUPS_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-speedups.txt") -MOONRAKER_DEPS_JSON_FILE = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") -# introduced due to -# https://github.com/Arksine/moonraker/issues/349 -# https://github.com/Arksine/moonraker/pull/346 -POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla") -POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules") -POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules") -POLKIT_SCRIPT = MOONRAKER_DIR.joinpath("scripts/set-policykit-rules.sh") -MOONRAKER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_SERVICE_NAME}") -MOONRAKER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_ENV_FILE_NAME}") - - -EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..." diff --git a/kiauh/components/moonraker/assets/moonraker.conf b/kiauh/components/moonraker/assets/moonraker.conf deleted file mode 100644 index 8e592f2..0000000 --- a/kiauh/components/moonraker/assets/moonraker.conf +++ /dev/null @@ -1,30 +0,0 @@ -[server] -host: 0.0.0.0 -port: %PORT% -klippy_uds_address: %UDS% - -[authorization] -trusted_clients: - 10.0.0.0/8 - 127.0.0.0/8 - 169.254.0.0/16 - 172.16.0.0/12 - 192.168.0.0/16 - FC00::/7 - FE80::/10 - ::1/128 -cors_domains: - *.lan - *.local - *://localhost - *://localhost:* - *://my.mainsail.xyz - *://app.fluidd.xyz - -[octoprint_compat] - -[history] - -[update_manager] -channel: dev -refresh_interval: 168 diff --git a/kiauh/components/moonraker/assets/moonraker.env b/kiauh/components/moonraker/assets/moonraker.env deleted file mode 100644 index bca6af5..0000000 --- a/kiauh/components/moonraker/assets/moonraker.env +++ /dev/null @@ -1 +0,0 @@ -MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%" \ No newline at end of file diff --git a/kiauh/components/moonraker/assets/moonraker.service b/kiauh/components/moonraker/assets/moonraker.service deleted file mode 100644 index 696d7ba..0000000 --- a/kiauh/components/moonraker/assets/moonraker.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=API Server for Klipper SV1 -Documentation=https://moonraker.readthedocs.io/ -Requires=network-online.target -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -SupplementaryGroups=moonraker-admin -RemainAfterExit=yes -WorkingDirectory=%MOONRAKER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $MOONRAKER_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py deleted file mode 100644 index 6729c3e..0000000 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ /dev/null @@ -1,110 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class MoonrakerRemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - - self.title = "Remove Moonraker" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BACK - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.rm_pk = False - self.select_state = False - - self.mrsvc = MoonrakerSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_remove_moonraker_service), - "2": Option(method=self.toggle_remove_moonraker_dir), - "3": Option(method=self.toggle_remove_moonraker_env), - "4": Option(method=self.toggle_remove_moonraker_polkit), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.rm_svc else unchecked - o2 = checked if self.rm_dir else unchecked - o3 = checked if self.rm_env else unchecked - o4 = checked if self.rm_pk else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Enter a number and hit enter to select / deselect β•‘ - β•‘ the specific option for removal. β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ a) {sel_state:49} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) {o1} Remove Service β•‘ - β•‘ 2) {o2} Remove Local Repository β•‘ - β•‘ 3) {o3} Remove Python Environment β•‘ - β•‘ 4) {o4} Remove Policy Kit Rules β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ C) Continue β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.rm_svc = self.select_state - self.rm_dir = self.select_state - self.rm_env = self.select_state - self.rm_pk = self.select_state - - def toggle_remove_moonraker_service(self, **kwargs) -> None: - self.rm_svc = not self.rm_svc - - def toggle_remove_moonraker_dir(self, **kwargs) -> None: - self.rm_dir = not self.rm_dir - - def toggle_remove_moonraker_env(self, **kwargs) -> None: - self.rm_env = not self.rm_env - - def toggle_remove_moonraker_polkit(self, **kwargs) -> None: - self.rm_pk = not self.rm_pk - - def run_removal_process(self, **kwargs) -> None: - if not self.rm_svc and not self.rm_dir and not self.rm_env and not self.rm_pk: - msg = "Nothing selected! Select options to remove first." - print(Color.apply(msg, Color.RED)) - return - - self.mrsvc.remove(self.rm_svc, self.rm_dir, self.rm_env, self.rm_pk) - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.rm_pk = False diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py deleted file mode 100644 index 81040c3..0000000 --- a/kiauh/components/moonraker/moonraker.py +++ /dev/null @@ -1,146 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.klipper.klipper import Klipper -from components.moonraker import ( - MOONRAKER_CFG_NAME, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_ENV_FILE_NAME, - MOONRAKER_ENV_FILE_TEMPLATE, - MOONRAKER_LOG_NAME, - MOONRAKER_SERVICE_TEMPLATE, -) -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass -class Moonraker: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = MOONRAKER_LOG_NAME - moonraker_dir: Path = MOONRAKER_DIR - env_dir: Path = MOONRAKER_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - env_file: Path = field(init=False) - backup_dir: Path = field(init=False) - certs_dir: Path = field(init=False) - db_dir: Path = field(init=False) - port: int | None = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Klipper, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(Moonraker, self.suffix) - self.data_dir: Path = self.base.data_dir - 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.certs_dir: Path = self.base.data_dir.joinpath("certs") - self.db_dir: Path = self.base.data_dir.joinpath("database") - self.port: int | None = self._get_port() - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Moonraker Instance ...") - - try: - create_folders(self.base.base_folders) - - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = MOONRAKER_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%MOONRAKER_DIR%", - self.moonraker_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = MOONRAKER_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%MOONRAKER_DIR%", - self.moonraker_dir.as_posix(), - ) - env_file_content = env_file_content.replace( - "%PRINTER_DATA%", - self.base.data_dir.as_posix(), - ) - - return env_file_content - - def _get_port(self) -> int | None: - if not self.cfg_file or not self.cfg_file.is_file(): - return None - - scp = SimpleConfigParser() - scp.read_file(self.cfg_file) - port: int | None = scp.getint("server", "port", fallback=None) - - return port diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py deleted file mode 100644 index aa7acf5..0000000 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ /dev/null @@ -1,75 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 textwrap -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.menus.base_menu import print_back_footer -from core.types.color import Color - - -def print_moonraker_overview( - klipper_instances: List[Klipper], - moonraker_instances: List[Moonraker], - show_index=False, - show_select_all=False, -): - headline = Color.apply("The following instances were found:", Color.GREEN) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - β•‘{headline:^64}β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - - if show_select_all: - select_all = Color.apply("a) Select all", Color.YELLOW) - dialog += f"β•‘ {select_all:<63}β•‘\n" - dialog += "β•‘ β•‘\n" - - instance_map = { - k.service_file_path.stem: ( - k.service_file_path.stem.replace("klipper", "moonraker") - if k.suffix in [m.suffix for m in moonraker_instances] - else "" - ) - for k in klipper_instances - } - - for i, k in enumerate(instance_map): - mr_name = instance_map.get(k) - m = f"<-> {mr_name}" if mr_name != "" else "" - line = Color.apply(f"{f'{i + 1})' if show_index else '●'} {k} {m}", Color.CYAN) - dialog += f"β•‘ {line:<63}β•‘\n" - - warn_l1 = Color.apply("PLEASE NOTE:", Color.YELLOW) - warn_l2 = Color.apply( - "If you select an instance with an existing Moonraker", Color.YELLOW - ) - warn_l3 = Color.apply( - "instance, that Moonraker instance will be re-created!", Color.YELLOW - ) - warning = textwrap.dedent( - f""" - β•‘ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {warn_l1:<63}β•‘ - β•‘ {warn_l2:<63}β•‘ - β•‘ {warn_l3:<63}β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - - dialog += warning - - print(dialog, end="") - print_back_footer() diff --git a/kiauh/components/moonraker/services/__init__.py b/kiauh/components/moonraker/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/moonraker/services/moonraker_instance_service.py b/kiauh/components/moonraker/services/moonraker_instance_service.py deleted file mode 100644 index 86b8b19..0000000 --- a/kiauh/components/moonraker/services/moonraker_instance_service.py +++ /dev/null @@ -1,49 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import Dict, List - -from components.moonraker.moonraker import Moonraker -from utils.instance_utils import get_instances - - -class MoonrakerInstanceService: - __cls_instance = None - __instances: List[Moonraker] = [] - - def __new__(cls) -> "MoonrakerInstanceService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MoonrakerInstanceService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - - def load_instances(self) -> None: - self.__instances = get_instances(Moonraker) - - def create_new_instance(self, suffix: str) -> Moonraker: - instance = Moonraker(suffix) - self.__instances.append(instance) - return instance - - def get_all_instances(self) -> List[Moonraker]: - return self.__instances - - def get_instance_by_suffix(self, suffix: str) -> Moonraker | None: - instances: List[Moonraker] = [i for i in self.__instances if i.suffix == suffix] - return instances[0] if instances else None - - def get_instance_port_map(self) -> Dict[str, int]: - return {i.suffix: i.port for i in self.__instances} diff --git a/kiauh/components/moonraker/services/moonraker_setup_service.py b/kiauh/components/moonraker/services/moonraker_setup_service.py deleted file mode 100644 index 45edd6f..0000000 --- a/kiauh/components/moonraker/services/moonraker_setup_service.py +++ /dev/null @@ -1,408 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from copy import copy -from subprocess import DEVNULL, PIPE, CalledProcessError, run -from typing import List - -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import print_instance_overview -from components.klipper.services.klipper_instance_service import KlipperInstanceService -from components.moonraker import ( - EXIT_MOONRAKER_SETUP, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_REPO_URL, - MOONRAKER_REQ_FILE, - MOONRAKER_SPEEDUPS_REQ_FILE, - POLKIT_FILE, - POLKIT_LEGACY_FILE, - POLKIT_SCRIPT, - POLKIT_USR_FILE, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.moonraker_dialogs import print_moonraker_overview -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from components.moonraker.utils.utils import ( - backup_moonraker_dir, - create_example_moonraker_conf, - install_moonraker_packages, - remove_polkit_rules, -) -from components.webui_client.client_utils import ( - enable_mainsail_remotemode, - get_existing_clients, -) -from components.webui_client.mainsail_data import MainsailData -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.message_service import Message, MessageService -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.common import check_install_dependencies -from utils.fs_utils import check_file_exist, run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import ( - get_confirm, - get_selection_input, -) -from utils.sys_utils import ( - check_python_version, - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - get_ipv4_addr, - install_python_requirements, - unit_file_exists, -) - - -# noinspection PyMethodMayBeStatic -class MoonrakerSetupService: - __cls_instance = None - - kisvc: KlipperInstanceService - misvc: MoonrakerInstanceService - msgsvc = MessageService - - settings: KiauhSettings - klipper_list: List[Klipper] - moonraker_list: List[Moonraker] - - def __new__(cls) -> "MoonrakerSetupService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MoonrakerSetupService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__init_state() - - def __init_state(self) -> None: - self.settings = KiauhSettings() - - self.kisvc = KlipperInstanceService() - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc = MoonrakerInstanceService() - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - self.msgsvc = MessageService() - - def __refresh_state(self) -> None: - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - def install(self) -> None: - self.__refresh_state() - - if not self.__check_requirements(self.klipper_list): - return - - new_instances: List[Moonraker] = [] - selected_option: str | Klipper - - if len(self.klipper_list) == 1: - suffix: str = self.klipper_list[0].suffix - new_inst = self.misvc.create_new_instance(suffix) - new_instances.append(new_inst) - - else: - print_moonraker_overview( - self.klipper_list, - self.moonraker_list, - show_index=True, - show_select_all=True, - ) - options = {str(i + 1): k for i, k in enumerate(self.klipper_list)} - additional_options = {"a": None, "b": None} - options = {**options, **additional_options} - question = "Select Klipper instance to setup Moonraker for" - selected_option = get_selection_input(question, options) - - if selected_option == "b": - Logger.print_status(EXIT_MOONRAKER_SETUP) - return - - if selected_option == "a": - new_inst_list: List[Moonraker] = [ - self.misvc.create_new_instance(k.suffix) for k in self.klipper_list - ] - new_instances.extend(new_inst_list) - else: - klipper_instance: Klipper | None = options.get(selected_option) - if klipper_instance is None: - raise Exception("Error selecting instance!") - new_inst = self.misvc.create_new_instance(klipper_instance.suffix) - new_instances.append(new_inst) - - create_example_cfg = get_confirm("Create example moonraker.conf?") - - try: - self.__run_setup(new_instances, create_example_cfg) - except Exception as e: - Logger.print_error(f"Error while installing Moonraker: {e}") - return - - def update(self) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Be careful if there are ongoing prints running!", - "All Moonraker instances will be restarted during the update process and " - "ongoing prints COULD FAIL.", - ], - ) - - if not get_confirm("Update Moonraker now?"): - return - - self.__refresh_state() - - if self.settings.kiauh.backup_before_update: - backup_moonraker_dir() - - InstanceManager.stop_all(self.moonraker_list) - git_pull_wrapper(MOONRAKER_DIR) - install_moonraker_packages() - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - InstanceManager.start_all(self.moonraker_list) - - def remove( - self, - remove_service: bool, - remove_dir: bool, - remove_env: bool, - remove_polkit: bool, - ) -> None: - self.__refresh_state() - - completion_msg = Message( - title="Moonraker Removal Process completed", - color=Color.GREEN, - ) - - if remove_service: - Logger.print_status("Removing Moonraker instances ...") - if self.moonraker_list: - instances_to_remove = self.__get_instances_to_remove() - self.__remove_instances(instances_to_remove) - if instances_to_remove: - instance_names = [ - i.service_file_path.stem for i in instances_to_remove - ] - txt = f"● Moonraker instances removed: {', '.join(instance_names)}" - completion_msg.text.append(txt) - else: - Logger.print_info("No Moonraker Services installed! Skipped ...") - - if (remove_polkit or remove_dir or remove_env) and unit_file_exists( - "moonraker", suffix="service" - ): - completion_msg.text = [ - "Some Klipper services are still installed:", - "● Moonraker PolicyKit rules were not removed, even though selected for removal.", - f"● '{MOONRAKER_DIR}' was not removed, even though selected for removal.", - f"● '{MOONRAKER_ENV_DIR}' was not removed, even though selected for removal.", - ] - else: - if remove_polkit: - Logger.print_status("Removing all Moonraker policykit rules ...") - if remove_polkit_rules(): - completion_msg.text.append("● Moonraker policykit rules removed") - if remove_dir: - Logger.print_status("Removing Moonraker local repository ...") - if run_remove_routines(MOONRAKER_DIR): - completion_msg.text.append("● Moonraker local repository removed") - if remove_env: - Logger.print_status("Removing Moonraker Python environment ...") - if run_remove_routines(MOONRAKER_ENV_DIR): - completion_msg.text.append("● Moonraker Python environment removed") - - if completion_msg.text: - completion_msg.text.insert(0, "The following actions were performed:") - else: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text = ["Nothing to remove."] - - self.msgsvc.set_message(completion_msg) - - def __run_setup( - self, new_instances: List[Moonraker], create_example_cfg: bool - ) -> None: - check_install_dependencies() - self.__install_deps() - - ports_map = self.misvc.get_instance_port_map() - for i in new_instances: - i.create() - cmd_sysctl_service(i.service_file_path.name, "enable") - - if create_example_cfg: - # if a webclient and/or it's config is installed, patch - # its update section to the config - clients = get_existing_clients() - create_example_moonraker_conf(i, ports_map, clients) - - cmd_sysctl_service(i.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # if mainsail is installed, and we installed - # multiple moonraker instances, we enable mainsails remote mode - if MainsailData().client_dir.exists() and len(self.moonraker_list) > 1: - enable_mainsail_remotemode() - - self.misvc.load_instances() - new_instances = [ - self.misvc.get_instance_by_suffix(i.suffix) for i in new_instances - ] - - ip: str = get_ipv4_addr() - # noinspection HttpUrlsUsage - url_list = [ - f"● {i.service_file_path.stem}: http://{ip}:{i.port}" - for i in new_instances - if i.port - ] - dialog_content = [] - if url_list: - dialog_content.append("You can access Moonraker via the following URL:") - dialog_content.extend(url_list) - - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Moonraker successfully installed!", - custom_color=Color.GREEN, - content=dialog_content, - ) - - def __check_requirements(self, klipper_list: List[Klipper]) -> bool: - is_klipper_installed = len(klipper_list) >= 1 - if not is_klipper_installed: - Logger.print_warn("Klipper not installed!") - Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") - - is_python_ok = check_python_version(3, 7) - - return is_klipper_installed and is_python_ok - - def __install_deps(self) -> None: - default_repo = (MOONRAKER_REPO_URL, "master") - repo = self.settings.moonraker.repositories - # pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo - repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo - git_clone_wrapper(repo, MOONRAKER_DIR, branch) - - try: - install_moonraker_packages() - if create_python_venv(MOONRAKER_ENV_DIR, False, False, self.settings.moonraker.use_python_binary): - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - if self.settings.moonraker.optional_speedups: - install_python_requirements( - MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE - ) - self.__install_polkit() - except Exception: - Logger.print_error("Error during installation of Moonraker requirements!") - raise - - def __install_polkit(self) -> None: - Logger.print_status("Installing Moonraker policykit rules ...") - - legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True) - polkit_file_exists = check_file_exist(POLKIT_FILE, True) - usr_file_exists = check_file_exist(POLKIT_USR_FILE, True) - - if legacy_file_exists or (polkit_file_exists and usr_file_exists): - Logger.print_info("Moonraker policykit rules are already installed.") - return - - try: - command = [POLKIT_SCRIPT, "--disable-systemctl"] - result = run( - command, - stderr=PIPE, - stdout=DEVNULL, - text=True, - ) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Installing Moonraker policykit rules failed!") - return - - Logger.print_ok("Moonraker policykit rules successfully installed!") - except CalledProcessError as e: - log = ( - f"Error while installing Moonraker policykit rules: {e.stderr.decode()}" - ) - Logger.print_error(log) - - def __get_instances_to_remove(self) -> List[Moonraker] | None: - start_index = 1 - curr_instances: List[Moonraker] = self.moonraker_list - instance_count = len(curr_instances) - - options = [str(i + start_index) for i in range(instance_count)] - options.extend(["a", "b"]) - instance_map = { - options[i]: self.moonraker_list[i] for i in range(instance_count) - } - - print_instance_overview( - self.moonraker_list, - start_index=start_index, - show_index=True, - show_select_all=True, - ) - selection = get_selection_input("Select Moonraker instance to remove", options) - - if selection == "b": - return None - elif selection == "a": - return copy(self.moonraker_list) - - return [instance_map[selection]] - - def __remove_instances( - self, - instance_list: List[Moonraker] | None, - ) -> None: - if not instance_list: - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - self.__delete_env_file(instance) - - self.__refresh_state() - - def __delete_env_file(self, instance: Moonraker): - Logger.print_status(f"Remove '{instance.env_file}'") - if not instance.env_file.exists(): - msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." - Logger.print_info(msg) - return - run_remove_routines(instance.env_file) diff --git a/kiauh/components/moonraker/utils/__init__.py b/kiauh/components/moonraker/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/moonraker/utils/sysdeps_parser.py b/kiauh/components/moonraker/utils/sysdeps_parser.py deleted file mode 100644 index 15dad17..0000000 --- a/kiauh/components/moonraker/utils/sysdeps_parser.py +++ /dev/null @@ -1,173 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# It was modified by Dominik Willner # -# # -# The original file is part of Moonraker: # -# https://github.com/Arksine/moonraker # -# Copyright (C) 2025 Eric Callahan # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -import logging -import pathlib -import re -import shlex -from typing import Any, Dict, List, Tuple - - -def _get_distro_info() -> Dict[str, Any]: - release_file = pathlib.Path("/etc/os-release") - release_info: Dict[str, str] = {} - with release_file.open("r") as f: - lexer = shlex.shlex(f, posix=True) - lexer.whitespace_split = True - for item in list(lexer): - if "=" in item: - key, val = item.split("=", maxsplit=1) - release_info[key] = val - return dict( - distro_id=release_info.get("ID", ""), - distro_version=release_info.get("VERSION_ID", ""), - aliases=release_info.get("ID_LIKE", "").split(), - ) - - -def _convert_version(version: str) -> Tuple[str | int, ...]: - version = version.strip() - ver_match = re.match(r"\d+(\.\d+)*((?:-|\.).+)?", version) - if ver_match is not None: - return tuple( - [ - int(part) if part.isdigit() else part - for part in re.split(r"\.|-", version) - ] - ) - return (version,) - - -class SysDepsParser: - def __init__(self, distro_info: Dict[str, Any] | None = None) -> None: - if distro_info is None: - distro_info = _get_distro_info() - self.distro_id: str = distro_info.get("distro_id", "") - self.aliases: List[str] = distro_info.get("aliases", []) - self.distro_version: Tuple[int | str, ...] = tuple() - version = distro_info.get("distro_version") - if version: - self.distro_version = _convert_version(version) - - def _parse_spec(self, full_spec: str) -> str | None: - parts = full_spec.split(";", maxsplit=1) - if len(parts) == 1: - return full_spec - pkg_name = parts[0].strip() - expressions = re.split(r"( and | or )", parts[1].strip()) - if not len(expressions) & 1: - # There should always be an odd number of expressions. Each - # expression is separated by an "and" or "or" operator - logging.info( - f"Requirement specifier is missing an expression " - f"between logical operators : {full_spec}" - ) - return None - last_result: bool = True - last_logical_op: str | None = "and" - for idx, exp in enumerate(expressions): - if idx & 1: - if last_logical_op is not None: - logging.info( - "Requirement specifier contains sequential logical " - f"operators: {full_spec}" - ) - return None - logical_op = exp.strip() - if logical_op not in ("and", "or"): - logging.info( - f"Invalid logical operator {logical_op} in requirement " - f"specifier: {full_spec}" - ) - return None - last_logical_op = logical_op - continue - elif last_logical_op is None: - logging.info( - f"Requirement specifier contains two seqential expressions " - f"without a logical operator: {full_spec}" - ) - return None - dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip()) - req_var = dep_parts[0].strip().lower() - if len(dep_parts) != 3: - logging.info(f"Invalid comparison, must be 3 parts: {full_spec}") - return None - elif req_var == "distro_id": - left_op: str | Tuple[int | str, ...] = self.distro_id - right_op = dep_parts[2].strip().strip("\"'") - elif req_var == "distro_version": - if not self.distro_version: - logging.info( - "Distro Version not detected, cannot satisfy requirement: " - f"{full_spec}" - ) - return None - left_op = self.distro_version - right_op = _convert_version(dep_parts[2].strip().strip("\"'")) - else: - logging.info(f"Invalid requirement specifier: {full_spec}") - return None - operator = dep_parts[1].strip() - try: - compfunc = { - "<": lambda x, y: x < y, - ">": lambda x, y: x > y, - "==": lambda x, y: x == y, - "!=": lambda x, y: x != y, - ">=": lambda x, y: x >= y, - "<=": lambda x, y: x <= y, - }.get(operator, lambda x, y: False) - result = compfunc(left_op, right_op) - if last_logical_op == "and": - last_result &= result - else: - last_result |= result - last_logical_op = None - except Exception: - logging.exception(f"Error comparing requirements: {full_spec}") - return None - if last_result: - return pkg_name - return None - - def parse_dependencies(self, sys_deps: Dict[str, List[str]]) -> List[str]: - if not self.distro_id: - logging.info( - "Failed to detect current distro ID, cannot parse dependencies" - ) - return [] - all_ids = [self.distro_id] + self.aliases - for distro_id in all_ids: - if distro_id in sys_deps: - if not sys_deps[distro_id]: - logging.info( - f"Dependency data contains an empty package definition " - f"for linux distro '{distro_id}'" - ) - continue - processed_deps: List[str] = [] - for dep in sys_deps[distro_id]: - parsed_dep = self._parse_spec(dep) - if parsed_dep is not None: - processed_deps.append(parsed_dep) - return processed_deps - else: - logging.info( - f"Dependency data has no package definition for linux " - f"distro '{self.distro_id}'" - ) - return [] diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py deleted file mode 100644 index e358367..0000000 --- a/kiauh/components/moonraker/utils/utils.py +++ /dev/null @@ -1,196 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -import shutil -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, run -from typing import Dict, List, Optional - -from components.moonraker import ( - MODULE_PATH, - MOONRAKER_BACKUP_DIR, - MOONRAKER_DB_BACKUP_DIR, - MOONRAKER_DEFAULT_PORT, - MOONRAKER_DEPS_JSON_FILE, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_INSTALL_SCRIPT, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.utils.sysdeps_parser import SysDepsParser -from components.webui_client.base_data import BaseWebClient -from core.backup_manager.backup_manager import BackupManager -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from core.types.component_status import ComponentStatus -from utils.common import check_install_dependencies, get_install_status -from utils.instance_utils import get_instances -from utils.sys_utils import ( - get_ipv4_addr, - parse_packages_from_file, -) - - -def get_moonraker_status() -> ComponentStatus: - return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker) - - -def install_moonraker_packages() -> None: - Logger.print_status("Parsing Moonraker system dependencies ...") - - moonraker_deps = [] - if MOONRAKER_DEPS_JSON_FILE.exists(): - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..." - ) - parser = SysDepsParser() - sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE) - moonraker_deps.extend(parser.parse_dependencies(sysdeps)) - - elif MOONRAKER_INSTALL_SCRIPT.exists(): - Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!") - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..." - ) - moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) - - if not moonraker_deps: - raise ValueError("Error parsing Moonraker dependencies!") - - check_install_dependencies({*moonraker_deps}) - - -def remove_polkit_rules() -> bool: - if not MOONRAKER_DIR.exists(): - log = "Cannot remove policykit rules. Moonraker directory not found." - Logger.print_warn(log) - return False - - try: - cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] - run(cmd, stderr=PIPE, stdout=DEVNULL, check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Error while removing policykit rules: {e}") - return False - - -def create_example_moonraker_conf( - instance: Moonraker, - ports_map: Dict[str, int], - clients: Optional[List[BaseWebClient]] = None, -) -> None: - Logger.print_status(f"Creating example moonraker.conf in '{instance.base.cfg_dir}'") - if instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' already exists.") - return - - source = MODULE_PATH.joinpath("assets/moonraker.conf") - target = instance.cfg_file - try: - shutil.copy(source, target) - except OSError as e: - Logger.print_error(f"Unable to create example moonraker.conf:\n{e}") - return - - ports = [ - ports_map.get(instance) - for instance in ports_map - if ports_map.get(instance) is not None - ] - if ports_map.get(instance.suffix) is None: - # this could be improved to not increment the max value of the ports list and assign it as the port - # as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port - # of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1 - # and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning - # the correct port to each instance and the user will likely be required to correct the value manually. - port = max(ports) + 1 if ports else MOONRAKER_DEFAULT_PORT - else: - port = ports_map.get(instance.suffix) - - ports_map[instance.suffix] = port - - ip = get_ipv4_addr().split(".")[:2] - ip.extend(["0", "0/16"]) - uds = instance.base.comms_dir.joinpath("klippy.sock") - - scp = SimpleConfigParser() - scp.read_file(target) - trusted_clients: List[str] = [ - f" {'.'.join(ip)}\n", - *scp.getval("authorization", "trusted_clients"), - ] - - scp.set_option("server", "port", str(port)) - scp.set_option("server", "klippy_uds_address", str(uds)) - scp.set_option("authorization", "trusted_clients", trusted_clients) - - # add existing client and client configs in the update section - if clients is not None and len(clients) > 0: - for c in clients: - # client part - c_section = f"update_manager {c.name}" - c_options = [ - ("type", "web"), - ("channel", "stable"), - ("repo", c.repo_path), - ("path", c.client_dir), - ] - scp.add_section(section=c_section) - for option in c_options: - scp.set_option(c_section, option[0], option[1]) - - # client config part - c_config = c.client_config - if c_config.config_dir.exists(): - c_config_section = f"update_manager {c_config.name}" - c_config_options = [ - ("type", "git_repo"), - ("primary_branch", "master"), - ("path", c_config.config_dir), - ("origin", c_config.repo_url), - ("managed_services", "klipper"), - ] - scp.add_section(section=c_config_section) - for option in c_config_options: - scp.set_option(c_config_section, option[0], option[1]) - - scp.write_file(target) - Logger.print_ok(f"Example moonraker.conf created in '{instance.base.cfg_dir}'") - - -def backup_moonraker_dir() -> None: - bm = BackupManager() - bm.backup_directory("moonraker", source=MOONRAKER_DIR, target=MOONRAKER_BACKUP_DIR) - bm.backup_directory( - "moonraker-env", source=MOONRAKER_ENV_DIR, target=MOONRAKER_BACKUP_DIR - ) - - -def backup_moonraker_db_dir() -> None: - instances: List[Moonraker] = get_instances(Moonraker) - bm = BackupManager() - - for instance in instances: - name = f"database-{instance.data_dir.name}" - bm.backup_directory( - name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR - ) - - -def load_sysdeps_json(file: Path) -> Dict[str, List[str]]: - try: - sysdeps: Dict[str, List[str]] = json.loads(file.read_bytes()) - except json.JSONDecodeError as e: - Logger.print_error(f"Unable to parse {file.name}:\n{e}") - return {} - else: - return sysdeps diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py deleted file mode 100644 index 8bfcd7a..0000000 --- a/kiauh/components/webui_client/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/components/webui_client/assets/common_vars.conf b/kiauh/components/webui_client/assets/common_vars.conf deleted file mode 100644 index 9c3f85e..0000000 --- a/kiauh/components/webui_client/assets/common_vars.conf +++ /dev/null @@ -1,6 +0,0 @@ -# /etc/nginx/conf.d/common_vars.conf - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} \ No newline at end of file diff --git a/kiauh/components/webui_client/assets/nginx_cfg b/kiauh/components/webui_client/assets/nginx_cfg deleted file mode 100644 index d7aabf4..0000000 --- a/kiauh/components/webui_client/assets/nginx_cfg +++ /dev/null @@ -1,95 +0,0 @@ -server { - listen %PORT%; - # uncomment the next line to activate IPv6 - # listen [::]:%PORT%; - - access_log /var/log/nginx/%NAME%-access.log; - error_log /var/log/nginx/%NAME%-error.log; - - # disable this section on smaller hardware like a pi zero - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_proxied expired no-cache no-store private auth; - gzip_comp_level 4; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml; - - # web_path from %NAME% static files - root %ROOT_DIR%; - - index index.html; - server_name _; - - # disable max upload size checks - client_max_body_size 0; - - # disable proxy request buffering - proxy_request_buffering off; - - location / { - try_files $uri $uri/ /index.html; - } - - location = /index.html { - add_header Cache-Control "no-store, no-cache, must-revalidate"; - } - - location /websocket { - proxy_pass http://apiserver/websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 86400; - } - - location ~ ^/(printer|api|access|machine|server)/ { - proxy_pass http://apiserver$request_uri; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Scheme $scheme; - } - - location /webcam/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer1/; - } - - location /webcam2/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer2/; - } - - location /webcam3/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer3/; - } - - location /webcam4/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer4/; - } -} diff --git a/kiauh/components/webui_client/assets/upstreams.conf b/kiauh/components/webui_client/assets/upstreams.conf deleted file mode 100644 index d04e04a..0000000 --- a/kiauh/components/webui_client/assets/upstreams.conf +++ /dev/null @@ -1,25 +0,0 @@ -# /etc/nginx/conf.d/upstreams.conf -upstream apiserver { - ip_hash; - server 127.0.0.1:7125; -} - -upstream mjpgstreamer1 { - ip_hash; - server 127.0.0.1:8080; -} - -upstream mjpgstreamer2 { - ip_hash; - server 127.0.0.1:8081; -} - -upstream mjpgstreamer3 { - ip_hash; - server 127.0.0.1:8082; -} - -upstream mjpgstreamer4 { - ip_hash; - server 127.0.0.1:8083; -} \ No newline at end of file diff --git a/kiauh/components/webui_client/base_data.py b/kiauh/components/webui_client/base_data.py deleted file mode 100644 index b53edb7..0000000 --- a/kiauh/components/webui_client/base_data.py +++ /dev/null @@ -1,57 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from abc import ABC -from dataclasses import dataclass -from enum import Enum -from pathlib import Path - - -class WebClientType(Enum): - MAINSAIL: str = "mainsail" - FLUIDD: str = "fluidd" - - -class WebClientConfigType(Enum): - MAINSAIL: str = "mainsail-config" - FLUIDD: str = "fluidd-config" - - -@dataclass() -class BaseWebClient(ABC): - """Base class for webclient data""" - - client: WebClientType - name: str - display_name: str - client_dir: Path - config_file: Path - backup_dir: Path - repo_path: str - download_url: str - nginx_config: Path - nginx_access_log: Path - nginx_error_log: Path - client_config: BaseWebClientConfig - - -@dataclass() -class BaseWebClientConfig(ABC): - """Base class for webclient config data""" - - client_config: WebClientConfigType - name: str - display_name: str - config_filename: str - config_dir: Path - backup_dir: Path - repo_url: str - config_section: str diff --git a/kiauh/components/webui_client/client_config/__init__.py b/kiauh/components/webui_client/client_config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py deleted file mode 100644 index 75a1d8b..0000000 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ /dev/null @@ -1,91 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import BaseWebClientConfig -from core.logger import Logger -from core.services.message_service import Message -from core.types.color import Color -from utils.config_utils import remove_config_section -from utils.fs_utils import run_remove_routines -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -def run_client_config_removal( - client_config: BaseWebClientConfig, - kl_instances: List[Klipper], - mr_instances: List[Moonraker], -) -> Message: - completion_msg = Message( - title=f"{client_config.display_name} Removal Process completed", - color=Color.GREEN, - ) - Logger.print_status(f"Removing {client_config.display_name} ...") - 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_cfg_symlink(client_config: BaseWebClientConfig, message: Message) -> Message: - instances: List[Klipper] = get_instances(Klipper) - kl_instances = [] - for instance in instances: - cfg = instance.base.cfg_dir.joinpath(client_config.config_filename) - if run_remove_routines(cfg): - kl_instances.append(instance) - text = f"{client_config.display_name} removed from instance" - return update_msg(kl_instances, message, text) - - -def remove_printer_config_section( - message: Message, client_config: BaseWebClientConfig, kl_instances: List[Klipper] -) -> Message: - kl_section = client_config.config_section - kl_instances = remove_config_section(kl_section, kl_instances) - text = f"Klipper config section '{kl_section}' removed for instance" - return update_msg(kl_instances, message, text) - - -def remove_moonraker_config_section( - message: Message, client_config: BaseWebClientConfig, mr_instances: List[Moonraker] -) -> Message: - mr_section = f"update_manager {client_config.name}" - mr_instances = remove_config_section(mr_section, mr_instances) - text = f"Moonraker config section '{mr_section}' removed for instance" - return update_msg(mr_instances, message, text) - - -def update_msg(instances: List[InstanceType], message: Message, text: str) -> Message: - if not instances: - return message - - instance_names = [i.service_file_path.stem for i in instances] - message.text.append(f"● {text}: {', '.join(instance_names)}") - return message diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py deleted file mode 100644 index 9d31fdb..0000000 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ /dev/null @@ -1,126 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig -from components.webui_client.client_dialogs import ( - print_client_already_installed_dialog, -) -from components.webui_client.client_utils import ( - backup_client_config_data, - detect_client_cfg_conflict, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from core.settings.kiauh_settings import KiauhSettings -from utils.common import backup_printer_config_dir -from utils.config_utils import add_config_section, add_config_section_at_top -from utils.fs_utils import create_symlink -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 - - -def install_client_config(client_data: BaseWebClient, cfg_backup=True) -> None: - client_config: BaseWebClientConfig = client_data.client_config - display_name = client_config.display_name - - if detect_client_cfg_conflict(client_data): - Logger.print_info("Another Client-Config is already installed! Skipped ...") - return - - if client_config.config_dir.exists(): - print_client_already_installed_dialog(display_name) - if get_confirm(f"Re-install {display_name}?", allow_go_back=True): - shutil.rmtree(client_config.config_dir) - else: - return - - mr_instances: List[Moonraker] = get_instances(Moonraker) - kl_instances = get_instances(Klipper) - - try: - download_client_config(client_config) - create_client_config_symlink(client_config, kl_instances) - - if cfg_backup: - backup_printer_config_dir() - - add_config_section( - section=f"update_manager {client_config.name}", - instances=mr_instances, - options=[ - ("type", "git_repo"), - ("primary_branch", "master"), - ("path", str(client_config.config_dir)), - ("origin", str(client_config.repo_url)), - ("managed_services", "klipper"), - ], - ) - add_config_section_at_top(client_config.config_section, kl_instances) - InstanceManager.restart_all(kl_instances) - - except Exception as e: - Logger.print_error(f"{display_name} installation failed!\n{e}") - return - - Logger.print_ok(f"{display_name} installation complete!", start="\n") - - -def download_client_config(client_config: BaseWebClientConfig) -> None: - try: - Logger.print_status(f"Downloading {client_config.display_name} ...") - repo = client_config.repo_url - target_dir = client_config.config_dir - git_clone_wrapper(repo, target_dir) - except Exception: - Logger.print_error(f"Downloading {client_config.display_name} failed!") - raise - - -def update_client_config(client: BaseWebClient) -> None: - client_config: BaseWebClientConfig = client.client_config - - Logger.print_status(f"Updating {client_config.display_name} ...") - - if not client_config.config_dir.exists(): - Logger.print_info( - f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..." - ) - return - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - backup_client_config_data(client) - - git_pull_wrapper(client_config.config_dir) - - Logger.print_ok(f"Successfully updated {client_config.display_name}.") - Logger.print_info("Restart Klipper to reload the configuration!") - - -def create_client_config_symlink( - client_config: BaseWebClientConfig, klipper_instances: List[Klipper] -) -> None: - for instance in klipper_instances: - Logger.print_status(f"Create symlink for {client_config.config_filename} ...") - source = Path(client_config.config_dir, client_config.config_filename) - target = instance.base.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py deleted file mode 100644 index a8e9468..0000000 --- a/kiauh/components/webui_client/client_dialogs.py +++ /dev/null @@ -1,93 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import List - -from components.webui_client.base_data import BaseWebClient -from core.logger import DialogType, Logger - - -def print_moonraker_not_found_dialog(name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "No local Moonraker installation was found!", - "\n\n", - 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 " - f"another machine in your network. Otherwise {name} will NOT work " - "correctly.", - ], - ) - - -def print_client_already_installed_dialog(name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - f"{name} seems to be already installed!", - f"If you continue, your current {name} installation will be overwritten.", - ], - ) - - -def print_client_port_select_dialog( - name: str, port: int, ports_in_use: List[int] -) -> None: - dialog_content: List[str] = [ - f"Please select the port, {name} should be served on. If your are unsure " - f"what to select, hit Enter to apply the suggested value of: {port}", - "\n\n", - f"In case you need {name} to be served on a specific port, you can set it " - f"now. Make sure that the port is not already used by another application " - f"on your system!", - ] - - if ports_in_use: - dialog_content.extend( - [ - "\n\n", - "The following ports were found to be already in use:", - *[f"● {p}" for p in ports_in_use if p != port], - ] - ) - - Logger.print_dialog(DialogType.CUSTOM, dialog_content) - - -def print_install_client_config_dialog(client: BaseWebClient) -> None: - name = client.display_name - url = client.client_config.repo_url.replace(".git", "") - Logger.print_dialog( - DialogType.INFO, - [ - f"It is recommended to use special macros in order to have {name} fully " - f"functional and working.", - "\n\n", - f"The recommended macros for {name} can be seen here:", - url, - "\n\n", - "If you already use these macros skip this step. Otherwise you should " - "consider to answer with 'Y' to download the recommended macros.", - ], - ) - - -def print_ipv6_warning_dialog() -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "It looks like IPv6 is enabled on this system!", - "This may cause issues with the installation of NGINX in the following " - "steps! It is recommended to disable IPv6 on your system to avoid this issue.", - "\n\n", - "If you think this warning is a false alarm, and you are sure that " - "IPv6 is disabled, you can continue with the installation.", - ], - ) diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py deleted file mode 100644 index 471b3a0..0000000 --- a/kiauh/components/webui_client/client_remove.py +++ /dev/null @@ -1,112 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import ( - BaseWebClient, -) -from components.webui_client.client_config.client_config_remove import ( - run_client_config_removal, -) -from core.backup_manager.backup_manager import BackupManager -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from core.logger import Logger -from core.services.message_service import Message -from core.types.color import Color -from utils.config_utils import remove_config_section -from utils.fs_utils import ( - remove_with_sudo, - run_remove_routines, -) -from utils.instance_utils import get_instances - - -def run_client_removal( - client: BaseWebClient, - remove_client: bool, - remove_client_cfg: bool, - backup_config: bool, -) -> Message: - completion_msg = Message( - title=f"{client.display_name} Removal Process completed", - color=Color.GREEN, - ) - mr_instances: List[Moonraker] = get_instances(Moonraker) - kl_instances: List[Klipper] = get_instances(Klipper) - - if backup_config: - bm = BackupManager() - if bm.backup_file(client.config_file): - completion_msg.text.append(f"● {client.config_file.name} backup created") - - if remove_client: - client_name = client.name - if remove_client_dir(client): - completion_msg.text.append(f"● {client.display_name} removed") - if remove_client_nginx_config(client_name): - completion_msg.text.append("● NGINX config removed") - if remove_client_nginx_logs(client, kl_instances): - completion_msg.text.append("● NGINX logs removed") - - section = f"update_manager {client_name}" - 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: - cfg_completion_msg = run_client_config_removal( - client.client_config, - kl_instances, - mr_instances, - ) - if cfg_completion_msg.color == Color.GREEN: - completion_msg.text.extend(cfg_completion_msg.text[1:]) - - if not completion_msg.text: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text.append("Nothing to remove.") - else: - completion_msg.text.insert(0, "The following actions were performed:") - - return completion_msg - - -def remove_client_dir(client: BaseWebClient) -> bool: - Logger.print_status(f"Removing {client.display_name} ...") - return run_remove_routines(client.client_dir) - - -def remove_client_nginx_config(name: str) -> bool: - Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") - return remove_with_sudo( - [ - NGINX_SITES_AVAILABLE.joinpath(name), - NGINX_SITES_ENABLED.joinpath(name), - ] - ) - - -def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> bool: - Logger.print_status(f"Removing NGINX logs for {client.display_name} ...") - - files = [client.nginx_access_log, client.nginx_error_log] - if instances: - for instance in instances: - files.append(instance.base.log_dir.joinpath(client.nginx_access_log.name)) - files.append(instance.base.log_dir.joinpath(client.nginx_error_log.name)) - - return remove_with_sudo(files) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py deleted file mode 100644 index 6185241..0000000 --- a/kiauh/components/webui_client/client_setup.py +++ /dev/null @@ -1,186 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 -import tempfile -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client import MODULE_PATH -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientType, -) -from components.webui_client.client_config.client_config_setup import ( - install_client_config, -) -from components.webui_client.client_dialogs import ( - print_install_client_config_dialog, - print_moonraker_not_found_dialog, -) -from components.webui_client.client_utils import ( - copy_common_vars_nginx_cfg, - copy_upstream_nginx_cfg, - create_nginx_cfg, - detect_client_cfg_conflict, - enable_mainsail_remotemode, - get_client_port_selection, - symlink_webui_nginx_log, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.common import backup_printer_config_dir, check_install_dependencies -from utils.config_utils import add_config_section -from utils.fs_utils import unzip -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - download_file, - get_ipv4_addr, -) - - -def install_client( - client: BaseWebClient, - settings: KiauhSettings, - reinstall: bool = False, -) -> None: - mr_instances: List[Moonraker] = get_instances(Moonraker) - - enable_remotemode = False - if not mr_instances: - print_moonraker_not_found_dialog(client.display_name) - if not get_confirm(f"Continue {client.display_name} installation?"): - return - - # if moonraker is not installed or multiple instances - # are installed we enable mainsails remote mode - if ( - client.client == WebClientType.MAINSAIL - and not mr_instances - or len(mr_instances) > 1 - ): - enable_remotemode = True - - kl_instances = get_instances(Klipper) - install_client_cfg = False - client_config: BaseWebClientConfig = client.client_config - if ( - kl_instances - and not client_config.config_dir.exists() - and not detect_client_cfg_conflict(client) - ): - print_install_client_config_dialog(client) - question = f"Download the recommended {client_config.display_name}?" - install_client_cfg = get_confirm(question, allow_go_back=False) - - default_port: int = int(settings.get(client.name, "port")) - port: int = ( - default_port if reinstall else get_client_port_selection(client, settings) - ) - - check_install_dependencies({"nginx"}) - - try: - download_client(client) - if enable_remotemode and client.client == WebClientType.MAINSAIL: - enable_mainsail_remotemode() - - backup_printer_config_dir() - add_config_section( - section=f"update_manager {client.name}", - instances=mr_instances, - options=[ - ("type", "web"), - ("channel", "stable"), - ("repo", str(client.repo_path)), - ("path", str(client.client_dir)), - ], - ) - InstanceManager.restart_all(mr_instances) - - if install_client_cfg and kl_instances: - install_client_config(client, False) - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_nginx_cfg( - display_name=client.display_name, - cfg_name=client.name, - template_src=MODULE_PATH.joinpath("assets/nginx_cfg"), - PORT=port, - ROOT_DIR=client.client_dir, - NAME=client.name, - ) - - if kl_instances: - symlink_webui_nginx_log(client, kl_instances) - cmd_sysctl_service("nginx", "restart") - - except Exception as e: - Logger.print_error(e) - Logger.print_dialog( - DialogType.ERROR, - center_content=True, - content=[f"{client.display_name} installation failed!"], - ) - return - - # noinspection HttpUrlsUsage - Logger.print_dialog( - DialogType.CUSTOM, - custom_title=f"{client.display_name} installation complete!", - custom_color=Color.GREEN, - center_content=True, - content=[ - f"Open {client.display_name} now on: http://{get_ipv4_addr()}{'' if port == 80 else f':{port}'}", - ], - ) - - -def download_client(client: BaseWebClient) -> None: - zipfile = f"{client.name.lower()}.zip" - target = Path().home().joinpath(zipfile) - try: - Logger.print_status( - f"Downloading {client.display_name} from {client.download_url} ..." - ) - download_file(client.download_url, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status(f"Extracting {zipfile} ...") - unzip(target, client.client_dir) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error(f"Downloading {client.display_name} failed!") - raise - - -def update_client(client: BaseWebClient) -> None: - Logger.print_status(f"Updating {client.display_name} ...") - if not client.client_dir.exists(): - Logger.print_info( - f"Unable to update {client.display_name}. Directory does not exist! Skipping ..." - ) - return - - with tempfile.NamedTemporaryFile(suffix=".json") as tmp_file: - Logger.print_status( - f"Creating temporary backup of {client.config_file} as {tmp_file.name} ..." - ) - shutil.copy(client.config_file, tmp_file.name) - download_client(client) - shutil.copy(tmp_file.name, client.config_file) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py deleted file mode 100644 index 6eab1ea..0000000 --- a/kiauh/components/webui_client/client_utils.py +++ /dev/null @@ -1,453 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 json -import re -import shutil -from pathlib import Path -from subprocess import PIPE, CalledProcessError, run -from typing import List, get_args - -from components.klipper.klipper import Klipper -from components.webui_client import MODULE_PATH -from components.webui_client.base_data import ( - BaseWebClient, - WebClientType, -) -from components.webui_client.client_dialogs import print_client_port_select_dialog -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.backup_manager.backup_manager import BackupManager -from core.constants import ( - NGINX_CONFD, - NGINX_SITES_AVAILABLE, - NGINX_SITES_ENABLED, -) -from core.logger import Logger -from core.settings.kiauh_settings import KiauhSettings, WebUiSettings -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from core.types.color import Color -from core.types.component_status import ComponentStatus -from utils.common import get_install_status -from utils.fs_utils import create_symlink, remove_file -from utils.git_utils import ( - get_latest_remote_tag, - get_latest_unstable_tag, -) -from utils.input_utils import get_number_input -from utils.instance_utils import get_instances - - -def get_client_status( - client: BaseWebClient, fetch_remote: bool = False -) -> ComponentStatus: - files = [ - NGINX_SITES_AVAILABLE.joinpath(client.name), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ] - comp_status: ComponentStatus = get_install_status(client.client_dir, files=files) - - # if the client dir does not exist, set the status to not - # installed even if the other files are present - if not client.client_dir.exists(): - comp_status.status = 0 - - comp_status.local = get_local_client_version(client) - comp_status.remote = get_remote_client_version(client) if fetch_remote else None - return comp_status - - -def get_client_config_status(client: BaseWebClient) -> ComponentStatus: - return get_install_status(client.client_config.config_dir) - - -def get_current_client_config() -> str: - mainsail, fluidd = MainsailData(), FluiddData() - clients: List[BaseWebClient] = [mainsail, fluidd] - installed = [c for c in clients if c.client_config.config_dir.exists()] - - if not installed: - return Color.apply("-", Color.CYAN) - elif len(installed) == 1: - cfg = installed[0].client_config - return Color.apply(cfg.display_name, Color.CYAN) - - # at this point, both client config folders exists, so we need to check - # which are actually included in the printer.cfg of all klipper instances - 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: - Logger.print_status("Enable Mainsails remote mode ...") - c_json = MainsailData().client_dir.joinpath("config.json") - with open(c_json, "r") as f: - config_data = json.load(f) - - if config_data["instancesDB"] == "browser": - Logger.print_info("Remote mode already configured. Skipped ...") - return - - Logger.print_status("Setting instance storage location to 'browser' ...") - config_data["instancesDB"] = "browser" - - with open(c_json, "w") as f: - json.dump(config_data, f, indent=4) - Logger.print_ok("Mainsails remote mode enabled!") - - -def symlink_webui_nginx_log( - client: BaseWebClient, klipper_instances: List[Klipper] -) -> None: - Logger.print_status("Link NGINX logs into log directory ...") - access_log = client.nginx_access_log - error_log = client.nginx_error_log - - for instance in klipper_instances: - desti_access = instance.base.log_dir.joinpath(access_log.name) - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.base.log_dir.joinpath(error_log.name) - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -def get_local_client_version(client: BaseWebClient) -> str | None: - relinfo_file = client.client_dir.joinpath("release_info.json") - version_file = client.client_dir.joinpath(".version") - - if not client.client_dir.exists(): - return None - if not relinfo_file.is_file() and not version_file.is_file(): - return "n/a" - - if relinfo_file.is_file(): - with open(relinfo_file, "r") as f: - return str(json.load(f)["version"]) - else: - with open(version_file, "r") as f: - return f.readlines()[0] - - -def get_remote_client_version(client: BaseWebClient) -> str | None: - try: - if (tag := get_latest_remote_tag(client.repo_path)) != "": - return str(tag) - return None - except Exception: - return None - - -def backup_client_data(client: BaseWebClient) -> None: - name = client.name - src = client.client_dir - dest = client.backup_dir - - with open(src.joinpath(".version"), "r") as v: - version = v.readlines()[0] - - bm = BackupManager() - bm.backup_directory(f"{name}-{version}", src, dest) - bm.backup_file(client.config_file, dest) - bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest) - - -def backup_client_config_data(client: BaseWebClient) -> None: - client_config = client.client_config - name = client_config.name - source = client_config.config_dir - target = client_config.backup_dir - bm = BackupManager() - bm.backup_directory(name, source, target) - - -def get_existing_clients() -> List[BaseWebClient]: - clients = list(get_args(WebClientType)) - installed_clients: List[BaseWebClient] = [] - for client in clients: - if client.client_dir.exists(): - installed_clients.append(client) - - return installed_clients - - -def detect_client_cfg_conflict(curr_client: BaseWebClient) -> bool: - """ - Check if any other client configs are present on the system. - It is usually not harmful, but chances are they can conflict each other. - Multiple client configs are, at least, redundant to have them installed - :param curr_client: The client name to check for the conflict - :return: True, if other client configs were found, else False - """ - - mainsail_cfg_status: ComponentStatus = get_client_config_status(MainsailData()) - fluidd_cfg_status: ComponentStatus = get_client_config_status(FluiddData()) - - if curr_client.client == WebClientType.MAINSAIL and fluidd_cfg_status.status == 2: - return True - if curr_client.client == WebClientType.FLUIDD and mainsail_cfg_status.status == 2: - return True - - return False - - -def get_download_url(base_url: str, client: BaseWebClient) -> str: - settings = KiauhSettings() - use_unstable = settings.get(client.name, "unstable_releases") - stable_url = f"{base_url}/latest/download/{client.name}.zip" - - if not use_unstable: - return stable_url - - try: - unstable_tag = get_latest_unstable_tag(client.repo_path) - if unstable_tag == "": - raise Exception - return f"{base_url}/download/{unstable_tag}/{client.name}.zip" - except Exception: - return stable_url - - -################################################# -## NGINX RELATED FUNCTIONS -################################################# - - -def copy_upstream_nginx_cfg() -> None: - """ - Creates an upstream.conf in /etc/nginx/conf.d - :return: None - """ - source = MODULE_PATH.joinpath("assets/upstreams.conf") - target = NGINX_CONFD.joinpath("upstreams.conf") - try: - command = ["sudo", "cp", source, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def copy_common_vars_nginx_cfg() -> None: - """ - Creates a common_vars.conf in /etc/nginx/conf.d - :return: None - """ - source = MODULE_PATH.joinpath("assets/common_vars.conf") - target = NGINX_CONFD.joinpath("common_vars.conf") - try: - command = ["sudo", "cp", source, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> None: - """ - Creates an NGINX config from a template file and - replaces all placeholders passed as kwargs. A placeholder must be defined - in the template file as %{placeholder}%. - :param name: name of the config to create - :param template_src: the path to the template file - :return: None - """ - tmp = Path.home().joinpath(f"{name}.tmp") - shutil.copy(template_src, tmp) - with open(tmp, "r+") as f: - content = f.read() - - for key, value in kwargs.items(): - content = content.replace(f"%{key}%", str(value)) - - f.seek(0) - f.write(content) - f.truncate() - - target = NGINX_SITES_AVAILABLE.joinpath(name) - try: - command = ["sudo", "mv", tmp, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create '{target}': {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def create_nginx_cfg( - display_name: str, - cfg_name: str, - template_src: Path, - **kwargs, -) -> None: - from utils.sys_utils import set_nginx_permissions - - try: - Logger.print_status(f"Creating NGINX config for {display_name} ...") - - source = NGINX_SITES_AVAILABLE.joinpath(cfg_name) - target = NGINX_SITES_ENABLED.joinpath(cfg_name) - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - generate_nginx_cfg_from_template(cfg_name, template_src=template_src, **kwargs) - create_symlink(source, target, True) - set_nginx_permissions() - - Logger.print_ok(f"NGINX config for {display_name} successfully created.") - except Exception: - Logger.print_error(f"Creating NGINX config for {display_name} failed!") - raise - - -def get_nginx_config_list() -> List[Path]: - """ - Get a list of all NGINX config files in /etc/nginx/sites-enabled - :return: List of NGINX config files - """ - configs: List[Path] = [] - for config in NGINX_SITES_ENABLED.iterdir(): - if not config.is_file(): - continue - configs.append(config) - return configs - - -def get_nginx_listen_port(config: Path) -> int | None: - """ - Get the listen port from an NGINX config file - :param config: The NGINX config file to read the port from - :return: The listen port as int or None if not found/parsable - """ - - # noinspection HttpUrlsUsage - pattern = r"default_server|http://|https://|[;\[\]]" - port = "" - with open(config, "r") as cfg: - for line in cfg.readlines(): - line = re.sub(pattern, "", line.strip()) - if line.startswith("listen"): - if ":" not in line: - port = line.split()[-1] - else: - port = line.split(":")[-1] - try: - return int(port) - except ValueError: - Logger.print_error( - f"Unable to parse listen port {port} from {config.name}!" - ) - return None - - -def read_ports_from_nginx_configs() -> List[int]: - """ - Helper function to iterate over all NGINX configs - and read all ports defined for listen - :return: A sorted list of listen ports - """ - if not NGINX_SITES_ENABLED.exists(): - return [] - - port_list: List[int] = [] - for config in get_nginx_config_list(): - port = get_nginx_listen_port(config) - if port is not None: - port_list.append(port) - - return sorted(port_list, key=lambda x: int(x)) - - -def get_client_port_selection( - client: BaseWebClient, - settings: KiauhSettings, - reconfigure=False, -) -> int: - default_port: int = int(settings.get(client.name, "port")) - ports_in_use: List[int] = read_ports_from_nginx_configs() - next_free_port: int = get_next_free_port(ports_in_use) - - port: int = ( - next_free_port - if not reconfigure and default_port in ports_in_use - else default_port - ) - - print_client_port_select_dialog(client.display_name, port, ports_in_use) - - while True: - _type = "Reconfigure" if reconfigure else "Configure" - question = f"{_type} {client.display_name} for port" - port_input = get_number_input(question, min_value=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: - valid_ports = set(range(80, 7125)) - used_ports = set(map(int, ports_in_use)) - - return min(valid_ports - used_ports) - - -def set_listen_port(client: BaseWebClient, curr_port: int, new_port: int) -> None: - """ - Set the port the client should listen on in the NGINX config - :param curr_port: The current port the client listens on - :param new_port: The new port to set - :param client: The client to set the port for - :return: None - """ - config = NGINX_SITES_AVAILABLE.joinpath(client.name) - with open(config, "r") as f: - lines = f.readlines() - - for i, line in enumerate(lines): - if "listen" in line: - lines[i] = line.replace(str(curr_port), str(new_port)) - - with open(config, "w") as f: - f.writelines(lines) diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py deleted file mode 100644 index 8958995..0000000 --- a/kiauh/components/webui_client/fluidd_data.py +++ /dev/null @@ -1,58 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientConfigType, - WebClientType, -) -from core.backup_manager import BACKUP_ROOT_DIR -from core.constants import NGINX_SITES_AVAILABLE - - -@dataclass() -class FluiddConfigWeb(BaseWebClientConfig): - client_config: WebClientConfigType = WebClientConfigType.FLUIDD - name: str = client_config.value - display_name: str = name.title() - config_dir: Path = Path.home().joinpath("fluidd-config") - config_filename: str = "fluidd.cfg" - config_section: str = f"include {config_filename}" - backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups") - repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" - - -@dataclass() -class FluiddData(BaseWebClient): - BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases" - - client: WebClientType = WebClientType.FLUIDD - name: str = client.value - display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("fluidd") - config_file: Path = client_dir.joinpath("config.json") - backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups") - repo_path: str = "fluidd-core/fluidd" - nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") - nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log") - nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log") - client_config: BaseWebClientConfig = None - download_url: str | None = None - - def __post_init__(self): - from components.webui_client.client_utils import get_download_url - - self.client_config = FluiddConfigWeb() - self.download_url = get_download_url(self.BASE_DL_URL, self) diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py deleted file mode 100644 index 2323746..0000000 --- a/kiauh/components/webui_client/mainsail_data.py +++ /dev/null @@ -1,58 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientConfigType, - WebClientType, -) -from core.backup_manager import BACKUP_ROOT_DIR -from core.constants import NGINX_SITES_AVAILABLE - - -@dataclass() -class MainsailConfigWeb(BaseWebClientConfig): - client_config: WebClientConfigType = WebClientConfigType.MAINSAIL - name: str = client_config.value - display_name: str = name.title() - config_dir: Path = Path.home().joinpath("mainsail-config") - config_filename: str = "mainsail.cfg" - config_section: str = f"include {config_filename}" - backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups") - repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" - - -@dataclass() -class MainsailData(BaseWebClient): - BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases" - - client: WebClientType = WebClientType.MAINSAIL - name: str = WebClientType.MAINSAIL.value - display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("mainsail") - config_file: Path = client_dir.joinpath("config.json") - backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups") - repo_path: str = "mainsail-crew/mainsail" - nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") - nginx_access_log: Path = Path("/var/log/nginx/mainsail-access.log") - nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log") - client_config: BaseWebClientConfig = None - download_url: str | None = None - - def __post_init__(self): - from components.webui_client.client_utils import get_download_url - - self.client_config = MainsailConfigWeb() - self.download_url = get_download_url(self.BASE_DL_URL, self) diff --git a/kiauh/components/webui_client/menus/__init__.py b/kiauh/components/webui_client/menus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py deleted file mode 100644 index f9f67af..0000000 --- a/kiauh/components/webui_client/menus/client_install_menu.py +++ /dev/null @@ -1,105 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py deleted file mode 100644 index 82ce69d..0000000 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ /dev/null @@ -1,114 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 import client_remove -from components.webui_client.base_data import BaseWebClient -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class ClientRemoveMenu(BaseMenu): - def __init__( - self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = f"Remove {client.display_name}" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.client: BaseWebClient = client - self.remove_client: bool = False - self.remove_client_cfg: bool = False - self.backup_config_json: bool = False - self.select_state: bool = False - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_rm_client), - "2": Option(method=self.toggle_rm_client_config), - "3": Option(method=self.toggle_backup_config_json), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - client_name = self.client.display_name - client_config = self.client.client_config - client_config_name = client_config.display_name - - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.remove_client else unchecked - o2 = checked if self.remove_client_cfg else unchecked - o3 = checked if self.backup_config_json else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Enter a number and hit enter to select / deselect β•‘ - β•‘ the specific option for removal. β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ a) {sel_state:49} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) {o1} Remove {client_name:16} β•‘ - β•‘ 2) {o2} Remove {client_config_name:24} β•‘ - β•‘ 3) {o3} Backup config.json β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ C) Continue β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.remove_client = self.select_state - self.remove_client_cfg = self.select_state - self.backup_config_json = self.select_state - - def toggle_rm_client(self, **kwargs) -> None: - self.remove_client = not self.remove_client - - def toggle_rm_client_config(self, **kwargs) -> None: - self.remove_client_cfg = not self.remove_client_cfg - - def toggle_backup_config_json(self, **kwargs) -> None: - self.backup_config_json = not self.backup_config_json - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_client - and not self.remove_client_cfg - and not self.backup_config_json - ): - print(Color.apply("Nothing selected ...", Color.RED)) - return - - completion_msg = client_remove.run_client_removal( - client=self.client, - remove_client=self.remove_client, - remove_client_cfg=self.remove_client_cfg, - backup_config=self.backup_config_json, - ) - self.message_service.set_message(completion_msg) - - self.remove_client = False - self.remove_client_cfg = False - self.backup_config_json = False - self.select_state = False diff --git a/kiauh/core/__init__.py b/kiauh/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/backup_manager/__init__.py b/kiauh/core/backup_manager/__init__.py deleted file mode 100644 index 0e9e8e8..0000000 --- a/kiauh/core/backup_manager/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -BACKUP_ROOT_DIR = Path.home().joinpath("kiauh-backups") diff --git a/kiauh/core/backup_manager/backup_manager.py b/kiauh/core/backup_manager/backup_manager.py deleted file mode 100644 index 0497898..0000000 --- a/kiauh/core/backup_manager/backup_manager.py +++ /dev/null @@ -1,108 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -from pathlib import Path -from typing import List - -from core.backup_manager import BACKUP_ROOT_DIR -from core.logger import Logger -from utils.common import get_current_date - - -class BackupManagerException(Exception): - pass - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BackupManager: - def __init__(self, backup_root_dir: Path = BACKUP_ROOT_DIR): - self._backup_root_dir: Path = backup_root_dir - self._ignore_folders: List[str] = [] - - @property - def backup_root_dir(self) -> Path: - return self._backup_root_dir - - @backup_root_dir.setter - def backup_root_dir(self, value: Path): - self._backup_root_dir = value - - @property - def ignore_folders(self) -> List[str]: - return self._ignore_folders - - @ignore_folders.setter - def ignore_folders(self, value: List[str]): - self._ignore_folders = value - - def backup_file( - self, file: Path, target: Path | None = None, custom_filename=None - ) -> bool: - Logger.print_status(f"Creating backup of {file} ...") - - if not file.exists(): - Logger.print_info("File does not exist! Skipping ...") - return False - - target = self.backup_root_dir if target is None else target - - if Path(file).is_file(): - date = get_current_date().get("date") - time = get_current_date().get("time") - filename = f"{file.stem}-{date}-{time}{file.suffix}" - filename = custom_filename if custom_filename is not None else filename - try: - Path(target).mkdir(exist_ok=True) - shutil.copyfile(file, target.joinpath(filename)) - Logger.print_ok("Backup successful!") - return True - except OSError as e: - Logger.print_error(f"Unable to backup '{file}':\n{e}") - return False - else: - Logger.print_info(f"File '{file}' not found ...") - return False - - def backup_directory( - self, name: str, source: Path, target: Path | None = None - ) -> Path | None: - Logger.print_status(f"Creating backup of {name} in {target} ...") - - if source is None or not Path(source).exists(): - Logger.print_info("Source directory does not exist! Skipping ...") - return None - - target = self.backup_root_dir if target is None else target - try: - date = get_current_date().get("date") - time = get_current_date().get("time") - backup_target = target.joinpath(f"{name.lower()}-{date}-{time}") - shutil.copytree( - source, - backup_target, - ignore=self.ignore_folders_func, - ignore_dangling_symlinks=True, - ) - Logger.print_ok("Backup successful!") - - return backup_target - - except OSError as e: - Logger.print_error(f"Unable to backup directory '{source}':\n{e}") - raise BackupManagerException(f"Unable to backup directory '{source}':\n{e}") - - def ignore_folders_func(self, dirpath, filenames) -> List[str]: - return ( - [f for f in filenames if f in self._ignore_folders] - if self._ignore_folders - else [] - ) diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py deleted file mode 100644 index f9ccba2..0000000 --- a/kiauh/core/constants.py +++ /dev/null @@ -1,30 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 os -import pwd -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR - -# global dependencies -GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] - -# strings -INVALID_CHOICE = "Invalid choice. Please select a valid value." - -# current user -CURRENT_USER = pwd.getpwuid(os.getuid())[0] - -# dirs -SYSTEMD = Path("/etc/systemd/system") -PRINTER_DATA_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-data-backups") -NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") -NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") -NGINX_CONFD = Path("/etc/nginx/conf.d") diff --git a/kiauh/core/decorators.py b/kiauh/core/decorators.py deleted file mode 100644 index 46dc2e6..0000000 --- a/kiauh/core/decorators.py +++ /dev/null @@ -1,24 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 warnings -from typing import Callable - - -def deprecated(info: str = "", replaced_by: Callable | None = None) -> Callable: - def decorator(func) -> Callable: - def wrapper(*args, **kwargs): - msg = f"{info}{replaced_by.__name__ if replaced_by else ''}" - warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/kiauh/core/instance_manager/__init__.py b/kiauh/core/instance_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py deleted file mode 100644 index 06f2a27..0000000 --- a/kiauh/core/instance_manager/base_instance.py +++ /dev/null @@ -1,58 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from dataclasses import dataclass, field -from pathlib import Path -from typing import List - -from utils.fs_utils import get_data_dir - -SUFFIX_BLACKLIST: List[str] = ["None", "mcu", "obico", "bambu", "companion"] - - -@dataclass(repr=True) -class BaseInstance: - instance_type: type - suffix: str - log_file_name: str | None = None - data_dir: Path = field(init=False) - base_folders: List[Path] = field(init=False) - cfg_dir: Path = field(init=False) - log_dir: Path = field(init=False) - gcodes_dir: Path = field(init=False) - comms_dir: Path = field(init=False) - sysd_dir: Path = field(init=False) - is_legacy_instance: bool = field(init=False) - - def __post_init__(self): - self.data_dir = get_data_dir(self.instance_type, self.suffix) - # the following attributes require the data_dir to be set - self.cfg_dir = self.data_dir.joinpath("config") - self.log_dir = self.data_dir.joinpath("logs") - self.gcodes_dir = self.data_dir.joinpath("gcodes") - self.comms_dir = self.data_dir.joinpath("comms") - self.sysd_dir = self.data_dir.joinpath("systemd") - self.is_legacy_instance = self._set_is_legacy_instance() - self.base_folders = [ - self.data_dir, - self.cfg_dir, - self.log_dir, - self.gcodes_dir, - self.comms_dir, - self.sysd_dir, - ] - - def _set_is_legacy_instance(self) -> bool: - legacy_pattern = r"^(?!printer)(.+)_data" - match = re.search(legacy_pattern, self.data_dir.name) - - return True if (match and self.suffix != "") else False diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py deleted file mode 100644 index d41b66c..0000000 --- a/kiauh/core/instance_manager/instance_manager.py +++ /dev/null @@ -1,108 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from pathlib import Path -from subprocess import CalledProcessError -from typing import List - -from core.logger import Logger -from utils.instance_type import InstanceType -from utils.sys_utils import cmd_sysctl_service - - -class InstanceManager: - @staticmethod - def enable(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "enable") - except CalledProcessError as e: - Logger.print_error(f"Error enabling service {service_name}:") - Logger.print_error(f"{e}") - - @staticmethod - def disable(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "disable") - except CalledProcessError as e: - Logger.print_error(f"Error disabling {service_name}: {e}") - raise - - @staticmethod - def start(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "start") - except CalledProcessError as e: - Logger.print_error(f"Error starting {service_name}: {e}") - raise - - @staticmethod - def stop(instance: InstanceType) -> None: - name: str = instance.service_file_path.name - try: - cmd_sysctl_service(name, "stop") - except CalledProcessError as e: - Logger.print_error(f"Error stopping {name}: {e}") - raise - - @staticmethod - def restart(instance: InstanceType) -> None: - name: str = instance.service_file_path.name - try: - cmd_sysctl_service(name, "restart") - except CalledProcessError as e: - Logger.print_error(f"Error restarting {name}: {e}") - raise - - @staticmethod - def start_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.start(instance) - - @staticmethod - def stop_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.stop(instance) - - @staticmethod - def restart_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.restart(instance) - - @staticmethod - def remove(instance: InstanceType) -> None: - from utils.fs_utils import run_remove_routines - from utils.sys_utils import remove_system_service - - try: - # remove the service file - service_file_path: Path = instance.service_file_path - if service_file_path is not None: - remove_system_service(service_file_path.name) - - # then remove all the log files - if ( - not instance.log_file_name - or not instance.base.log_dir - or not instance.base.log_dir.exists() - ): - return - - files = instance.base.log_dir.iterdir() - logs = [f for f in files if f.name.startswith(instance.log_file_name)] - for log in logs: - Logger.print_status(f"Remove '{log}'") - run_remove_routines(log) - - except Exception as e: - Logger.print_error(f"Error removing service: {e}") - raise diff --git a/kiauh/core/logger.py b/kiauh/core/logger.py deleted file mode 100644 index cae4873..0000000 --- a/kiauh/core/logger.py +++ /dev/null @@ -1,169 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 enum import Enum -from typing import List - -from core.types.color import Color - - -class DialogType(Enum): - INFO = ("INFO", Color.WHITE) - SUCCESS = ("SUCCESS", Color.GREEN) - ATTENTION = ("ATTENTION", Color.YELLOW) - WARNING = ("WARNING", Color.YELLOW) - ERROR = ("ERROR", Color.RED) - CUSTOM = (None, None) - - -LINE_WIDTH = 53 - - -BORDER_TOP: str = "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" -BORDER_BOTTOM: str = "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" -BORDER_TITLE: str = "┠───────────────────────────────────────────────────────┨" -BORDER_LEFT: str = "┃" -BORDER_RIGHT: str = "┃" - - -class Logger: - @staticmethod - def print_info(msg, prefix=True, start="", end="\n") -> None: - message = f"[INFO] {msg}" if prefix else msg - Logger.__print(Color.WHITE, start, message, end) - - @staticmethod - def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None: - message = f"[OK] {msg}" if prefix else msg - Logger.__print(Color.GREEN, start, message, end) - - @staticmethod - def print_warn(msg, prefix=True, start="", end="\n") -> None: - message = f"[WARN] {msg}" if prefix else msg - Logger.__print(Color.YELLOW, start, message, end) - - @staticmethod - def print_error(msg, prefix=True, start="", end="\n") -> None: - message = f"[ERROR] {msg}" if prefix else msg - Logger.__print(Color.RED, start, message, end) - - @staticmethod - def print_status(msg, prefix=True, start="", end="\n") -> None: - message = f"\n###### {msg}" if prefix else msg - Logger.__print(Color.MAGENTA, start, message, end) - - @staticmethod - def __print(color: Color, start: str, message: str, end: str) -> None: - print(Color.apply(f"{start}{message}", color), end=end) - - @staticmethod - def print_dialog( - title: DialogType, - content: List[str], - center_content: bool = False, - custom_title: str | None = None, - custom_color: Color | None = None, - margin_top: int = 0, - margin_bottom: int = 0, - ) -> None: - """ - Prints a dialog with the given title and content. - Those dialogs should be used to display verbose messages to the user which - require simple interaction like confirmation or input. Do not use this for - navigating through the application. - - :param title: The type of the dialog. - :param content: The content of the dialog. - :param center_content: Whether to center the content or not. - :param custom_title: A custom title for the dialog. - :param custom_color: A custom color for the dialog. - :param margin_top: The number of empty lines to print before the dialog. - :param margin_bottom: The number of empty lines to print after the dialog. - """ - color = Logger._get_dialog_color(title, custom_color) - dialog_title = Logger._get_dialog_title(title, custom_title) - - if margin_top > 0: - print("\n" * margin_top, end="") - - print(Color.apply(BORDER_TOP, color)) - - if dialog_title: - print(Color.apply(f"┃ {dialog_title:^{LINE_WIDTH}} ┃", color)) - print(Color.apply(BORDER_TITLE, color)) - - if content: - print( - Logger.format_content( - content, - LINE_WIDTH, - color, - center_content, - ) - ) - - print(Color.apply(BORDER_BOTTOM, color)) - - if margin_bottom > 0: - print("\n" * margin_bottom, end="") - - @staticmethod - def _get_dialog_title( - title: DialogType, custom_title: str | None = None - ) -> str | None: - if title == DialogType.CUSTOM and custom_title: - return f"[ {custom_title} ]" - return f"[ {title.value[0]} ]" if title.value[0] else None - - @staticmethod - def _get_dialog_color( - title: DialogType, custom_color: Color | None = None - ) -> Color: - if title == DialogType.CUSTOM and custom_color: - return custom_color - - color: Color = title.value[1] if title.value[1] else Color.WHITE - - return color - - @staticmethod - def format_content( - content: List[str], - line_width: int, - color: Color = Color.WHITE, - center_content: bool = False, - border_left: str = "┃", - border_right: str = "┃", - ) -> str: - wrapper = textwrap.TextWrapper(line_width) - - lines = [] - for i, c in enumerate(content): - paragraph = wrapper.wrap(c) - lines.extend(paragraph) - - # add a full blank line if we have a double newline - # character unless we are at the end of the list - if c == "\n\n" and i < len(content) - 1: - lines.append(" " * line_width) - - if not center_content: - formatted_lines = [ - Color.apply(f"{border_left} {line:<{line_width}} {border_right}", color) - for line in lines - ] - else: - formatted_lines = [ - Color.apply(f"{border_left} {line:^{line_width}} {border_right}", color) - for line in lines - ] - - return "\n".join(formatted_lines) diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py deleted file mode 100644 index 568fcd3..0000000 --- a/kiauh/core/menus/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass -from enum import Enum -from typing import Any, Callable, Type - - -@dataclass -class Option: - """ - Represents a menu option. - :param method: Method that will be used to call the menu option - :param opt_index: Can be used to pass the user input to the menu option - :param opt_data: Can be used to pass any additional data to the menu option - """ - - def __repr__(self): - return f"Option(method={self.method.__name__}, opt_index={self.opt_index}, opt_data={self.opt_data})" - - method: Type[Callable] - opt_index: str = "" - opt_data: Any = None - - -class FooterType(Enum): - QUIT = "QUIT" - BACK = "BACK" - BACK_HELP = "BACK_HELP" - BLANK = "BLANK" diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py deleted file mode 100644 index 7311d39..0000000 --- a/kiauh/core/menus/advanced_menu.py +++ /dev/null @@ -1,106 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper import KLIPPER_DIR -from components.klipper.klipper import Klipper -from components.klipper.klipper_utils import install_input_shaper_deps -from components.klipper_firmware.menus.klipper_build_menu import ( - KlipperBuildFirmwareMenu, - KlipperKConfigMenu, -) -from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashMethodMenu, - KlipperSelectMcuConnectionMenu, -) -from components.moonraker import MOONRAKER_DIR -from components.moonraker.moonraker import Moonraker -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from procedures.system import change_system_hostname -from utils.git_utils import rollback_repository - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class AdvancedMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Advanced Menu" - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.build), - "2": Option(method=self.flash), - "3": Option(method=self.build_flash), - "4": Option(method=self.get_id), - "5": Option(method=self.input_shaper), - "6": Option(method=self.klipper_rollback), - "7": Option(method=self.moonraker_rollback), - "8": Option(method=self.change_hostname), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Klipper Firmware: β”‚ Repository Rollback: β•‘ - β•‘ 1) [Build] β”‚ 6) [Klipper] β•‘ - β•‘ 2) [Flash] β”‚ 7) [Moonraker] β•‘ - β•‘ 3) [Build + Flash] β”‚ β•‘ - β•‘ 4) [Get MCU ID] β”‚ System: β•‘ - β•‘ β”‚ 8) [Change hostname] β•‘ - β•‘ Extra Dependencies: β”‚ β•‘ - β•‘ 5) [Input Shaper] β”‚ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def klipper_rollback(self, **kwargs) -> None: - rollback_repository(KLIPPER_DIR, Klipper) - - def moonraker_rollback(self, **kwargs) -> None: - rollback_repository(MOONRAKER_DIR, Moonraker) - - def build(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() - - def flash(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperFlashMethodMenu(previous_menu=self.__class__).run() - - def build_flash(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() - KlipperFlashMethodMenu(previous_menu=self.__class__).run() - - def get_id(self, **kwargs) -> None: - KlipperSelectMcuConnectionMenu( - previous_menu=self.__class__, - standalone=True, - ).run() - - def change_hostname(self, **kwargs) -> None: - change_system_hostname() - - def input_shaper(self, **kwargs) -> None: - install_input_shaper_deps() diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py deleted file mode 100644 index a9d7500..0000000 --- a/kiauh/core/menus/backup_menu.py +++ /dev/null @@ -1,107 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper.klipper_utils import backup_klipper_dir -from components.klipperscreen.klipperscreen import backup_klipperscreen_dir -from components.moonraker.utils.utils import ( - backup_moonraker_db_dir, - backup_moonraker_dir, -) -from components.webui_client.client_utils import ( - backup_client_config_data, - backup_client_data, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from utils.common import backup_printer_config_dir - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BackupMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Backup Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.backup_klipper), - "2": Option(method=self.backup_moonraker), - "3": Option(method=self.backup_printer_config), - "4": Option(method=self.backup_moonraker_db), - "5": Option(method=self.backup_mainsail), - "6": Option(method=self.backup_fluidd), - "7": Option(method=self.backup_mainsail_config), - "8": Option(method=self.backup_fluidd_config), - "9": Option(method=self.backup_klipperscreen), - } - - def print_menu(self) -> None: - line1 = Color.apply( - "INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW - ) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {line1:^62} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Klipper & Moonraker API: β”‚ Client-Config: β•‘ - β•‘ 1) [Klipper] β”‚ 7) [Mainsail-Config] β•‘ - β•‘ 2) [Moonraker] β”‚ 8) [Fluidd-Config] β•‘ - β•‘ 3) [Config Folder] β”‚ β•‘ - β•‘ 4) [Moonraker Database] β”‚ Touchscreen GUI: β•‘ - β•‘ β”‚ 9) [KlipperScreen] β•‘ - β•‘ Webinterface: β”‚ β•‘ - β•‘ 5) [Mainsail] β”‚ β•‘ - β•‘ 6) [Fluidd] β”‚ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def backup_klipper(self, **kwargs) -> None: - backup_klipper_dir() - - def backup_moonraker(self, **kwargs) -> None: - backup_moonraker_dir() - - def backup_printer_config(self, **kwargs) -> None: - backup_printer_config_dir() - - def backup_moonraker_db(self, **kwargs) -> None: - backup_moonraker_db_dir() - - def backup_mainsail(self, **kwargs) -> None: - backup_client_data(MainsailData()) - - def backup_fluidd(self, **kwargs) -> None: - backup_client_data(FluiddData()) - - def backup_mainsail_config(self, **kwargs) -> None: - backup_client_config_data(MainsailData()) - - def backup_fluidd_config(self, **kwargs) -> None: - backup_client_config_data(FluiddData()) - - def backup_klipperscreen(self, **kwargs) -> None: - backup_klipperscreen_dir() diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py deleted file mode 100644 index 1e8796c..0000000 --- a/kiauh/core/menus/base_menu.py +++ /dev/null @@ -1,239 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 subprocess -import sys -import textwrap -import traceback -from abc import abstractmethod -from enum import Enum -from typing import Dict, Type - -from core.logger import Logger -from core.menus import FooterType, Option -from core.services.message_service import MessageService -from core.spinner import Spinner -from core.types.color import Color -from utils.input_utils import get_selection_input - - -def clear() -> None: - subprocess.call("clear -x", shell=True) - - -def print_header() -> None: - line1 = " [ KIAUH ] " - line2 = "Klipper Installation And Update Helper" - line3 = "" - color = Color.CYAN - count = 62 - len(str(color)) - len(str(Color.RST)) - header = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - β•‘ {Color.apply(f"{line1:~^{count}}", color)} β•‘ - β•‘ {Color.apply(f"{line2:^{count}}", color)} β•‘ - β•‘ {Color.apply(f"{line3:~^{count}}", color)} β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - """ - )[1:] - print(header, end="") - - -def print_quit_footer() -> None: - text = "Q) Quit" - color = Color.RED - count = 62 - len(str(color)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - β•‘ {color}{text:^{count}}{Color.RST} β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - """ - )[1:] - print(footer, end="") - - -def print_back_footer() -> None: - text = "B) Β« Back" - color = Color.GREEN - count = 62 - len(str(color)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - β•‘ {color}{text:^{count}}{Color.RST} β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - """ - )[1:] - print(footer, end="") - - -def print_back_help_footer() -> None: - text1 = "B) Β« Back" - text2 = "H) Help [?]" - color1 = Color.GREEN - color2 = Color.YELLOW - count = 34 - len(str(color1)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - β•‘ {color1}{text1:^{count}}{Color.RST} β”‚ {color2}{text2:^{count}}{Color.RST} β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•§β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - """ - )[1:] - print(footer, end="") - - -def print_blank_footer() -> None: - print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•") - - -class MenuTitleStyle(Enum): - PLAIN = "plain" - STYLED = "styled" - - -class PostInitCaller(type): - def __call__(cls, *args, **kwargs): - obj = type.__call__(cls, *args, **kwargs) - obj.__post_init__() - return obj - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BaseMenu(metaclass=PostInitCaller): - options: Dict[str, Option] = {} - options_offset: int = 0 - default_option: Option = None - input_label_txt: str = "Perform action" - header: bool = False - - loading_msg: str = "" - spinner: Spinner | None = None - - title: str = "" - title_style: MenuTitleStyle = MenuTitleStyle.STYLED - title_color: Color = Color.WHITE - - previous_menu: Type[BaseMenu] | None = None - help_menu: Type[BaseMenu] | None = None - footer_type: FooterType = FooterType.BACK - - message_service = MessageService() - - def __init__(self, **kwargs) -> None: - if type(self) is BaseMenu: - raise NotImplementedError("BaseMenu cannot be instantiated directly.") - - def __post_init__(self) -> None: - self.set_previous_menu(self.previous_menu) - self.set_options() - - # conditionally add options based on footer type - if self.footer_type is FooterType.QUIT: - self.options["q"] = Option(method=self.__exit) - if self.footer_type is FooterType.BACK: - self.options["b"] = Option(method=self.__go_back) - if self.footer_type is FooterType.BACK_HELP: - self.options["b"] = Option(method=self.__go_back) - self.options["h"] = Option(method=self.__go_to_help) - # if defined, add the default option to the options dict - if self.default_option is not None: - self.options[""] = self.default_option - - def __go_back(self, **kwargs) -> None: - if self.previous_menu is None: - return - self.previous_menu().run() - - def __go_to_help(self, **kwargs) -> None: - if self.help_menu is None: - return - self.help_menu(previous_menu=self.__class__).run() - - def __exit(self, **kwargs) -> None: - Logger.print_ok("###### Happy printing!", False) - sys.exit(0) - - @abstractmethod - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - raise NotImplementedError - - @abstractmethod - def set_options(self) -> None: - raise NotImplementedError - - @abstractmethod - def print_menu(self) -> None: - raise NotImplementedError - - def is_loading(self, state: bool) -> None: - if not self.spinner and state: - self.spinner = Spinner(self.loading_msg) - self.spinner.start() - else: - self.spinner.stop() - self.spinner = None - - def __print_menu_title(self) -> None: - count = 62 - len(str(self.title_color)) - len(str(Color.RST)) - menu_title = "╔═══════════════════════════════════════════════════════╗\n" - if self.title: - title = ( - f" [ {self.title} ] " - if self.title_style == MenuTitleStyle.STYLED - else self.title - ) - line = ( - f"{title:~^{count}}" - if self.title_style == MenuTitleStyle.STYLED - else f"{title:^{count}}" - ) - menu_title += f"β•‘ {Color.apply(line, self.title_color)} β•‘\n" - print(menu_title, end="") - - def __print_footer(self) -> None: - if self.footer_type is FooterType.QUIT: - print_quit_footer() - elif self.footer_type is FooterType.BACK: - print_back_footer() - elif self.footer_type is FooterType.BACK_HELP: - print_back_help_footer() - elif self.footer_type is FooterType.BLANK: - print_blank_footer() - else: - raise NotImplementedError("FooterType not correctly implemented!") - - def __display_menu(self) -> None: - self.message_service.display_message() - - if self.header: - print_header() - - self.__print_menu_title() - self.print_menu() - self.__print_footer() - - def run(self) -> None: - """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" - try: - self.__display_menu() - option = get_selection_input(self.input_label_txt, self.options) - selected_option: Option = self.options.get(option) - - selected_option.method( - opt_index=selected_option.opt_index, - opt_data=selected_option.opt_data, - ) - - self.run() - - except Exception as e: - Logger.print_error( - f"An unexpected error occured:\n{e}\n{traceback.format_exc()}" - ) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py deleted file mode 100644 index 4fccfb9..0000000 --- a/kiauh/core/menus/install_menu.py +++ /dev/null @@ -1,109 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.crowsnest.crowsnest import install_crowsnest -from components.klipper.services.klipper_setup_service import KlipperSetupService -from components.klipperscreen.klipperscreen import install_klipperscreen -from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from components.webui_client.client_config.client_config_setup import ( - install_client_config, -) -from components.webui_client.client_setup import install_client -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from components.webui_client.menus.client_install_menu import ClientInstallMenu -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class InstallMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Installation Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.klsvc = KlipperSetupService() - self.mrsvc = MoonrakerSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.install_klipper), - "2": Option(method=self.install_moonraker), - "3": Option(method=self.install_mainsail), - "4": Option(method=self.install_fluidd), - "5": Option(method=self.install_mainsail_config), - "6": Option(method=self.install_fluidd_config), - "7": Option(method=self.install_klipperscreen), - "8": Option(method=self.install_crowsnest), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Firmware & API: β”‚ Touchscreen GUI: β•‘ - β•‘ 1) [Klipper] β”‚ 7) [KlipperScreen] β•‘ - β•‘ 2) [Moonraker] β”‚ β•‘ - β•‘ β”‚ Webcam Streamer: β•‘ - β•‘ Webinterface: β”‚ 8) [Crowsnest] β•‘ - β•‘ 3) [Mainsail] β”‚ β•‘ - β•‘ 4) [Fluidd] β”‚ β•‘ - β•‘ β”‚ β•‘ - β•‘ Client-Config: β”‚ β•‘ - β•‘ 5) [Mainsail-Config] β”‚ β•‘ - β•‘ 6) [Fluidd-Config] β”‚ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def install_klipper(self, **kwargs) -> None: - self.klsvc.install() - - def install_moonraker(self, **kwargs) -> None: - self.mrsvc.install() - - def install_mainsail(self, **kwargs) -> None: - 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: - install_client_config(MainsailData()) - - def install_fluidd(self, **kwargs) -> None: - 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: - install_client_config(FluiddData()) - - def install_klipperscreen(self, **kwargs) -> None: - install_klipperscreen() - - def install_crowsnest(self, **kwargs) -> None: - install_crowsnest() diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py deleted file mode 100644 index 7fb55ff..0000000 --- a/kiauh/core/menus/main_menu.py +++ /dev/null @@ -1,179 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 sys -import textwrap -from typing import Callable, Type - -from components.crowsnest.crowsnest import get_crowsnest_status -from components.klipper.klipper_utils import get_klipper_status -from components.klipperscreen.klipperscreen import get_klipperscreen_status -from components.log_uploads.menus.log_upload_menu import LogUploadMenu -from components.moonraker.utils.utils import get_moonraker_status -from components.webui_client.client_utils import ( - get_client_status, - get_current_client_config, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.logger import Logger -from core.menus import FooterType -from core.menus.advanced_menu import AdvancedMenu -from core.menus.backup_menu import BackupMenu -from core.menus.base_menu import BaseMenu, Option -from core.menus.install_menu import InstallMenu -from core.menus.remove_menu import RemoveMenu -from core.menus.settings_menu import SettingsMenu -from core.menus.update_menu import UpdateMenu -from core.types.color import Color -from core.types.component_status import ComponentStatus, StatusMap, StatusText -from extensions.extensions_menu import ExtensionsMenu -from utils.common import get_kiauh_version, trunc_string - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class MainMenu(BaseMenu): - def __init__(self) -> None: - super().__init__() - - self.header: bool = True - self.title = "Main Menu" - self.title_color = Color.CYAN - self.footer_type: FooterType = FooterType.QUIT - - self.version = "" - self.kl_status, self.kl_owner, self.kl_repo = "", "", "" - self.mr_status, self.mr_owner, self.mr_repo = "", "", "" - self.ms_status, self.fl_status, self.ks_status = "", "", "" - self.cn_status, self.cc_status = "", "" - self._init_status() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - """MainMenu does not have a previous menu""" - pass - - def set_options(self) -> None: - self.options = { - "0": Option(method=self.log_upload_menu), - "1": Option(method=self.install_menu), - "2": Option(method=self.update_menu), - "3": Option(method=self.remove_menu), - "4": Option(method=self.advanced_menu), - "5": Option(method=self.backup_menu), - "e": Option(method=self.extension_menu), - "s": Option(method=self.settings_menu), - } - - def _init_status(self) -> None: - status_vars = ["kl", "mr", "ms", "fl", "ks", "cn"] - for var in status_vars: - setattr( - self, - f"{var}_status", - Color.apply("Not installed", Color.RED), - ) - - def _fetch_status(self) -> None: - self.version = get_kiauh_version() - self._get_component_status("kl", get_klipper_status) - self._get_component_status("mr", get_moonraker_status) - self._get_component_status("ms", get_client_status, MainsailData()) - self._get_component_status("fl", get_client_status, FluiddData()) - self._get_component_status("ks", get_klipperscreen_status) - self._get_component_status("cn", get_crowsnest_status) - self.cc_status = get_current_client_config() - - def _get_component_status(self, name: str, status_fn: Callable, *args) -> None: - status_data: ComponentStatus = status_fn(*args) - code: int = status_data.status - status: StatusText = StatusMap[code] - owner: str = trunc_string(status_data.owner, 23) - repo: str = trunc_string(status_data.repo, 23) - instance_count: int = status_data.instances - - count_txt: str = "" - if instance_count > 0 and code == 2: - count_txt = f": {instance_count}" - - setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) - setattr(self, f"{name}_owner", Color.apply(owner, Color.CYAN)) - setattr(self, f"{name}_repo", Color.apply(repo, Color.CYAN)) - - def _format_by_code(self, code: int, status: str, count: str) -> str: - color = Color.RED - if code == 0: - color = Color.RED - elif code == 1: - color = Color.YELLOW - elif code == 2: - color = Color.GREEN - - return Color.apply(f"{status}{count}", color) - - def print_menu(self) -> None: - self._fetch_status() - - footer1 = Color.apply(self.version, Color.CYAN) - link = Color.apply("https://git.io/JnmlX", Color.MAGENTA) - footer2 = f"Changelog: {link}" - pad1 = 32 - pad2 = 26 - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 0) [Log-Upload] β”‚ Klipper: {self.kl_status:<{pad1}} β•‘ - β•‘ β”‚ Owner: {self.kl_owner:<{pad1}} β•‘ - β•‘ 1) [Install] β”‚ Repo: {self.kl_repo:<{pad1}} β•‘ - β•‘ 2) [Update] β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 3) [Remove] β”‚ Moonraker: {self.mr_status:<{pad1}} β•‘ - β•‘ 4) [Advanced] β”‚ Owner: {self.mr_owner:<{pad1}} β•‘ - β•‘ 5) [Backup] β”‚ Repo: {self.mr_repo:<{pad1}} β•‘ - β•‘ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ S) [Settings] β”‚ Mainsail: {self.ms_status:<{pad2}} β•‘ - β•‘ β”‚ Fluidd: {self.fl_status:<{pad2}} β•‘ - β•‘ Community: β”‚ Client-Config: {self.cc_status:<{pad2}} β•‘ - β•‘ E) [Extensions] β”‚ β•‘ - β•‘ β”‚ KlipperScreen: {self.ks_status:<{pad2}} β•‘ - β•‘ β”‚ Crowsnest: {self.cn_status:<{pad2}} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {footer1:^25} β”‚ {footer2:^43} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def exit(self, **kwargs) -> None: - Logger.print_ok("###### Happy printing!", False) - sys.exit(0) - - def log_upload_menu(self, **kwargs) -> None: - LogUploadMenu().run() - - def install_menu(self, **kwargs) -> None: - InstallMenu(previous_menu=self.__class__).run() - - def update_menu(self, **kwargs) -> None: - UpdateMenu(previous_menu=self.__class__).run() - - def remove_menu(self, **kwargs) -> None: - RemoveMenu(previous_menu=self.__class__).run() - - def advanced_menu(self, **kwargs) -> None: - AdvancedMenu(previous_menu=self.__class__).run() - - def backup_menu(self, **kwargs) -> None: - BackupMenu(previous_menu=self.__class__).run() - - def settings_menu(self, **kwargs) -> None: - SettingsMenu(previous_menu=self.__class__).run() - - def extension_menu(self, **kwargs) -> None: - ExtensionsMenu(previous_menu=self.__class__).run() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py deleted file mode 100644 index 710f359..0000000 --- a/kiauh/core/menus/remove_menu.py +++ /dev/null @@ -1,86 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.crowsnest.crowsnest import remove_crowsnest -from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from components.klipperscreen.klipperscreen import remove_klipperscreen -from components.moonraker.menus.moonraker_remove_menu import ( - MoonrakerRemoveMenu, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from components.webui_client.menus.client_remove_menu import ClientRemoveMenu -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class RemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Remove Menu" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.remove_klipper), - "2": Option(method=self.remove_moonraker), - "3": Option(method=self.remove_mainsail), - "4": Option(method=self.remove_fluidd), - "5": Option(method=self.remove_klipperscreen), - "6": Option(method=self.remove_crowsnest), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ INFO: Configurations and/or any backups will be kept! β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Firmware & API: β”‚ Touchscreen GUI: β•‘ - β•‘ 1) [Klipper] β”‚ 5) [KlipperScreen] β•‘ - β•‘ 2) [Moonraker] β”‚ β•‘ - β•‘ β”‚ Webcam Streamer: β•‘ - β•‘ Klipper Webinterface: β”‚ 6) [Crowsnest] β•‘ - β•‘ 3) [Mainsail] β”‚ β•‘ - β•‘ 4) [Fluidd] β”‚ β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def remove_klipper(self, **kwargs) -> None: - KlipperRemoveMenu(previous_menu=self.__class__).run() - - def remove_moonraker(self, **kwargs) -> None: - MoonrakerRemoveMenu(previous_menu=self.__class__).run() - - def remove_mainsail(self, **kwargs) -> None: - ClientRemoveMenu(previous_menu=self.__class__, client=MainsailData()).run() - - def remove_fluidd(self, **kwargs) -> None: - ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() - - def remove_klipperscreen(self, **kwargs) -> None: - remove_klipperscreen() - - def remove_crowsnest(self, **kwargs) -> None: - remove_crowsnest() diff --git a/kiauh/core/menus/repo_select_menu.py b/kiauh/core/menus/repo_select_menu.py deleted file mode 100644 index afe0e6c..0000000 --- a/kiauh/core/menus/repo_select_menu.py +++ /dev/null @@ -1,79 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import List, Literal, Type - -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.settings.kiauh_settings import KiauhSettings, Repository -from core.types.color import Color -from procedures.switch_repo import run_switch_repo_routine - - -class RepoSelectMenu(BaseMenu): - def __init__( - self, - name: Literal["klipper", "moonraker"], - repos: List[Repository], - previous_menu: Type[BaseMenu] | None = None, - ) -> None: - super().__init__() - self.title_color = Color.CYAN - self.previous_menu = previous_menu - self.settings = KiauhSettings() - self.input_label_txt = "Select repository" - self.name = name - self.repos = repos - - if self.name == "klipper": - self.title = "Klipper Repository Selection Menu" - - elif self.name == "moonraker": - self.title = "Moonraker Repository Selection Menu" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.settings_menu import SettingsMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else SettingsMenu - ) - - def set_options(self) -> None: - self.options = {} - - if not self.repos: - return - - for idx, repo in enumerate(self.repos, start=1): - self.options[str(idx)] = Option( - method=self.select_repository, opt_data=repo - ) - - def print_menu(self) -> None: - menu = "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - menu += "β•‘ Available Repositories: β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - for idx, repo in enumerate(self.repos, start=1): - url = f"● Repo: {repo.url.replace('.git', '')}" - branch = f"β””β–Ί Branch: {repo.branch}" - menu += f"β•‘ {idx}) {Color.apply(url, Color.CYAN):<59} β•‘\n" - menu += f"β•‘ {Color.apply(branch, Color.CYAN):<59} β•‘\n" - - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - print(menu, end="") - - def select_repository(self, **kwargs) -> None: - repo: Repository = kwargs.get("opt_data") - Logger.print_status( - f"Switching to {self.name.capitalize()}'s new source repository ..." - ) - run_switch_repo_routine(self.name, repo.url, repo.branch) diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py deleted file mode 100644 index e4559bb..0000000 --- a/kiauh/core/menus/settings_menu.py +++ /dev/null @@ -1,148 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper.klipper_utils import get_klipper_status -from components.moonraker.utils.utils import get_moonraker_status -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.menus.repo_select_menu import RepoSelectMenu -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from core.types.component_status import ComponentStatus - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class SettingsMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Settings Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.mainsail_unstable: bool | None = None - self.fluidd_unstable: bool | None = None - self.auto_backups_enabled: bool | None = None - - na: str = "Not available!" - self.kl_repo_url: str = Color.apply(na, Color.RED) - self.kl_branch: str = Color.apply(na, Color.RED) - self.mr_repo_url: str = Color.apply(na, Color.RED) - self.mr_branch: str = Color.apply(na, Color.RED) - - self._load_settings() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.switch_klipper_repo), - "2": Option(method=self.switch_moonraker_repo), - "3": Option(method=self.toggle_mainsail_release), - "4": Option(method=self.toggle_fluidd_release), - "5": Option(method=self.toggle_backup_before_update), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.GREEN)}]" - unchecked = "[ ]" - - o1 = checked if self.mainsail_unstable else unchecked - o2 = checked if self.fluidd_unstable else unchecked - o3 = checked if self.auto_backups_enabled else unchecked - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) Switch Klipper source repository β•‘ - β•‘ ● Current repository: β•‘ - β•‘ β””β–Ί Repo: {self.kl_repo_url:50} β•‘ - β•‘ β””β–Ί Branch: {self.kl_branch:48} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 2) Switch Moonraker source repository β•‘ - β•‘ ● Current repository: β•‘ - β•‘ β””β–Ί Repo: {self.mr_repo_url:50} β•‘ - β•‘ β””β–Ί Branch: {self.mr_branch:48} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Install unstable releases: β•‘ - β•‘ 3) {o1} Mainsail β•‘ - β•‘ 4) {o2} Fluidd β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ Auto-Backup: β•‘ - β•‘ 5) {o3} Backup before update β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def _load_settings(self) -> None: - self.settings = KiauhSettings() - self.auto_backups_enabled = self.settings.kiauh.backup_before_update - self.mainsail_unstable = self.settings.mainsail.unstable_releases - self.fluidd_unstable = self.settings.fluidd.unstable_releases - - klipper_status: ComponentStatus = get_klipper_status() - moonraker_status: ComponentStatus = get_moonraker_status() - - def trim_repo_url(repo: str) -> str: - return repo.replace(".git", "").replace("https://", "").replace("git@", "") - - if not klipper_status.repo == "-": - url = trim_repo_url(klipper_status.repo_url) - self.kl_repo_url = Color.apply(url, Color.CYAN) - self.kl_branch = Color.apply(klipper_status.branch, Color.CYAN) - if not moonraker_status.repo == "-": - url = trim_repo_url(moonraker_status.repo_url) - self.mr_repo_url = Color.apply(url, Color.CYAN) - self.mr_branch = Color.apply(moonraker_status.branch, Color.CYAN) - - def _warn_no_repos(self, name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [f"No {name} repositories configured in kiauh.cfg!"], - center_content=True, - ) - - def switch_klipper_repo(self, **kwargs) -> None: - name = "Klipper" - repos = self.settings.klipper.repositories - if not repos: - self._warn_no_repos(name) - return - RepoSelectMenu(name.lower(), repos=repos, previous_menu=self.__class__).run() - - def switch_moonraker_repo(self, **kwargs) -> None: - name = "Moonraker" - repos = self.settings.moonraker.repositories - if not repos: - self._warn_no_repos(name) - return - RepoSelectMenu(name.lower(), repos=repos, previous_menu=self.__class__).run() - - def toggle_mainsail_release(self, **kwargs) -> None: - self.mainsail_unstable = not self.mainsail_unstable - self.settings.mainsail.unstable_releases = self.mainsail_unstable - self.settings.save() - - def toggle_fluidd_release(self, **kwargs) -> None: - self.fluidd_unstable = not self.fluidd_unstable - self.settings.fluidd.unstable_releases = self.fluidd_unstable - self.settings.save() - - def toggle_backup_before_update(self, **kwargs) -> None: - self.auto_backups_enabled = not self.auto_backups_enabled - self.settings.kiauh.backup_before_update = self.auto_backups_enabled - self.settings.save() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py deleted file mode 100644 index d1050f3..0000000 --- a/kiauh/core/menus/update_menu.py +++ /dev/null @@ -1,327 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 Callable, List, Type - -from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest -from components.klipper.klipper_utils import ( - get_klipper_status, -) -from components.klipper.services.klipper_setup_service import KlipperSetupService -from components.klipperscreen.klipperscreen import ( - get_klipperscreen_status, - update_klipperscreen, -) -from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from components.moonraker.utils.utils import get_moonraker_status -from components.webui_client.client_config.client_config_setup import ( - update_client_config, -) -from components.webui_client.client_setup import update_client -from components.webui_client.client_utils import ( - get_client_config_status, - get_client_status, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from core.types.component_status import ComponentStatus -from utils.input_utils import get_confirm -from utils.sys_utils import ( - get_upgradable_packages, - update_system_package_lists, - upgrade_system_packages, -) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class UpdateMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.loading_msg = "Loading update menu, please wait" - self.is_loading(True) - - self.title = "Update Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.packages: List[str] = [] - self.package_count: int = 0 - - self.klipper_local = self.klipper_remote = "" - self.moonraker_local = self.moonraker_remote = "" - self.mainsail_local = self.mainsail_remote = "" - self.mainsail_config_local = self.mainsail_config_remote = "" - self.fluidd_local = self.fluidd_remote = "" - self.fluidd_config_local = self.fluidd_config_remote = "" - self.klipperscreen_local = self.klipperscreen_remote = "" - self.crowsnest_local = self.crowsnest_remote = "" - - self.mainsail_data = MainsailData() - self.fluidd_data = FluiddData() - self.status_data = { - "klipper": { - "display_name": "Klipper", - "installed": False, - "local": None, - "remote": None, - }, - "moonraker": { - "display_name": "Moonraker", - "installed": False, - "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: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "a": Option(self.update_all), - "1": Option(self.update_klipper), - "2": Option(self.update_moonraker), - "3": Option(self.update_mainsail), - "4": Option(self.update_fluidd), - "5": Option(self.update_mainsail_config), - "6": Option(self.update_fluidd_config), - "7": Option(self.update_klipperscreen), - "8": Option(self.update_crowsnest), - "9": Option(self.upgrade_system_packages), - } - - def print_menu(self) -> None: - sysupgrades: str = "No upgrades available." - padding = 29 - if self.package_count > 0: - sysupgrades = Color.apply( - f"{self.package_count} upgrades available!", Color.GREEN - ) - padding = 38 - - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ a) Update all β”‚ β”‚ β•‘ - β•‘ β”‚ Current: β”‚ Latest: β•‘ - β•‘ Klipper & API: β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) Klipper β”‚ {self.klipper_local:<22} β”‚ {self.klipper_remote:<22} β•‘ - β•‘ 2) Moonraker β”‚ {self.moonraker_local:<22} β”‚ {self.moonraker_remote:<22} β•‘ - β•‘ β”‚ β”‚ β•‘ - β•‘ Webinterface: β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 3) Mainsail β”‚ {self.mainsail_local:<22} β”‚ {self.mainsail_remote:<22} β•‘ - β•‘ 4) Fluidd β”‚ {self.fluidd_local:<22} β”‚ {self.fluidd_remote:<22} β•‘ - β•‘ β”‚ β”‚ β•‘ - β•‘ Client-Config: β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 5) Mainsail-Config β”‚ {self.mainsail_config_local:<22} β”‚ {self.mainsail_config_remote:<22} β•‘ - β•‘ 6) Fluidd-Config β”‚ {self.fluidd_config_local:<22} β”‚ {self.fluidd_config_remote:<22} β•‘ - β•‘ β”‚ β”‚ β•‘ - β•‘ Other: β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 7) KlipperScreen β”‚ {self.klipperscreen_local:<22} β”‚ {self.klipperscreen_remote:<22} β•‘ - β•‘ 8) Crowsnest β”‚ {self.crowsnest_local:<22} β”‚ {self.crowsnest_remote:<22} β•‘ - β•‘ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 9) System β”‚ {sysupgrades:^{padding}} β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - print(menu, end="") - - def update_all(self, **kwargs) -> None: - 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: - klsvc = KlipperSetupService() - self._run_update_routine("klipper", klsvc.update) - - def update_moonraker(self, **kwargs) -> None: - mrsvc = MoonrakerSetupService() - self._run_update_routine("moonraker", mrsvc.update) - - def update_mainsail(self, **kwargs) -> None: - self._run_update_routine( - "mainsail", - update_client, - self.mainsail_data, - ) - - def update_mainsail_config(self, **kwargs) -> None: - self._run_update_routine( - "mainsail_config", - update_client_config, - self.mainsail_data, - ) - - def update_fluidd(self, **kwargs) -> None: - self._run_update_routine( - "fluidd", - update_client, - self.fluidd_data, - ) - - def update_fluidd_config(self, **kwargs) -> None: - self._run_update_routine( - "fluidd_config", - update_client_config, - self.fluidd_data, - ) - - def update_klipperscreen(self, **kwargs) -> None: - self._run_update_routine("klipperscreen", update_klipperscreen) - - def update_crowsnest(self, **kwargs) -> None: - self._run_update_routine("crowsnest", update_crowsnest) - - def upgrade_system_packages(self, **kwargs) -> None: - self._run_system_updates() - - def _fetch_update_status(self) -> None: - self._set_status_data("klipper", get_klipper_status) - self._set_status_data("moonraker", get_moonraker_status) - self._set_status_data("mainsail", get_client_status, self.mainsail_data, True) - self._set_status_data( - "mainsail_config", get_client_config_status, self.mainsail_data - ) - self._set_status_data("fluidd", get_client_status, self.fluidd_data, True) - self._set_status_data( - "fluidd_config", get_client_config_status, self.fluidd_data - ) - self._set_status_data("klipperscreen", get_klipperscreen_status) - self._set_status_data("crowsnest", get_crowsnest_status) - - update_system_package_lists(silent=True) - self.packages = get_upgradable_packages() - self.package_count = len(self.packages) - - def _format_local_status(self, local_version, remote_version) -> str: - color = Color.RED - if not local_version: - color = Color.RED - elif local_version == remote_version: - color = Color.GREEN - elif local_version != remote_version: - color = Color.YELLOW - - return Color.apply(local_version or "-", color) - - def _set_status_data(self, name: str, status_fn: Callable, *args) -> None: - comp_status: ComponentStatus = status_fn(*args) - - self.status_data[name]["installed"] = True if comp_status.status == 2 else False - self.status_data[name]["local"] = comp_status.local - self.status_data[name]["remote"] = comp_status.remote - - self._set_status_string(name) - - def _set_status_string(self, name: str) -> None: - local_status = self.status_data[name].get("local", None) - remote_status = self.status_data[name].get("remote", None) - - color = Color.GREEN if remote_status else Color.RED - local_txt = self._format_local_status(local_status, remote_status) - remote_txt = Color.apply(remote_status or "-", color) - - setattr(self, f"{name}_local", local_txt) - setattr(self, f"{name}_remote", remote_txt) - - def _check_is_installed(self, name: str) -> bool: - return self.status_data[name]["installed"] - - def _is_update_available(self, name: str) -> bool: - 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: - if not self.packages: - Logger.print_info("No system upgrades available!") - return - - try: - pkgs: str = ", ".join(self.packages) - Logger.print_dialog( - DialogType.CUSTOM, - ["The following packages will be upgraded:", "\n\n", pkgs], - custom_title="UPGRADABLE SYSTEM UPDATES", - ) - if not get_confirm("Continue?"): - return - Logger.print_status("Upgrading system packages ...") - upgrade_system_packages(self.packages) - except Exception as e: - Logger.print_error(f"Error upgrading system packages:\n{e}") - raise diff --git a/kiauh/core/services/__init__.py b/kiauh/core/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/services/message_service.py b/kiauh/core/services/message_service.py deleted file mode 100644 index bf7edda..0000000 --- a/kiauh/core/services/message_service.py +++ /dev/null @@ -1,61 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import List - -from core.logger import DialogType, Logger -from core.types.color import Color - - -@dataclass() -class Message: - title: str = field(default="") - text: List[str] = field(default_factory=list) - color: Color = field(default=Color.WHITE) - centered: bool = field(default=False) - - -class MessageService: - __cls_instance = None - __message: Message | None - - def __new__(cls) -> "MessageService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MessageService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__message = None - - def set_message(self, message: Message) -> None: - self.__message = message - - def display_message(self) -> None: - if self.__message is None: - return - - Logger.print_dialog( - title=DialogType.CUSTOM, - content=self.__message.text, - custom_title=self.__message.title, - custom_color=self.__message.color, - center_content=self.__message.centered, - ) - - self.__clear_message() - - def __clear_message(self) -> None: - self.__message = None diff --git a/kiauh/core/settings/__init__.py b/kiauh/core/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py deleted file mode 100644 index 6c61a68..0000000 --- a/kiauh/core/settings/kiauh_settings.py +++ /dev/null @@ -1,388 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any, List - -from core.backup_manager.backup_manager import BackupManager -from core.logger import DialogType, Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - NoOptionError, - NoSectionError, - SimpleConfigParser, -) -from utils.input_utils import get_confirm -from utils.sys_utils import kill - -from kiauh import PROJECT_ROOT - -DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg") -CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") - - -class NoValueError(Exception): - """Raised when a required value is not defined for an option""" - - def __init__(self, section: str, option: str): - msg = f"Missing value for option '{option}' in section '{section}'" - super().__init__(msg) - - -class InvalidValueError(Exception): - """Raised when a value is invalid for an option""" - - def __init__(self, section: str, option: str, value: str): - msg = f"Invalid value '{value}' for option '{option}' in section '{section}'" - super().__init__(msg) - - -@dataclass -class AppSettings: - backup_before_update: bool | None = field(default=None) - - -@dataclass -class Repository: - url: str - branch: str - -@dataclass -class KlipperSettings: - repositories: List[Repository] | None = field(default=None) - use_python_binary: str | None = field(default=None) - -@dataclass -class MoonrakerSettings: - optional_speedups: bool | None = field(default=None) - repositories: List[Repository] | None = field(default=None) - use_python_binary: str | None = field(default=None) - -@dataclass -class WebUiSettings: - port: int | None = field(default=None) - unstable_releases: bool | None = field(default=None) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KiauhSettings: - __instance = None - - def __new__(cls, *args, **kwargs) -> "KiauhSettings": - if cls.__instance is None: - cls.__instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs) - 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: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.config = SimpleConfigParser() - self.kiauh = AppSettings() - self.klipper = KlipperSettings() - self.moonraker = MoonrakerSettings() - self.mainsail = WebUiSettings() - self.fluidd = WebUiSettings() - - self._load_config() - - 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. 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. - :return: The value of the option as string, int or bool. - """ - - try: - section = getattr(self, section) - value = getattr(section, option) - return value # type: ignore - except AttributeError: - raise - - def save(self) -> None: - self._set_config_options_state() - self.config.write_file(CUSTOM_CFG) - self._load_config() - - def _load_config(self) -> None: - if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists(): - self.__kill() - - cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG - self.config.read_file(cfg) - - needs_migration = self._check_deprecated_repo_config() - if needs_migration: - self._prompt_migration_dialog() - return - else: - # Only validate if no migration was needed - self._validate_cfg() - self.__set_internal_state() - - def _validate_cfg(self) -> None: - def __err_and_kill(error: str) -> None: - Logger.print_error(f"Error validating kiauh.cfg: {error}") - kill() - - try: - self._validate_bool("kiauh", "backup_before_update") - - self._validate_repositories("klipper", "repositories") - self._validate_repositories("moonraker", "repositories") - - self._validate_int("mainsail", "port") - self._validate_bool("mainsail", "unstable_releases") - - self._validate_int("fluidd", "port") - self._validate_bool("fluidd", "unstable_releases") - - self._validate_bool("moonraker", "optional_speedups") - - except ValueError: - err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'" - __err_and_kill(err) - except NoSectionError: - err = f"Missing section '{self._v_section}' in config file" - __err_and_kill(err) - except NoOptionError: - err = f"Missing option '{self._v_option}' in section '{self._v_section}'" - __err_and_kill(err) - except NoValueError: - err = f"Missing value for option '{self._v_option}' in section '{self._v_section}'" - __err_and_kill(err) - - def _validate_bool(self, section: str, option: str) -> None: - self._v_section, self._v_option = (section, option) - (bool(self.config.getboolean(section, option))) - - def _validate_int(self, section: str, option: str) -> None: - self._v_section, self._v_option = (section, option) - int(self.config.getint(section, option)) - - def _validate_str(self, section: str, option: str) -> None: - self._v_section, self._v_option = (section, option) - v = self.config.getval(section, option) - - if not v: - raise ValueError - - def _validate_repositories(self, section: str, option: str) -> None: - self._v_section, self._v_option = (section, option) - repos = self.config.getval(section, option) - if not repos: - raise NoValueError(section, option) - - for repo in repos: - if repo.strip().startswith("#") or repo.strip().startswith(";"): - continue - try: - if "," in repo: - url, branch = repo.strip().split(",") - if not url: - raise InvalidValueError(section, option, repo) - else: - url = repo.strip() - if not url: - raise InvalidValueError(section, option, repo) - except ValueError: - raise InvalidValueError(section, option, repo) - - def __set_internal_state(self) -> None: - self.kiauh.backup_before_update = self.config.getboolean( - "kiauh", "backup_before_update" - ) - - self.moonraker.optional_speedups = self.config.getboolean("moonraker", "optional_speedups", True) - - kl_repos = self.config.getval("klipper", "repositories") - self.klipper.repositories = self.__set_repo_state(kl_repos) - - mr_repos = self.config.getval("moonraker", "repositories") - self.moonraker.repositories = self.__set_repo_state(mr_repos) - - self.klipper.use_python_binary = self.config.getval("klipper", "use_python_binary", None) - self.moonraker.use_python_binary = self.config.getval("moonraker", "use_python_binary", None) - - self.mainsail.port = self.config.getint("mainsail", "port") - self.mainsail.unstable_releases = self.config.getboolean( - "mainsail", "unstable_releases" - ) - self.fluidd.port = self.config.getint("fluidd", "port") - self.fluidd.unstable_releases = self.config.getboolean( - "fluidd", "unstable_releases" - ) - - def __set_repo_state(self, repos: List[str]) -> List[Repository]: - _repos: List[Repository] = [] - for repo in repos: - if repo.strip().startswith("#") or repo.strip().startswith(";"): - continue - if "," in repo: - url, branch = repo.strip().split(",") - if not branch: - branch = "master" - else: - url = repo.strip() - branch = "master" - _repos.append(Repository(url.strip(), branch.strip())) - return _repos - - def _set_config_options_state(self) -> None: - """Updates the config with current settings, preserving values that haven't been modified""" - if self.kiauh.backup_before_update is not None: - self.config.set_option( - "kiauh", - "backup_before_update", - str(self.kiauh.backup_before_update), - ) - - # Handle repositories - if self.klipper.repositories is not None: - repos = [f"{repo.url}, {repo.branch}" for repo in self.klipper.repositories] - self.config.set_option("klipper", "repositories", repos) - - if self.moonraker.repositories is not None: - repos = [ - f"{repo.url}, {repo.branch}" for repo in self.moonraker.repositories - ] - self.config.set_option("moonraker", "repositories", repos) - - # Handle Mainsail settings - if self.mainsail.port is not None: - self.config.set_option("mainsail", "port", str(self.mainsail.port)) - if self.mainsail.unstable_releases is not None: - self.config.set_option( - "mainsail", - "unstable_releases", - str(self.mainsail.unstable_releases), - ) - - # Handle Fluidd settings - if self.fluidd.port is not None: - self.config.set_option("fluidd", "port", str(self.fluidd.port)) - if self.fluidd.unstable_releases is not None: - self.config.set_option( - "fluidd", "unstable_releases", str(self.fluidd.unstable_releases) - ) - - def _check_deprecated_repo_config(self) -> bool: - # repo_url and branch are deprecated - 2025.03.23 - for section in ["klipper", "moonraker"]: - if self.config.has_option(section, "repo_url") or self.config.has_option( - section, "branch" - ): - return True - return False - - def _prompt_migration_dialog(self) -> None: - migration_1: List[str] = [ - "The old 'repo_url' and 'branch' options are now combined under 'repositories'.", - "\n\n", - "Example format:", - "[klipper]", - "repositories:", - " https://github.com/Klipper3d/klipper, master", - "\n\n", - "[moonraker]", - "repositories:", - " https://github.com/Arksine/moonraker, master", - ] - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Deprecated repository configuration found!", - "KAIUH can now attempt to automatically migrate your configuration.", - "\n\n", - *migration_1, - ], - ) - if get_confirm("Migrate to the new format?"): - self._migrate_repo_config() - else: - Logger.print_dialog( - DialogType.ERROR, - [ - "Please update your configuration file manually.", - ], - center_content=True, - ) - kill() - - def _migrate_repo_config(self) -> None: - bm = BackupManager() - if not bm.backup_file(CUSTOM_CFG): - Logger.print_dialog( - DialogType.ERROR, - [ - "Failed to create backup of kiauh.cfg. Aborting migration. Please migrate manually." - ], - ) - kill() - - # run migrations - try: - # migrate deprecated repo_url and branch options - 2025.03.23 - for section in ["klipper", "moonraker"]: - if not self.config.has_section(section): - continue - - repo_url = self.config.getval(section, "repo_url", fallback="") - branch = self.config.getval(section, "branch", fallback="master") - - if repo_url: - # create repositories option with the old values - repositories = [f"{repo_url}, {branch}\n"] - self.config.set_option(section, "repositories", repositories) - - # remove deprecated options - self.config.remove_option(section, "repo_url") - self.config.remove_option(section, "branch") - - Logger.print_ok(f"Successfully migrated {section} configuration") - - self.config.write_file(CUSTOM_CFG) - self.config.read_file(CUSTOM_CFG) # reload config - - # Validate the migrated config - self._validate_cfg() - self.__set_internal_state() - - except Exception as e: - Logger.print_error(f"Error migrating configuration: {e}") - Logger.print_error("Please migrate manually.") - kill() - - def __kill(self) -> None: - Logger.print_dialog( - DialogType.ERROR, - [ - "No KIAUH configuration file found! Please make sure you have at least " - "one of the following configuration files in KIAUH's root directory:", - "● default.kiauh.cfg", - "● kiauh.cfg", - ], - ) - kill() diff --git a/kiauh/core/spinner.py b/kiauh/core/spinner.py deleted file mode 100644 index db603ac..0000000 --- a/kiauh/core/spinner.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -import threading -import time -from typing import List, Literal - -from core.types.color import Color - -SpinnerColor = Literal["white", "red", "green", "yellow"] - - -class Spinner: - def __init__( - self, - message: str = "Loading", - interval: float = 0.2, - ) -> None: - self.message = f"{message} ..." - self.interval = interval - self._stop_event = threading.Event() - self._thread = threading.Thread(target=self._animate) - - def _animate(self) -> None: - animation: List[str] = ["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"] - while not self._stop_event.is_set(): - for char in animation: - sys.stdout.write(f"\r{Color.GREEN}{char}{Color.RST} {self.message}") - sys.stdout.flush() - time.sleep(self.interval) - if self._stop_event.is_set(): - break - sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r") - sys.stdout.flush() - - def start(self) -> None: - self._stop_event.clear() - if not self._thread.is_alive(): - self._thread = threading.Thread(target=self._animate) - self._thread.start() - - def stop(self) -> None: - self._stop_event.set() - self._thread.join() diff --git a/kiauh/core/submodules/__init__.py b/kiauh/core/submodules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/.editorconfig b/kiauh/core/submodules/simple_config_parser/.editorconfig deleted file mode 100644 index 2546a60..0000000 --- a/kiauh/core/submodules/simple_config_parser/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# see https://editorconfig.org/ -root = true - -[*] -end_of_line = lf -trim_trailing_whitespace = true -indent_style = space -insert_final_newline = true -indent_size = 4 -charset = utf-8 - -[*.py] -max_line_length = 88 diff --git a/kiauh/core/submodules/simple_config_parser/.gitignore b/kiauh/core/submodules/simple_config_parser/.gitignore deleted file mode 100644 index a5d5089..0000000 --- a/kiauh/core/submodules/simple_config_parser/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*.py[cod] -*.pyc -__pycache__ -.pytest_cache/ - -.idea/ -.vscode/ - -.venv*/ -venv*/ - -.coverage -htmlcov/ diff --git a/kiauh/core/submodules/simple_config_parser/LICENSE b/kiauh/core/submodules/simple_config_parser/LICENSE deleted file mode 100644 index f288702..0000000 --- a/kiauh/core/submodules/simple_config_parser/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/kiauh/core/submodules/simple_config_parser/README.md b/kiauh/core/submodules/simple_config_parser/README.md deleted file mode 100644 index c04af42..0000000 --- a/kiauh/core/submodules/simple_config_parser/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Simple Config Parser - -A custom config parser inspired by Python's configparser module. -Specialized for handling Klipper style config files. - ---- - -### When parsing a config file, it will be split into the following elements: -- Header: All lines before the first section -- Section: A section is defined by a line starting with a `[` and ending with a `]` -- Option: A line starting with a word, followed by a `:` or `=` and a value -- Option Block: A line starting with a word, followed by a `:` or `=` and a newline -- Comment: A line starting with a `#` or `;` -- Blank: A line containing only whitespace characters - ---- - -### Internally, the config is stored as a dictionary of sections, each containing a header and a list of elements: -```python -config = { - "section_name": { - "header": "[section_name]\n", - "elements": [ - { - "type": "comment", - "content": "# This is a comment\n" - }, - { - "type": "option", - "name": "option1", - "value": "value1", - "raw": "option1: value1\n" - }, - { - "type": "blank", - "content": "\n" - }, - { - "type": "option_block", - "name": "option2", - "value": [ - "value2", - "value3" - ], - "raw": "option2:" - } - ] - } - } -``` diff --git a/kiauh/core/submodules/simple_config_parser/pyproject.toml b/kiauh/core/submodules/simple_config_parser/pyproject.toml deleted file mode 100644 index 8b7b9ed..0000000 --- a/kiauh/core/submodules/simple_config_parser/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[project] -name = "simple-config-parser" -version = "0.0.1" -description = "A simple config parser for Python" -authors = [ - {name = "Dominik Willner", email = "th33xitus@gmail.com"}, -] -readme = "README.md" -license = {text = "GPL-3.0-only"} -requires-python = ">=3.8" - -[project.urls] -homepage = "https://github.com/dw-0/simple-config-parser" -repository = "https://github.com/dw-0/simple-config-parser" -documentation = "https://github.com/dw-0/simple-config-parser" - -[project.optional-dependencies] -dev=["ruff"] - -[tool.ruff] -required-version = ">=0.3.4" -respect-gitignore = true -exclude = [".git",".github", "./docs"] -line-length = 88 -indent-width = 4 -output-format = "full" - -[tool.ruff.format] -indent-style = "space" -line-ending = "lf" -quote-style = "double" - -[tool.ruff.lint] -extend-select = ["I"] - -[tool.pytest.ini_options] -minversion = "8.2.1" -testpaths = ["tests/**/*.py"] -addopts = "-svvv --cov --cov-config=pyproject.toml --cov-report=html" - -[tool.coverage.run] -branch = true -source = ["src.simple_config_parser"] - -[tool.coverage.report] -# Regexes for lines to exclude from consideration -exclude_also = [ - # Don't complain about missing debug-only code: - "def __repr__", - "if self\\.debug", - - # Don't complain if tests don't hit defensive assertion code: - "raise AssertionError", - "raise NotImplementedError", - - # Don't complain if non-runnable code isn't run: - "if 0:", - "if __name__ == .__main__.:", - - # Don't complain about abstract methods, they aren't run: - "@(abc\\.)?abstractmethod", - ] - -[tool.coverage.html] -title = "SimpleConfigParser Coverage Report" -directory = "htmlcov" diff --git a/kiauh/core/submodules/simple_config_parser/requirements-dev.txt b/kiauh/core/submodules/simple_config_parser/requirements-dev.txt deleted file mode 100644 index 7e73e5f..0000000 --- a/kiauh/core/submodules/simple_config_parser/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -ruff >= 0.3.4 -pytest >= 8.2.1 -pytest-cov >= 5.0.0 diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/__init__.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py deleted file mode 100644 index 5afe9af..0000000 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py +++ /dev/null @@ -1,71 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import re -from enum import Enum - -# 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" - -INDENT = " " * 4 - -class LineType(Enum): - OPTION = "option" - OPTION_BLOCK = "option_block" - COMMENT = "comment" - BLANK = "blank" diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py deleted file mode 100644 index 842d7bd..0000000 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py +++ /dev/null @@ -1,394 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from pathlib import Path -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, LineType, INDENT, -) - -_UNSET = object() - - -class NoSectionError(Exception): - """Raised when a section is not defined""" - - def __init__(self, section: str): - msg = f"Section '{section}' is not defined" - super().__init__(msg) - - -class DuplicateSectionError(Exception): - """Raised when a section is defined more than once""" - - def __init__(self, section: str): - msg = f"Section '{section}' is defined more than once" - 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 UnknownLineError(Exception): - """Raised when a line is not recognized as any known type""" - - def __init__(self, line: str): - msg = f"Unknown line: '{line}'" - super().__init__(msg) - - -# noinspection PyMethodMayBeStatic -class SimpleConfigParser: - """A customized config parser targeted at handling Klipper style config files""" - - def __init__(self) -> None: - self.header: List[str] = [] - self.config: Dict = {} - self.current_section: str | None = None - self.current_opt_block: str | None = None - self.in_option_block: bool = False - - def _match_section(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a section""" - return SECTION_RE.match(line) is not None - - def _match_option(self, line: str) -> bool: - """Wheter or not the given line matches the definition of an option""" - return OPTION_RE.match(line) is not None - - def _match_options_block_start(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a multiline option""" - return OPTIONS_BLOCK_START_RE.match(line) is not None - - def _match_line_comment(self, line: str) -> bool: - """Wheter or not the given line matches the definition of a comment""" - return LINE_COMMENT_RE.match(line) is not None - - def _match_empty_line(self, line: str) -> bool: - """Wheter or not the given line matches the definition of an empty line""" - return EMPTY_LINE_RE.match(line) is not None - - def _parse_line(self, line: str) -> None: - """Parses a line and determines its type""" - if self._match_section(line): - self.current_opt_block = None - self.current_section = SECTION_RE.match(line).group(1) - self.config[self.current_section] = { - "header": line, - "elements": [] - } - - elif self._match_option(line): - self.current_opt_block = None - option = OPTION_RE.match(line).group(1) - value = OPTION_RE.match(line).group(2) - self.config[self.current_section]["elements"].append({ - "type": LineType.OPTION.value, - "name": option, - "value": value, - "raw": line - }) - - elif self._match_options_block_start(line): - option = OPTIONS_BLOCK_START_RE.match(line).group(1) - self.current_opt_block = option - self.config[self.current_section]["elements"].append({ - "type": LineType.OPTION_BLOCK.value, - "name": option, - "value": [], - "raw": line - }) - - elif self.current_opt_block is not None: - # we are in an option block, so we add the line to the option's value - for element in reversed(self.config[self.current_section]["elements"]): - if element["type"] == LineType.OPTION_BLOCK.value and element["name"] == self.current_opt_block: - element["value"].append(line.strip()) # indentation is removed - break - - elif self._match_empty_line(line) or self._match_line_comment(line): - self.current_opt_block = None - - # if current_section is None, we are at the beginning of the file, - # 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: - element_type = LineType.BLANK.value if self._match_empty_line(line) else LineType.COMMENT.value - self.config[self.current_section]["elements"].append({ - "type": element_type, - "content": line - }) - - 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) - - def write_file(self, path: str | Path) -> None: - """Write the config to a file""" - if path is None: - raise ValueError("File path cannot be None") - - with open(path, "w", encoding="utf-8") as f: - if HEADER_IDENT in self.config: - for line in self.config[HEADER_IDENT]: - f.write(line) - - sections = self.get_sections() - for i, section in enumerate(sections): - f.write(self.config[section]["header"]) - - for element in self.config[section]["elements"]: - if element["type"] == LineType.OPTION.value: - f.write(element["raw"]) - elif element["type"] == LineType.OPTION_BLOCK.value: - f.write(element["raw"]) - for line in element["value"]: - f.write(INDENT + line.strip() + "\n") - elif element["type"] in [LineType.COMMENT.value, LineType.BLANK.value]: - f.write(element["content"]) - else: - raise UnknownLineError(element["raw"]) - - # Ensure file ends with a single newline - if sections: # Only if we have any sections - last_section = sections[-1] - last_elements = self.config[last_section]["elements"] - - if last_elements: - last_element = last_elements[-1] - if "raw" in last_element: - last_line = last_element["raw"] - else: # comment or blank line - last_line = last_element["content"] - - if not last_line.endswith("\n"): - f.write("\n") - - 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: - """Add a new section to the config""" - if section in self.get_sections(): - raise DuplicateSectionError(section) - - if len(self.get_sections()) >= 1: - self._check_set_section_spacing() - - self.config[section] = { - "header": f"[{section}]\n", - "elements": [] - } - - def _check_set_section_spacing(self): - """Check if there is a blank line between the last section and the new section""" - prev_section_name: str = self.get_sections()[-1] - prev_section = self.config[prev_section_name] - prev_elements = prev_section["elements"] - - if prev_elements: - last_element = prev_elements[-1] - - # If the last element is a comment or blank line - if last_element["type"] in [LineType.COMMENT.value, LineType.BLANK.value]: - last_content = last_element["content"] - - # If the last element doesn't end with a newline, add one - if not last_content.endswith("\n"): - last_element["content"] += "\n" - - # If the last element is not a blank line, add a blank line - if last_content.strip() != "": - prev_elements.append({ - "type": "blank", - "content": "\n" - }) - else: - # If the last element is an option, add a blank line - prev_elements.append({ - "type": LineType.BLANK.value, - "content": "\n" - }) - - def remove_section(self, section: str) -> None: - """Remove a section from the config""" - self.config.pop(section, None) - - def get_options(self, section: str) -> List[str]: - """Return a list of all option names for a given section""" - options = [] - if self.has_section(section): - for element in self.config[section]["elements"]: - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value]: - options.append(element["name"]) - return options - - def has_option(self, section: str, option: str) -> bool: - """Check if an option exists in a section""" - return self.has_section(section) and option in self.get_options(section) - - def set_option(self, section: str, option: str, value: str | List[str]) -> None: - """ - 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) - - # Check if option already exists - for element in self.config[section]["elements"]: - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option: - # Update existing option - if isinstance(value, list): - element["type"] = LineType.OPTION_BLOCK.value - element["value"] = value - element["raw"] = f"{option}:\n" - else: - element["type"] = LineType.OPTION.value - element["value"] = value - element["raw"] = f"{option}: {value}\n" - return - - # Option doesn't exist, create new one - if isinstance(value, list): - new_element = { - "type": LineType.OPTION_BLOCK.value, - "name": option, - "value": value, - "raw": f"{option}:\n" - } - else: - new_element = { - "type": LineType.OPTION.value, - "name": option, - "value": value, - "raw": f"{option}: {value}\n" - } - - # scan through elements to find the last option, after which we insert the new option - insert_pos = 0 - elements = self.config[section]["elements"] - for i, element in enumerate(elements): - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value]: - insert_pos = i + 1 - - elements.insert(insert_pos, new_element) - - def remove_option(self, section: str, option: str) -> None: - """Remove an option from a section""" - if self.has_section(section): - elements = self.config[section]["elements"] - for i, element in enumerate(elements): - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option: - elements.pop(i) - break - - def getval( - self, section: str, option: str, fallback: str | _UNSET = _UNSET - ) -> str | List[str]: - """ - Return the value of the given option in the given section - - If the key is not found and 'fallback' is provided, it is used as - a fallback value. - """ - try: - if section not in self.get_sections(): - raise NoSectionError(section) - if option not in self.get_options(section): - raise NoOptionError(option, section) - - # Find the option in the elements list - for element in self.config[section]["elements"]: - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option: - raw_value = element["value"] - if isinstance(raw_value, str) and raw_value.endswith("\n"): - return raw_value[:-1].strip() - elif isinstance(raw_value, list): - values: List[str] = [] - for i, val in enumerate(raw_value): - val = val.strip().strip("\n") - if len(val) < 1: - continue - values.append(val.strip()) - return values - return str(raw_value) - raise NoOptionError(option, section) - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - return fallback - - 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 self._get_conv(section, option, int, fallback=fallback) - - def getfloat( - self, section: str, option: str, fallback: float | _UNSET = _UNSET - ) -> float: - """Return the value of the given option in the given section as a float""" - return self._get_conv(section, option, float, fallback=fallback) - - def getboolean( - self, section: str, option: str, fallback: bool | _UNSET = _UNSET - ) -> bool: - """Return the value of the given option in the given section as a boolean""" - return self._get_conv( - section, option, self._convert_to_boolean, fallback=fallback - ) - - def _convert_to_boolean(self, value: str) -> bool: - """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) - return BOOLEAN_STATES[value.lower()] - - def _get_conv( - self, - section: str, - option: str, - conv: Callable[[str], int | float | bool], - fallback: _UNSET = _UNSET, - ) -> int | float | bool: - """Return the value of the given option in the given section as a converted value""" - try: - return conv(self.getval(section, option, fallback)) - except (ValueError, TypeError, AttributeError) as e: - if fallback is not _UNSET: - return fallback - raise ValueError( - f"Cannot convert {self.getval(section, option)} to {conv.__name__}" - ) from e diff --git a/kiauh/core/submodules/simple_config_parser/tests/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt b/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt deleted file mode 100644 index 383c625..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt +++ /dev/null @@ -1,1337 +0,0 @@ -[mcu] -serial: -baud: 250000 -canbus_uuid: -canbus_interface: -restart_method: -[printer] -kinematics: -max_velocity: -max_accel: -minimum_cruise_ratio: 0.5 -square_corner_velocity: 5.0 -max_accel_to_decel: -[stepper_x] -step_pin: -dir_pin: -enable_pin: -rotation_distance: -microsteps: -full_steps_per_rotation: 200 -gear_ratio: -step_pulse_duration: -endstop_pin: -position_min: 0 -position_endstop: -position_max: -homing_speed: 5.0 -homing_retract_dist: 5.0 -homing_retract_speed: -second_homing_speed: -homing_positive_dir: -[printer] -kinematics: cartesian -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: delta -max_z_velocity: -max_z_accel: -minimum_z_position: 0 -delta_radius: -print_radius: -[stepper_a] -position_endstop: -arm_length: -angle: -[stepper_b] -[stepper_c] -[delta_calibrate] -radius: -speed: 50 -horizontal_move_z: 5 -[printer] -kinematics: deltesian -max_z_velocity: -max_z_accel: -minimum_z_position: 0 -min_angle: 5 -print_width: -slow_ratio: 3 -[stepper_left] -position_endstop: -arm_length: -arm_x_length: -[stepper_right] -[stepper_y] -[printer] -kinematics: corexy -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: corexz -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: hybrid_corexy -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: hybrid_corexz -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: polar -max_z_velocity: -max_z_accel: -[stepper_bed] -gear_ratio: -[stepper_arm] -[stepper_z] -[printer] -kinematics: rotary_delta -max_z_velocity: -minimum_z_position: 0 -shoulder_radius: -shoulder_height: -[stepper_a] -gear_ratio: -position_endstop: -upper_arm_length: -lower_arm_length: -angle: -[stepper_b] -[stepper_c] -[delta_calibrate] -radius: -speed: 50 -horizontal_move_z: 5 -[printer] -kinematics: winch -[stepper_a] -rotation_distance: -anchor_x: -anchor_y: -anchor_z: -[printer] -kinematics: none -max_velocity: 1 -max_accel: 1 -[extruder] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -full_steps_per_rotation: -gear_ratio: -nozzle_diameter: -filament_diameter: -max_extrude_cross_section: -instantaneous_corner_velocity: 1.000 -max_extrude_only_distance: 50.0 -max_extrude_only_velocity: -max_extrude_only_accel: -pressure_advance: 0.0 -pressure_advance_smooth_time: 0.040 -heater_pin: -max_power: 1.0 -sensor_type: -sensor_pin: -pullup_resistor: 4700 -smooth_time: 1.0 -control: -pid_Kp: -pid_Ki: -pid_Kd: -max_delta: 2.0 -pwm_cycle_time: 0.100 -min_extrude_temp: 170 -min_temp: -max_temp: -[heater_bed] -heater_pin: -sensor_type: -sensor_pin: -control: -min_temp: -max_temp: -[bed_mesh] -speed: 50 -horizontal_move_z: 5 -mesh_radius: -mesh_origin: -mesh_min: -mesh_max: -probe_count: 3, 3 -round_probe_count: 5 -fade_start: 1.0 -fade_end: 0.0 -fade_target: -split_delta_z: .025 -move_check_distance: 5.0 -mesh_pps: 2, 2 -algorithm: lagrange -bicubic_tension: .2 -zero_reference_position: -faulty_region_1_min: -faulty_region_1_max: -adaptive_margin: -scan_overshoot: -[bed_tilt] -x_adjust: 0 -y_adjust: 0 -z_adjust: 0 -points: -speed: 50 -horizontal_move_z: 5 -[bed_screws] -screw1: -screw1_name: -screw1_fine_adjust: -screw2: -screw2_name: -screw2_fine_adjust: -horizontal_move_z: 5 -probe_height: 0 -speed: 50 -probe_speed: 5 -[screws_tilt_adjust] -screw1: -screw1_name: -screw2: -screw2_name: -speed: 50 -horizontal_move_z: 5 -screw_thread: CW-M3 -[z_tilt] -z_positions: -points: -speed: 50 -horizontal_move_z: 5 -retries: 0 -retry_tolerance: 0 -[quad_gantry_level] -gantry_corners: -points: -speed: 50 -horizontal_move_z: 5 -max_adjust: 4 -retries: 0 -retry_tolerance: 0 -[skew_correction] -[z_thermal_adjust] -temp_coeff: -smooth_time: -z_adjust_off_above: -max_z_adjustment: -sensor_type: -sensor_pin: -min_temp: -max_temp: -gcode_id: -[safe_z_home] -home_xy_position: -speed: 50.0 -z_hop: -z_hop_speed: 15.0 -move_to_previous: False -[homing_override] -gcode: -axes: xyz -set_position_x: -set_position_y: -set_position_z: -[endstop_phase stepper_z] -endstop_accuracy: -trigger_phase: -endstop_align_zero: False -[gcode_macro my_cmd] -gcode: -variable_: -rename_existing: -description: G-Code macro -[delayed_gcode my_delayed_gcode] -gcode: -initial_duration: 0.0 -[save_variables] -filename: -[idle_timeout] -gcode: -timeout: 600 -[virtual_sdcard] -path: -on_error_gcode: -[sdcard_loop] -[force_move] -enable_force_move: False -[pause_resume] -recover_velocity: 50. -[firmware_retraction] -retract_length: 0 -retract_speed: 20 -unretract_extra_length: 0 -unretract_speed: 10 -[gcode_arcs] -resolution: 1.0 -[respond] -default_type: echo -default_prefix: echo: -[exclude_object] -[input_shaper] -shaper_freq_x: 0 -shaper_freq_y: 0 -shaper_type: mzv -shaper_type_x: -shaper_type_y: -damping_ratio_x: 0.1 -damping_ratio_y: 0.1 -[adxl345] -cs_pin: -spi_speed: 5000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -axes_map: x, y, z -rate: 3200 -[lis2dw] -cs_pin: -spi_speed: 5000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -axes_map: x, y, z -[mpu9250 my_accelerometer] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: 400000 -axes_map: x, y, z -[resonance_tester] -probe_points: -accel_chip: -accel_chip_x: -accel_chip_y: -max_smoothing: -min_freq: 5 -max_freq: 133.33 -accel_per_hz: 75 -hz_per_sec: 1 -[board_pins my_aliases] -mcu: mcu -aliases: -aliases_: -[duplicate_pin_override] -pins: -[probe] -pin: -deactivate_on_each_sample: True -x_offset: 0.0 -y_offset: 0.0 -z_offset: -speed: 5.0 -samples: 1 -sampleretract_dist: 2.0 -lift_speed: -samples_result: average -samples_tolerance: 0.100 -samples_toleranceretries: 0 -activate_gcode: -deactivate_gcode: -[bltouch] -sensor_pin: -control_pin: -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 -set_output_mode: -x_offset: -y_offset: -z_offset: -speed: -lift_speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -[smart_effector] -pin: -control_pin: -probe_accel: -recovery_time: 0.4 -x_offset: -y_offset: -z_offset: -speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -activate_gcode: -deactivate_gcode: -deactivate_on_each_sample: -[probe_eddy_current my_eddy_probe] -sensor_type: ldc1612 -intb_pin: -z_offset: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -x_offset: -y_offset: -speed: -lift_speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -[axis_twist_compensation] -speed: 50 -horizontal_move_z: 5 -calibrate_start_x: 20 -calibrate_end_x: 200 -calibrate_y: 112.5 -[stepper_z1] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -endstop_pin: -[extruder1] -step_pin: -dir_pin: -shared_heater: -[dual_carriage] -axis: -safe_distance: -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -endstop_pin: -position_endstop: -position_min: -position_max: -[extruder_stepper my_extra_stepper] -extruder: -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -[manual_stepper my_stepper] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -velocity: -accel: -endstop_pin: -[verify_heater heater_config_name] -max_error: 120 -check_gain_time: -hysteresis: 5 -heating_gain: 2 -[homing_heaters] -steppers: -heaters: -[thermistor my_thermistor] -temperature1: -resistance1: -temperature2: -resistance2: -temperature3: -resistance3: -beta: -[adc_temperature my_sensor] -temperature1: -voltage1: -temperature2: -voltage2: -temperature1: -resistance1: -temperature2: -resistance2: -[heater_generic my_generic_heater] -gcode_id: -heater_pin: -max_power: -sensor_type: -sensor_pin: -smooth_time: -control: -pid_Kp: -pid_Ki: -pid_Kd: -pwm_cycle_time: -min_temp: -max_temp: -[temperature_sensor my_sensor] -sensor_type: -sensor_pin: -min_temp: -max_temp: -gcode_id: -[temperature_probe my_probe] -sensor_type: -sensor_pin: -min_temp: -max_temp: -smooth_time: -gcode_id: -speed: -horizontal_move_z: -resting_z: -calibration_position: -calibration_bed_temp: -calibration_extruder_temp: -extruder_heating_z: 50. -max_validation_temp: 60. -sensor_type: -sensor_pin: -pullup_resistor: 4700 -inlineresistor: 0 -sensor_type: -sensor_pin: -adc_voltage: 5.0 -voltage_offset: 0 -sensor_type: PT1000 -sensor_pin: -pullup_resistor: 4700 -sensor_type: -sensor_pin: -spi_speed: 4000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -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 -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -sensor_type: AHT10 -i2c_address: -i2c_mcu: -i2c_bus: -i2c_speed: -aht10_report_time: -sensor_type: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -htu21d_hold_master: -htu21d_resolution: -htu21d_report_time: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -lm75_report_time: -sensor_type: temperature_mcu -sensor_mcu: mcu -sensor_temperature1: -sensor_adc1: -sensor_temperature2: -sensor_adc2: -sensor_type: temperature_host -sensor_path: -sensor_type: DS18B20 -serial_no: -ds18_report_time: -sensor_mcu: -sensor_type: temperature_combined -sensor_list: -combination_method: -maximum_deviation: -[fan] -pin: -max_power: 1.0 -shutdown_speed: 0 -cycle_time: 0.010 -hardware_pwm: False -kick_start_time: 0.100 -off_below: 0.0 -tachometer_pin: -tachometer_ppr: 2 -tachometer_poll_interval: 0.0015 -enable_pin: -[heater_fan heatbreak_cooling_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -heater: extruder -heater_temp: 50.0 -fan_speed: 1.0 -[controller_fan my_controller_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -fan_speed: 1.0 -idle_timeout: -idle_speed: -heater: -stepper: -[temperature_fan my_temp_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -sensor_type: -sensor_pin: -control: -max_delta: -min_temp: -max_temp: -pid_Kp: -pid_Ki: -pid_Kd: -pid_deriv_time: 2.0 -target_temp: 40.0 -max_speed: 1.0 -min_speed: 0.3 -gcode_id: -[fan_generic extruder_partfan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -[led my_led] -red_pin: -green_pin: -blue_pin: -white_pin: -cycle_time: 0.010 -hardware_pwm: False -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[neopixel my_neopixel] -pin: -chain_count: -color_order: GRB -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[dotstar my_dotstar] -data_pin: -clock_pin: -chain_count: -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -[pca9533 my_pca9533] -i2c_address: 98 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[pca9632 my_pca9632] -i2c_address: 98 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -scl_pin: -sda_pin: -color_order: RGBW -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[servo my_servo] -pin: -maximum_servo_angle: 180 -minimum_pulse_width: 0.001 -maximum_pulse_width: 0.002 -initial_angle: -initial_pulse_width: -[gcode_button my_gcode_button] -pin: -analog_range: -analog_pullup_resistor: -press_gcode: -release_gcode: -[output_pin my_pin] -pin: -pwm: False -value: -shutdown_value: -cycle_time: 0.100 -hardware_pwm: False -scale: -maximum_mcu_duration: -static_value: -[pwm_tool my_tool] -pin: -maximum_mcu_duration: -value: -shutdown_value: -cycle_time: 0.100 -hardware_pwm: False -scale: -[pwm_cycle_time my_pin] -pin: -value: -shutdown_value: -cycle_time: 0.100 -scale: -[static_digital_output my_output_pins] -pins: -[multi_pin my_multi_pin] -pins: -[tmc2130 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -senseresistor: 0.110 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -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 -diag0_pin: -diag1_pin: -[tmc2208 stepper_x] -uart_pin: -tx_pin: -select_pins: -interpolate: True -run_current: -hold_current: -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 -[tmc2209 stepper_x] -uart_pin: -tx_pin: -select_pins: -interpolate: True -run_current: -hold_current: -sense_resistor: 0.110 -stealthchop_threshold: 0 -coolstep_threshold: -uart_address: -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 -diag_pin: -[tmc2660 stepper_x] -cs_pin: -spi_speed: 4000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -interpolate: True -run_current: -sense_resistor: -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 -[tmc2240 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -uart_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -rref: 12000 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -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 -diag0_pin: -diag1_pin: -[tmc5160 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -sense_resistor: 0.075 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -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 -diag0_pin: -diag1_pin: -[ad5206 my_digipot] -enable_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -channel_1: -channel_2: -channel_3: -channel_4: -channel_5: -channel_6: -scale: -[mcp4451 my_digipot] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -wiper_0: -wiper_1: -wiper_2: -wiper_3: -scale: -[mcp4728 my_dac] -i2c_address: 96 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -channel_a: -channel_b: -channel_c: -channel_d: -scale: -[mcp4018 my_digipot] -scl_pin: -sda_pin: -wiper: -scale: -[display] -lcd_type: -display_group: -menu_timeout: -menu_root: -menu_reverse_navigation: -encoder_pins: -encoder_steps_per_detent: -click_pin: -back_pin: -up_pin: -down_pin: -kill_pin: -analog_pullup_resistor: 4700 -analog_range_click_pin: -analog_range_back_pin: -analog_range_up_pin: -analog_range_down_pin: -analog_range_kill_pin: -[display] -lcd_type: hd44780 -rs_pin: -e_pin: -d4_pin: -d5_pin: -d6_pin: -d7_pin: -hd44780_protocol_init: True -line_length: -[display] -lcd_type: hd44780_spi -latch_pin: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -hd44780_protocol_init: True -line_length: -[display] -lcd_type: st7920 -cs_pin: -sclk_pin: -sid_pin: -[display] -lcd_type: emulated_st7920 -en_pin: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -[display] -lcd_type: uc1701 -cs_pin: -a0_pin: -rst_pin: -contrast: -[display] -lcd_type: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -cs_pin: -dc_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -reset_pin: -contrast: -vcomh: 0 -invert: False -x_offset: 0 -[display_data my_group_name my_data_name] -position: -text: -[display_template my_template_name] -param_: -text: -[display_glyph my_display_glyph] -data: -hd44780_data: -hd44780_slot: -[menu __some_list __some_name] -type: disabled -[menu some_name] -type: -name: -enable: -index: -[menu some_list] -type: list -name: -enable: -[menu some_list some_command] -type: command -name: -enable: -gcode: -[menu some_list some_input] -type: input -name: -enable: -input: -input_min: -input_max: -input_step: -realtime: -gcode: -[filament_switch_sensor my_sensor] -pause_on_runout: True -runout_gcode: -insert_gcode: -event_delay: 3.0 -pause_delay: 0.5 -switch_pin: -[filament_motion_sensor my_sensor] -detection_length: 7.0 -extruder: -switch_pin: -pause_on_runout: -runout_gcode: -insert_gcode: -event_delay: -pause_delay: -[tsl1401cl_filament_width_sensor] -pin: -default_nominal_filament_diameter: 1.75 -max_difference: 0.2 -measurement_delay: 100 -[hall_filament_width_sensor] -adc1: -adc2: -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 -max_diameter: -use_current_dia_while_delay: False -pause_on_runout: -runout_gcode: -insert_gcode: -event_delay: -pause_delay: -[load_cell] -sensor_type: -[load_cell] -sensor_type: hx711 -sclk_pin: -dout_pin: -gain: A-128 -sample_rate: 80 -[load_cell] -sensor_type: hx717 -sclk_pin: -dout_pin: -gain: A-128 -sample_rate: 320 -[load_cell] -sensor_type: ads1220 -cs_pin: -spi_speed: 512000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -data_ready_pin: -gain: 128 -sample_rate: 660 -[sx1509 my_sx1509] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -[samd_sercom my_sercom] -sercom: -tx_pin: -rx_pin: -clk_pin: -[adc_scaled my_name] -vref_pin: -vssa_pin: -smooth_time: 2.0 -[replicape] -revision: -enable_pin: !gpio0_20 -host_mcu: -standstill_power_down: False -stepper_x_microstep_mode: -stepper_y_microstep_mode: -stepper_z_microstep_mode: -stepper_e_microstep_mode: -stepper_h_microstep_mode: -stepper_x_current: -stepper_y_current: -stepper_z_current: -stepper_e_current: -stepper_h_current: -stepper_x_chopper_off_time_high: -stepper_y_chopper_off_time_high: -stepper_z_chopper_off_time_high: -stepper_e_chopper_off_time_high: -stepper_h_chopper_off_time_high: -stepper_x_chopper_hysteresis_high: -stepper_y_chopper_hysteresis_high: -stepper_z_chopper_hysteresis_high: -stepper_e_chopper_hysteresis_high: -stepper_h_chopper_hysteresis_high: -stepper_x_chopper_blank_time_high: -stepper_y_chopper_blank_time_high: -stepper_z_chopper_blank_time_high: -stepper_e_chopper_blank_time_high: -stepper_h_chopper_blank_time_high: -[palette2] -serial: -baud: 115200 -feedrate_splice: 0.8 -feedrate_normal: 1.0 -auto_load_speed: 2 -auto_cancel_variation: 0.1 -[angle my_angle_sensor] -sensor_type: -sample_period: 0.000400 -stepper: -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg deleted file mode 100644 index 0da8889..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg +++ /dev/null @@ -1,32 +0,0 @@ -# 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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg deleted file mode 100644 index 3224ca6..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg +++ /dev/null @@ -1,33 +0,0 @@ -# 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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg deleted file mode 100644 index 00a0a20..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg +++ /dev/null @@ -1,94 +0,0 @@ -# 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] Cancel the current object - # [P] Cancel the object with the given index - # [S] 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 isn’t an object and shouldn’t be skipped. - # [T] Reset the state and set the number of objects - # [U] 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 %} diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg deleted file mode 100644 index f53531b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment -new_option: new_value - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg deleted file mode 100644 index 0d7aa1f..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg deleted file mode 100644 index 0d7aa1f..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg deleted file mode 100644 index 949f7e7..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_to_remove: value_to_remove -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg deleted file mode 100644 index d0fdca3..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -option_1: value_1 -option_2: value_2 - -# comment -[section_2] -option_5: value_5 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg deleted file mode 100644 index cb7575e..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[section_1] -option_1: value_1 -option_2: value_2 - -# comment -[section_to_remove] -option_3: value_3 -option_4: value_4 - -[section_2] -option_5: value_5 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt deleted file mode 100644 index 6fb66a5..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt deleted file mode 100644 index ceadadf..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt +++ /dev/null @@ -1,7 +0,0 @@ -not_empty -[also_not_empty] -# -; - ; - # -option: value diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py deleted file mode 100644 index ff5f3ba..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt deleted file mode 100644 index e9c232d..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt +++ /dev/null @@ -1,28 +0,0 @@ -;[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] diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt deleted file mode 100644 index 0a585e8..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt +++ /dev/null @@ -1,5 +0,0 @@ -not_a_comment: nono - -[also not a comment] -not_a_comment: ; comment -not_a_comment: # comment diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py deleted file mode 100644 index 2e1f9df..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt deleted file mode 100644 index ef6286d..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt +++ /dev/null @@ -1,461 +0,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 -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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt deleted file mode 100644 index 582572b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt +++ /dev/null @@ -1,37 +0,0 @@ -[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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py deleted file mode 100644 index 5dba4a0..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt deleted file mode 100644 index 89d43f2..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt +++ /dev/null @@ -1,15 +0,0 @@ -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_(%) = diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt deleted file mode 100644 index 02da2de..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt +++ /dev/null @@ -1,31 +0,0 @@ -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 =: diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py deleted file mode 100644 index 904b2f7..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/__init__,py.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/__init__,py.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt deleted file mode 100644 index a3e335a..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt +++ /dev/null @@ -1,127 +0,0 @@ -[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] diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt deleted file mode 100644 index 42371ab..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt +++ /dev/null @@ -1,19 +0,0 @@ -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: - -# -; diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py deleted file mode 100644 index e950dd2..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_parsing/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py b/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py deleted file mode 100644 index 7656420..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py +++ /dev/null @@ -1,79 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -from pathlib import Path - -import pytest - -from src.simple_config_parser.constants import HEADER_IDENT, LineType -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"] is not None - assert parser.config["section_2"]["header"] == "[section_2] ; comment" - assert parser.config["section_2"]["elements"] is not None - assert len(parser.config["section_2"]["elements"]) > 0 - - -def test_option_parsing(parser): - assert parser.config["section_1"]["elements"][0]["type"] == LineType.OPTION.value - assert parser.config["section_1"]["elements"][0]["name"] == "option_1" - assert parser.config["section_1"]["elements"][0]["value"] == "value_1" - assert parser.config["section_1"]["elements"][0]["raw"] == "option_1: value_1" - - -def test_header_parsing(parser): - header = parser.config[HEADER_IDENT] - assert isinstance(header, list) - assert len(header) > 0 - - -def test_option_block_parsing(parser): - section = "section number 5" - option_block = None - for element in parser.config[section]["elements"]: - if (element["type"] == LineType.OPTION_BLOCK.value and - element["name"] == "multi_option"): - option_block = element - break - - assert option_block is not None, "multi_option block not found" - assert option_block["type"] == LineType.OPTION_BLOCK.value - assert option_block["name"] == "multi_option" - assert option_block["raw"] == "multi_option:" - - expected_values = [ - "# these are multi-line values", - "value_5_1", - "value_5_2 ; here is a comment", - "value_5_3" - ] - assert option_block["value"] == expected_values, ( - f"Expected values: {expected_values}, " - f"got: {option_block['value']}" - ) diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py deleted file mode 100644 index 7c932c6..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py deleted file mode 100644 index a423745..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py +++ /dev/null @@ -1,186 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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.constants import LineType -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"]["elements"][4] is not None - assert parser.config["section_1"]["elements"][4]["type"] == LineType.OPTION.value - assert parser.config["section_1"]["elements"][4]["name"] == "new_option" - assert parser.config["section_1"]["elements"][4]["value"] == "new_value" - assert parser.config["section_1"]["elements"][4]["raw"] == "new_option: new_value\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"]["elements"][1] is not None - assert parser.config["section_2"]["elements"][1]["type"] == LineType.OPTION_BLOCK.value - assert parser.config["section_2"]["elements"][1]["name"] == "array_option" - assert parser.config["section_2"]["elements"][1]["value"] == [ - "value_1", - "value_2", - "value_3", - ] - assert parser.config["section_2"]["elements"][1]["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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py deleted file mode 100644 index f9272df..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py +++ /dev/null @@ -1,22 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 - -from src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") - - -def test_read_file(): - parser = SimpleConfigParser() - parser.read_file(TEST_DATA_PATH) - assert parser.config is not None - assert parser.config.keys() is not None diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py deleted file mode 100644 index 0b731ce..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py +++ /dev/null @@ -1,65 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 ( - DuplicateSectionError, -) - - -def test_get_sections(parser): - expected_keys = { - "section_1", - "section_2", - "section_3", - "section_4", - "section number 5", - } - assert expected_keys.issubset( - parser.get_sections() - ), f"Expected keys: {expected_keys}, got: {parser.get_sections()}" - - -def test_has_section(parser): - assert parser.has_section("section_1") is True - assert parser.has_section("not_available") is False - - -def test_add_section(parser): - pre_add_count = len(parser.get_sections()) - parser.add_section("new_section") - parser.add_section("new_section2") - assert parser.has_section("new_section") is True - assert parser.has_section("new_section2") is True - assert len(parser.get_sections()) == pre_add_count + 2 - - new_section = parser.config["new_section"] - assert isinstance(new_section, dict) - assert new_section["header"] == "[new_section]\n" - assert new_section["elements"] is not None - assert new_section["elements"] == [] - - new_section2 = parser.config["new_section2"] - assert isinstance(new_section2, dict) - assert new_section2["header"] == "[new_section2]\n" - assert new_section2["elements"] is not None - assert new_section2["elements"] == [] - - -def test_add_section_duplicate(parser): - with pytest.raises(DuplicateSectionError): - parser.add_section("section_1") - - -def test_remove_section(parser): - pre_remove_count = len(parser.get_sections()) - parser.remove_section("section_1") - assert parser.has_section("section_1") is False - assert len(parser.get_sections()) == pre_remove_count - 1 - assert "section_1" not in parser.config diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py deleted file mode 100644 index 67b205b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py +++ /dev/null @@ -1,119 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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, -) - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") -# TEST_DATA_PATH_2 = BASE_DIR.joinpath("test_config_1_write.cfg") - - -def test_write_file_exception(): - parser = SimpleConfigParser() - with pytest.raises(ValueError): - parser.write_file(None) # noqa - - -def test_write_to_file(tmp_path): - tmp_file = Path(tmp_path).joinpath("tmp_config.cfg") - parser1 = SimpleConfigParser() - parser1.read_file(TEST_DATA_PATH) - # parser1.write_file(TEST_DATA_PATH_2) - parser1.write_file(tmp_file) - - parser2 = SimpleConfigParser() - parser2.read_file(tmp_file) - - assert tmp_file.exists() - assert parser2.config is not None - - with open(TEST_DATA_PATH, "r") as original, open(tmp_file, "r") as written: - assert original.read() == written.read() - -def test_remove_option_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/remove_option") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and remove option - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.remove_option("section_1", "option_to_remove") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert not parser2.has_option("section_1", "option_to_remove") - -def test_remove_section_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/remove_section") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and remove section - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.remove_section("section_to_remove") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert not parser2.has_section("section_to_remove") - assert "section_1" in parser2.get_sections() - assert "section_2" in parser2.get_sections() - -def test_add_option_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/add_option") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and add option - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.set_option("section_1", "new_option", "new_value") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert parser2.has_option("section_1", "new_option") - assert parser2.getval("section_1", "new_option") == "new_value" diff --git a/kiauh/core/submodules/simple_config_parser/tests/utils.py b/kiauh/core/submodules/simple_config_parser/tests/utils.py deleted file mode 100644 index 91299cf..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 - - -def load_testdata_from_file(file_path: Path): - """Helper function to load test data from a text file""" - - with open(file_path, "r") as f: - return [line.replace("\n", "") for line in f] diff --git a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/value_conversion/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py b/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py deleted file mode 100644 index 893ecc1..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py +++ /dev/null @@ -1,89 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") - - -@pytest.fixture -def parser(): - parser = SimpleConfigParser() - for line in load_testdata_from_file(TEST_DATA_PATH): - parser._parse_line(line) # noqa - - return parser - - -def test_get_int_conv(parser): - should_be_int = parser._get_conv("section_1", "option_1_2", int) - assert isinstance(should_be_int, int) - - -def test_get_float_conv(parser): - should_be_float = parser._get_conv("section_1", "option_1_3", float) - assert isinstance(should_be_float, float) - - -def test_get_bool_conv(parser): - should_be_bool = parser._get_conv( - "section_1", "option_1_1", parser._convert_to_boolean - ) - assert isinstance(should_be_bool, bool) - - -def test_get_int_conv_fallback(parser): - should_be_fallback_int = parser._get_conv( - "section_1", "option_128", int, fallback=128 - ) - assert isinstance(should_be_fallback_int, int) - assert should_be_fallback_int == 128 - assert parser._get_conv("section_1", "option_128", int, None) is None - - -def test_get_float_conv_fallback(parser): - should_be_fallback_float = parser._get_conv( - "section_1", "option_128", float, fallback=1.234 - ) - assert isinstance(should_be_fallback_float, float) - assert should_be_fallback_float == 1.234 - - assert parser._get_conv("section_1", "option_128", float, None) is None - - -def test_get_bool_conv_fallback(parser): - should_be_fallback_bool = parser._get_conv( - "section_1", "option_128", parser._convert_to_boolean, fallback=True - ) - assert isinstance(should_be_fallback_bool, bool) - assert should_be_fallback_bool is True - - assert ( - parser._get_conv("section_1", "option_128", parser._convert_to_boolean, None) - is None - ) - - -def test_get_int_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", int) - - -def test_get_float_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", float) - - -def test_get_bool_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", parser._convert_to_boolean) diff --git a/kiauh/core/types/__init__.py b/kiauh/core/types/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/types/color.py b/kiauh/core/types/color.py deleted file mode 100644 index 11a317a..0000000 --- a/kiauh/core/types/color.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from enum import Enum - - -class Color(Enum): - WHITE = "\033[37m" # white - MAGENTA = "\033[35m" # magenta - GREEN = "\033[92m" # bright green - YELLOW = "\033[93m" # bright yellow - RED = "\033[91m" # bright red - CYAN = "\033[96m" # bright cyan - RST = "\033[0m" # reset format - - def __str__(self): - return self.value - - @staticmethod - def apply(text: str | int, color: "Color") -> str: - """Apply a given color to a given text string.""" - return f"{color}{text}{Color.RST}" diff --git a/kiauh/core/types/component_status.py b/kiauh/core/types/component_status.py deleted file mode 100644 index 09a02ce..0000000 --- a/kiauh/core/types/component_status.py +++ /dev/null @@ -1,32 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass -from typing import Dict, Literal - -StatusText = Literal["Installed", "Not installed", "Incomplete"] -StatusCode = Literal[0, 1, 2] -StatusMap: Dict[StatusCode, StatusText] = { - 0: "Not installed", - 1: "Incomplete", - 2: "Installed", -} - - -@dataclass -class ComponentStatus: - status: StatusCode - owner: str | None = None - repo: str | None = None - repo_url: str | None = None - branch: str = "" - local: str | None = None - remote: str | None = None - instances: int | None = None diff --git a/kiauh/extensions/__init__.py b/kiauh/extensions/__init__.py deleted file mode 100644 index b287ee9..0000000 --- a/kiauh/extensions/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions") diff --git a/kiauh/extensions/base_extension.py b/kiauh/extensions/base_extension.py deleted file mode 100644 index b0c34e4..0000000 --- a/kiauh/extensions/base_extension.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 abc import ABC, abstractmethod -from typing import Dict - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BaseExtension(ABC): - def __init__(self, metadata: Dict[str, str]): - self.metadata = metadata - - @abstractmethod - def install_extension(self, **kwargs) -> None: - raise NotImplementedError - - def update_extension(self, **kwargs) -> None: - raise NotImplementedError - - @abstractmethod - def remove_extension(self, **kwargs) -> None: - raise NotImplementedError diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py deleted file mode 100644 index e1c0b8e..0000000 --- a/kiauh/extensions/extensions_menu.py +++ /dev/null @@ -1,160 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 importlib -import inspect -import json -import textwrap -from pathlib import Path -from typing import Dict, List, Type - -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from extensions import EXTENSION_ROOT -from extensions.base_extension import BaseExtension - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class ExtensionsMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Extensions Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.extensions: Dict[str, BaseExtension] = self.discover_extensions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - i: Option(self.extension_submenu, opt_data=self.extensions.get(i)) - for i in self.extensions - } - - def discover_extensions(self) -> Dict[str, BaseExtension]: - ext_dict = {} - - for ext in EXTENSION_ROOT.iterdir(): - metadata_json = Path(ext).joinpath("metadata.json") - if not metadata_json.exists(): - continue - - try: - with open(metadata_json, "r") as m: - # read extension metadata from json - metadata = json.load(m).get("metadata") - module_name = metadata.get("module") - module_path = f"kiauh.extensions.{ext.name}.{module_name}" - - # get the class name of the extension - module = importlib.import_module(module_path) - - def predicate(o): - return ( - inspect.isclass(o) - and issubclass(o, BaseExtension) - and o != BaseExtension - ) - - ext_class: type = inspect.getmembers(module, predicate)[0][1] - - # instantiate the extension with its metadata and add to dict - ext_instance: BaseExtension = ext_class(metadata) - ext_dict[f"{metadata.get('index')}"] = ext_instance - - except (IOError, json.JSONDecodeError, ImportError) as e: - print(f"Failed loading extension {ext}: {e}") - - return dict(sorted(ext_dict.items(), key=lambda x: int(x[0]))) - - def extension_submenu(self, **kwargs): - ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() - - def print_menu(self) -> None: - line1 = Color.apply("Available Extensions:", Color.YELLOW) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {line1:<62} β•‘ - β•‘ β•‘ - """ - )[1:] - print(menu, end="") - - for extension in self.extensions.values(): - index = extension.metadata.get("index") - name = extension.metadata.get("display_name") - row = f"{index}) {name}" - print(f"β•‘ {row:<53} β•‘") - print("β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’") - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class ExtensionSubmenu(BaseMenu): - def __init__( - self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = extension.metadata.get("display_name") - self.title_color = Color.YELLOW - self.extension = extension - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else ExtensionsMenu - ) - - def set_options(self) -> None: - self.options["1"] = Option(self.extension.install_extension) - if self.extension.metadata.get("updates"): - self.options["2"] = Option(self.extension.update_extension) - self.options["3"] = Option(self.extension.remove_extension) - else: - self.options["2"] = Option(self.extension.remove_extension) - - def print_menu(self) -> None: - line_width = 53 - description: List[str] = self.extension.metadata.get("description", []) - description_text = Logger.format_content( - description, - line_width, - border_left="β•‘", - border_right="β•‘", - ) - - menu = textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - menu += f"{description_text}\n" - menu += textwrap.dedent( - """ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ 1) Install β•‘ - """ - )[1:] - - if self.extension.metadata.get("updates"): - menu += "β•‘ 2) Update β•‘\n" - menu += "β•‘ 3) Remove β•‘\n" - else: - menu += "β•‘ 2) Remove β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - - print(menu, end="") diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py deleted file mode 100644 index 6714ef1..0000000 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXT_MODULE_NAME = "gcode_shell_command.py" -MODULE_PATH = Path(__file__).resolve().parent -MODULE_ASSETS = MODULE_PATH.joinpath("assets") -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") -EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) -EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) -EXAMPLE_CFG_SRC = MODULE_ASSETS.joinpath("shell_command.cfg") diff --git a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py deleted file mode 100644 index 85b664b..0000000 --- a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py +++ /dev/null @@ -1,94 +0,0 @@ -# Run a shell command via gcode -# -# Copyright (C) 2019 Eric Callahan -# -# This file may be distributed under the terms of the GNU GPLv3 license. -import logging -import os -import shlex -import subprocess - - -class ShellCommand: - def __init__(self, config): - self.name = config.get_name().split()[-1] - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object("gcode") - cmd = config.get("command") - cmd = os.path.expanduser(cmd) - self.command = shlex.split(cmd) - self.timeout = config.getfloat("timeout", 2.0, above=0.0) - self.verbose = config.getboolean("verbose", True) - self.proc_fd = None - self.partial_output = "" - self.gcode.register_mux_command( - "RUN_SHELL_COMMAND", - "CMD", - self.name, - self.cmd_RUN_SHELL_COMMAND, - desc=self.cmd_RUN_SHELL_COMMAND_help, - ) - - def _process_output(self, eventime): - if self.proc_fd is None: - return - try: - data = os.read(self.proc_fd, 4096) - except Exception: - pass - data = self.partial_output + data.decode() - if "\n" not in data: - self.partial_output = data - return - elif data[-1] != "\n": - split = data.rfind("\n") + 1 - self.partial_output = data[split:] - data = data[:split] - else: - self.partial_output = "" - self.gcode.respond_info(data) - - cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" - - def cmd_RUN_SHELL_COMMAND(self, params): - gcode_params = params.get("PARAMS", "") - gcode_params = shlex.split(gcode_params) - reactor = self.printer.get_reactor() - try: - proc = subprocess.Popen( - self.command + gcode_params, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - except Exception: - logging.exception("shell_command: Command {%s} failed" % (self.name)) - raise self.gcode.error("Error running command {%s}" % (self.name)) - if self.verbose: - self.proc_fd = proc.stdout.fileno() - self.gcode.respond_info("Running Command {%s}...:" % (self.name)) - hdl = reactor.register_fd(self.proc_fd, self._process_output) - eventtime = reactor.monotonic() - endtime = eventtime + self.timeout - complete = False - while eventtime < endtime: - eventtime = reactor.pause(eventtime + 0.05) - if proc.poll() is not None: - complete = True - break - if not complete: - proc.terminate() - if self.verbose: - if self.partial_output: - self.gcode.respond_info(self.partial_output) - self.partial_output = "" - if complete: - msg = "Command {%s} finished\n" % (self.name) - else: - msg = "Command {%s} timed out" % (self.name) - self.gcode.respond_info(msg) - reactor.unregister_fd(hdl) - self.proc_fd = None - - -def load_config_prefix(config): - return ShellCommand(config) diff --git a/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg b/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg deleted file mode 100644 index 34e7581..0000000 --- a/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[gcode_shell_command hello_world] -command: echo hello world -timeout: 2. -verbose: True -[gcode_macro HELLO_WORLD] -gcode: - RUN_SHELL_COMMAND CMD=hello_world \ No newline at end of file diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py deleted file mode 100644 index 511aef0..0000000 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ /dev/null @@ -1,131 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 os -import shutil -from typing import List - -from components.klipper.klipper import Klipper -from core.backup_manager.backup_manager import BackupManager -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from extensions.gcode_shell_cmd import ( - EXAMPLE_CFG_SRC, - EXTENSION_SRC, - EXTENSION_TARGET_PATH, - KLIPPER_DIR, - KLIPPER_EXTRAS, -) -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances - - -# noinspection PyMethodMayBeStatic -class GcodeShellCmdExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - install_example = get_confirm("Create an example shell command?", False, False) - - klipper_dir_exists = check_file_exist(KLIPPER_DIR) - if not klipper_dir_exists: - Logger.print_warn( - "No Klipper directory found! Unable to install extension." - ) - return - - extension_installed = check_file_exist(EXTENSION_TARGET_PATH) - overwrite = True - if extension_installed: - overwrite = get_confirm( - "Extension seems to be installed already. Overwrite?", - True, - False, - ) - - if not overwrite: - Logger.print_warn("Installation aborted due to user request.") - return - - instances = get_instances(Klipper) - InstanceManager.stop_all(instances) - - try: - Logger.print_status(f"Copy extension to '{KLIPPER_EXTRAS}' ...") - shutil.copy(EXTENSION_SRC, EXTENSION_TARGET_PATH) - except OSError as e: - Logger.print_error(f"Unable to install extension: {e}") - return - - if install_example: - self.install_example_cfg(instances) - - InstanceManager.start_all(instances) - - Logger.print_ok("Installing G-Code Shell Command extension successful!") - - def remove_extension(self, **kwargs) -> None: - extension_installed = check_file_exist(EXTENSION_TARGET_PATH) - if not extension_installed: - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - - question = "Do you really want to remove the extension?" - if get_confirm(question, True, False): - try: - Logger.print_status(f"Removing '{EXTENSION_TARGET_PATH}' ...") - os.remove(EXTENSION_TARGET_PATH) - Logger.print_ok("Extension successfully removed!") - except OSError as e: - Logger.print_error(f"Unable to remove extension: {e}") - - Logger.print_warn("PLEASE NOTE:") - Logger.print_warn( - "Remaining gcode shell command will cause Klipper to throw an error." - ) - Logger.print_warn("Make sure to remove them from the printer.cfg!") - - def install_example_cfg(self, instances: List[Klipper]): - cfg_dirs = [instance.base.cfg_dir for instance in instances] - # copy extension to klippy/extras - for cfg_dir in cfg_dirs: - Logger.print_status(f"Create shell_command.cfg in '{cfg_dir}' ...") - if check_file_exist(cfg_dir.joinpath("shell_command.cfg")): - Logger.print_info("File already exists! Skipping ...") - continue - try: - shutil.copy(EXAMPLE_CFG_SRC, cfg_dir) - Logger.print_ok("Done!") - except OSError as e: - Logger.warn(f"Unable to create example config: {e}") - - # backup each printer.cfg before modification - bm = BackupManager() - for instance in instances: - bm.backup_file( - instance.cfg_file, - custom_filename=f"{instance.suffix}.printer.cfg", - ) - - # add section to printer.cfg if not already defined - section = "include shell_command.cfg" - cfg_files = [instance.cfg_file for instance in instances] - for cfg_file in cfg_files: - Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...") - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if scp.has_section(section): - Logger.print_info("Section already defined! Skipping ...") - continue - scp.add_section(section) - scp.write_file(cfg_file) - Logger.print_ok("Done!") diff --git a/kiauh/extensions/gcode_shell_cmd/metadata.json b/kiauh/extensions/gcode_shell_cmd/metadata.json deleted file mode 100644 index 7d7ccdc..0000000 --- a/kiauh/extensions/gcode_shell_cmd/metadata.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "metadata": { - "index": 1, - "module": "gcode_shell_cmd_extension", - "maintained_by": "dw-0", - "display_name": "G-Code Shell Command", - "description": ["Run a shell commands from gcode."] - } -} diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py deleted file mode 100644 index e65e0f5..0000000 --- a/kiauh/extensions/klipper_backup/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # -# https://github.com/Staubgeborener/klipper-backup # -# https://klipperbackup.xyz # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXT_MODULE_NAME = "klipper_backup_extension.py" -MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") -KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") -KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") -KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py deleted file mode 100644 index a3e1242..0000000 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ /dev/null @@ -1,160 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # -# https://github.com/Staubgeborener/klipper-backup # -# https://klipperbackup.xyz # -# # -# 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 os -import subprocess -from pathlib import Path - -from core.constants import SYSTEMD -from core.logger import Logger -from extensions.base_extension import BaseExtension -from extensions.klipper_backup import ( - KLIPPERBACKUP_CONFIG_DIR, - KLIPPERBACKUP_DIR, - KLIPPERBACKUP_REPO_URL, - MOONRAKER_CONF, -) -from utils.fs_utils import check_file_exist, remove_with_sudo -from utils.git_utils import git_cmd_clone -from utils.input_utils import get_confirm -from utils.sys_utils import cmd_sysctl_manage, remove_system_service, unit_file_exists - - -class KlipperbackupExtension(BaseExtension): - def remove_extension(self, **kwargs) -> None: - if not check_file_exist(KLIPPERBACKUP_DIR): - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - - def uninstall_service(service_name: str, unit_type: str) -> bool: - try: - full_service_name = f"{service_name}.{unit_type}" - if unit_type == "service": - remove_system_service(full_service_name) - elif unit_type == "timer": - full_service_path: Path = SYSTEMD.joinpath(full_service_name) - Logger.print_status(f"Removing {full_service_name} ...") - remove_with_sudo(full_service_path) - Logger.print_ok(f"{service_name}.{unit_type} successfully removed!") - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - else: - Logger.print_error( - f"Unknown unit type {unit_type} of {full_service_name}" - ) - except: - Logger.print_error(f"Failed to remove {full_service_name}: {str(e)}") - - def check_crontab_entry(entry) -> bool: - try: - crontab_content = subprocess.check_output( - ["crontab", "-l"], stderr=subprocess.DEVNULL, text=True - ) - except subprocess.CalledProcessError: - return False - return any(entry in line for line in crontab_content.splitlines()) - - def remove_moonraker_entry(): - original_file_path = MOONRAKER_CONF - comparison_file_path = os.path.join( - str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf" - ) - if not ( - os.path.exists(original_file_path) - and os.path.exists(comparison_file_path) - ): - return False - with open(original_file_path, "r") as original_file, open( - comparison_file_path, "r" - ) as comparison_file: - original_content = original_file.read() - comparison_content = comparison_file.read() - if comparison_content in original_content: - Logger.print_status("Removing Klipper-Backup moonraker entry ...") - modified_content = original_content.replace( - comparison_content, "" - ).strip() - modified_content = "\n".join( - line for line in modified_content.split("\n") if line.strip() - ) - with open(original_file_path, "w") as original_file: - original_file.write(modified_content) - Logger.print_ok("Klipper-Backup moonraker entry successfully removed!") - return True - return False - - if get_confirm("Do you really want to remove the extension?", True, False): - # Remove systemd timer and services - service_names = [ - "klipper-backup-on-boot", - "klipper-backup-filewatch", - "klipper-backup", - ] - unit_types = ["timer", "service"] - - for service_name in service_names: - for unit_type in unit_types: - if unit_file_exists(service_name, unit_type): - uninstall_service(service_name, unit_type) - - # Remnove crontab entry - try: - if check_crontab_entry("/klipper-backup/script.sh"): - Logger.print_status("Removing Klipper-Backup crontab entry ...") - crontab_content = subprocess.check_output( - ["crontab", "-l"], text=True - ) - modified_content = "\n".join( - line - for line in crontab_content.splitlines() - if "/klipper-backup/script.sh" not in line - ) - subprocess.run( - ["crontab", "-"], - input=modified_content + "\n", - text=True, - check=True, - ) - Logger.print_ok( - "Klipper-Backup crontab entry successfully removed!" - ) - except subprocess.CalledProcessError: - Logger.print_error("Unable to remove the Klipper-Backup cron entry") - - # Remove moonraker entry - try: - remove_moonraker_entry() - except: - Logger.print_error( - "Unable to remove the Klipper-Backup moonraker entry" - ) - - # Remove Klipper-backup extension - Logger.print_status("Removing Klipper-Backup extension ...") - try: - remove_with_sudo(KLIPPERBACKUP_DIR) - if check_file_exist(KLIPPERBACKUP_CONFIG_DIR): - remove_with_sudo(KLIPPERBACKUP_CONFIG_DIR) - Logger.print_ok("Extension Klipper-Backup successfully removed!") - except: - Logger.print_error("Unable to remove Klipper-Backup extension") - - def install_extension(self, **kwargs) -> None: - if not KLIPPERBACKUP_DIR.exists(): - git_cmd_clone(KLIPPERBACKUP_REPO_URL, KLIPPERBACKUP_DIR) - subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")]) - subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")]) - - def update_extension(self, **kwargs) -> None: - if not check_file_exist(KLIPPERBACKUP_DIR): - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"]) diff --git a/kiauh/extensions/klipper_backup/metadata.json b/kiauh/extensions/klipper_backup/metadata.json deleted file mode 100644 index 68f215d..0000000 --- a/kiauh/extensions/klipper_backup/metadata.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "metadata": { - "index": 4, - "module": "klipper_backup_extension", - "maintained_by": "Staubgeborener", - "display_name": "Klipper-Backup", - "description": ["Backup all your Klipper files to GitHub"], - "updates": true - } -} diff --git a/kiauh/extensions/mainsail_theme_installer/__init__.py b/kiauh/extensions/mainsail_theme_installer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py deleted file mode 100644 index 82c50e0..0000000 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ /dev/null @@ -1,188 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 csv -import shutil -import textwrap -import urllib.request -from dataclasses import dataclass -from typing import Any, Dict, List, Type - -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - DisplayType, - print_instance_overview, -) -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from extensions.base_extension import BaseExtension -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_selection_input -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -@dataclass -class ThemeData: - name: str - short_note: str - author: str - repo: str - - -# noinspection PyMethodMayBeStatic -class MainsailThemeInstallerExtension(BaseExtension): - instances: List[Klipper] = get_instances(Klipper) - - def install_extension(self, **kwargs) -> None: - MainsailThemeInstallMenu(self.instances).run() - - def remove_extension(self, **kwargs) -> None: - print_instance_overview( - self.instances, - display_type=DisplayType.PRINTER_NAME, - show_headline=True, - show_index=True, - show_select_all=True, - ) - printer_list = get_printer_selection(self.instances, True) - if printer_list is None: - return - - for printer in printer_list: - Logger.print_status(f"Uninstalling theme from {printer.base.cfg_dir} ...") - theme_dir = printer.base.cfg_dir.joinpath(".theme") - if not theme_dir.exists(): - Logger.print_info(f"{theme_dir} not found. Skipping ...") - continue - try: - shutil.rmtree(theme_dir) - Logger.print_ok("Theme successfully uninstalled!") - except OSError as e: - Logger.print_error("Unable to uninstall theme") - Logger.print_error(e) - - -# noinspection PyMethodMayBeStatic -class MainsailThemeInstallMenu(BaseMenu): - THEMES_URL: str = ( - "https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" - ) - - def __init__(self, instances: List[Klipper]): - super().__init__() - self.title = "Mainsail Theme Installer" - self.title_color = Color.YELLOW - self.themes: List[ThemeData] = self.load_themes() - self.instances = instances - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from extensions.extensions_menu import ExtensionsMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else ExtensionsMenu - ) - - def set_options(self) -> None: - self.options = { - f"{index}": Option(self.install_theme, opt_index=f"{index}") - for index in range(len(self.themes)) - } - - def print_menu(self) -> None: - line1 = Color.apply( - "A preview of each Mainsail theme can be found here:", Color.YELLOW - ) - menu = textwrap.dedent( - f""" - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - β•‘ {line1:<62} β•‘ - β•‘ https://docs.mainsail.xyz/theming/themes β•‘ - β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’ - """ - )[1:] - for i, theme in enumerate(self.themes): - j: str = f" {i}" if i < 10 else f"{i}" - row: str = f"{j}) [{theme.name}]" - menu += f"β•‘ {row:<53} β•‘\n" - menu += "β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’\n" - print(menu, end="") - - def load_themes(self) -> List[ThemeData]: - with urllib.request.urlopen(self.THEMES_URL) as response: - themes: List[ThemeData] = [] - content: str = response.read().decode() - csv_data: List[str] = content.splitlines() - fieldnames = ["name", "short_note", "author", "repo"] - csv_reader = csv.DictReader(csv_data, fieldnames=fieldnames, delimiter=",") - next(csv_reader) # skip the header of the csv file - for row in csv_reader: - row: Dict[str, str] # type: ignore - theme: ThemeData = ThemeData(**row) - themes.append(theme) - - return themes - - def install_theme(self, **kwargs: Any): - opt_index: str | None = kwargs.get("opt_index", None) - - if not opt_index: - raise ValueError("No option index provided") - - index: int = int(opt_index) - theme_data: ThemeData = self.themes[index] - theme_author: str = theme_data.author - theme_repo: str = theme_data.repo - theme_repo_url: str = f"https://github.com/{theme_author}/{theme_repo}" - - print_instance_overview( - self.instances, - display_type=DisplayType.PRINTER_NAME, - show_headline=True, - show_index=True, - show_select_all=True, - ) - - printer_list = get_printer_selection(self.instances, True) - if printer_list is None: - return - - for printer in printer_list: - git_clone_wrapper(theme_repo_url, printer.base.cfg_dir.joinpath(".theme")) - - if len(theme_data.short_note) > 1: - Logger.print_warn("Info from the creator:", prefix=False, start="\n") - Logger.print_info(theme_data.short_note, prefix=False, end="\n\n") - - -def get_printer_selection( - instances: List[InstanceType], is_install: bool -) -> List[InstanceType] | None: - options = [str(i) for i in range(len(instances))] - options.extend(["a", "b"]) - - if is_install: - q = "Select the printer to install the theme for" - else: - q = "Select the printer to remove the theme from" - selection = get_selection_input(q, options) - - install_for = [] - if selection == "b": - return None - elif selection == "a": - install_for.extend(instances) - else: - instance = instances[int(selection)] - install_for.append(instance) - - return install_for diff --git a/kiauh/extensions/mainsail_theme_installer/metadata.json b/kiauh/extensions/mainsail_theme_installer/metadata.json deleted file mode 100644 index ffb802a..0000000 --- a/kiauh/extensions/mainsail_theme_installer/metadata.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "metadata": { - "index": 2, - "module": "mainsail_theme_installer_extension", - "maintained_by": "dw-0", - "display_name": "Mainsail Theme Installer", - "description": ["Install Mainsail Themes maintained by the Mainsail community."] - } -} diff --git a/kiauh/extensions/mobileraker/__init__.py b/kiauh/extensions/mobileraker/__init__.py deleted file mode 100644 index 8241381..0000000 --- a/kiauh/extensions/mobileraker/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.backup_manager import BACKUP_ROOT_DIR -from core.constants import SYSTEMD - -# repo -MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" - -# names -MOBILERAKER_SERVICE_NAME = "mobileraker.service" -MOBILERAKER_UPDATER_SECTION_NAME = "update_manager mobileraker" -MOBILERAKER_LOG_NAME = "mobileraker.log" - -# directories -MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") -MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") -MOBILERAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mobileraker-backups") - -# files -MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") -MOBILERAKER_REQ_FILE = MOBILERAKER_DIR.joinpath("scripts/mobileraker-requirements.txt") -MOBILERAKER_SERVICE_FILE = SYSTEMD.joinpath(MOBILERAKER_SERVICE_NAME) diff --git a/kiauh/extensions/mobileraker/metadata.json b/kiauh/extensions/mobileraker/metadata.json deleted file mode 100644 index 42aa311..0000000 --- a/kiauh/extensions/mobileraker/metadata.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "metadata": { - "index": 3, - "module": "mobileraker_extension", - "maintained_by": "Clon1998", - "display_name": "Mobileraker", - "description": [ - "Companion for Mobileraker, enabling push notification for Klipper using Moonraker." - ], - "updates": true - } -} diff --git a/kiauh/extensions/mobileraker/mobileraker_extension.py b/kiauh/extensions/mobileraker/mobileraker_extension.py deleted file mode 100644 index a584105..0000000 --- a/kiauh/extensions/mobileraker/mobileraker_extension.py +++ /dev/null @@ -1,192 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.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 extensions.base_extension import BaseExtension -from extensions.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 utils.common import check_install_dependencies -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, -) - - -# noinspection PyMethodMayBeStatic -class MobilerakerExtension(BaseExtension): - def install_extension(self, **kwargs) -> 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: - self._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 update_extension(self, **kwargs) -> None: - try: - if not MOBILERAKER_DIR.exists(): - Logger.print_info( - "Mobileraker's companion doesn't 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: - self._backup_mobileraker_dir() - - git_pull_wrapper(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 remove_extension(self, **kwargs) -> 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 _patch_mobileraker_update_manager(self, 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 _backup_mobileraker_dir(self) -> 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, - ) diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py deleted file mode 100644 index 6780bb1..0000000 --- a/kiauh/extensions/obico/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -# repo -OBICO_REPO = "https://github.com/TheSpaghettiDetective/moonraker-obico.git" - -# names -OBICO_SERVICE_NAME = "moonraker-obico.service" -OBICO_ENV_FILE_NAME = "moonraker-obico.env" -OBICO_CFG_NAME = "moonraker-obico.cfg" -OBICO_CFG_SAMPLE_NAME = "moonraker-obico.cfg.sample" -OBICO_LOG_NAME = "moonraker-obico.log" -OBICO_UPDATE_CFG_NAME = "moonraker-obico-update.cfg" -OBICO_UPDATE_CFG_SAMPLE_NAME = "moonraker-obico-update.cfg.sample" -OBICO_MACROS_CFG_NAME = "moonraker_obico_macros.cfg" - -# directories -OBICO_DIR = Path.home().joinpath("moonraker-obico") -OBICO_ENV_DIR = Path.home().joinpath("moonraker-obico-env") - -# files -OBICO_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_SERVICE_NAME}") -OBICO_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_ENV_FILE_NAME}") -OBICO_LINK_SCRIPT = OBICO_DIR.joinpath("scripts/link.sh") -OBICO_REQ_FILE = OBICO_DIR.joinpath("requirements.txt") diff --git a/kiauh/extensions/obico/assets/moonraker-obico.env b/kiauh/extensions/obico/assets/moonraker-obico.env deleted file mode 100644 index 3c3d32b..0000000 --- a/kiauh/extensions/obico/assets/moonraker-obico.env +++ /dev/null @@ -1 +0,0 @@ -OBICO_ARGS="-m moonraker_obico.app -c %CFG%" diff --git a/kiauh/extensions/obico/assets/moonraker-obico.service b/kiauh/extensions/obico/assets/moonraker-obico.service deleted file mode 100644 index e6bed45..0000000 --- a/kiauh/extensions/obico/assets/moonraker-obico.service +++ /dev/null @@ -1,16 +0,0 @@ -#Systemd service file for moonraker-obico -[Unit] -Description=Moonraker-Obico -After=network-online.target moonraker.service - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -WorkingDirectory=%OBICO_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python3 $OBICO_ARGS -Restart=always -RestartSec=5 diff --git a/kiauh/extensions/obico/metadata.json b/kiauh/extensions/obico/metadata.json deleted file mode 100644 index cdf5753..0000000 --- a/kiauh/extensions/obico/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "metadata": { - "index": 6, - "module": "moonraker_obico_extension", - "maintained_by": "Obico", - "display_name": "Obico for Klipper", - "description": [ - "Open source 3D Printing cloud and AI", - "- AI-Powered Failure Detection", - "- Free Remote Monitoring and Access", - "- 25FPS High-Def Webcam Streaming", - "- Free 4.9-Star Mobile App" - ], - "updates": true - } -} diff --git a/kiauh/extensions/obico/moonraker_obico.py b/kiauh/extensions/obico/moonraker_obico.py deleted file mode 100644 index ddf442c..0000000 --- a/kiauh/extensions/obico/moonraker_obico.py +++ /dev/null @@ -1,145 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker.moonraker import Moonraker -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from extensions.obico import ( - OBICO_CFG_NAME, - OBICO_DIR, - OBICO_ENV_DIR, - OBICO_ENV_FILE_NAME, - OBICO_ENV_FILE_TEMPLATE, - OBICO_LINK_SCRIPT, - OBICO_LOG_NAME, - OBICO_SERVICE_TEMPLATE, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class MoonrakerObico: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = OBICO_LOG_NAME - dir: Path = OBICO_DIR - env_dir: Path = OBICO_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - is_linked: bool = False - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - MoonrakerObico, self.suffix - ) - self.data_dir: Path = self.base.data_dir - self.cfg_file = self.base.cfg_dir.joinpath(OBICO_CFG_NAME) - self.is_linked: bool = self._check_link_status() - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Obico for Klipper Instance ...") - - try: - create_folders(self.base.base_folders) - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(OBICO_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def link(self) -> None: - Logger.print_status( - f"Linking instance for printer {self.data_dir.name} to the Obico server ..." - ) - try: - cmd = [f"{OBICO_LINK_SCRIPT} -q -c {self.cfg_file}"] - if self.suffix: - cmd.append(f"-n {self.suffix}") - run(cmd, check=True, shell=True) - except CalledProcessError as e: - Logger.print_error(f"Error during Obico linking: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = OBICO_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%OBICO_DIR%", - self.dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(OBICO_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = OBICO_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - env_file_content = env_template_file_content.replace( - "%CFG%", - f"{self.cfg_file}", - ) - return env_file_content - - def _check_link_status(self) -> bool: - if not self.cfg_file or not self.cfg_file.exists(): - return False - - scp = SimpleConfigParser() - scp.read_file(self.cfg_file) - return scp.getval("server", "auth_token", None) is not None diff --git a/kiauh/extensions/obico/moonraker_obico_extension.py b/kiauh/extensions/obico/moonraker_obico_extension.py deleted file mode 100644 index 82f92b2..0000000 --- a/kiauh/extensions/obico/moonraker_obico_extension.py +++ /dev/null @@ -1,373 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import SUFFIX_BLACKLIST -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from extensions.obico import ( - OBICO_CFG_SAMPLE_NAME, - OBICO_DIR, - OBICO_ENV_DIR, - OBICO_MACROS_CFG_NAME, - OBICO_REPO, - OBICO_REQ_FILE, - OBICO_UPDATE_CFG_NAME, - OBICO_UPDATE_CFG_SAMPLE_NAME, -) -from extensions.obico.moonraker_obico import ( - MoonrakerObico, -) -from utils.common import check_install_dependencies, moonraker_exists -from utils.config_utils import ( - add_config_section, - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm, get_selection_input, get_string_input -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class ObicoExtension(BaseExtension): - server_url: str - - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Obico for Klipper ...") - - # check if moonraker is installed. if not, notify the user and exit - if not moonraker_exists(): - return - - # if obico is already installed, ask if the user wants to repair an - # incomplete installation or link to the obico server - force_clone = False - obico_instances: List[MoonrakerObico] = get_instances(MoonrakerObico) - if obico_instances: - self._print_is_already_installed() - options = ["l", "r", "b"] - action = get_selection_input("Perform action", option_list=options) - if action.lower() == "b": - Logger.print_info("Exiting Obico for Klipper installation ...") - return - elif action.lower() == "l": - unlinked_instances: List[MoonrakerObico] = [ - obico for obico in obico_instances if not obico.is_linked - ] - self._link_obico_instances(unlinked_instances) - return - else: - Logger.print_status("Re-Installing Obico for Klipper ...") - force_clone = True - - # let the user confirm installation - kl_instances: List[Klipper] = get_instances(Klipper) - mr_instances: List[Moonraker] = get_instances(Moonraker) - self._print_moonraker_instances(mr_instances) - if not get_confirm( - "Continue Obico for Klipper installation?", - default_choice=True, - allow_go_back=True, - ): - return - - try: - git_clone_wrapper(OBICO_REPO, OBICO_DIR, force=force_clone) - self._install_dependencies() - - # ask the user for the obico server url - self._get_server_url() - - # create obico instances - for moonraker in mr_instances: - instance = MoonrakerObico(suffix=moonraker.suffix) - instance.create() - - cmd_sysctl_service(instance.service_file_path.name, "enable") - - # create obico config - self._create_obico_cfg(instance, moonraker) - - # create obico macros - self._create_obico_macros_cfg(moonraker) - - # create obico update manager - self._create_obico_update_manager_cfg(moonraker) - - cmd_sysctl_service(instance.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # add to klippers config - self._patch_printer_cfg(kl_instances) - InstanceManager.restart_all(kl_instances) - - # add to moonraker update manager - self._patch_moonraker_conf(mr_instances) - InstanceManager.restart_all(mr_instances) - - # check linking of / ask for linking instances - self._check_and_opt_link_instances() - - Logger.print_dialog( - DialogType.SUCCESS, - ["Obico for Klipper successfully installed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper installation:\n{e}") - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Obico for Klipper ...") - try: - instances = get_instances(MoonrakerObico) - InstanceManager.stop_all(instances) - - git_pull_wrapper(OBICO_DIR) - self._install_dependencies() - - InstanceManager.start_all(instances) - Logger.print_ok("Obico for Klipper successfully updated!") - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper update:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Obico for Klipper ...") - - kl_instances: List[Klipper] = get_instances(Klipper) - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico) - - try: - self._remove_obico_instances(ob_instances) - self._remove_obico_dir() - self._remove_obico_env() - remove_config_section(f"include {OBICO_MACROS_CFG_NAME}", kl_instances) - remove_config_section(f"include {OBICO_UPDATE_CFG_NAME}", mr_instances) - Logger.print_dialog( - DialogType.SUCCESS, - ["Obico for Klipper successfully removed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper removal:\n{e}") - - def _obico_server_url_prompt(self) -> None: - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Obico Server URL", - content=[ - "You can use a self-hosted Obico Server or the Obico Cloud. " - "For more information, please visit:", - "https://obico.io.", - "\n\n", - "For the Obico Cloud, leave it as the default:", - "https://app.obico.io.", - "\n\n", - "For self-hosted server, specify:", - "http://server_ip:port", - "For instance, 'http://192.168.0.5:3334'.", - ], - ) - - def _print_moonraker_instances(self, mr_instances: List[Moonraker]) -> None: - 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 Obico!", - ], - ) - - def _print_is_already_installed(self) -> None: - Logger.print_dialog( - DialogType.INFO, - [ - "Obico is already installed!", - "It is safe to run the installer again to link your " - "printer or repair any issues.", - "\n\n", - "You can perform the following actions:", - "L) Link printer to the Obico server", - "R) Repair installation", - ], - ) - - def _get_server_url(self) -> None: - self._obico_server_url_prompt() - pattern = r"^(http|https)://[a-zA-Z0-9./?=_%:-]*$" - self.server_url = get_string_input( - "Obico Server URL", - regex=pattern, - default="https://app.obico.io", - ) - - def _install_dependencies(self) -> None: - # install dependencies - script = OBICO_DIR.joinpath("install.sh") - package_list = parse_packages_from_file(script) - check_install_dependencies({*package_list}) - - # create virtualenv - if create_python_venv(OBICO_ENV_DIR): - install_python_requirements(OBICO_ENV_DIR, OBICO_REQ_FILE) - - def _create_obico_macros_cfg(self, moonraker: Moonraker) -> None: - macros_cfg = OBICO_DIR.joinpath(f"include_cfgs/{OBICO_MACROS_CFG_NAME}") - macros_target = moonraker.base.cfg_dir.joinpath(OBICO_MACROS_CFG_NAME) - if not macros_target.exists(): - shutil.copy(macros_cfg, macros_target) - else: - Logger.print_info( - f"Obico's '{OBICO_MACROS_CFG_NAME}' in {moonraker.base.cfg_dir} already exists! Skipped ..." - ) - - def _create_obico_update_manager_cfg(self, moonraker: Moonraker) -> None: - update_cfg = OBICO_DIR.joinpath(OBICO_UPDATE_CFG_SAMPLE_NAME) - update_cfg_target = moonraker.base.cfg_dir.joinpath(OBICO_UPDATE_CFG_NAME) - if not update_cfg_target.exists(): - shutil.copy(update_cfg, update_cfg_target) - else: - Logger.print_info( - f"Obico's '{OBICO_UPDATE_CFG_NAME}' in {moonraker.base.cfg_dir} already exists! Skipped ..." - ) - - def _create_obico_cfg( - self, current_instance: MoonrakerObico, moonraker: Moonraker - ) -> None: - cfg_template = OBICO_DIR.joinpath(OBICO_CFG_SAMPLE_NAME) - cfg_target_file = current_instance.cfg_file - - if not cfg_template.exists(): - Logger.print_error( - f"Obico config template file {cfg_target_file} does not exist!" - ) - return - - if not cfg_target_file.exists(): - shutil.copy(cfg_template, cfg_target_file) - self._patch_obico_cfg(moonraker, current_instance) - else: - Logger.print_info( - f"Obico config in {current_instance.base.cfg_dir} already exists! Skipped ..." - ) - - def _patch_obico_cfg(self, moonraker: Moonraker, obico: MoonrakerObico) -> None: - scp = SimpleConfigParser() - scp.read_file(obico.cfg_file) - scp.set_option("server", "url", self.server_url) - scp.set_option("moonraker", "port", str(moonraker.port)) - scp.set_option( - "logging", - "path", - obico.base.log_dir.joinpath(obico.log_file_name).as_posix(), - ) - scp.write_file(obico.cfg_file) - - def _patch_printer_cfg(self, klipper: List[Klipper]) -> None: - add_config_section( - section=f"include {OBICO_MACROS_CFG_NAME}", instances=klipper - ) - - def _patch_moonraker_conf(self, instances: List[Moonraker]) -> None: - add_config_section( - section=f"include {OBICO_UPDATE_CFG_NAME}", instances=instances - ) - - def _link_obico_instances(self, unlinked_instances) -> None: - for obico in unlinked_instances: - obico.link() - - def _check_and_opt_link_instances(self) -> None: - Logger.print_status("Checking link status of Obico instances ...") - - suffix_blacklist: List[str] = [ - suffix for suffix in SUFFIX_BLACKLIST if suffix != "obico" - ] - ob_instances: List[MoonrakerObico] = get_instances( - MoonrakerObico, suffix_blacklist=suffix_blacklist - ) - unlinked_instances: List[MoonrakerObico] = [ - obico for obico in ob_instances if not obico.is_linked - ] - if unlinked_instances: - Logger.print_dialog( - DialogType.INFO, - [ - "The Obico instances for the following printers are not " - "linked to the server:", - *[f"● {obico.data_dir.name}" for obico in unlinked_instances], - "\n\n", - "It will take only 10 seconds to link the printer to the Obico server.", - "For more information visit:", - "https://www.obico.io/docs/user-guides/klipper-setup/", - "\n\n", - "If you don't want to link the printer now, you can restart the " - "linking process later by running this installer again.", - ], - ) - if not get_confirm("Do you want to link the printers now?"): - Logger.print_info("Linking to Obico server skipped ...") - return - - self._link_obico_instances(unlinked_instances) - - def _remove_obico_instances( - self, - instance_list: List[MoonrakerObico], - ) -> None: - if not instance_list: - Logger.print_info("No Obico 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_obico_dir(self) -> None: - Logger.print_status("Removing Obico for Klipper directory ...") - - if not OBICO_DIR.exists(): - Logger.print_info(f"'{OBICO_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OBICO_DIR) - - def _remove_obico_env(self) -> None: - Logger.print_status("Removing Obico for Klipper environment ...") - - if not OBICO_ENV_DIR.exists(): - Logger.print_info(f"'{OBICO_ENV_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OBICO_ENV_DIR) diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py deleted file mode 100644 index 9ce8939..0000000 --- a/kiauh/extensions/octoapp/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -# repo -OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git" - -# directories -OA_DIR = Path.home().joinpath("octoapp") -OA_ENV_DIR = Path.home().joinpath("octoapp-env") - -# files -OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") -OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json") -OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh") -OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh") -OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log") - -# filenames -OA_CFG_NAME = "octoapp.conf" -OA_LOG_NAME = "octoapp.log" -OA_SYS_CFG_NAME = "octoapp-system.cfg" diff --git a/kiauh/extensions/octoapp/metadata.json b/kiauh/extensions/octoapp/metadata.json deleted file mode 100644 index 4660ab8..0000000 --- a/kiauh/extensions/octoapp/metadata.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "metadata": { - "index": 9, - "module": "octoapp_extension", - "maintained_by": "crysxd", - "display_name": "OctoApp for Klipper", - "description": [ - "Your favorite 3D printing app for iOS & Android", - "- Print notifications on your phone & watch", - "- Control and start prints from your phone", - "- Live webcam view", - "- Live Gcode preview", - "- And much much more!" - ], - "updates": true - } -} diff --git a/kiauh/extensions/octoapp/octoapp.py b/kiauh/extensions/octoapp/octoapp.py deleted file mode 100644 index 8480d70..0000000 --- a/kiauh/extensions/octoapp/octoapp.py +++ /dev/null @@ -1,73 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker import MOONRAKER_CFG_NAME -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.octoapp import ( - OA_CFG_NAME, - OA_DIR, - OA_ENV_DIR, - OA_INSTALL_SCRIPT, - OA_LOG_NAME, - OA_SYS_CFG_NAME, - OA_UPDATE_SCRIPT, -) -from utils.sys_utils import get_service_file_path - - -@dataclass -class Octoapp: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name = OA_LOG_NAME - dir: Path = OA_DIR - env_dir: Path = OA_ENV_DIR - data_dir: Path = field(init=False) - store_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - sys_cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(Octoapp, self.suffix) - self.store_dir = self.base.data_dir.joinpath("store") - self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME) - self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME) - self.data_dir = self.base.data_dir - self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME) - - def create(self) -> None: - Logger.print_status("Creating OctoApp for Klipper Instance ...") - - try: - cmd = f"{OA_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}" - run(cmd, check=True, shell=True) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - - @staticmethod - def update() -> None: - try: - run(OA_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OA_DIR) - - except CalledProcessError as e: - Logger.print_error(f"Error updating OctoApp for Klipper: {e}") - raise diff --git a/kiauh/extensions/octoapp/octoapp_extension.py b/kiauh/extensions/octoapp/octoapp_extension.py deleted file mode 100644 index ff20c52..0000000 --- a/kiauh/extensions/octoapp/octoapp_extension.py +++ /dev/null @@ -1,204 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from extensions.octoapp import ( - OA_DEPS_JSON_FILE, - OA_DIR, - OA_ENV_DIR, - OA_INSTALL_SCRIPT, - OA_INSTALLER_LOG_FILE, - OA_REPO, - OA_REQ_FILE, - OA_SYS_CFG_NAME, -) -from extensions.octoapp.octoapp import Octoapp -from utils.common import ( - check_install_dependencies, - moonraker_exists, -) -from utils.config_utils import ( - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class OctoappExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing OctoApp for Klipper ...") - - # check if moonraker is installed. if not, notify the user and exit - if not moonraker_exists(): - return - - force_clone = False - OA_instances: List[Octoapp] = get_instances(Octoapp) - if OA_instances: - Logger.print_dialog( - DialogType.INFO, - [ - "OctoApp is already installed!", - "It is safe to run the installer again to link your " - "printer or repair any issues.", - ], - ) - if not get_confirm("Re-run OctoApp installation?"): - Logger.print_info("Exiting OctoApp for Klipper installation ...") - return - else: - Logger.print_status("Re-Installing OctoApp for Klipper ...") - force_clone = True - - mr_instances: List[Moonraker] = get_instances(Moonraker) - - mr_names = [f"● {moonraker.data_dir.name}" for moonraker in mr_instances] - if len(mr_names) > 1: - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - "The setup will apply the same names to OctoApp!", - ], - ) - - if not get_confirm( - "Continue OctoApp for Klipper installation?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting OctoApp for Klipper installation ...") - return - - try: - git_clone_wrapper(OA_REPO, OA_DIR, force=force_clone) - - for moonraker in mr_instances: - instance = Octoapp(suffix=moonraker.suffix) - instance.create() - - InstanceManager.restart_all(mr_instances) - - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully installed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper installation:\n{e}") - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating OctoApp for Klipper ...") - try: - Octoapp.update() - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully updated!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper update:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing OctoApp for Klipper ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[Octoapp] = get_instances(Octoapp) - - try: - self._remove_OA_instances(ob_instances) - self._remove_OA_store_dirs() - self._remove_OA_dir() - self._remove_OA_env() - remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances) - run_remove_routines(OA_INSTALLER_LOG_FILE) - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully removed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper removal:\n{e}") - - def _install_OA_dependencies(self) -> None: - OA_deps = [] - if OA_DEPS_JSON_FILE.exists(): - with open(OA_DEPS_JSON_FILE, "r") as deps: - OA_deps = json.load(deps).get("debian", []) - elif OA_INSTALL_SCRIPT.exists(): - OA_deps = parse_packages_from_file(OA_INSTALL_SCRIPT) - - if not OA_deps: - raise ValueError("Error reading OctoApp dependencies!") - - check_install_dependencies({*OA_deps}) - install_python_requirements(OA_ENV_DIR, OA_REQ_FILE) - - def _remove_OA_instances( - self, - instance_list: List[Octoapp], - ) -> None: - if not instance_list: - Logger.print_info("No OctoApp instances found. Skipped ...") - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_OA_dir(self) -> None: - Logger.print_status("Removing OctoApp for Klipper directory ...") - - if not OA_DIR.exists(): - Logger.print_info(f"'{OA_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OA_DIR) - - def _remove_OA_store_dirs(self) -> None: - Logger.print_status("Removing OctoApp for Klipper store directory ...") - - klipper_instances: List[Moonraker] = get_instances(Klipper) - - for instance in klipper_instances: - store_dir = instance.data_dir.joinpath("octoapp-store") - if not store_dir.exists(): - Logger.print_info(f"'{store_dir}' does not exist. Skipped ...") - return - - run_remove_routines(store_dir) - - def _remove_OA_env(self) -> None: - Logger.print_status("Removing OctoApp for Klipper environment ...") - - if not OA_ENV_DIR.exists(): - Logger.print_info(f"'{OA_ENV_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OA_ENV_DIR) diff --git a/kiauh/extensions/octoeverywhere/__init__.py b/kiauh/extensions/octoeverywhere/__init__.py deleted file mode 100644 index e05c859..0000000 --- a/kiauh/extensions/octoeverywhere/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -# repo -OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" - -# directories -OE_DIR = Path.home().joinpath("octoeverywhere") -OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") -OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") - -# files -OE_REQ_FILE = OE_DIR.joinpath("requirements.txt") -OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") -OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") -OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") -OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") - -# filenames -OE_CFG_NAME = "octoeverywhere.conf" -OE_LOG_NAME = "octoeverywhere.log" -OE_SYS_CFG_NAME = "octoeverywhere-system.cfg" diff --git a/kiauh/extensions/octoeverywhere/metadata.json b/kiauh/extensions/octoeverywhere/metadata.json deleted file mode 100644 index 212688d..0000000 --- a/kiauh/extensions/octoeverywhere/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "metadata": { - "index": 7, - "module": "octoeverywhere_extension", - "maintained_by": "QuinnDamerell", - "display_name": "OctoEverywhere for Klipper", - "description": [ - "Cloud Empower Your Klipper 3D Printers With:", - "- Free, Private, And Secure Remote Access", - "- AI Print Failure Detection", - "- Real-time Notifications", - "- Live Streaming, and More!" - ], - "updates": true - } -} diff --git a/kiauh/extensions/octoeverywhere/octoeverywhere.py b/kiauh/extensions/octoeverywhere/octoeverywhere.py deleted file mode 100644 index 7de2122..0000000 --- a/kiauh/extensions/octoeverywhere/octoeverywhere.py +++ /dev/null @@ -1,75 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker import MOONRAKER_CFG_NAME -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.octoeverywhere import ( - OE_CFG_NAME, - OE_DIR, - OE_ENV_DIR, - OE_INSTALL_SCRIPT, - OE_LOG_NAME, - OE_SYS_CFG_NAME, - OE_UPDATE_SCRIPT, -) -from utils.sys_utils import get_service_file_path - - -@dataclass -class Octoeverywhere: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name = OE_LOG_NAME - dir: Path = OE_DIR - env_dir: Path = OE_ENV_DIR - data_dir: Path = field(init=False) - store_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - sys_cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - Octoeverywhere, self.suffix - ) - self.store_dir = self.base.data_dir.joinpath("store") - self.cfg_file = self.base.cfg_dir.joinpath(OE_CFG_NAME) - self.sys_cfg_file = self.base.cfg_dir.joinpath(OE_SYS_CFG_NAME) - self.data_dir = self.base.data_dir - self.sys_cfg_file = self.base.cfg_dir.joinpath(OE_SYS_CFG_NAME) - - def create(self) -> None: - Logger.print_status("Creating OctoEverywhere for Klipper Instance ...") - - try: - cmd = f"{OE_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}" - run(cmd, check=True, shell=True) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - - @staticmethod - def update() -> None: - try: - run(OE_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OE_DIR) - - except CalledProcessError as e: - Logger.print_error(f"Error updating OctoEverywhere for Klipper: {e}") - raise diff --git a/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py b/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py deleted file mode 100644 index 2907923..0000000 --- a/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py +++ /dev/null @@ -1,191 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from extensions.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 extensions.octoeverywhere.octoeverywhere import Octoeverywhere -from utils.common import ( - check_install_dependencies, - moonraker_exists, -) -from utils.config_utils import ( - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class OctoeverywhereExtension(BaseExtension): - def install_extension(self, **kwargs) -> 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_extension(self, **kwargs) -> 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_extension(self, **kwargs) -> None: - Logger.print_status("Removing OctoEverywhere for Klipper ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[Octoeverywhere] = get_instances(Octoeverywhere) - - try: - self._remove_oe_instances(ob_instances) - self._remove_oe_dir() - self._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(self) -> 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( - self, - 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(self) -> 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(self) -> 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) diff --git a/kiauh/extensions/pretty_gcode/__init__.py b/kiauh/extensions/pretty_gcode/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf b/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf deleted file mode 100644 index eab6162..0000000 --- a/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf +++ /dev/null @@ -1,19 +0,0 @@ -# PrettyGCode website configuration -# copy this file to /etc/nginx/sites-available/pgcode.local.conf -# then to enable: -# sudo ln -s /etc/nginx/sites-available/pgcode.local.conf /etc/nginx/sites-enabled/pgcode.local.conf -# then restart ngninx: -# sudo systemctl reload nginx -server { - listen %PORT%; - listen [::]:%PORT%; - server_name pgcode.local; - - root %ROOT_DIR%; - - index pgcode.html; - - location / { - try_files $uri $uri/ =404; - } -} diff --git a/kiauh/extensions/pretty_gcode/metadata.json b/kiauh/extensions/pretty_gcode/metadata.json deleted file mode 100644 index 0470f01..0000000 --- a/kiauh/extensions/pretty_gcode/metadata.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "metadata": { - "index": 8, - "module": "pretty_gcode_extension", - "maintained_by": "Kragrathea", - "display_name": "PrettyGCode for Klipper", - "description": ["3D G-Code viewer for Klipper"], - "updates": true - } -} diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py deleted file mode 100644 index 0fe60ce..0000000 --- a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py +++ /dev/null @@ -1,101 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 components.webui_client.client_utils import create_nginx_cfg -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from utils.common import check_install_dependencies -from utils.fs_utils import ( - remove_file, -) -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_number_input -from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr - -MODULE_PATH = Path(__file__).resolve().parent -PGC_DIR = Path.home().joinpath("pgcode") -PGC_REPO = "https://github.com/Kragrathea/pgcode" -PGC_CONF = "pgcode.local.conf" - - -# noinspection PyMethodMayBeStatic -class PrettyGcodeExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing PrettyGCode for Klipper ...") - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Make sure you don't select a port which is already in use by " - "another application. Your input will not be validated! Choosing a port " - "which is already in use by another application may cause issues!", - "The default port is 7136.", - ], - ) - - port = get_number_input( - "On which port should PrettyGCode run", - min_value=0, - default=7136, - allow_go_back=True, - ) - - check_install_dependencies({"nginx"}) - - try: - if PGC_DIR.exists(): - shutil.rmtree(PGC_DIR) - - git_clone_wrapper(PGC_REPO, PGC_DIR) - - create_nginx_cfg( - "PrettyGCode for Klipper", - cfg_name=PGC_CONF, - template_src=MODULE_PATH.joinpath(f"assets/{PGC_CONF}"), - ROOT_DIR=PGC_DIR, - PORT=port, - ) - - cmd_sysctl_service("nginx", "restart") - - log = f"Open PrettyGCode now on: http://{get_ipv4_addr()}:{port}" - Logger.print_ok("PrettyGCode installation complete!", start="\n") - Logger.print_ok(log, prefix=False, end="\n\n") - - except Exception as e: - Logger.print_error( - f"Error during PrettyGCode for Klipper installation: {e}" - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating PrettyGCode for Klipper ...") - try: - git_pull_wrapper(PGC_DIR) - - except Exception as e: - Logger.print_error(f"Error during PrettyGCode for Klipper update: {e}") - - def remove_extension(self, **kwargs) -> None: - try: - Logger.print_status("Removing PrettyGCode for Klipper ...") - - # remove pgc dir - shutil.rmtree(PGC_DIR) - # remove nginx config - remove_file(NGINX_SITES_AVAILABLE.joinpath(PGC_CONF), True) - remove_file(NGINX_SITES_ENABLED.joinpath(PGC_CONF), True) - # restart nginx - cmd_sysctl_service("nginx", "restart") - - Logger.print_ok("PrettyGCode for Klipper removed!") - - except Exception as e: - Logger.print_error(f"Error during PrettyGCode for Klipper removal: {e}") diff --git a/kiauh/extensions/simply_print/__init__.py b/kiauh/extensions/simply_print/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/simply_print/metadata.json b/kiauh/extensions/simply_print/metadata.json deleted file mode 100644 index 74213f1..0000000 --- a/kiauh/extensions/simply_print/metadata.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "metadata": { - "index": 10, - "module": "simply_print_extension", - "maintained_by": "dw-0", - "display_name": "SimplyPrint", - "description": [ - "3D Printer Cloud Management Software.", - "\n\n", - "3D printing doesn't have to be a complicated, analog, SD card-filled experience; step into the future of modern 3D printing" - ] - } -} diff --git a/kiauh/extensions/simply_print/simply_print_extension.py b/kiauh/extensions/simply_print/simply_print_extension.py deleted file mode 100644 index 495e098..0000000 --- a/kiauh/extensions/simply_print/simply_print_extension.py +++ /dev/null @@ -1,131 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import List - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from utils.common import backup_printer_config_dir, moonraker_exists -from utils.input_utils import get_confirm - - -# noinspection PyMethodMayBeStatic -class SimplyPrintExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing SimplyPrint ...") - - if not (mr_instances := moonraker_exists("SimplyPrint Installer")): - return - - Logger.print_dialog( - DialogType.INFO, - self._construct_dialog(mr_instances, True), - ) - - if not get_confirm( - "Continue SimplyPrint installation?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting SimplyPrint installation ...") - return - - try: - self._patch_moonraker_confs(mr_instances, True) - - except Exception as e: - Logger.print_error(f"Error during SimplyPrint installation:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing SimplyPrint ...") - - if not (mr_instances := moonraker_exists("SimplyPrint Uninstaller")): - return - - Logger.print_dialog( - DialogType.INFO, - self._construct_dialog(mr_instances, False), - ) - - if not get_confirm( - "Do you really want to uninstall SimplyPrint?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting SimplyPrint uninstallation ...") - return - - try: - self._patch_moonraker_confs(mr_instances, False) - - except Exception as e: - Logger.print_error(f"Error during SimplyPrint installation:\n{e}") - - def _construct_dialog( - self, mr_instances: List[Moonraker], is_install: bool - ) -> List[str]: - mr_names = [f"● {m.service_file_path.name}" for m in mr_instances] - _type = "install" if is_install else "uninstall" - - return [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - f"The setup will {_type} SimplyPrint for all Moonraker instances. " - f"After {_type}ation, all Moonraker services will be restarted!", - ] - - def _patch_moonraker_confs( - self, mr_instances: List[Moonraker], is_install: bool - ) -> None: - section = "simplyprint" - _type, _ft = ("Adding", "to") if is_install else ("Removing", "from") - - patched_files = [] - for moonraker in mr_instances: - Logger.print_status( - f"{_type} section 'simplyprint' {_ft} {moonraker.cfg_file} ..." - ) - scp = SimpleConfigParser() - scp.read_file(moonraker.cfg_file) - - install_and_has_section = is_install and scp.has_section(section) - uninstall_and_has_no_section = not is_install and not scp.has_section( - section - ) - - if install_and_has_section or uninstall_and_has_no_section: - status = "already" if is_install else "does not" - Logger.print_info( - f"Section 'simplyprint' {status} exists! Skipping ..." - ) - continue - - if is_install and not scp.has_section("simplyprint"): - backup_printer_config_dir() - scp.add_section(section) - elif not is_install and scp.has_section("simplyprint"): - backup_printer_config_dir() - scp.remove_section(section) - scp.write_file(moonraker.cfg_file) - patched_files.append(moonraker.cfg_file) - - if patched_files: - InstanceManager.restart_all(mr_instances) - - install_state = "successfully" if patched_files else "was already" - Logger.print_dialog( - DialogType.SUCCESS, - [f"SimplyPrint {install_state} {'' if is_install else 'un'}installed!"], - center_content=True, - ) diff --git a/kiauh/extensions/spoolman/__init__.py b/kiauh/extensions/spoolman/__init__.py deleted file mode 100644 index d1cff1b..0000000 --- a/kiauh/extensions/spoolman/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent -SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest" -SPOOLMAN_DIR = Path.home().joinpath("spoolman") -SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data") -SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml") -SPOOLMAN_DEFAULT_PORT = 7912 diff --git a/kiauh/extensions/spoolman/assets/docker-compose.yml b/kiauh/extensions/spoolman/assets/docker-compose.yml deleted file mode 100644 index 686c6a7..0000000 --- a/kiauh/extensions/spoolman/assets/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - spoolman: - image: ghcr.io/donkie/spoolman:latest - restart: unless-stopped - volumes: - # Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory - - type: bind - source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`. - target: /home/app/.local/share/spoolman # Do NOT modify this line - ports: - # Map the host machine's port 7912 to the container's port 8000 - - "7912:8000" - environment: - - TZ=Europe/Stockholm # Optional, defaults to UTC diff --git a/kiauh/extensions/spoolman/metadata.json b/kiauh/extensions/spoolman/metadata.json deleted file mode 100644 index 6d6160f..0000000 --- a/kiauh/extensions/spoolman/metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "index": 11, - "module": "spoolman_extension", - "maintained_by": "dw-0", - "display_name": "Spoolman (Docker)", - "description": [ - "Filament manager for 3D printing", - "- Track your filament inventory", - "- Monitor filament usage", - "- Manage vendors, materials, and spools", - "- Integrates with Moonraker", - "\n\n", - "Note: This extension installs Spoolman using Docker. Docker must be installed on your system before installing Spoolman." - ], - "updates": true - } -} diff --git a/kiauh/extensions/spoolman/spoolman.py b/kiauh/extensions/spoolman/spoolman.py deleted file mode 100644 index bedd0d6..0000000 --- a/kiauh/extensions/spoolman/spoolman.py +++ /dev/null @@ -1,190 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.spoolman import ( - MODULE_PATH, - SPOOLMAN_COMPOSE_FILE, - SPOOLMAN_DIR, - SPOOLMAN_DOCKER_IMAGE, -) -from utils.sys_utils import get_system_timezone - - -@dataclass -class Spoolman: - suffix: str - base: BaseInstance = field(init=False, repr=False) - dir: Path = SPOOLMAN_DIR - data_dir: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.data_dir = self.base.data_dir - - @staticmethod - def is_container_running() -> bool: - """Check if the Spoolman container is running""" - try: - result = run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "ps", "-q"], - capture_output=True, - text=True, - check=True, - ) - return bool(result.stdout.strip()) - except CalledProcessError: - return False - - @staticmethod - def is_docker_available() -> bool: - """Check if Docker is installed and available""" - try: - run(["docker", "--version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - return False - - @staticmethod - def is_docker_compose_available() -> bool: - """Check if Docker Compose is installed and available""" - try: - # Try modern docker compose command - run(["docker", "compose", "version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - # Try legacy docker-compose command - try: - run(["docker-compose", "--version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - return False - - @staticmethod - def create_docker_compose() -> bool: - """Copy the docker-compose.yml file for Spoolman and set system timezone""" - try: - shutil.copy( - MODULE_PATH.joinpath("assets/docker-compose.yml"), - SPOOLMAN_COMPOSE_FILE, - ) - - # get system timezone - timezone = get_system_timezone() - - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - - content = content.replace("TZ=Europe/Stockholm", f"TZ={timezone}") - - with open(SPOOLMAN_COMPOSE_FILE, "w") as f: - f.write(content) - - return True - except Exception as e: - Logger.print_error(f"Error creating Docker Compose file: {e}") - return False - - @staticmethod - def start_container() -> bool: - """Start the Spoolman container""" - try: - run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "up", "-d"], - check=True, - ) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to start Spoolman container: {e}") - return False - - @staticmethod - def update_container() -> bool: - """Update the Spoolman container""" - - def __get_image_id() -> str: - """Get the image ID of the Spoolman Docker image""" - try: - result = run( - ["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - except CalledProcessError: - raise Exception("Failed to get Spoolman Docker image ID") - - try: - old_image_id = __get_image_id() - Logger.print_status("Pulling latest Spoolman image...") - Spoolman.pull_image() - new_image_id = __get_image_id() - Logger.print_status("Tearing down old Spoolman container...") - Spoolman.tear_down_container() - Logger.print_status("Spinning up new Spoolman container...") - Spoolman.start_container() - if old_image_id != new_image_id: - Logger.print_status("Removing old Spoolman image...") - run(["docker", "rmi", old_image_id], check=True) - return True - - except CalledProcessError as e: - Logger.print_error(f"Failed to update Spoolman container: {e}") - return False - - @staticmethod - def tear_down_container() -> bool: - """Stop and remove the Spoolman container""" - try: - run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "down"], - check=True, - ) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to tear down Spoolman container: {e}") - return False - - @staticmethod - def pull_image() -> bool: - """Pull the Spoolman Docker image""" - try: - run(["docker", "pull", SPOOLMAN_DOCKER_IMAGE], check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to pull Spoolman Docker image: {e}") - return False - - @staticmethod - def remove_image() -> bool: - """Remove the Spoolman Docker image""" - try: - image_exists = run( - ["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE], - capture_output=True, - text=True, - ).stdout.strip() - if not image_exists: - Logger.print_info("Spoolman Docker image not found. Nothing to remove.") - return False - - run(["docker", "rmi", SPOOLMAN_DOCKER_IMAGE], check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to remove Spoolman Docker image: {e}") - return False diff --git a/kiauh/extensions/spoolman/spoolman_extension.py b/kiauh/extensions/spoolman/spoolman_extension.py deleted file mode 100644 index 8360928..0000000 --- a/kiauh/extensions/spoolman/spoolman_extension.py +++ /dev/null @@ -1,344 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from subprocess import CalledProcessError, run -from typing import List, Tuple - -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from core.backup_manager.backup_manager import BackupManager -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from extensions.spoolman import ( - SPOOLMAN_COMPOSE_FILE, - SPOOLMAN_DATA_DIR, - SPOOLMAN_DEFAULT_PORT, - SPOOLMAN_DIR, -) -from extensions.spoolman.spoolman import Spoolman -from utils.config_utils import ( - add_config_section, - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.input_utils import get_confirm, get_number_input -from utils.sys_utils import get_ipv4_addr - - -# noinspection PyMethodMayBeStatic -class SpoolmanExtension(BaseExtension): - ip: str = "" - port: int = SPOOLMAN_DEFAULT_PORT - - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Spoolman using Docker...") - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - if not self.__handle_existing_installation(): - self.ip: str = get_ipv4_addr() - self.__run_setup() - - # noinspection HttpUrlsUsage - Logger.print_dialog( - DialogType.SUCCESS, - [ - "Spoolman successfully installed using Docker!", - "You can access Spoolman via the following URL:", - f"http://{self.ip}:{self.port}", - ], - center_content=True, - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Spoolman Docker container...") - - if not SPOOLMAN_DIR.exists() or not SPOOLMAN_COMPOSE_FILE.exists(): - Logger.print_error("Spoolman installation not found or incomplete.") - return - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - Logger.print_status("Updating Spoolman container...") - if not Spoolman.update_container(): - return - - Logger.print_dialog( - DialogType.SUCCESS, - ["Spoolman Docker container successfully updated!"], - center_content=True, - ) - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Spoolman Docker container...") - - if not SPOOLMAN_DIR.exists(): - Logger.print_info("Spoolman is not installed. Nothing to remove.") - return - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - # remove moonraker integration - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances: List[Moonraker] = mrsvc.get_all_instances() - - Logger.print_status("Removing Spoolman configuration from moonraker.conf...") - remove_config_section("spoolman", mr_instances) - - Logger.print_status("Removing Spoolman from moonraker.asvc...") - self.__remove_from_moonraker_asvc() - - # stop and remove the container if docker-compose exists - if SPOOLMAN_COMPOSE_FILE.exists(): - Logger.print_status("Stopping and removing Spoolman container...") - - if Spoolman.tear_down_container(): - Logger.print_ok("Spoolman container removed!") - else: - Logger.print_error( - "Failed to remove Spoolman container! Please remove it manually." - ) - - if Spoolman.remove_image(): - Logger.print_ok("Spoolman container and image removed!") - else: - Logger.print_error( - "Failed to remove Spoolman image! Please remove it manually." - ) - - # backup Spoolman directory to ~/spoolman_data- before removing it - try: - bm = BackupManager() - result = bm.backup_directory( - f"{SPOOLMAN_DIR.name}_data", - source=SPOOLMAN_DIR, - target=SPOOLMAN_DIR.parent, - ) - if result: - Logger.print_ok(f"Spoolman data backed up to {result}") - Logger.print_status("Removing Spoolman directory...") - if run_remove_routines(SPOOLMAN_DIR): - Logger.print_ok("Spoolman directory removed!") - else: - Logger.print_error( - "Failed to remove Spoolman directory! Please remove it manually." - ) - except Exception as e: - Logger.print_error(f"Failed to backup Spoolman directory: {e}") - Logger.print_info("Skipping Spoolman directory removal...") - - Logger.print_dialog( - DialogType.SUCCESS, - ["Spoolman successfully removed!"], - center_content=True, - ) - - def __run_setup(self) -> None: - # Create Spoolman directory and data directory - Logger.print_status("Setting up Spoolman directories...") - SPOOLMAN_DIR.mkdir(parents=True) - Logger.print_ok(f"Directory {SPOOLMAN_DIR} created!") - SPOOLMAN_DATA_DIR.mkdir(parents=True) - Logger.print_ok(f"Directory {SPOOLMAN_DATA_DIR} created!") - - # Set correct permissions for data directory - try: - Logger.print_status("Setting permissions for Spoolman data directory...") - run(["chown", "1000:1000", str(SPOOLMAN_DATA_DIR)], check=True) - Logger.print_ok("Permissions set!") - except CalledProcessError: - Logger.print_warn( - "Could not set permissions on data directory. This might cause issues." - ) - - Logger.print_status("Creating Docker Compose file...") - if Spoolman.create_docker_compose(): - Logger.print_ok("Docker Compose file created!") - else: - Logger.print_error("Failed to create Docker Compose file!") - - self.__port_config_prompt() - - Logger.print_status("Spinning up Spoolman container...") - if Spoolman.start_container(): - Logger.print_ok("Spoolman container started!") - else: - Logger.print_error("Failed to start Spoolman container!") - - if self.__add_moonraker_integration(): - Logger.print_ok("Spoolman integration added to Moonraker!") - else: - Logger.print_info("Moonraker integration skipped.") - - def __check_docker_prereqs(self) -> Tuple[bool, bool]: - # check if Docker is available - is_docker_available = Spoolman.is_docker_available() - if not is_docker_available: - Logger.print_error("Docker is not installed or not available.") - Logger.print_info( - "Please install Docker first: https://docs.docker.com/engine/install/" - ) - - # check if Docker Compose is available - is_docker_compose_available = Spoolman.is_docker_compose_available() - if not is_docker_compose_available: - Logger.print_error("Docker Compose is not installed or not available.") - - return is_docker_available, is_docker_compose_available - - def __port_config_prompt(self) -> None: - """Prompt for advanced configuration options""" - Logger.print_dialog( - DialogType.INFO, - [ - "You can configure Spoolman to run on a different port than the default. " - "Make sure you don't select a port which is already in use by " - "another application. Your input will not be validated! " - "The default port is 7912.", - ], - ) - if not get_confirm("Continue with default port 7912?", default_choice=True): - self.__set_port() - - def __set_port(self) -> None: - """Configure advanced options for Spoolman Docker container""" - port = get_number_input( - "Which port should Spoolman run on?", - default=SPOOLMAN_DEFAULT_PORT, - min_value=1024, - max_value=65535, - ) - - if port != SPOOLMAN_DEFAULT_PORT: - self.port = port - - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - - port_mapping_pattern = r'"(\d+):8000"' - content = re.sub(port_mapping_pattern, f'"{port}:8000"', content) - - with open(SPOOLMAN_COMPOSE_FILE, "w") as f: - f.write(content) - - Logger.print_ok(f"Port set to {port}...") - - def __handle_existing_installation(self) -> bool: - if not (SPOOLMAN_DIR.exists() and SPOOLMAN_DIR.is_dir()): - return False - - compose_file_exists = SPOOLMAN_COMPOSE_FILE.exists() - container_running = Spoolman.is_container_running() - - if container_running and compose_file_exists: - Logger.print_info("Spoolman is already installed!") - return True - elif container_running and not compose_file_exists: - Logger.print_status( - "Spoolman container is running but Docker Compose file is missing..." - ) - if get_confirm( - "Do you want to recreate the Docker Compose file?", - default_choice=True, - ): - Spoolman.create_docker_compose() - self.__port_config_prompt() - return True - elif not container_running and compose_file_exists: - Logger.print_status( - "Docker Compose file exists but container is not running..." - ) - Spoolman.start_container() - return True - return False - - def __add_moonraker_integration(self) -> bool: - """Enable Moonraker integration for Spoolman Docker container""" - if not get_confirm("Add Moonraker integration?", default_choice=True): - return False - - Logger.print_status("Adding Spoolman integration to Moonraker...") - - # read port from the docker-compose file - port = SPOOLMAN_DEFAULT_PORT - if SPOOLMAN_COMPOSE_FILE.exists(): - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - # Extract port from the port mapping - port_match = re.search(r'"(\d+):8000"', content) - if port_match: - port = port_match.group(1) - - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - - # noinspection HttpUrlsUsage - add_config_section( - section="spoolman", - instances=mr_instances, - options=[("server", f"http://{self.ip}:{port}")], - ) - - Logger.print_status("Adding Spoolman to moonraker.asvc...") - self.__add_to_moonraker_asvc() - - InstanceManager.restart_all(mr_instances) - - return True - - def __add_to_moonraker_asvc(self) -> None: - """Add Spoolman to moonraker.asvc""" - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - for instance in mr_instances: - asvc_path = instance.data_dir.joinpath("moonraker.asvc") - if asvc_path.exists(): - if "Spoolman" in open(asvc_path).read(): - Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...") - continue - - with open(asvc_path, "a") as f: - f.write("Spoolman\n") - - Logger.print_ok(f"Spoolman added to {asvc_path}!") - - def __remove_from_moonraker_asvc(self) -> None: - """Remove Spoolman from moonraker.asvc""" - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - for instance in mr_instances: - asvc_path = instance.data_dir.joinpath("moonraker.asvc") - if asvc_path.exists(): - if "Spoolman" not in open(asvc_path).read(): - Logger.print_info(f"Spoolman not in {asvc_path}. Skipping...") - continue - - with open(asvc_path, "r") as f: - lines = f.readlines() - - new_lines = [line for line in lines if "Spoolman" not in line] - - with open(asvc_path, "w") as f: - f.writelines(new_lines) - - Logger.print_ok(f"Spoolman removed from {asvc_path}!") diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py deleted file mode 100644 index c37a838..0000000 --- a/kiauh/extensions/telegram_bot/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -# repo -TG_BOT_REPO = "https://github.com/nlef/moonraker-telegram-bot.git" - -# names -TG_BOT_CFG_NAME = "telegram.conf" -TG_BOT_LOG_NAME = "telegram.log" -TG_BOT_SERVICE_NAME = "moonraker-telegram-bot.service" -TG_BOT_ENV_FILE_NAME = "moonraker-telegram-bot.env" - -# directories -TG_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") -TG_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") - -# files -TG_BOT_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_SERVICE_NAME}") -TG_BOT_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_ENV_FILE_NAME}") -TG_BOT_REQ_FILE = TG_BOT_DIR.joinpath("scripts/requirements.txt") diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env deleted file mode 100644 index 280f165..0000000 --- a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env +++ /dev/null @@ -1 +0,0 @@ -TELEGRAM_BOT_ARGS="%TELEGRAM_BOT_DIR%/bot/main.py -c %CFG% -l %LOG%" \ No newline at end of file diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service deleted file mode 100644 index ded7475..0000000 --- a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Moonraker Telegram Bot SV1 -Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -WorkingDirectory=%TELEGRAM_BOT_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $TELEGRAM_BOT_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/extensions/telegram_bot/metadata.json b/kiauh/extensions/telegram_bot/metadata.json deleted file mode 100644 index 3ffadc7..0000000 --- a/kiauh/extensions/telegram_bot/metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "metadata": { - "index": 5, - "module": "moonraker_telegram_bot_extension", - "maintained_by": "nlef", - "display_name": "Moonraker Telegram Bot", - "description": ["Control your printer with the Telegram messenger app."], - "project_url": "https://github.com/nlef/moonraker-telegram-bot", - "updates": true - } -} diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py deleted file mode 100644 index e3d4191..0000000 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py +++ /dev/null @@ -1,124 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.moonraker.moonraker import Moonraker -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.telegram_bot import ( - TG_BOT_CFG_NAME, - TG_BOT_DIR, - TG_BOT_ENV, - TG_BOT_ENV_FILE_NAME, - TG_BOT_ENV_FILE_TEMPLATE, - TG_BOT_LOG_NAME, - TG_BOT_SERVICE_TEMPLATE, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class MoonrakerTelegramBot: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = TG_BOT_LOG_NAME - bot_dir: Path = TG_BOT_DIR - env_dir: Path = TG_BOT_ENV - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - MoonrakerTelegramBot, self.suffix - ) - self.data_dir: Path = self.base.data_dir - self.cfg_file = self.base.cfg_dir.joinpath(TG_BOT_CFG_NAME) - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Moonraker Telegram Bot Instance ...") - - try: - create_folders(self.base.base_folders) - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(TG_BOT_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = TG_BOT_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%TELEGRAM_BOT_DIR%", - self.bot_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(TG_BOT_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = TG_BOT_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%TELEGRAM_BOT_DIR%", - self.bot_dir.as_posix(), - ) - env_file_content = env_file_content.replace("%CFG%", self.cfg_file.as_posix()) - env_file_content = env_file_content.replace( - "%LOG%", - self.base.log_dir.joinpath(self.log_file_name).as_posix(), - ) - return env_file_content diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py deleted file mode 100644 index 458b3e8..0000000 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py +++ /dev/null @@ -1,226 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 subprocess import run -from typing import List - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from extensions.telegram_bot import TG_BOT_REPO, TG_BOT_REQ_FILE -from extensions.telegram_bot.moonraker_telegram_bot import ( - TG_BOT_DIR, - TG_BOT_ENV, - MoonrakerTelegramBot, -) -from utils.common import check_install_dependencies -from utils.config_utils import add_config_section, remove_config_section -from utils.fs_utils import remove_file -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class TelegramBotExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Moonraker Telegram Bot ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "No Moonraker instances found!", - "Moonraker Telegram Bot requires Moonraker to be installed. " - "Please install Moonraker first!", - ], - ) - return - - instance_names = [ - f"● {instance.service_file_path.name}" for instance in mr_instances - ] - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *instance_names, - "\n\n", - "The setup will apply the same names to Telegram Bot!", - ], - ) - if not get_confirm( - "Continue Moonraker Telegram Bot installation?", - default_choice=True, - allow_go_back=True, - ): - return - - create_example_cfg = get_confirm("Create example telegram.conf?") - - try: - git_clone_wrapper(TG_BOT_REPO, TG_BOT_DIR) - self._install_dependencies() - - # create and start services / create bot configs - show_config_dialog = False - tb_names = [mr_i.suffix for mr_i in mr_instances] - for name in tb_names: - instance = MoonrakerTelegramBot(suffix=name) - instance.create() - - cmd_sysctl_service(instance.service_file_path.name, "enable") - - if create_example_cfg: - Logger.print_status( - f"Creating Telegram Bot config in {instance.base.cfg_dir} ..." - ) - template = TG_BOT_DIR.joinpath("scripts/base_install_template") - target_file = instance.cfg_file - if not target_file.exists(): - show_config_dialog = True - run(["cp", template, target_file], check=True) - else: - Logger.print_info( - f"Telegram Bot config in {instance.base.cfg_dir} already exists! Skipped ..." - ) - - cmd_sysctl_service(instance.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # add to moonraker update manager - self._patch_bot_update_manager(mr_instances) - - # restart moonraker - InstanceManager.restart_all(mr_instances) - - if show_config_dialog: - Logger.print_dialog( - DialogType.ATTENTION, - [ - "During the installation of the Moonraker Telegram Bot, " - "a basic config was created per instance. You need to edit the " - "config file to set up your Telegram Bot. Please refer to the " - "following wiki page for further information:", - "https://github.com/nlef/moonraker-telegram-bot/wiki", - ], - margin_bottom=1, - ) - - Logger.print_ok("Telegram Bot installation complete!") - except Exception as e: - Logger.print_error( - f"Error during installation of Moonraker Telegram Bot:\n{e}" - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Moonraker Telegram Bot ...") - - instances = get_instances(MoonrakerTelegramBot) - InstanceManager.stop_all(instances) - - git_pull_wrapper(TG_BOT_DIR) - self._install_dependencies() - - InstanceManager.start_all(instances) - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Moonraker Telegram Bot ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - tb_instances: List[MoonrakerTelegramBot] = get_instances(MoonrakerTelegramBot) - - try: - self._remove_bot_instances(tb_instances) - self._remove_bot_dir() - self._remove_bot_env() - remove_config_section("update_manager moonraker-telegram-bot", mr_instances) - self._delete_bot_logs(tb_instances) - except Exception as e: - Logger.print_error(f"Error during removal of Moonraker Telegram Bot:\n{e}") - - Logger.print_ok("Moonraker Telegram Bot removed!") - - def _install_dependencies(self) -> None: - # install dependencies - script = TG_BOT_DIR.joinpath("scripts/install.sh") - package_list = parse_packages_from_file(script) - - check_install_dependencies({*package_list}) - - # create virtualenv - if create_python_venv(TG_BOT_ENV, allow_access_to_system_site_packages=True): - install_python_requirements(TG_BOT_ENV, TG_BOT_REQ_FILE) - - def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None: - env_py = f"{TG_BOT_ENV}/bin/python" - add_config_section( - section="update_manager moonraker-telegram-bot", - instances=instances, - options=[ - ("type", "git_repo"), - ("path", str(TG_BOT_DIR)), - ("origin", TG_BOT_REPO), - ("env", env_py), - ("requirements", "scripts/requirements.txt"), - ("install_script", "scripts/install.sh"), - ], - ) - - def _remove_bot_instances( - self, - instance_list: List[MoonrakerTelegramBot], - ) -> None: - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_bot_dir(self) -> None: - if not TG_BOT_DIR.exists(): - Logger.print_info(f"'{TG_BOT_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(TG_BOT_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{TG_BOT_DIR}':\n{e}") - - def _remove_bot_env(self) -> None: - if not TG_BOT_ENV.exists(): - Logger.print_info(f"'{TG_BOT_ENV}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(TG_BOT_ENV) - except OSError as e: - Logger.print_error(f"Unable to delete '{TG_BOT_ENV}':\n{e}") - - def _delete_bot_logs(self, instances: List[MoonrakerTelegramBot]) -> None: - all_logfiles = [] - for instance in instances: - all_logfiles = list(instance.base.log_dir.glob("telegram_bot.log*")) - if not all_logfiles: - Logger.print_info("No Moonraker Telegram Bot logs found. Skipped ...") - return - - for log in all_logfiles: - Logger.print_status(f"Remove '{log}'") - remove_file(log) diff --git a/kiauh/main.py b/kiauh/main.py deleted file mode 100644 index 06512ea..0000000 --- a/kiauh/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 io -import sys - -from core.logger import Logger -from core.menus.main_menu import MainMenu -from core.settings.kiauh_settings import KiauhSettings - - -def ensure_encoding() -> None: - if sys.stdout.encoding == "UTF-8" or not isinstance(sys.stdout, io.TextIOWrapper): - return - sys.stdout.reconfigure(encoding="utf-8") - - -def main() -> None: - try: - KiauhSettings() - ensure_encoding() - MainMenu().run() - except KeyboardInterrupt: - Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/procedures/__init__.py b/kiauh/procedures/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/procedures/switch_repo.py b/kiauh/procedures/switch_repo.py deleted file mode 100644 index 920e440..0000000 --- a/kiauh/procedures/switch_repo.py +++ /dev/null @@ -1,151 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -from pathlib import Path -from typing import Literal - -from components.klipper import ( - KLIPPER_BACKUP_DIR, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_REQ_FILE, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_utils import install_klipper_packages -from components.moonraker import ( - MOONRAKER_BACKUP_DIR, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_REQ_FILE, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_setup_service import ( - install_moonraker_packages, -) -from core.backup_manager.backup_manager import BackupManager, BackupManagerException -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.git_utils import GitException, get_repo_name, git_clone_wrapper -from utils.instance_utils import get_instances -from utils.sys_utils import ( - VenvCreationFailedException, - create_python_venv, - install_python_requirements, -) - - -class RepoSwitchFailedException(Exception): - pass - - -def run_switch_repo_routine( - name: Literal["klipper", "moonraker"], repo_url: str, branch: str -) -> None: - repo_dir: Path = KLIPPER_DIR if name == "klipper" else MOONRAKER_DIR - env_dir: Path = KLIPPER_ENV_DIR if name == "klipper" else MOONRAKER_ENV_DIR - req_file = KLIPPER_REQ_FILE if name == "klipper" else MOONRAKER_REQ_FILE - backup_dir: Path = KLIPPER_BACKUP_DIR if name == "klipper" else MOONRAKER_BACKUP_DIR - _type = Klipper if name == "klipper" else Moonraker - - # step 1: stop all instances - Logger.print_status(f"Stopping all {_type.__name__} instances ...") - instances = get_instances(_type) - InstanceManager.stop_all(instances) - - repo_dir_backup_path: Path | None = None - env_dir_backup_path: Path | None = None - - try: - # step 2: backup old repo and env - org, _ = get_repo_name(repo_dir) - backup_dir = backup_dir.joinpath(org) - bm = BackupManager() - repo_dir_backup_path = bm.backup_directory( - repo_dir.name, - repo_dir, - backup_dir, - ) - env_dir_backup_path = bm.backup_directory( - env_dir.name, - env_dir, - backup_dir, - ) - - if not (repo_url or branch): - error = f"Invalid repository URL ({repo_url}) or branch ({branch})!" - raise ValueError(error) - - # step 4: clone new repo - git_clone_wrapper(repo_url, repo_dir, branch, force=True) - - # step 5: install os dependencies - if name == "klipper": - install_klipper_packages() - elif name == "moonraker": - install_moonraker_packages() - - # step 6: recreate python virtualenv - Logger.print_status(f"Recreating {_type.__name__} virtualenv ...") - if not create_python_venv(env_dir, force=True): - raise GitException(f"Failed to recreate virtualenv for {_type.__name__}") - else: - install_python_requirements(env_dir, req_file) - - Logger.print_ok(f"Switched to {repo_url} at branch {branch}!") - - except BackupManagerException as e: - Logger.print_error(f"Error during backup of repository: {e}") - raise RepoSwitchFailedException(e) - - except (GitException, VenvCreationFailedException) as e: - # if something goes wrong during cloning or recreating the virtualenv, - # we restore the backup of the repo and env - Logger.print_error(f"Error during repository switch: {e}", start="\n") - Logger.print_status(f"Restoring last backup of {_type.__name__} ...") - _restore_repo_backup( - _type.__name__, - env_dir, - env_dir_backup_path, - repo_dir, - repo_dir_backup_path, - ) - - except RepoSwitchFailedException as e: - Logger.print_error(f"Something went wrong: {e}") - return - - Logger.print_status(f"Restarting all {_type.__name__} instances ...") - InstanceManager.start_all(instances) - - -def _restore_repo_backup( - name: str, - env_dir: Path, - env_dir_backup_path: Path | None, - repo_dir: Path, - repo_dir_backup_path: Path | None, -) -> None: - # if repo_dir_backup_path is not None and env_dir_backup_path is not None: - if not repo_dir_backup_path or not env_dir_backup_path: - raise RepoSwitchFailedException( - f"Unable to restore backup of {name}! Path of backups directory is None!" - ) - - try: - if repo_dir.exists(): - shutil.rmtree(repo_dir) - shutil.copytree(repo_dir_backup_path, repo_dir) - if env_dir.exists(): - shutil.rmtree(env_dir) - shutil.copytree(env_dir_backup_path, env_dir) - Logger.print_warn(f"Restored backup of {name} successfully!") - except Exception as e: - raise RepoSwitchFailedException(f"Error restoring backup: {e}") diff --git a/kiauh/procedures/system.py b/kiauh/procedures/system.py deleted file mode 100644 index dc937c2..0000000 --- a/kiauh/procedures/system.py +++ /dev/null @@ -1,103 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path -from subprocess import PIPE, CalledProcessError, run - -from core.logger import DialogType, Logger -from utils.common import check_install_dependencies, get_current_date -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm, get_string_input - - -def change_system_hostname() -> None: - """ - Procedure to change the system hostname. - :return: - """ - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Changing the hostname of this system allows you to access an installed " - "webinterface by simply typing the hostname like this in the browser:", - "\n\n", - "http://.local", - "\n\n", - "Example: If you set your hostname to 'my-printer', you can access an " - "installed webinterface by typing 'http://my-printer.local' in the " - "browser.", - ], - custom_title="CHANGE SYSTEM HOSTNAME", - ) - if not get_confirm("Do you want to change the hostname?", default_choice=False): - return - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Allowed characters: a-z, 0-9 and '-'", - "The name must not contain the following:", - "\n\n", - "● Any special characters", - "● No leading or trailing '-'", - ], - ) - hostname = get_string_input( - "Enter the new hostname", - regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$", - ) - if not get_confirm(f"Change the hostname to '{hostname}'?", default_choice=False): - Logger.print_info("Aborting hostname change ...") - return - - try: - Logger.print_status("Changing hostname ...") - - Logger.print_status("Checking for dependencies ...") - check_install_dependencies({"avahi-daemon"}, include_global=False) - - # create or backup hosts file - Logger.print_status("Creating backup of hosts file ...") - hosts_file = Path("/etc/hosts") - if not check_file_exist(hosts_file, True): - cmd = ["sudo", "touch", hosts_file.as_posix()] - run(cmd, stderr=PIPE, check=True) - else: - date_time = get_current_date() - name = f"hosts.{date_time.get('date')}-{date_time.get('time')}.bak" - hosts_file_backup = Path(f"/etc/{name}") - cmd = [ - "sudo", - "cp", - hosts_file.as_posix(), - hosts_file_backup.as_posix(), - ] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok() - - # call hostnamectl set-hostname - Logger.print_status(f"Setting hostname to '{hostname}' ...") - cmd = ["sudo", "hostnamectl", "set-hostname", hostname] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok() - - # add hostname to hosts file at the end of the file - Logger.print_status("Writing new hostname to /etc/hosts ...") - stdin = f"127.0.0.1 {hostname}\n" - cmd = ["sudo", "tee", "-a", hosts_file.as_posix()] - run(cmd, input=stdin.encode(), stderr=PIPE, stdout=PIPE, check=True) - Logger.print_ok() - - Logger.print_ok("New hostname successfully configured!") - Logger.print_ok("Remember to reboot for the changes to take effect!\n") - - except CalledProcessError as e: - Logger.print_error(f"Error during change hostname procedure: {e}") - return diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py deleted file mode 100644 index 8bfcd7a..0000000 --- a/kiauh/utils/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py deleted file mode 100644 index 2e61e22..0000000 --- a/kiauh/utils/common.py +++ /dev/null @@ -1,200 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Literal, Set - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.constants import ( - GLOBAL_DEPS, - PRINTER_DATA_BACKUP_DIR, -) -from core.logger import DialogType, Logger -from core.types.color import Color -from core.types.component_status import ComponentStatus, StatusCode -from utils.git_utils import ( - get_current_branch, - get_local_commit, - get_local_tags, - get_remote_commit, - get_repo_name, - get_repo_url, -) -from utils.instance_utils import get_instances -from utils.sys_utils import ( - check_package_install, - install_system_packages, - update_system_package_lists, -) - - -def get_kiauh_version() -> str: - """ - Helper method to get the current KIAUH version by reading the latest tag - :return: string of the latest tag - """ - lastest_tag: str = get_local_tags(Path(__file__).parent.parent)[-1] - return lastest_tag - - -def convert_camelcase_to_kebabcase(name: str) -> str: - return re.sub(r"(? Dict[Literal["date", "time"], str]: - """ - Get the current date | - :return: Dict holding a date and time key:value pair - """ - now: datetime = datetime.today() - date: str = now.strftime("%Y%m%d") - time: str = now.strftime("%H%M%S") - - return {"date": date, "time": time} - - -def check_install_dependencies( - deps: Set[str] | None = None, include_global: bool = True -) -> None: - """ - Common helper method to check if dependencies are installed - and if not, install them automatically | - :param include_global: Wether to include the global dependencies or not - :param deps: List of strings of package names to check if installed - :return: None - """ - if deps is None: - deps = set() - - if include_global: - deps.update(GLOBAL_DEPS) - - requirements = check_package_install(deps) - if requirements: - Logger.print_status("Installing dependencies ...") - Logger.print_info("The following packages need installation:") - for r in requirements: - print(Color.apply(f"● {r}", Color.CYAN)) - update_system_package_lists(silent=False) - install_system_packages(requirements) - - -def get_install_status( - repo_dir: Path, - env_dir: Path | None = None, - instance_type: type | None = None, - files: List[Path] | None = None, -) -> ComponentStatus: - """ - Helper method to get the installation status of software components - :param repo_dir: the repository directory - :param env_dir: the python environment directory - :param instance_type: The component type - :param files: List of optional files to check for existence - :return: Dictionary with status string, statuscode and instance count - """ - from utils.instance_utils import get_instances - - checks = [] - branch: str = "" - - if repo_dir.exists(): - checks.append(True) - branch = get_current_branch(repo_dir) - - if env_dir is not None: - checks.append(env_dir.exists()) - - instances = 0 - if instance_type is not None: - instances = len(get_instances(instance_type)) - checks.append(instances > 0) - - if files is not None: - for f in files: - checks.append(f.exists()) - - status: StatusCode - if checks and all(checks): - status = 2 # installed - elif not any(checks): - status = 0 # not installed - else: - status = 1 # incomplete - - org, repo = get_repo_name(repo_dir) - repo_url = get_repo_url(repo_dir) if repo_dir.exists() else None - - return ComponentStatus( - status=status, - instances=instances, - owner=org, - repo=repo, - repo_url=repo_url, - branch=branch, - local=get_local_commit(repo_dir), - remote=get_remote_commit(repo_dir), - ) - - -def backup_printer_config_dir() -> None: - # local import to prevent circular import - from core.backup_manager.backup_manager import BackupManager - - instances: List[Klipper] = get_instances(Klipper) - bm = BackupManager() - - if not instances: - Logger.print_info("Unable to find directory to backup!") - Logger.print_info("Are there no Klipper instances installed?") - return - - for instance in instances: - bm.backup_directory( - instance.data_dir.name, - source=instance.base.cfg_dir, - target=PRINTER_DATA_BACKUP_DIR, - ) - - -def moonraker_exists(name: str = "") -> List[Moonraker]: - """ - Helper method to check if a Moonraker instance exists - :param name: Optional name of an installer where the check is performed - :return: True if at least one Moonraker instance exists, False otherwise - """ - mr_instances: List[Moonraker] = get_instances(Moonraker) - - info = ( - f"{name} requires Moonraker to be installed" - if name - else "A Moonraker installation is required" - ) - - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "No Moonraker instances found!", - f"{info}. Please install Moonraker first!", - ], - ) - return [] - return mr_instances - - -def trunc_string(input_str: str, length: int) -> str: - if len(input_str) > length: - return f"{input_str[: length - 3]}..." - return input_str diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py deleted file mode 100644 index 59cfa96..0000000 --- a/kiauh/utils/config_utils.py +++ /dev/null @@ -1,105 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 shutil -import tempfile -from pathlib import Path -from typing import List, Tuple - -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from utils.instance_type import InstanceType - -ConfigOption = Tuple[str, str] - - -def add_config_section( - section: str, - instances: List[InstanceType], - options: List[ConfigOption] | None = None, -) -> None: - if not instances: - return - - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if scp.has_section(section): - Logger.print_info("Section already exist. Skipped ...") - continue - - scp.add_section(section) - - if options is not None: - for option in reversed(options): - scp.set_option(section, option[0], option[1]) - - scp.write_file(cfg_file) - - Logger.print_ok("OK!") - - -def add_config_section_at_top(section: str, instances: List[InstanceType]) -> None: - # TODO: this could be implemented natively in SimpleConfigParser - for instance in instances: - tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) - tmp_cfg_path = Path(tmp_cfg.name) - scp = SimpleConfigParser() - scp.read_file(tmp_cfg_path) - scp.add_section(section) - scp.write_file(tmp_cfg_path) - tmp_cfg.close() - - cfg_file = instance.cfg_file - with open(cfg_file, "r") as org: - org_content = org.readlines() - with open(tmp_cfg_path, "a") as tmp: - tmp.writelines(org_content) - - cfg_file.unlink() - shutil.move(tmp_cfg_path, cfg_file) - - Logger.print_ok("OK!") - - -def remove_config_section( - section: str, instances: List[InstanceType] -) -> List[InstanceType]: - removed_from: List[instances] = [] - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if not scp.has_section(section): - Logger.print_info("Section does not exist. Skipped ...") - continue - - scp.remove_section(section) - scp.write_file(cfg_file) - - removed_from.append(instance) - Logger.print_ok("OK!") - - return removed_from diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py deleted file mode 100644 index cabdaf8..0000000 --- a/kiauh/utils/fs_utils.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 os -import re -import shutil -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, call, check_output, run -from typing import List -from zipfile import ZipFile - -from core.decorators import deprecated -from core.logger import Logger - - -def check_file_exist(file_path: Path, sudo=False) -> bool: - """ - Helper function for checking the existence of a file | - :param file_path: the absolute path of the file to check - :param sudo: use sudo if required - :return: True, if file exists, otherwise False - """ - if sudo: - command = ["sudo", "find", file_path.as_posix()] - try: - check_output(command, stderr=DEVNULL) - return True - except CalledProcessError: - return False - else: - if os.access(file_path, os.F_OK): - return file_path.exists() - else: - return False - - -def create_symlink(source: Path, target: Path, sudo=False) -> None: - try: - cmd = ["ln", "-sf", source.as_posix(), target.as_posix()] - if sudo: - cmd.insert(0, "sudo") - run(cmd, stderr=PIPE, check=True) - except CalledProcessError as e: - Logger.print_error(f"Failed to create symlink: {e}") - raise - - -def remove_with_sudo(files: Path | List[Path]) -> bool: - _files = [] - _removed = [] - if isinstance(files, list): - _files = files - else: - _files.append(files) - - for f in _files: - try: - cmd = ["sudo", "find", f.as_posix()] - if call(cmd, stderr=DEVNULL, stdout=DEVNULL) == 1: - Logger.print_info(f"File '{f}' does not exist. Skipped ...") - continue - cmd = ["sudo", "rm", "-rf", f.as_posix()] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok(f"File '{f}' was successfully removed!") - _removed.append(f) - except CalledProcessError as e: - Logger.print_error(f"Error removing file '{f}': {e}") - - return len(_removed) > 0 - - -@deprecated(info="Use remove_with_sudo instead", replaced_by=remove_with_sudo) -def remove_file(file_path: Path, sudo=False) -> None: - try: - cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" - run(cmd, stderr=PIPE, check=True, shell=True) - except CalledProcessError as e: - log = f"Cannot remove file {file_path}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def run_remove_routines(file: Path) -> bool: - try: - if not file.is_symlink() and not file.exists(): - Logger.print_info(f"File '{file}' does not exist. Skipped ...") - return False - - if file.is_dir(): - shutil.rmtree(file) - elif file.is_file() or file.is_symlink(): - file.unlink() - else: - Logger.print_error(f"File '{file}' is neither a file nor a directory!") - return False - Logger.print_ok(f"File '{file}' was successfully removed!") - return True - except OSError as e: - Logger.print_error(f"Unable to delete '{file}':\n{e}") - try: - Logger.print_info("Trying to remove with sudo ...") - if remove_with_sudo(file): - Logger.print_ok(f"File '{file}' was successfully removed!") - return True - except CalledProcessError as e: - Logger.print_error(f"Error deleting '{file}' with sudo:\n{e}") - Logger.print_error("Remove this directory manually!") - return False - - -def unzip(filepath: Path, target_dir: Path) -> None: - """ - Helper function to unzip a zip-archive into a target directory | - :param filepath: the path to the zip-file to unzip - :param target_dir: the target directory to extract the files into - :return: None - """ - with ZipFile(filepath, "r") as _zip: - _zip.extractall(target_dir) - - -def create_folders(dirs: List[Path]) -> None: - try: - for _dir in dirs: - if _dir.exists(): - continue - _dir.mkdir(exist_ok=True) - Logger.print_ok(f"Created directory '{_dir}'!") - except OSError as e: - Logger.print_error(f"Error creating directories: {e}") - raise - - -def get_data_dir(instance_type: type, suffix: str) -> Path: - from utils.sys_utils import get_service_file_path - - # if the service file exists, we read the data dir path from it - # this also ensures compatibility with pre v6.0.0 instances - service_file_path: Path = get_service_file_path(instance_type, suffix) - if service_file_path and service_file_path.exists(): - with open(service_file_path, "r") as service_file: - lines = service_file.readlines() - for line in lines: - pattern = r"^EnvironmentFile=(.+)(/systemd/.+\.env)" - match = re.search(pattern, line) - if match: - return Path(match.group(1)) - - if suffix != "": - # this is the new data dir naming scheme introduced in v6.0.0 - return Path.home().joinpath(f"printer_{suffix}_data") - - return Path.home().joinpath("printer_data") diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py deleted file mode 100644 index 857c0ad..0000000 --- a/kiauh/utils/git_utils.py +++ /dev/null @@ -1,360 +0,0 @@ -from __future__ import annotations - -import json -import re -import shutil -import urllib.request -from http.client import HTTPResponse -from json import JSONDecodeError -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run -from typing import List, Tuple, Type - -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.input_utils import get_confirm, get_number_input -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -class GitException(Exception): - pass - - -def git_clone_wrapper( - repo: str, target_dir: Path, branch: str | None = None, force: bool = False -) -> None: - """ - Clones a repository from the given URL and checks out the specified branch if given. - The clone will be performed with the '--filter=blob:none' flag to perform a blobless clone. - - :param repo: The URL of the repository to clone. - :param branch: The branch to check out. If None, master or main, no checkout will be performed. - :param target_dir: The directory where the repository will be cloned. - :param force: Force the cloning of the repository even if it already exists. - :return: None - """ - log = f"Cloning repository from '{repo}'" - Logger.print_status(log) - try: - if Path(target_dir).exists(): - question = f"'{target_dir}' already exists. Overwrite?" - if not force and not get_confirm(question, default_choice=False): - Logger.print_info("Skip cloning of repository ...") - return - shutil.rmtree(target_dir) - - git_cmd_clone(repo, target_dir, blobless=True) - - if branch not in ("master", "main"): - git_cmd_checkout(branch, target_dir) - - except CalledProcessError: - log = "An unexpected error occured during cloning of the repository." - Logger.print_error(log) - raise GitException(log) - except OSError as e: - Logger.print_error(f"Error removing existing repository: {e.strerror}") - raise GitException(f"Error removing existing repository: {e.strerror}") - - -def git_pull_wrapper(target_dir: Path) -> None: - """ - A function that updates a repository using git pull. - - :param target_dir: The directory of the repository. - :return: None - """ - Logger.print_status("Updating repository ...") - try: - git_cmd_pull(target_dir) - except CalledProcessError: - log = "An unexpected error occured during updating the repository." - Logger.print_error(log) - return - - -def get_repo_name(repo: Path) -> Tuple[str, str]: - """ - Helper method to extract the organisation and name of a repository | - :param repo: repository to extract the values from - :return: String in form of "/" or None - """ - if not repo.exists() or not repo.joinpath(".git").exists(): - return "-", "-" - - try: - cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"] - result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8") - substrings: List[str] = result.strip().split("/")[-2:] - - orga: str = substrings[0] if substrings[0] else "-" - name: str = substrings[1] if substrings[1] else "-" - - return orga, name.replace(".git", "") - - except CalledProcessError: - return "-", "-" - - -def get_current_branch(repo: Path) -> str: - """ - Get the current branch of a local Git repository - :param repo: Path to the local Git repository - :return: Current branch - """ - try: - cmd = ["git", "branch", "--show-current"] - result: str = check_output(cmd, stderr=DEVNULL, cwd=repo).decode( - encoding="utf-8" - ) - return result.strip() if result else "-" - - except CalledProcessError: - return "-" - - -def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]: - """ - Get all tags of a local Git repository - :param repo_path: Path to the local Git repository - :param _filter: Optional filter to filter the tags by - :return: List of tags - """ - try: - cmd: List[str] = ["git", "tag", "-l"] - - if _filter is not None: - cmd.append(f"'${_filter}'") - - result: str = check_output( - cmd, - stderr=DEVNULL, - cwd=repo_path.as_posix(), - ).decode(encoding="utf-8") - - tags: List[str] = result.split("\n")[:-1] - - return sorted( - tags, - key=lambda x: [int(i) if i.isdigit() else i for i in re.split(r"(\d+)", x)], - ) - - except CalledProcessError: - return [] - - -def get_remote_tags(repo_path: str) -> List[str]: - """ - Gets the tags of a GitHub repostiory - :param repo_path: path of the GitHub repository - e.g. `/` - :return: List of tags - """ - try: - url = f"https://api.github.com/repos/{repo_path}/tags" - with urllib.request.urlopen(url) as r: - response: HTTPResponse = r - if response.getcode() != 200: - Logger.print_error( - f"Error retrieving tags: HTTP status code {response.getcode()}" - ) - return [] - - data = json.loads(response.read()) - return [item["name"] for item in data] - except (JSONDecodeError, TypeError) as e: - Logger.print_error(f"Error while processing the response: {e}") - raise - - -def get_latest_remote_tag(repo_path: str) -> str: - """ - Gets the latest stable tag of a GitHub repostiory - :param repo_path: path of the GitHub repository - e.g. `/` - :return: tag or empty string - """ - try: - if len(latest_tag := get_remote_tags(repo_path)) > 0: - return latest_tag[0] - else: - return "" - except Exception: - raise - - -def get_latest_unstable_tag(repo_path: str) -> str: - """ - Gets the latest unstable (alpha, beta, rc) tag of a GitHub repository - :param repo_path: path of the GitHub repository - e.g. `/` - :return: tag or empty string - """ - try: - if ( - len(unstable_tags := [t for t in get_remote_tags(repo_path) if "-" in t]) - > 0 - ): - return unstable_tags[0] - else: - return "" - except Exception: - Logger.print_error("Error while getting the latest unstable tag") - raise - - -def compare_semver_tags(tag1: str, tag2: str) -> bool: - """ - Compare two semver version strings. - Does not support comparing pre-release versions (e.g. 1.0.0-rc.1, 1.0.0-beta.1) - :param tag1: First version string - :param tag2: Second version string - :return: True if tag1 is greater than tag2, False otherwise - """ - if tag1 == tag2: - return False - - def parse_version(v) -> List[int]: - return list(map(int, v[1:].split("."))) - - tag1_parts = parse_version(tag1) - tag2_parts = parse_version(tag2) - - max_len = max(len(tag1_parts), len(tag2_parts)) - tag1_parts += [0] * (max_len - len(tag1_parts)) - tag2_parts += [0] * (max_len - len(tag2_parts)) - - for part1, part2 in zip(tag1_parts, tag2_parts): - if part1 != part2: - return part1 > part2 - - return False - - -def get_local_commit(repo: Path) -> str | None: - if not repo.exists() or not repo.joinpath(".git").exists(): - return None - - try: - cmd = "git describe HEAD --always --tags | cut -d '-' -f 1,2" - return check_output(cmd, shell=True, text=True, cwd=repo).strip() - except CalledProcessError: - return None - - -def get_remote_commit(repo: Path) -> str | None: - if not repo.exists() or not repo.joinpath(".git").exists(): - return None - - try: - branch = get_current_branch(repo) - cmd = f"git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" - return check_output( - cmd, - shell=True, - text=True, - cwd=repo, - stderr=DEVNULL, - ).strip() - except CalledProcessError: - return None - - -def git_cmd_clone(repo: str, target_dir: Path, blobless: bool = False) -> None: - """ - Clones a repository with optional blobless clone. - - :param repo: URL of the repository to clone. - :param target_dir: Path where the repository will be cloned. - :param blobless: If True, perform a blobless clone by adding the '--filter=blob:none' flag. - """ - try: - command = ["git", "clone"] - - if blobless: - command.append("--filter=blob:none") - - command += [repo, target_dir.as_posix()] - - run(command, check=True) - Logger.print_ok("Clone successful!") - except CalledProcessError as e: - error = e.stderr.decode() if e.stderr else "Unknown error" - log = f"Error cloning repository {repo}: {error}" - Logger.print_error(log) - raise - - -def git_cmd_checkout(branch: str | None, target_dir: Path) -> None: - if branch is None: - return - - try: - command = ["git", "checkout", f"{branch}"] - run(command, cwd=target_dir, check=True) - - Logger.print_ok("Checkout successful!") - except CalledProcessError as e: - log = f"Error checking out branch {branch}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def git_cmd_pull(target_dir: Path) -> None: - try: - command = ["git", "pull"] - run(command, cwd=target_dir, check=True) - except CalledProcessError as e: - log = f"Error on git pull: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def rollback_repository(repo_dir: Path, instance: Type[InstanceType]) -> None: - q1 = "How many commits do you want to roll back" - amount = get_number_input(q1, 1, allow_go_back=True) - - instances = get_instances(instance) - - Logger.print_warn("Do not continue if you have ongoing prints!", start="\n") - Logger.print_warn( - f"All currently running {instance.__name__} services will be stopped!" - ) - if not get_confirm( - f"Roll back {amount} commit{'s' if amount > 1 else ''}", - default_choice=False, - allow_go_back=True, - ): - Logger.print_info("Aborting roll back ...") - return - - InstanceManager.stop_all(instances) - - try: - cmd = ["git", "reset", "--hard", f"HEAD~{amount}"] - run(cmd, cwd=repo_dir, check=True, stdout=PIPE, stderr=PIPE) - Logger.print_ok(f"Rolled back {amount} commits!", start="\n") - except CalledProcessError as e: - Logger.print_error(f"An error occured during repo rollback:\n{e}") - - InstanceManager.start_all(instances) - - -def get_repo_url(repo_dir: Path) -> str | None: - """ - Get the remote repository URL for a git repository - :param repo_dir: Path to the git repository - :return: URL of the remote repository or None if not found - """ - if not repo_dir.exists(): - return None - - try: - result = run( - ["git", "config", "--get", "remote.origin.url"], - cwd=repo_dir, - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - except CalledProcessError: - return None diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py deleted file mode 100644 index 6b77e15..0000000 --- a/kiauh/utils/input_utils.py +++ /dev/null @@ -1,177 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from typing import Dict, List - -from core.constants import INVALID_CHOICE -from core.logger import Logger -from core.types.color import Color - - -def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool | None: - """ - Helper method for validating confirmation (yes/no) user input. | - :param question: The question to display - :param default_choice: A default if input was submitted without input - :param allow_go_back: Navigate back to a previous dialog - :return: Either True or False, or None on go_back - """ - options_confirm = ["y", "yes"] - options_decline = ["n", "no"] - options_go_back = ["b", "B"] - - if default_choice: - def_choice = "(Y/n)" - options_confirm.append("") - else: - def_choice = "(y/N)" - options_decline.append("") - - while True: - choice = ( - input(format_question(question + f" {def_choice}", None)).strip().lower() - ) - - if choice in options_confirm: - return True - elif choice in options_decline: - return False - elif allow_go_back and choice in options_go_back: - return None - else: - Logger.print_error(INVALID_CHOICE) - - -def get_number_input( - question: str, - min_value: int, - max_value: int | None = None, - default: int | None = None, - allow_go_back: bool = False, -) -> int | None: - """ - Helper method to get a number input from the user - :param question: The question to display - :param min_value: The lowest allowed value - :param max_value: The highest allowed value (or None) - :param default: Optional default value - :param allow_go_back: Navigate back to a previous dialog - :return: Either the validated number input, or None on go_back - """ - options_go_back = ["b", "B"] - _question = format_question(question, default) - while True: - _input = input(_question) - if allow_go_back and _input in options_go_back: - return None - - if _input == "" and default is not None: - return default - - try: - return validate_number_input(_input, min_value, max_value) - except ValueError: - Logger.print_error(INVALID_CHOICE) - - -def get_string_input( - question: str, - regex: str | None = None, - exclude: List[str] | None = None, - allow_empty: bool = False, - allow_special_chars: bool = False, - default: str | None = None, -) -> str: - """ - Helper method to get a string input from the user - :param question: The question to display - :param regex: An optional regex pattern to validate the input against - :param exclude: List of strings which are not allowed - :param allow_empty: Whether to allow empty input - :param allow_special_chars: Wheter to allow special characters in the input - :param default: Optional default value - :return: The validated string value - """ - _exclude = [] if exclude is None else exclude - _question = format_question(question, default) - _pattern = re.compile(regex) if regex is not None else None - while True: - _input = input(_question) - - if default is not None and _input == "": - return default - elif _input == "" and not allow_empty: - Logger.print_error("Input must not be empty!") - elif _pattern is not None and _pattern.match(_input): - return _input - elif _input.lower() in _exclude: - Logger.print_error("This value is already in use/reserved.") - elif allow_special_chars: - return _input - elif not allow_special_chars and _input.isalnum(): - return _input - else: - Logger.print_error(INVALID_CHOICE) - - -def get_selection_input(question: str, option_list: List | Dict, default=None) -> str: - """ - Helper method to get a selection from a list of options from the user - :param question: The question to display - :param option_list: The list of options the user can select from - :param default: Optional default value - :return: The option that was selected by the user - """ - while True: - _input = input(format_question(question, default)).strip().lower() - - if isinstance(option_list, list): - if _input in option_list: - return _input - elif isinstance(option_list, dict): - if _input in option_list.keys(): - return _input - else: - raise ValueError("Invalid option_list type") - - Logger.print_error("Invalid option! Please select a valid option.", False) - - -def format_question(question: str, default=None) -> str: - """ - Helper method to have a standardized formatting of questions | - :param question: The question to display - :param default: If defined, the default option will be displayed to the user - :return: The formatted question string - """ - formatted_q = question - if default is not None: - formatted_q += f" (default={default})" - - return Color.apply(f"###### {formatted_q}: ", Color.CYAN) - - -def validate_number_input(value: str, min_count: int, max_count: int | None) -> int: - """ - Helper method for a simple number input validation. | - :param value: The value to validate - :param min_count: The lowest allowed value - :param max_count: The highest allowed value (or None) - :return: The validated value as Integer - :raises: ValueError if value is invalid - """ - if max_count is not None: - if min_count <= int(value) <= max_count: - return int(value) - elif int(value) >= min_count: - return int(value) - - raise ValueError diff --git a/kiauh/utils/instance_type.py b/kiauh/utils/instance_type.py deleted file mode 100644 index 99f963c..0000000 --- a/kiauh/utils/instance_type.py +++ /dev/null @@ -1,27 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 typing import TypeVar - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from extensions.obico.moonraker_obico import MoonrakerObico -from extensions.octoeverywhere.octoeverywhere import Octoeverywhere -from extensions.octoapp.octoapp import Octoapp -from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot - -InstanceType = TypeVar( - "InstanceType", - Klipper, - Moonraker, - MoonrakerTelegramBot, - MoonrakerObico, - Octoeverywhere, - Octoapp, -) diff --git a/kiauh/utils/instance_utils.py b/kiauh/utils/instance_utils.py deleted file mode 100644 index c73f690..0000000 --- a/kiauh/utils/instance_utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 re -from pathlib import Path -from typing import List - -from core.constants import SYSTEMD -from core.instance_manager.base_instance import SUFFIX_BLACKLIST -from utils.instance_type import InstanceType - - -def get_instances( - instance_type: type, suffix_blacklist: List[str] = SUFFIX_BLACKLIST -) -> List[InstanceType]: - from utils.common import convert_camelcase_to_kebabcase - - if not isinstance(instance_type, type): - raise ValueError("instance_type must be a class") - - name = convert_camelcase_to_kebabcase(instance_type.__name__) - pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") - - service_list = [ - Path(SYSTEMD, service) - for service in SYSTEMD.iterdir() - if pattern.search(service.name) - and not any(s in service.name for s in suffix_blacklist) - ] - - instance_list = [ - instance_type(get_instance_suffix(name, service)) for service in service_list - ] - - def _sort_instance_list(suffix: int | str | None): - if suffix is None: - return - elif isinstance(suffix, str) and suffix.isdigit(): - return f"{int(suffix):04}" - else: - return suffix - - return sorted(instance_list, key=lambda x: _sort_instance_list(x.suffix)) - - -def get_instance_suffix(name: str, file_path: Path) -> str: - # to get the suffix of the instance, we remove the name of the instance from - # the file name, if the remaining part an empty string we return it - # otherwise there is and hyphen left, and we return the part after the hyphen - suffix = file_path.stem[len(name) :] - return suffix[1:] if suffix else "" diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py deleted file mode 100644 index add3ef2..0000000 --- a/kiauh/utils/sys_utils.py +++ /dev/null @@ -1,644 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# 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 os -import re -import select -import shutil -import socket -import sys -import time -import urllib.error -import urllib.request -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, check_output, run -from typing import List, Literal, Set, Tuple - -from core.constants import SYSTEMD -from core.logger import Logger -from utils.fs_utils import check_file_exist, remove_with_sudo -from utils.input_utils import get_confirm - -SysCtlServiceAction = Literal[ - "start", - "stop", - "restart", - "reload", - "enable", - "disable", - "mask", - "unmask", -] -SysCtlManageAction = Literal["daemon-reload", "reset-failed"] - - -class VenvCreationFailedException(Exception): - pass - - -def kill(opt_err_msg: str = "") -> None: - """ - Kills the application | - :param opt_err_msg: an optional, additional error message - :return: None - """ - - if opt_err_msg: - Logger.print_error(opt_err_msg) - Logger.print_error("A critical error has occured. KIAUH was terminated.") - sys.exit(1) - - -def check_python_version(major: int, minor: int) -> bool: - """ - Checks the python version and returns True if it's at least the given version - :param major: the major version to check - :param minor: the minor version to check - :return: bool - """ - if not (sys.version_info.major >= major and sys.version_info.minor >= minor): - Logger.print_error("Versioncheck failed!") - Logger.print_error(f"Python {major}.{minor} or newer required.") - return False - return True - - -def parse_packages_from_file(source_file: Path) -> List[str]: - """ - Read the package names from bash scripts, when defined like: - PKGLIST="package1 package2 package3" | - :param source_file: path of the sourcefile to read from - :return: A list of package names - """ - - packages = [] - with open(source_file, "r") as file: - for line in file: - line = line.strip() - if line.startswith("PKGLIST="): - line = line.replace('"', "") - line = line.replace("PKGLIST=", "") - line = line.replace("${PKGLIST}", "") - packages.extend(line.split()) - - return packages - - -def create_python_venv( - target: Path, - force: bool = False, - allow_access_to_system_site_packages: bool = False, - use_python_binary: str | None = None -) -> bool: - """ - Create a python 3 virtualenv at the provided target destination. - Returns True if the virtualenv was created successfully. - Returns False if the virtualenv already exists, recreation was declined or creation failed. - :param target: Path where to create the virtualenv at - :param force: Force recreation of the virtualenv - :param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir - :param use_python_binary: allows to override default python binary - :return: bool - """ - Logger.print_status("Set up Python virtual environment ...") - # If binarry override is not set, we use default defined here - python_binary = use_python_binary if use_python_binary else "/usr/bin/python3" - cmd = ["virtualenv", "-p", python_binary, target.as_posix()] - cmd.append( - "--system-site-packages" - ) if allow_access_to_system_site_packages else None - - n = 2 - while(n > 0): - if not target.exists(): - try: - run(cmd, check=True) - Logger.print_ok("Setup of virtualenv successful!") - return True - except CalledProcessError as e: - Logger.print_error(f"Error setting up virtualenv:\n{e}") - return False - else: - if n == 1: - # This case should never happen, - # but the function should still behave correctly - Logger.print_error("Virtualenv still exists after deletion.") - return False - if not force and not get_confirm( - "Virtualenv already exists. Re-create?", default_choice=False - ): - Logger.print_info("Skipping re-creation of virtualenv ...") - return False - - try: - shutil.rmtree(target) - n -= 1 - except OSError as e: - log = f"Error removing existing virtualenv: {e.strerror}" - Logger.print_error(log, False) - return False - - -def update_python_pip(target: Path) -> None: - """ - Updates pip in the provided target destination | - :param target: Path of the virtualenv - :return: None - """ - Logger.print_status("Updating pip ...") - try: - pip_location: Path = target.joinpath("bin/pip") - pip_exists: bool = check_file_exist(pip_location) - - if not pip_exists: - raise FileNotFoundError("Error updating pip! Not found.") - - command = [pip_location.as_posix(), "install", "-U", "pip"] - result = run(command, stderr=PIPE, text=True) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Updating pip failed!") - return - - Logger.print_ok("Updating pip successful!") - except FileNotFoundError as e: - Logger.print_error(e) - raise - except CalledProcessError as e: - Logger.print_error(f"Error updating pip:\n{e.output.decode()}") - raise - - -def install_python_requirements(target: Path, requirements: Path) -> None: - """ - Installs the python packages based on a provided requirements.txt | - :param target: Path of the virtualenv - :param requirements: Path to the requirements.txt file - :return: None - """ - try: - # always update pip before installing requirements - update_python_pip(target) - - Logger.print_status("Installing Python requirements ...") - command = [ - target.joinpath("bin/pip").as_posix(), - "install", - "-r", - f"{requirements}", - ] - result = run(command, stderr=PIPE, text=True) - - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - raise VenvCreationFailedException("Installing Python requirements failed!") - - Logger.print_ok("Installing Python requirements successful!") - - except Exception as e: - log = f"Error installing Python requirements: {e}" - Logger.print_error(log) - raise VenvCreationFailedException(log) - - -def install_python_packages(target: Path, packages: List[str]) -> None: - """ - Installs the python packages based on a provided packages list | - :param target: Path of the virtualenv - :param packages: str list of required packages - :return: None - """ - try: - # always update pip before installing requirements - update_python_pip(target) - - Logger.print_status("Installing Python requirements ...") - command = [ - target.joinpath("bin/pip").as_posix(), - "install", - ] - for pkg in packages: - command.append(pkg) - result = run(command, stderr=PIPE, text=True) - - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - raise VenvCreationFailedException("Installing Python requirements failed!") - - Logger.print_ok("Installing Python requirements successful!") - - except Exception as e: - log = f"Error installing Python requirements: {e}" - Logger.print_error(log) - raise VenvCreationFailedException(log) - - -def update_system_package_lists(silent: bool, rls_info_change=False) -> None: - """ - Updates the systems package list | - :param silent: Log info to the console or not - :param rls_info_change: Flag for "--allow-releaseinfo-change" - :return: None - """ - cache_mtime: float = 0 - cache_files: List[Path] = [ - Path("/var/lib/apt/periodic/update-success-stamp"), - Path("/var/lib/apt/lists"), - ] - for cache_file in cache_files: - if cache_file.exists(): - cache_mtime = max(cache_mtime, os.path.getmtime(cache_file)) - - update_age = int(time.time() - cache_mtime) - update_interval = 6 * 3600 # 48hrs - - if update_age <= update_interval: - return - - if not silent: - Logger.print_status("Updating package list...") - - try: - command = ["sudo", "apt-get", "update"] - if rls_info_change: - command.append("--allow-releaseinfo-change") - - result = run(command, stderr=PIPE, text=True) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Updating system package list failed!") - return - - Logger.print_ok("System package list update successful!") - except CalledProcessError as e: - Logger.print_error(f"Error updating system package list:\n{e.stderr.decode()}") - raise - - -def get_upgradable_packages() -> List[str]: - """ - Reads all system packages that can be upgraded. - :return: A list of package names available for upgrade - """ - try: - command = ["apt", "list", "--upgradable"] - output: str = check_output(command, stderr=DEVNULL, text=True, encoding="utf-8") - pkglist = [] - for line in output.split("\n"): - if "/" not in line: - continue - pkg = line.split("/")[0] - pkglist.append(pkg) - return pkglist - except CalledProcessError as e: - raise Exception(f"Error reading upgradable packages: {e}") - - -def check_package_install(packages: Set[str]) -> List[str]: - """ - Checks the system for installed packages | - :param packages: List of strings of package names - :return: A list containing the names of packages that are not installed - """ - not_installed = [] - for package in packages: - command = ["dpkg-query", "-f'${Status}'", "--show", package] - result = run( - command, - stdout=PIPE, - stderr=DEVNULL, - text=True, - ) - if "installed" not in result.stdout.strip("'").split(): - not_installed.append(package) - - return not_installed - - -def install_system_packages(packages: List[str]) -> None: - """ - Installs a list of system packages | - :param packages: List of system package names - :return: None - """ - try: - command = ["sudo", "apt-get", "install", "-y"] - for pkg in packages: - command.append(pkg) - run(command, stderr=PIPE, check=True) - - Logger.print_ok("Packages successfully installed.") - except CalledProcessError as e: - Logger.print_error(f"Error installing packages:\n{e.stderr.decode()}") - raise - - -def upgrade_system_packages(packages: List[str]) -> None: - """ - Updates a list of system packages | - :param packages: List of system package names - :return: None - """ - try: - command = ["sudo", "apt-get", "upgrade", "-y"] - for pkg in packages: - command.append(pkg) - run(command, stderr=PIPE, check=True) - - Logger.print_ok("Packages successfully upgraded.") - except CalledProcessError as e: - raise Exception(f"Error upgrading packages:\n{e.stderr.decode()}") - - -# this feels hacky and not quite right, but for now it works -# see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib -def get_ipv4_addr() -> str: - """ - Helper function that returns the IPv4 of the current machine - by opening a socket and sending a package to an arbitrary IP. | - :return: Local IPv4 of the current machine - """ - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.settimeout(0) - try: - # doesn't even have to be reachable - s.connect(("192.255.255.255", 1)) - ipv4: str = str(s.getsockname()[0]) - s.close() - return ipv4 - except Exception: - s.close() - return "127.0.0.1" - - -def download_file(url: str, target: Path, show_progress=True) -> None: - """ - Helper method for downloading files from a provided URL | - :param url: the url to the file - :param target: the target path incl filename - :param show_progress: show download progress or not - :return: None - """ - try: - if show_progress: - urllib.request.urlretrieve(url, target, download_progress) - sys.stdout.write("\n") - else: - urllib.request.urlretrieve(url, target) - except urllib.error.HTTPError as e: - Logger.print_error(f"Download failed! HTTP error occured: {e}") - raise - except urllib.error.URLError as e: - Logger.print_error(f"Download failed! URL error occured: {e}") - raise - except Exception as e: - Logger.print_error(f"Download failed! An error occured: {e}") - raise - - -def download_progress(block_num, block_size, total_size) -> None: - """ - Reporthook method for urllib.request.urlretrieve() method call in download_file() | - :param block_num: - :param block_size: - :param total_size: total filesize in bytes - :return: None - """ - downloaded = block_num * block_size - percent = 100 if downloaded >= total_size else downloaded / total_size * 100 - mb = 1024 * 1024 - progress = int(percent / 5) - remaining = "-" * (20 - progress) - dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded / mb:.2f}/{total_size / mb:.2f}MB)" - sys.stdout.write(dl) - sys.stdout.flush() - - -def set_nginx_permissions() -> None: - """ - Check if permissions of the users home directory - grant execution rights to group and other and set them if not set. - Required permissions for NGINX to be able to serve Mainsail/Fluidd. - This seems to have become necessary with Ubuntu 21+. | - :return: None - """ - cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" - homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) - permissions = homedir_perm.stdout - - if permissions.count("x") < 3: - Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", Path.home()]) - Logger.print_ok("Permissions granted.") - - -def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: - """ - Helper method to execute several actions for a specific systemd service. | - :param name: the service name - :param action: Either "start", "stop", "restart" or "disable" - :return: None - """ - try: - Logger.print_status(f"{action.capitalize()} {name} ...") - run(["sudo", "systemctl", action, name], stderr=PIPE, check=True) - Logger.print_ok("OK!") - except CalledProcessError as e: - log = f"Failed to {action} {name}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def cmd_sysctl_manage(action: SysCtlManageAction) -> None: - try: - run(["sudo", "systemctl", action], stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Failed to run {action}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def unit_file_exists( - name: str, suffix: Literal["service", "timer"], exclude: List[str] | None = None -) -> bool: - """ - Checks if a systemd unit file of the provided suffix exists. - :param name: the name of the unit file - :param suffix: suffix of the unit file, either "service" or "timer" - :param exclude: List of strings of names to exclude - :return: True if the unit file exists, False otherwise - """ - exclude = exclude or [] - pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.{suffix}$") - service_list = [ - Path(SYSTEMD, service) - for service in SYSTEMD.iterdir() - if pattern.search(service.name) and not any(s in service.name for s in exclude) - ] - return any(service_list) - - -def log_process(process: Popen) -> None: - """ - Helper method to print stdout of a process in near realtime to the console. - :param process: Process to log the output from - :return: None - """ - while True: - if process.stdout is not None: - reads = [process.stdout.fileno()] - ret = select.select(reads, [], []) - for fd in ret[0]: - if fd == process.stdout.fileno(): - line = process.stdout.readline() - if line: - print(line.strip(), flush=True) - else: - break - - if process.poll() is not None: - break - - -def create_service_file(name: str, content: str) -> None: - """ - Creates a service file at the provided path with the provided content. - :param name: the name of the service file - :param content: the content of the service file - :return: None - """ - try: - run( - ["sudo", "tee", SYSTEMD.joinpath(name)], - input=content.encode(), - stdout=DEVNULL, - check=True, - ) - Logger.print_ok(f"Service file created: {SYSTEMD.joinpath(name)}") - except CalledProcessError as e: - Logger.print_error(f"Error creating service file: {e}") - raise - - -def create_env_file(path: Path, content: str) -> None: - """ - Creates an env file at the provided path with the provided content. - :param path: the path of the env file - :param content: the content of the env file - :return: None - """ - try: - with open(path, "w") as env_file: - env_file.write(content) - Logger.print_ok(f"Env file created: {path}") - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - -def remove_system_service(service_name: str) -> None: - """ - Disables and removes a systemd service - :param service_name: name of the service unit file - must end with '.service' - :return: None - """ - try: - if not service_name.endswith(".service"): - raise ValueError(f"service_name '{service_name}' must end with '.service'") - - file: Path = SYSTEMD.joinpath(service_name) - if not file.exists() or not file.is_file(): - Logger.print_info(f"Service '{service_name}' does not exist! Skipped ...") - return - - Logger.print_status(f"Removing {service_name} ...") - cmd_sysctl_service(service_name, "stop") - cmd_sysctl_service(service_name, "disable") - remove_with_sudo(file) - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - Logger.print_ok(f"{service_name} successfully removed!") - except Exception as e: - Logger.print_error(f"Error removing {service_name}: {e}") - raise - - -def get_service_file_path(instance_type: type, suffix: str) -> Path: - from utils.common import convert_camelcase_to_kebabcase - - if not isinstance(instance_type, type): - raise ValueError("instance_type must be a class") - - name: str = convert_camelcase_to_kebabcase(instance_type.__name__) - if suffix != "": - name += f"-{suffix}" - - file_path: Path = SYSTEMD.joinpath(f"{name}.service") - - return file_path - - -def get_distro_info() -> Tuple[str, str]: - distro_info: str = check_output(["cat", "/etc/os-release"]).decode().strip() - - if not distro_info: - raise ValueError("Error reading distro info!") - - distro_id: str = "" - distro_id_like: str = "" - distro_version: str = "" - - for line in distro_info.split("\n"): - if line.startswith("ID="): - distro_id = line.split("=")[1].strip('"').strip() - if line.startswith("ID_LIKE="): - distro_id_like = line.split("=")[1].strip('"').strip() - if line.startswith("VERSION_ID="): - distro_version = line.split("=")[1].strip('"').strip() - - if distro_id == "raspbian": - distro_id = distro_id_like - - if not distro_id: - raise ValueError("Error reading distro id!") - if not distro_version: - raise ValueError("Error reading distro version!") - - return distro_id.lower(), distro_version - - -def get_system_timezone() -> str: - timezone = "UTC" - try: - with open("/etc/timezone", "r") as f: - timezone = f.read().strip() - except FileNotFoundError: - # fallback to reading timezone from timedatectl - try: - result = run( - ["timedatectl", "show", "--property=Timezone"], - capture_output=True, - text=True, - check=True, - ) - timezone = result.stdout.strip().split("=")[1] - except CalledProcessError: - # fallback if timedatectl fails, try reading from readlink - try: - result = run( - ["readlink", "-f", "/etc/localtime"], - capture_output=True, - text=True, - check=True, - ) - timezone = result.stdout.strip().split("zoneinfo/")[1] - except (CalledProcessError, IndexError): - Logger.print_warn("Could not determine system timezone, using UTC") - return timezone diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..347b552 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,82 @@ +site_name: KIAUH Documentation +site_description: Documentation for the Klipper Installation And Update Helper +repo_url: https://github.com/dw-0/kiauh +repo_name: dw-0/kiauh +edit_uri: edit/master/docs + +copyright: Copyright © 2025 Dominik Willner + +theme: + name: material + logo: assets/logo.png + favicon: assets/logo.png + icon: + repo: fontawesome/brands/github + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: blue-grey + accent: cyan + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: blue-grey + accent: cyan + toggle: + icon: material/weather-sunny + name: Switch to light mode + features: + - navigation.instant + - navigation.tracking + - navigation.sections + - navigation.expand + - navigation.indexes + - navigation.top + - toc.follow + - content.code.copy + +plugins: + - search + - git-revision-date-localized: + enable_creation_date: true + - mkdocstrings: + handlers: + python: + paths: [.] + options: + docstring_style: google + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - tables + - attr_list + - md_in_html + +nav: + - Home: index.md + - Installation: installation.md + - Configuration: configuration.md + - Extensions: + - extensions/index.md + - extensions/gcode-shell-command.md + - Development: + - development/contributing.md + - development/changelog.md + +extra: + social: + - icon: simple/github + link: https://github.com/dw-0 + - icon: simple/kofi + link: https://ko-fi.com/dw__0 + - icon: simple/paypal + link: https://www.paypal.com/paypalme/dwillner0 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 002b70d..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -[project] -requires-python = ">=3.8" - -[project.optional-dependencies] -dev=["ruff", "mypy"] - -[tool.ruff] -required-version = ">=0.9.10" -respect-gitignore = true -exclude = [".git",".github", "./docs", "kiauh/core/submodules"] -line-length = 88 -indent-width = 4 -output-format = "full" -target-version = "py38" - -[tool.ruff.format] -indent-style = "space" -line-ending = "lf" -quote-style = "double" - -[tool.ruff.lint] -extend-select = ["I"] - -[tool.mypy] -python_version = "3.8" -platform = "linux" -# strict = true # TODO: enable this once everything is else is handled -check_untyped_defs = true -ignore_missing_imports = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_return_any = true -warn_unreachable = true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d7b7fdf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mkdocs-material +mkdocs +mkdocstrings[python] +mkdocs-git-revision-date-localized-plugin \ No newline at end of file diff --git a/resources/autocommit.sh b/resources/autocommit.sh deleted file mode 100755 index b870a22..0000000 --- a/resources/autocommit.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -##################################################################### -### Please set the paths accordingly. In case you don't have all ### -### the listed folders, just keep that line commented out. ### -##################################################################### -### Path to your config folder you want to back up -#config_folder=~/klipper_config - -### Path to your Klipper folder, by default that is '~/klipper' -#klipper_folder=~/klipper - -### Path to your Moonraker folder, by default that is '~/moonraker' -#moonraker_folder=~/moonraker - -### Path to your Mainsail folder, by default that is '~/mainsail' -#mainsail_folder=~/mainsail - -### Path to your Fluidd folder, by default that is '~/fluidd' -#fluidd_folder=~/fluidd - -##################################################################### -##################################################################### - - -##################################################################### -################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ -##################################################################### -grab_version() { - local klipper_commit moonraker_commit - local mainsail_ver fluidd_ver - - if [[ -n ${klipper_folder} ]]; then - cd "${klipper_folder}" - klipper_commit=$(git rev-parse --short=7 HEAD) - m1="Klipper on commit: ${klipper_commit}" - fi - if [[ -n ${moonraker_folder} ]]; then - cd "${moonraker_folder}" - moonraker_commit=$(git rev-parse --short=7 HEAD) - m2="Moonraker on commit: ${moonraker_commit}" - fi - if [[ -n ${mainsail_folder} ]]; then - mainsail_ver=$(head -n 1 "${mainsail_folder}/.version") - m3="Mainsail version: ${mainsail_ver}" - fi - if [[ -n ${fluidd_folder} ]]; then - fluidd_ver=$(head -n 1 "${fluidd_folder}/.version") - m4="Fluidd version: ${fluidd_ver}" - fi -} - -push_config() { - local current_date - - cd "${config_folder}" || exit 1 - git pull - git add . - current_date=$(date +"%Y-%m-%d %T") - git commit -m "Autocommit from ${current_date}" -m "${m1}" -m "${m2}" -m "${m3}" -m "${m4}" - git push -} - -grab_version -push_config diff --git a/resources/common_vars.conf b/resources/common_vars.conf deleted file mode 100644 index 9c3f85e..0000000 --- a/resources/common_vars.conf +++ /dev/null @@ -1,6 +0,0 @@ -# /etc/nginx/conf.d/common_vars.conf - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} \ No newline at end of file diff --git a/resources/example.printer.cfg b/resources/example.printer.cfg deleted file mode 100644 index 88fe7df..0000000 --- a/resources/example.printer.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[mcu] -serial: /dev/serial/by-id/ - -[virtual_sdcard] -path: %GCODES_DIR% -on_error_gcode: CANCEL_PRINT - -[printer] -kinematics: none -max_velocity: 1000 -max_accel: 1000 diff --git a/resources/fluidd b/resources/fluidd deleted file mode 100644 index 468fad4..0000000 --- a/resources/fluidd +++ /dev/null @@ -1,96 +0,0 @@ -# /etc/nginx/sites-available/fluidd - -server { - listen 80; - - access_log /var/log/nginx/fluidd-access.log; - error_log /var/log/nginx/fluidd-error.log; - - # disable this section on smaller hardware like a pi zero - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_proxied expired no-cache no-store private auth; - gzip_comp_level 4; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml; - - # web_path from fluidd static files - root /home/pi/fluidd; - - index index.html; - server_name _; - - # disable max upload size checks - client_max_body_size 0; - - # disable proxy request buffering - proxy_request_buffering off; - - location / { - try_files $uri $uri/ /index.html; - } - - location = /index.html { - add_header Cache-Control "no-store, no-cache, must-revalidate"; - } - - location /websocket { - proxy_pass http://apiserver/websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 86400; - } - - location ~ ^/(printer|api|access|machine|server)/ { - proxy_pass http://apiserver$request_uri; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Scheme $scheme; - proxy_read_timeout 600; - } - - location /webcam/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer1/; - } - - location /webcam2/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer2/; - } - - location /webcam3/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer3/; - } - - location /webcam4/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer4/; - } -} diff --git a/resources/gcode_shell_command.py b/resources/gcode_shell_command.py deleted file mode 100755 index 3f316e6..0000000 --- a/resources/gcode_shell_command.py +++ /dev/null @@ -1,94 +0,0 @@ -# Run a shell command via gcode -# -# Copyright (C) 2019 Eric Callahan -# -# This file may be distributed under the terms of the GNU GPLv3 license. -import os -import shlex -import subprocess -import logging - - -class ShellCommand: - def __init__(self, config): - self.name = config.get_name().split()[-1] - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object("gcode") - cmd = config.get("command") - cmd = os.path.expanduser(cmd) - self.command = shlex.split(cmd) - self.timeout = config.getfloat("timeout", 2.0, above=0.0) - self.verbose = config.getboolean("verbose", True) - self.proc_fd = None - self.partial_output = "" - self.gcode.register_mux_command( - "RUN_SHELL_COMMAND", - "CMD", - self.name, - self.cmd_RUN_SHELL_COMMAND, - desc=self.cmd_RUN_SHELL_COMMAND_help, - ) - - def _process_output(self, eventime): - if self.proc_fd is None: - return - try: - data = os.read(self.proc_fd, 4096) - except Exception: - pass - data = self.partial_output + data.decode() - if "\n" not in data: - self.partial_output = data - return - elif data[-1] != "\n": - split = data.rfind("\n") + 1 - self.partial_output = data[split:] - data = data[:split] - else: - self.partial_output = "" - self.gcode.respond_info(data) - - cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" - - def cmd_RUN_SHELL_COMMAND(self, params): - gcode_params = params.get("PARAMS", "") - gcode_params = shlex.split(gcode_params) - reactor = self.printer.get_reactor() - try: - proc = subprocess.Popen( - self.command + gcode_params, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - except Exception: - logging.exception("shell_command: Command {%s} failed" % (self.name)) - raise self.gcode.error("Error running command {%s}" % (self.name)) - if self.verbose: - self.proc_fd = proc.stdout.fileno() - self.gcode.respond_info("Running Command {%s}...:" % (self.name)) - hdl = reactor.register_fd(self.proc_fd, self._process_output) - eventtime = reactor.monotonic() - endtime = eventtime + self.timeout - complete = False - while eventtime < endtime: - eventtime = reactor.pause(eventtime + 0.05) - if proc.poll() is not None: - complete = True - break - if not complete: - proc.terminate() - if self.verbose: - if self.partial_output: - self.gcode.respond_info(self.partial_output) - self.partial_output = "" - if complete: - msg = "Command {%s} finished\n" % (self.name) - else: - msg = "Command {%s} timed out" % (self.name) - self.gcode.respond_info(msg) - reactor.unregister_fd(hdl) - self.proc_fd = None - - -def load_config_prefix(config): - return ShellCommand(config) diff --git a/resources/klipper.env b/resources/klipper.env deleted file mode 100644 index 6ab13d6..0000000 --- a/resources/klipper.env +++ /dev/null @@ -1 +0,0 @@ -KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %PRINTER% -l %LOG% -a %UDS%" \ No newline at end of file diff --git a/resources/klipper.service b/resources/klipper.service deleted file mode 100644 index b41788f..0000000 --- a/resources/klipper.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Klipper 3D Printer Firmware SV1 -Documentation=https://www.klipper3d.org/ -After=network-online.target -Wants=udev.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -RemainAfterExit=yes -WorkingDirectory=%KLIPPER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $KLIPPER_ARGS -Restart=always -RestartSec=10 diff --git a/resources/mainsail b/resources/mainsail deleted file mode 100644 index 6d63b07..0000000 --- a/resources/mainsail +++ /dev/null @@ -1,96 +0,0 @@ -# /etc/nginx/sites-available/mainsail - -server { - listen 80; - - access_log /var/log/nginx/mainsail-access.log; - error_log /var/log/nginx/mainsail-error.log; - - # disable this section on smaller hardware like a pi zero - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_proxied expired no-cache no-store private auth; - gzip_comp_level 4; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml; - - # web_path from mainsail static files - root /home/pi/mainsail; - - index index.html; - server_name _; - - # disable max upload size checks - client_max_body_size 0; - - # disable proxy request buffering - proxy_request_buffering off; - - location / { - try_files $uri $uri/ /index.html; - } - - location = /index.html { - add_header Cache-Control "no-store, no-cache, must-revalidate"; - } - - location /websocket { - proxy_pass http://apiserver/websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 86400; - } - - location ~ ^/(printer|api|access|machine|server)/ { - proxy_pass http://apiserver$request_uri; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Scheme $scheme; - proxy_read_timeout 600; - } - - location /webcam/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer1/; - } - - location /webcam2/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer2/; - } - - location /webcam3/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer3/; - } - - location /webcam4/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer4/; - } -} diff --git a/resources/mjpg-streamer/webcam.txt b/resources/mjpg-streamer/webcam.txt deleted file mode 100644 index e8bd44c..0000000 --- a/resources/mjpg-streamer/webcam.txt +++ /dev/null @@ -1,79 +0,0 @@ -### Windows users: To edit this file use Notepad++, VSCode, Atom or SublimeText. -### Do not use Notepad or WordPad. - -### MacOSX users: If you use Textedit to edit this file make sure to use -### "plain text format" and "disable smart quotes" in "Textedit > Preferences" - -### Configure which camera to use -# -# Available options are: -# - auto: tries first usb webcam, if that's not available tries raspi cam -# - usb: only tries usb webcam -# - raspi: only tries raspi cam -# -# Defaults to auto -# -#camera="auto" - -### Additional options to supply to MJPG Streamer for the USB camera -# -# See https://faq.octoprint.org/mjpg-streamer-config for available options -# -# Defaults to a resolution of 640x480 px and a framerate of 10 fps -# -#camera_usb_options="-r 640x480 -f 10" - -### Additional webcam devices known to cause problems with -f -# -# Apparently there a some devices out there that with the current -# mjpg_streamer release do not support the -f parameter (for specifying -# the capturing framerate) and will just refuse to output an image if it -# is supplied. -# -# The webcam daemon will detect those devices by their USB Vendor and Product -# ID and remove the -f parameter from the options provided to mjpg_streamer. -# -# By default, this is done for the following devices: -# Logitech C170 (046d:082b) -# GEMBIRD (1908:2310) -# Genius F100 (0458:708c) -# Cubeternet GL-UPC822 UVC WebCam (1e4e:0102) -# -# Using the following option it is possible to add additional devices. If -# your webcam happens to show above symptoms, try determining your cam's -# vendor and product id via lsusb, activating the line below by removing # and -# adding it, e.g. for two broken cameras "aabb:ccdd" and "aabb:eeff" -# -# additional_brokenfps_usb_devices=("aabb:ccdd" "aabb:eeff") -# -# -#additional_brokenfps_usb_devices=() - -### Additional options to supply to MJPG Streamer for the RasPi Cam -# -# See https://faq.octoprint.org/mjpg-streamer-config for available options -# -# Defaults to 10fps -# -#camera_raspi_options="-fps 10" - -### Configuration of camera HTTP output -# -# Usually you should NOT need to change this at all! Only touch if you -# know what you are doing and what the parameters mean. -# -# Below settings are used in the mjpg-streamer call like this: -# -# -o "output_http.so -w $camera_http_webroot $camera_http_options" -# -# Current working directory is the mjpg-streamer base directory. -# -#camera_http_webroot="./www-mainsail" -#camera_http_options="-n" - -### EXPERIMENTAL -# Support for different streamer types. -# -# Available options: -# mjpeg [default] - stable MJPG-streamer -#camera_streamer=mjpeg diff --git a/resources/mjpg-streamer/webcamd b/resources/mjpg-streamer/webcamd deleted file mode 100644 index 7d3dbed..0000000 --- a/resources/mjpg-streamer/webcamd +++ /dev/null @@ -1,303 +0,0 @@ -#!/bin/bash - -######################################################################## -### DO NOT EDIT THIS FILE TO CHANGE THE CONFIG!!! ### -### ---------------------------------------------------------------- ### -### There is no need to edit this file for changing resolution, ### -### frame rates or any other mjpg-streamer parameters. Please edit ### -### /home/pi/klipper_config/webcam.txt instead - that's what it's ### -### there for! You can even do this with your Pi powered down by ### -### directly accessing the file when using the SD card as thumb ### -### drive in your regular computer. ### -######################################################################## - -MJPGSTREAMER_HOME=/home/pi/mjpg-streamer -MJPGSTREAMER_INPUT_USB="input_uvc.so" -MJPGSTREAMER_INPUT_RASPICAM="input_raspicam.so" - -brokenfps_usb_devices=("046d:082b" "1908:2310" "0458:708c" "1e4e:0102" "0471:0311" "038f:6001" "046d:0804" "046d:0825" "046d:0994" "0ac8:3450") - -config_dir="/home/pi/klipper_config" - -echo "Starting up webcamDaemon..." -echo "" - -cfg_files=() -#cfg_files+=/boot/mainsail.txt -if [[ -d ${config_dir} ]]; then - cfg_files+=( `ls ${config_dir}/webcam*.txt` ) -fi - -array_camera_config=() -array_camera=() -array_camera_usb_options=() -array_camera_usb_device=() -array_camera_raspi_options=() -array_camera_http_webroot=() -array_camera_http_options=() -array_additional_brokenfps_usb_devices=() -array_camera_device=() -array_assigned_device=() - -echo "--- Configuration: ----------------------------" -for cfg_file in ${cfg_files[@]}; do - # init configuration - DO NOT EDIT, USE /home/pi/klipper_config/webcam*.txt INSTEAD! - camera="auto" - camera_usb_options="-r 640x480 -f 10" - camera_raspi_options="-fps 10" - camera_http_webroot="./www-mjpgstreamer" - camera_http_options="-n" - additional_brokenfps_usb_devices=() - - if [[ -e ${cfg_file} ]]; then - source "$cfg_file" - fi - usb_options="$camera_usb_options" - - # if webcam device is explicitly given in /home/pi/klipper_config/webcam*.txt, save the path of the device - # to a variable and remove its parameter from usb_options - extracted_device=`echo $usb_options | sed 's@.*-d \(/dev/\(video[0-9]\+\|v4l/[^ ]*\)\).*@\1@'` - if [ "$extracted_device" != "$usb_options" ] - then - # the camera options refer to a device, save it in a variable - # replace video device parameter with empty string and strip extra whitespace - usb_options=`echo $usb_options | sed 's/\-d \/dev\/\(video[0-9]\+\|v4l\/[^ ]*\)//g' | awk '$1=$1'` - else - extracted_device="" - fi - - # echo configuration - echo "cfg_file: $cfg_file" - echo "camera: $camera" - echo "usb options: $camera_usb_options" - echo "raspi options: $camera_raspi_options" - echo "http options: -w $camera_http_webroot $camera_http_options" - echo "" - echo "Explicitly USB device: $extracted_device" - echo "-----------------------------------------------" - echo "" - - array_camera_config+=( $cfg_file ) - array_camera+=( $camera ) - array_camera_usb_options+=("$usb_options") - array_camera_usb_device+=("$extracted_device") - array_camera_raspi_options+=("$camera_raspi_options") - array_camera_http_webroot+=("$camera_http_webroot") - array_camera_http_options+=("$camera_http_options") - array_camera_brokenfps_usb_devices+=("${brokenfps_usb_devices[*]} ${additional_brokenfps_usb_devices[*]}") - array_camera_device+=("") -done - -# check if array contains a string -function containsString() { - local e match="$1" - shift - for e; do [[ "$e" == "$match" ]] && return 0; done - return 1 -} - -# cleans up when the script receives a SIGINT or SIGTERM -function cleanup() { - # make sure that all child processed die when we die - local pids=$(jobs -pr) - [ -n "$pids" ] && kill $pids - exit 0 -} - -# says goodbye when the script shuts down -function goodbye() { - # say goodbye - echo "" - echo "Goodbye..." - echo "" -} - -# runs MJPG Streamer, using the provided input plugin + configuration -function runMjpgStreamer { - input=$1 - - # There are problems with 0x000137ab firmware on VL805 (Raspberry Pi 4}). - # Try to autodetect offending firmware and temporarily fix the issue - # by changing power management mode - echo "Checking for VL805 (Raspberry Pi 4)..." - if [[ -f /usr/bin/vl805 ]]; then - VL805_VERSION=$(/usr/bin/vl805) - VL805_VERSION=${VL805_VERSION#*: } - echo " - version 0x${VL805_VERSION} detected" - case "$VL805_VERSION" in - 00013701) - echo " - nothing to be done. It shouldn't cause USB problems." - ;; - 000137ab) - echo -e " - \e[31mThis version is known to cause problems with USB cameras.\e[39m" - echo -e " You may want to downgrade to 0x0013701." - echo -e " - [FIXING] Trying the setpci -s 01:00.0 0xD4.B=0x41 hack to mitigate the" - echo -e " issue. It disables ASPM L1 on the VL805. Your board may (or may not) get" - echo -e " slightly hotter. For details see:" - echo -e " https://www.raspberrypi.org/forums/viewtopic.php?f=28&t=244421" - setpci -s 01:00.0 0xD4.B=0x41 - ;; - *) - echo " - unknown firmware version. Doing nothing." - ;; - esac - else - echo " - It seems that you don't have VL805 (Raspberry Pi 4)." - echo " There should be no problems with USB (a.k.a. select() timeout)" - fi - - pushd $MJPGSTREAMER_HOME > /dev/null 2>&1 - echo Running ./mjpg_streamer -o "output_http.so -w $camera_http_webroot $camera_http_options" -i "$input" - LD_LIBRARY_PATH=. ./mjpg_streamer -o "output_http.so -w $camera_http_webroot $camera_http_options" -i "$input" & - sleep 1 & - sleep_pid=$! - wait ${sleep_pid} - popd > /dev/null 2>&1 -} - -# starts up the RasPiCam -function startRaspi { - logger -s "Starting Raspberry Pi camera" - runMjpgStreamer "$MJPGSTREAMER_INPUT_RASPICAM $camera_raspi_options" -} - -# starts up the USB webcam -function startUsb { - options="$usb_options" - device="video0" - - # check for parameter and set the device if it is given as a parameter - input=$1 - if [[ -n $input ]]; then - device=`basename "$input"` - fi - - # add video device into options - options="$options -d /dev/$device" - - uevent_file="/sys/class/video4linux/$device/device/uevent" - if [ -e $uevent_file ]; then - # let's see what kind of webcam we have here, fetch vid and pid... - product=`cat $uevent_file | grep PRODUCT | cut -d"=" -f2` - vid=`echo $product | cut -d"/" -f1` - pid=`echo $product | cut -d"/" -f2` - vidpid=`printf "%04x:%04x" "0x$vid" "0x$pid"` - - # ... then look if it is in our list of known broken-fps-devices and if so remove - # the -f parameter from the options (if it's in there, else that's just a no-op) - for identifier in ${brokenfps_usb_devices[@]}; - do - if [ "$vidpid" = "$identifier" ]; then - echo - echo "Camera model $vidpid is known to not work with -f parameter, stripping it out" - echo - options=`echo $options | sed -e "s/\(\s\+\|^\)-f\s\+[0-9]\+//g"` - fi - done - fi - - logger -s "Starting USB webcam" - runMjpgStreamer "$MJPGSTREAMER_INPUT_USB $options" -} - -# make sure our cleanup function gets called when we receive SIGINT, SIGTERM -trap "cleanup" SIGINT SIGTERM -# say goodbye when we EXIT -trap "goodbye" EXIT - -# we need this to prevent the later calls to vcgencmd from blocking -# I have no idea why, but that's how it is... -vcgencmd version > /dev/null 2>&1 - -# keep mjpg streamer running if some camera is attached -while true; do - - # get list of usb video devices into an array - video_devices=($(find /dev -regextype sed -regex '\/dev/video[0-9]\+' | sort -nk1.11 2> /dev/null)) - - # add list of raspi camera into an array - if [ "`vcgencmd get_camera`" = "supported=1 detected=1" ]; then - video_devices+=( "raspi" ) - fi - - echo "Found video devices:" - printf '%s\n' "${video_devices[@]}" - - for scan_mode in "usb" "usb-auto" "raspi" "auto"; do - camera=$scan_mode - if [[ "usb-auto" == "$scan_mode" ]]; then - camera="usb" - fi - for ((i=0;i<${#array_camera[@]};i++)); do - if [[ -z ${array_camera_device[${i}]} ]] && [[ $camera == ${array_camera[${i}]} ]]; then - camera_config="${array_camera_config[${i}]}" - usb_options="${array_camera_usb_options[${i}]}" - camera_usb_device="${array_camera_usb_device[${i}]}" - camera_raspi_options="${array_camera_raspi_options[${i}]}" - camera_http_webroot="${array_camera_http_webroot[${i}]}" - camera_http_options="${array_camera_http_options[${i}]}" - brokenfps_usb_devices="${array_camera_brokenfps_usb_devices[${i}]}" - if [[ ${camera_usb_device} ]] && { [[ "usb" == ${scan_mode} ]] || [[ "auto" == ${scan_mode} ]]; }; then - # usb device is explicitly set in options - usb_device_path=`readlink -f ${camera_usb_device}` - if containsString "$usb_device_path" "${array_camera_device[@]}"; then - if [[ "auto" != ${scan_mode} ]]; then - array_camera_device[${i}]="alredy_in_use" - echo "config file='$camera_config':Video device already in use." - continue - fi - elif containsString "$usb_device_path" "${video_devices[@]}"; then - array_camera_device[${i}]="$usb_device_path" - # explicitly set usb device was found in video_devices array, start usb with the found device - echo "config file='$camera_config':USB device was set in options and found in devices, start MJPG-streamer with the configured USB video device: $usb_device_path" - startUsb "$usb_device_path" - continue - fi - elif [[ -z ${camera_usb_device} ]] && { [[ "usb-auto" == ${scan_mode} ]] || [[ "auto" == ${scan_mode} ]]; }; then - for video_device in "${video_devices[@]}"; do - if [[ "raspi" != "$video_device" ]]; then - if containsString "$video_device" "${array_camera_device[@]}"; then - : #already in use - else - array_camera_device[${i}]="$video_device" - # device is not set explicitly in options, start usb with first found usb camera as the device - echo "config file='$camera_config':USB device was not set in options, start MJPG-streamer with the first found video device: ${video_device}" - startUsb "${video_device}" - break - fi - fi - done - if [[ -n ${array_camera_device[${i}]} ]]; then - continue - fi - fi - if [[ "raspi" == ${scan_mode} ]] || [[ "auto" == ${scan_mode} ]]; then - video_device="raspi" - if containsString "$video_device" "${array_camera_device[@]}"; then - if [[ "auto" != ${scan_mode} ]]; then - array_camera_device[${i}]="alredy_in_use" - echo "config file='$camera_config':RasPiCam device already in use." - fi - elif containsString "$video_device" "${video_devices[@]}"; then - array_camera_device[${i}]="$video_device" - echo "config file='$camera_config':Start MJPG-streamer with video device: ${video_device}" - startRaspi - sleep 30 & - sleep_pid=$! - wait ${sleep_pid} - fi - fi - fi - done - done - array_assigned_device=( ${array_camera_device[*]} ) - if [[ ${#array_camera[@]} -eq ${#array_assigned_device[@]} ]]; then - echo "Done bring up all configured video device" - exit 0 - else - echo "Scan again in two minutes" - sleep 120 & - sleep_pid=$! - wait ${sleep_pid} - fi -done diff --git a/resources/mjpg-streamer/webcamd.service b/resources/mjpg-streamer/webcamd.service deleted file mode 100644 index da19c63..0000000 --- a/resources/mjpg-streamer/webcamd.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=Starts mjpg-streamer on startup -After=network.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=forking -User=%USER% -WorkingDirectory=/usr/local/bin -StandardOutput=append:/var/log/webcamd.log -StandardError=append:/var/log/webcamd.log -ExecStart=/usr/local/bin/webcamd -Restart=always \ No newline at end of file diff --git a/resources/moonraker-telegram-bot.env b/resources/moonraker-telegram-bot.env deleted file mode 100644 index 280f165..0000000 --- a/resources/moonraker-telegram-bot.env +++ /dev/null @@ -1 +0,0 @@ -TELEGRAM_BOT_ARGS="%TELEGRAM_BOT_DIR%/bot/main.py -c %CFG% -l %LOG%" \ No newline at end of file diff --git a/resources/moonraker-telegram-bot.service b/resources/moonraker-telegram-bot.service deleted file mode 100644 index 567481d..0000000 --- a/resources/moonraker-telegram-bot.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Moonraker Telegram Bot SV1 %INST% -Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -WorkingDirectory=%TELEGRAM_BOT_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $TELEGRAM_BOT_ARGS -Restart=always -RestartSec=10 diff --git a/resources/moonraker.conf b/resources/moonraker.conf deleted file mode 100644 index 8fa0c86..0000000 --- a/resources/moonraker.conf +++ /dev/null @@ -1,31 +0,0 @@ -[server] -host: 0.0.0.0 -port: %PORT% -klippy_uds_address: %UDS% - -[authorization] -trusted_clients: - %LAN% - 10.0.0.0/8 - 127.0.0.0/8 - 169.254.0.0/16 - 172.16.0.0/12 - 192.168.0.0/16 - FE80::/10 - ::1/128 -cors_domains: - *.lan - *.local - *.internal - *://localhost - *://localhost:* - *://my.mainsail.xyz - *://app.fluidd.xyz - -[octoprint_compat] - -[history] - -[update_manager] -channel: dev -refresh_interval: 168 diff --git a/resources/moonraker.env b/resources/moonraker.env deleted file mode 100644 index bca6af5..0000000 --- a/resources/moonraker.env +++ /dev/null @@ -1 +0,0 @@ -MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%" \ No newline at end of file diff --git a/resources/moonraker.service b/resources/moonraker.service deleted file mode 100644 index 7bfe8e2..0000000 --- a/resources/moonraker.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=API Server for Klipper SV1 %INST% -Documentation=https://moonraker.readthedocs.io/ -Requires=network-online.target -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -SupplementaryGroups=moonraker-admin -RemainAfterExit=yes -WorkingDirectory=%MOONRAKER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $MOONRAKER_ARGS -Restart=always -RestartSec=10 \ No newline at end of file diff --git a/resources/screenshots/rpi_imager1.png b/resources/screenshots/rpi_imager1.png deleted file mode 100644 index 1e51b8b..0000000 Binary files a/resources/screenshots/rpi_imager1.png and /dev/null differ diff --git a/resources/screenshots/rpi_imager2.png b/resources/screenshots/rpi_imager2.png deleted file mode 100644 index 9b46e02..0000000 Binary files a/resources/screenshots/rpi_imager2.png and /dev/null differ diff --git a/resources/shell_command.cfg b/resources/shell_command.cfg deleted file mode 100644 index 34e7581..0000000 --- a/resources/shell_command.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[gcode_shell_command hello_world] -command: echo hello world -timeout: 2. -verbose: True -[gcode_macro HELLO_WORLD] -gcode: - RUN_SHELL_COMMAND CMD=hello_world \ No newline at end of file diff --git a/resources/upstreams.conf b/resources/upstreams.conf deleted file mode 100644 index d04e04a..0000000 --- a/resources/upstreams.conf +++ /dev/null @@ -1,25 +0,0 @@ -# /etc/nginx/conf.d/upstreams.conf -upstream apiserver { - ip_hash; - server 127.0.0.1:7125; -} - -upstream mjpgstreamer1 { - ip_hash; - server 127.0.0.1:8080; -} - -upstream mjpgstreamer2 { - ip_hash; - server 127.0.0.1:8081; -} - -upstream mjpgstreamer3 { - ip_hash; - server 127.0.0.1:8082; -} - -upstream mjpgstreamer4 { - ip_hash; - server 127.0.0.1:8083; -} \ No newline at end of file diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100755 index 1716963..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function get_date() { - local current_date - current_date=$(date +"%y%m%d-%H%M") - echo "${current_date}" -} - -function check_for_backup_dir() { - [[ -d ${BACKUP_DIR} ]] && return - - status_msg "Create KIAUH backup directory ..." - mkdir -p "${BACKUP_DIR}" && ok_msg "Directory created!" -} - -function backup_before_update() { - read_kiauh_ini "${FUNCNAME[0]}" - local state="${backup_before_update}" - [[ ${state} = "false" ]] && return - backup_"${1}" -} - -function backup_config_dir() { - check_for_backup_dir - local current_date config_pathes - - config_pathes=$(get_config_folders) - - if [[ -n "${config_pathes}" ]]; then - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - - local i=0 folder folder_name target_dir - for folder in ${config_pathes}; do - if [[ -d ${folder} ]]; then - status_msg "Create backup of ${folder} ..." - - folder_name=$(echo "${folder}" | rev | cut -d"/" -f2 | rev) - target_dir="${BACKUP_DIR}/configs/${current_date}/${folder_name}" - mkdir -p "${target_dir}" - cp -r "${folder}" "${target_dir}" - i=$(( i + 1 )) - - ok_msg "Backup created in:\n${target_dir}" - fi - done - else - ok_msg "No config directory found! Skipping backup ..." - fi -} - -function backup_moonraker_database() { - check_for_backup_dir - local current_date db_pathes - - db_pathes=$(get_instance_folder_path "database") - - if [[ -n ${db_pathes} ]]; then - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - - local i=0 database folder_name target_dir - for database in ${db_pathes}; do - status_msg "Create backup of ${database} ..." - - folder_name=$(echo "${database}" | rev | cut -d"/" -f2 | rev) - target_dir="${BACKUP_DIR}/moonraker_databases/${current_date}/${folder_name}" - mkdir -p "${target_dir}" - cp -r "${database}" "${target_dir}" - i=$(( i + 1 )) - - ok_msg "Backup created in:\n${target_dir}" - done - else - print_error "No Moonraker database found! Skipping backup ..." - fi -} - -function backup_klipper() { - local current_date - - if [[ -d ${KLIPPER_DIR} && -d ${KLIPPY_ENV} ]]; then - status_msg "Creating Klipper backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/klipper-backups/${current_date}" - cp -r "${KLIPPER_DIR}" "${_}" && cp -r "${KLIPPY_ENV}" "${_}" - print_confirm "Klipper backup complete!" - else - print_error "Can't back up 'klipper' and/or 'klipper-env' directory! Not found!" - fi -} - -function backup_mainsail() { - local current_date - - if [[ -d ${MAINSAIL_DIR} ]]; then - status_msg "Creating Mainsail backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/mainsail-backups/${current_date}" - cp -r "${MAINSAIL_DIR}" "${_}" - print_confirm "Mainsail backup complete!" - else - print_error "Can't back up 'mainsail' directory! Not found!" - fi -} - -function backup_fluidd() { - local current_date - - if [[ -d ${FLUIDD_DIR} ]]; then - status_msg "Creating Fluidd backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/fluidd-backups/${current_date}" - cp -r "${FLUIDD_DIR}" "${_}" - print_confirm "Fluidd backup complete!" - else - print_error "Can't back up 'fluidd' directory! Not found!" - fi -} - -function backup_moonraker() { - local current_date - - if [[ -d ${MOONRAKER_DIR} && -d ${MOONRAKER_ENV} ]]; then - status_msg "Creating Moonraker backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/moonraker-backups/${current_date}" - cp -r "${MOONRAKER_DIR}" "${_}" && cp -r "${MOONRAKER_ENV}" "${_}" - print_confirm "Moonraker backup complete!" - else - print_error "Can't back up moonraker and/or moonraker-env directory! Not found!" - fi -} - -function backup_octoprint() { - local current_date - - if [[ -d ${OCTOPRINT_DIR} && -d ${OCTOPRINT_CFG_DIR} ]]; then - status_msg "Creating OctoPrint backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/octoprint-backups/${current_date}" - cp -r "${OCTOPRINT_DIR}" "${_}" && cp -r "${OCTOPRINT_CFG_DIR}" "${_}" - print_confirm " OctoPrint backup complete!" - else - print_error "Can't back up OctoPrint and/or .octoprint directory!\n Not found!" - fi -} - -function backup_klipperscreen() { - local current_date - if [[ -d ${KLIPPERSCREEN_DIR} ]] ; then - status_msg "Creating KlipperScreen backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/klipperscreen-backups/${current_date}" - cp -r "${KLIPPERSCREEN_DIR}" "${_}" - print_confirm "KlipperScreen backup complete!" - else - print_error "Can't back up KlipperScreen directory!\n Not found!" - fi -} - -function backup_telegram_bot() { - local current_date - - if [[ -d ${TELEGRAM_BOT_DIR} ]] ; then - status_msg "Creating MoonrakerTelegramBot backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/MoonrakerTelegramBot-backups/${current_date}" - cp -r "${TELEGRAM_BOT_DIR}" "${_}" - print_confirm "MoonrakerTelegramBot backup complete!" - else - print_error "Can't back up MoonrakerTelegramBot directory!\n Not found!" - fi -} - -function backup_octoeverywhere() { - local current_date - - if [[ -d ${OCTOEVERYWHERE_DIR} ]] ; then - status_msg "Creating OctoEverywhere backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/OctoEverywhere-backups/${current_date}" - cp -r "${OCTOEVERYWHERE_DIR}" "${_}" && cp -r "${OCTOEVERYWHERE_ENV}" "${_}" - print_confirm "OctoEverywhere backup complete!" - else - print_error "Can't back up OctoEverywhere directory!\n Not found!" - fi -} - -function backup_spoolman() { - local current_date - - if [[ -d ${SPOOLMAN_DIR} ]] ; then - status_msg "Creating Spoolman backup ..." - check_for_backup_dir - current_date=$(get_date) - status_msg "Timestamp: ${current_date}" - mkdir -p "${BACKUP_DIR}/Spoolman-backups/${current_date}" - cp -r "${SPOOLMAN_DIR}" "${_}" && cp -r "${SPOOLMAN_DB_DIR}/spoolman.db" "${_}" - print_confirm "Spoolman backup complete!" - else - print_error "Can't back up Spoolman directory!\n Not found!" - fi -} diff --git a/scripts/crowsnest.sh b/scripts/crowsnest.sh deleted file mode 100644 index 788468f..0000000 --- a/scripts/crowsnest.sh +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -#=======================================================================# -# Crowsnest Installer brought to you by KwadFan # -# Copyright (C) 2022 KwadFan # -# https://github.com/KwadFan/crowsnest # -#=======================================================================# - -# Error Handling -set -e - -# Helper messages - -function multi_instance_message(){ - echo -e "Crowsnest is NOT designed to support multi instances." - echo -e "A workaround for this is to choose the most used instance as a 'master'" - echo -e "Use this instance to set up your 'crowsnest.conf' and steering it's service.\n" - echo -e "Found the following instances:\n" - for i in ${1}; do - select_msg "${i}" - done - echo -e "\nLaunching crowsnest's configuration tool ..." - continue_config -} - -# Helper funcs -function clone_crowsnest(){ - $(command -v git) clone "${CROWSNEST_REPO}" -b master "${CROWSNEST_DIR}" -} - -function check_multi_instance(){ - local -a instances - readarray -t instances < <(find "${HOME}" -regex "${HOME}/[a-zA-Z0-9_]+_data/*" -printf "%P\n" 2> /dev/null | sort) - if [[ "${#instances[@]}" -gt 1 ]]; then - status_msg "Multi instance install detected ..." - multi_instance_message "${instances[*]}" - if [[ -d "${HOME}/crowsnest" ]]; then - pushd "${HOME}/crowsnest" &> /dev/null || exit 1 - if ! make config ;then - error_msg "Something went wrong! Please try again..." - if [[ -f "tools/.config" ]]; then - rm -f tools/.config - fi - exit 1 - fi - if [[ ! -f "tools/.config" ]]; then - log_error "failure while generating .config" - error_msg "Generating .config failed, installation aborted" - exit 1 - fi - popd &> /dev/null || exit 1 - fi - fi -} - -function continue_config() { - local reply - while true; do - read -erp "${cyan}###### Continue with configuration? (y/N):${white} " reply - case "${reply}" in - Y|y|Yes|yes) - select_msg "Yes" - break;; - N|n|No|no|"") - select_msg "No" - warn_msg "Installation aborted by user ... Exiting!" - exit 1;; - *) - error_msg "Invalid Input!\n";; - esac - done - return 0 -} - -# Install func -function install_crowsnest(){ - - # Step 1: jump to home directory - pushd "${HOME}" &> /dev/null || exit 1 - - # Step 2: Clone crowsnest repo - status_msg "Cloning 'crowsnest' repository ..." - if [[ ! -d "${HOME}/crowsnest" && -z "$(ls -A "${HOME}/crowsnest" 2> /dev/null)" ]]; then - clone_crowsnest - else - ok_msg "crowsnest repository already exists ..." - fi - - # Step 3: Install dependencies - dependency_check git make - - # Step 4: Check for Multi Instance - check_multi_instance - - # Step 5: Launch crowsnest installer - pushd "${HOME}/crowsnest" &> /dev/null || exit 1 - title_msg "Installer will prompt you for sudo password!" - status_msg "Launching crowsnest installer ..." - if ! sudo make install; then - error_msg "Something went wrong! Please try again..." - exit 1 - fi - - # Step 5: Leave directory (twice due two pushd) - popd &> /dev/null || exit 1 - popd &> /dev/null || exit 1 -} - -# Remove func -function remove_crowsnest(){ - if [[ -d "${CROWSNEST_DIR}" ]]; then - pushd "${HOME}/crowsnest" &> /dev/null || exit 1 - title_msg "Uninstaller will prompt you for sudo password!" - status_msg "Launching crowsnest uninstaller ..." - - if ! make uninstall; then - error_msg "Something went wrong! Please try again..." - exit 1 - fi - - status_msg "Removing crowsnest directory ..." - rm -rf "${CROWSNEST_DIR}" - ok_msg "Directory removed!" - fi - - print_confirm "Crowsnest successfully removed!" -} - -# Status funcs -get_crowsnest_status(){ - local -a files - local env_file - env_file="$(grep "EnvironmentFile" /etc/systemd/system/crowsnest.service 2>/dev/null | cut -d "=" -f2)" - files=( - "${CROWSNEST_DIR}" - "/usr/local/bin/crowsnest" - "/etc/logrotate.d/crowsnest" - "/etc/systemd/system/crowsnest.service" - "${env_file}" - ) - local count - count=0 - - for file in "${files[@]}"; do - [[ -e "${file}" ]] && count=$(( count +1 )) - done - if [[ "${count}" -eq "${#files[*]}" ]]; then - echo "Installed" - elif [[ "${count}" -gt 0 ]]; then - echo "Incomplete!" - else - echo "Not installed!" - fi -} - -# Update funcs -# Shameless stolen from KlipperScreen.sh -function get_local_crowsnest_commit() { - [[ ! -d ${CROWSNEST_DIR} || ! -d "${CROWSNEST_DIR}/.git" ]] && return - - local commit - cd "${CROWSNEST_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_crowsnest_commit() { - [[ ! -d ${CROWSNEST_DIR} || ! -d "${CROWSNEST_DIR}/.git" ]] && return - - local commit - cd "${CROWSNEST_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_crowsnest_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_crowsnest_commit)" - remote_ver="$(get_remote_crowsnest_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "crowsnest" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -function install_crowsnest_dependencies() { - local packages log_name="Crowsnest" - local install_script="${CROWSNEST_DIR}/tools/install.sh" - - ### read PKGLIST from official install-script - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages="$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n')" - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -function update_crowsnest() { - do_action_service "stop" "crowsnest" - - if [[ ! -d ${CROWSNEST_DIR} ]]; then - clone_crowsnest - else - status_msg "Updating Crowsnest ..." - cd "${CROWSNEST_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_crowsnest_dependencies - fi - ok_msg "Update complete!" - do_action_service "restart" "crowsnest" -} diff --git a/scripts/flash_klipper.sh b/scripts/flash_klipper.sh deleted file mode 100644 index 4fd4f45..0000000 --- a/scripts/flash_klipper.sh +++ /dev/null @@ -1,494 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function init_flash_process() { - ### step 1: check for required userhgroups (tty & dialout) - check_usergroups - - top_border - echo -e "| ~~~~~~~~~~~~ [ Flash MCU ] ~~~~~~~~~~~~ |" - hr - echo -e "| Please select the flashing method to flash your MCU. |" - echo -e "| Make sure to only select a method your MCU supports. |" - echo -e "| Not all MCUs support both methods! |" - hr - blank_line - echo -e "| 1) Regular flashing method |" - echo -e "| 2) Updating via SD-Card Update |" - blank_line - back_help_footer - - local choice method - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - 1) - select_msg "Regular flashing method" - method="regular" - break;; - 2) - select_msg "SD-Card Update" - method="sdcard" - break;; - B|b) - advanced_menu - break;; - H|h) - clear && print_header - show_flash_method_help - break;; - *) - error_msg "Invalid command!";; - esac - done - - ### step 2: select how the mcu is flashed (flash/serialflash) - select_flash_command - - ### step 3: select how the mcu is connected to the host - select_mcu_connection - - ### step 4: select which detected mcu should be flashed - select_mcu_id "${method}" -} - -#================================================# -#=================== STEP 2 =====================# -#================================================# -function select_flash_command() { - unset flash_command - - top_border - echo -e "| How to flash MCU? |" - echo -e "| 1) make flash (default) |" - echo -e "| 2) make serialflash (stm32flash) |" - blank_line - back_help_footer - - local choice - while true; do - read -p "${cyan}###### Flashing command:${white} " -i "1" -e choice - case "${choice}" in - 1) - select_msg "Selected 'make flash' command" - flash_command="flash" - break;; - 2) - select_msg "Selected 'make serialflash' command" - flash_command="serialflash" - break;; - B|b) - advanced_menu - break;; - H|h) - clear && print_header - show_mcu_flash_command_help - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -#================================================# -#=================== STEP 3 =====================# -#================================================# -function select_mcu_connection() { - top_border - echo -e "| ${yellow}Make sure that the controller board is connected now!${white} |" - hr - blank_line - echo -e "| How is the controller board connected to the host? |" - echo -e "| 1) USB |" - echo -e "| 2) UART |" - echo -e "| 3) USB (DFU mode) |" - blank_line - back_help_footer - - local choice - while true; do - read -p "${cyan}###### Connection method:${white} " choice - case "${choice}" in - 1) - status_msg "Identifying MCU connected via USB ...\n" - get_usb_id || true # continue even after exit code 1 - break;; - 2) - status_msg "Identifying MCU possibly connected via UART ...\n" - get_uart_id || true # continue even after exit code 1 - break;; - 3) - status_msg "Identifying MCU connected via USB in DFU mode ...\n" - get_dfu_id || true # continue even after exit code 1 - break;; - B|b) - advanced_menu - break;; - H|h) - clear && print_header - show_mcu_connection_help - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function print_detected_mcu_to_screen() { - local i=1 - - if (( ${#mcu_list[@]} < 1 )); then - print_error "No MCU found!\n MCU either not connected or not detected!" - return - fi - - for mcu in "${mcu_list[@]}"; do - echo -e " ● MCU #${i}: ${cyan}${mcu}${white}" - i=$(( i + 1 )) - done - echo -} - -#================================================# -#=================== STEP 4 =====================# -#================================================# -function select_mcu_id() { - local i=0 sel_index=0 method=${1} - - if (( ${#mcu_list[@]} < 1 )); then - print_error "No MCU found!\n MCU either not connected or not detected!" - return - fi - - top_border - echo -e "| ${red}!!! ATTENTION !!!${white} |" - hr - echo -e "| Make sure, to select the correct MCU! |" - echo -e "| ${red}ONLY flash a firmware created for the respective MCU!${white} |" - bottom_border - echo -e "${cyan}###### List of available MCU:${white}" - - ### list all mcus - for mcu in "${mcu_list[@]}"; do - i=$(( i + 1 )) - mcu=$(echo "${mcu}" | rev | cut -d"/" -f1 | rev) - echo -e " ● MCU #${i}: ${cyan}${mcu}${white}" - done - - ### verify user input - local regex="^[1-9]+$" - while [[ ! ${sel_index} =~ ${regex} ]] || [[ ${sel_index} -gt ${i} ]]; do - echo - read -p "${cyan}###### Select MCU to flash:${white} " sel_index - - if [[ ! ${sel_index} =~ ${regex} ]]; then - error_msg "Invalid input!" - elif [[ ${sel_index} -lt 1 ]] || [[ ${sel_index} -gt ${i} ]]; then - error_msg "Please select a number between 1 and ${i}!" - fi - - local mcu_index=$(( sel_index - 1 )) - local selected_mcu_id="${mcu_list[${mcu_index}]}" - done - - ### confirm selection - local yn - while true; do - echo -e "\n###### You selected:\n ● MCU #${sel_index}: ${selected_mcu_id}\n" - read -p "${cyan}###### Continue? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - status_msg "Flashing ${selected_mcu_id} ..." - if [[ ${method} == "regular" ]]; then - log_info "Flashing device '${selected_mcu_id}' with method '${method}'" - start_flash_mcu "${selected_mcu_id}" - elif [[ ${method} == "sdcard" ]]; then - log_info "Flashing device '${selected_mcu_id}' with method '${method}'" - start_flash_sd "${selected_mcu_id}" - else - print_error "No flash method set! Aborting..." - log_error "No flash method set!" - return - fi - break;; - N|n|No|no) - select_msg "No" - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function start_flash_mcu() { - local device=${1} - do_action_service "stop" "klipper" - - if make ${flash_command} FLASH_DEVICE="${device}"; then - ok_msg "Flashing successfull!" - else - warn_msg "Flashing failed!" - warn_msg "Please read the console output above!" - fi - - do_action_service "start" "klipper" -} - -function start_flash_sd() { - local i=0 board_list=() device=${1} - local flash_script="${KLIPPER_DIR}/scripts/flash-sdcard.sh" - - ### write each supported board to the array to make it selectable - for board in $("${flash_script}" -l | tail -n +2); do - board_list+=("${board}") - done - - top_border - echo -e "| Please select the type of board that corresponds to |" - echo -e "| the currently selected MCU ID you chose before. |" - blank_line - echo -e "| The following boards are currently supported: |" - hr - ### display all supported boards to the user - for board in "${board_list[@]}"; do - if [[ ${i} -lt 10 ]]; then - printf "| ${i}) %-50s|\n" "${board_list[${i}]}" - else - printf "| ${i}) %-49s|\n" "${board_list[${i}]}" - fi - i=$(( i + 1 )) - done - quit_footer - - ### make the user select one of the boards - local choice - while true; do - read -p "${cyan}###### Please select board type:${white} " choice - if [[ ${choice} = "q" || ${choice} = "Q" ]]; then - clear && advanced_menu && break - elif [[ ${choice} -le ${#board_list[@]} ]]; then - local selected_board="${board_list[${choice}]}" - break - else - clear && print_header - error_msg "Invalid choice!" - flash_mcu_sd - fi - done - - while true; do - echo - top_border - echo -e "| If your board is flashed with firmware that connects |" - echo -e "| at a custom baud rate, please change it now. |" - blank_line - echo -e "| If you are unsure, stick to the default 250000! |" - bottom_border - - local baud_rate regex="^[0-9]+$" - echo -e "${cyan}###### Please set the baud rate:${white} " - while [[ ! ${baud_rate} =~ ${regex} ]]; do - read -e -i "250000" -e baud_rate - local selected_baud_rate=${baud_rate} - break - done - break - done - - ###flash process - do_action_service "stop" "klipper" - if "${flash_script}" -b "${selected_baud_rate}" "${device}" "${selected_board}"; then - print_confirm "Flashing successfull!" - log_info "Flash successfull!" - else - print_error "Flashing failed!\n Please read the console output above!" - log_error "Flash failed!" - fi - do_action_service "start" "klipper" -} - -function build_fw() { - local python_version - - if [[ ! -d ${KLIPPER_DIR} || ! -d ${KLIPPY_ENV} ]]; then - print_error "Klipper not found!\n Cannot build firmware without Klipper!" - return - fi - - python_version=$(get_klipper_python_ver) - - cd "${KLIPPER_DIR}" - status_msg "Initializing firmware build ..." - local dep=(build-essential dpkg-dev make) - dependency_check "${dep[@]}" - - make clean - - status_msg "Building firmware ..." - if (( python_version == 3 )); then - make PYTHON=python3 menuconfig - make PYTHON=python3 - elif (( python_version == 2 )); then - make PYTHON=python2 menuconfig - make PYTHON=python2 - else - warn_msg "Error reading Python version!" - return 1 - fi - - ok_msg "Firmware built!" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function get_usb_id() { - unset mcu_list - sleep 1 - mcus=$(find /dev/serial/by-id/* 2>/dev/null) - - for mcu in ${mcus}; do - mcu_list+=("${mcu}") - done -} - -function get_uart_id() { - unset mcu_list - sleep 1 - mcus=$(find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null) - - for mcu in ${mcus}; do - mcu_list+=("${mcu}") - done -} - -function get_dfu_id() { - unset mcu_list - sleep 1 - mcus=$(lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null) - - for mcu in ${mcus}; do - mcu_list+=("${mcu}") - done -} - -function show_flash_method_help() { - top_border - echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |" - hr - echo -e "| ${cyan}Regular flashing method:${white} |" - echo -e "| The default method to flash controller boards which |" - echo -e "| are connected and updated over USB and not by placing |" - echo -e "| a compiled firmware file onto an internal SD-Card. |" - blank_line - echo -e "| Common controllers that get flashed that way are: |" - echo -e "| - Arduino Mega 2560 |" - echo -e "| - Fysetc F6 / S6 (used without a Display + SD-Slot) |" - blank_line - echo -e "| ${cyan}Updating via SD-Card Update:${white} |" - echo -e "| Many popular controller boards ship with a bootloader |" - echo -e "| capable of updating the firmware via SD-Card. |" - echo -e "| Choose this method if your controller board supports |" - echo -e "| this way of updating. This method ONLY works for up- |" - echo -e "| grading firmware. The initial flashing procedure must |" - echo -e "| be done manually per the instructions that apply to |" - echo -e "| your controller board. |" - blank_line - echo -e "| Common controllers that can be flashed that way are: |" - echo -e "| - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 |" - echo -e "| - Fysetc F6 / S6 (used with a Display + SD-Slot) |" - echo -e "| - Fysetc Spider |" - blank_line - back_footer - - local choice - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - B|b) - clear && print_header - init_flash_process - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function show_mcu_flash_command_help() { - top_border - echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |" - hr - echo -e "| ${cyan}make flash:${white} |" - echo -e "| The default command to flash controller board, it |" - echo -e "| will detect selected microcontroller and use suitable |" - echo -e "| tool for flashing it. |" - blank_line - echo -e "| ${cyan}make serialflash:${white} |" - echo -e "| Special command to flash STM32 microcontrollers in |" - echo -e "| DFU mode but connected via serial. stm32flash command |" - echo -e "| will be used internally. |" - blank_line - back_footer - - local choice - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - B|b) - clear && print_header - select_flash_command - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function show_mcu_connection_help() { - top_border - echo -e "| ~~~~~~~~ < ? > Help: Flash MCU < ? > ~~~~~~~~ |" - hr - echo -e "| ${cyan}USB:${white} |" - echo -e "| Selecting USB as the connection method will scan the |" - echo -e "| USB ports for connected controller boards. This will |" - echo -e "| be similar to the 'ls /dev/serial/by-id/*' command |" - echo -e "| suggested by the official Klipper documentation for |" - echo -e "| determining successfull USB connections! |" - blank_line - echo -e "| ${cyan}UART:${white} |" - echo -e "| Selecting UART as the connection method will list all |" - echo -e "| possible UART serial ports. Note: This method ALWAYS |" - echo -e "| returns something as it seems impossible to determine |" - echo -e "| if a valid Klipper controller board is connected or |" - echo -e "| not. Because of that, you ${red}MUST${white} know which UART serial |" - echo -e "| port your controller board is connected to when using |" - echo -e "| this connection method. |" - blank_line - back_footer - - local choice - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - B|b) - clear && print_header - select_mcu_connection - break;; - *) - error_msg "Invalid command!";; - esac - done -} diff --git a/scripts/fluidd.sh b/scripts/fluidd.sh deleted file mode 100644 index 35620be..0000000 --- a/scripts/fluidd.sh +++ /dev/null @@ -1,510 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#================== INSTALL FLUIDD =================# -#===================================================# - -function install_fluidd() { - if [[ -z $(moonraker_systemd) ]]; then - local error="Moonraker not installed! It's recommended to install Moonraker first!" - print_error "${error}" - while true; do - local yn - read -p "${cyan}###### Proceed to install Fluidd without installing Moonraker? (y/N):${white} " yn - case "${yn}" in - Y|y|Yes|yes) - select_msg "Yes" - break;; - N|n|No|no|"") - select_msg "No" - abort_msg "Exiting Fluidd setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - fi - - ### checking dependencies - local dep=(wget nginx unzip) - dependency_check "${dep[@]}" - ### detect conflicting Haproxy and Apache2 installations - detect_conflicting_packages - - status_msg "Initializing Fluidd installation ..." - ### first, we create a backup of the full klipper_config dir - safety first! - backup_config_dir - - ### check for other enabled web interfaces - unset SET_LISTEN_PORT - detect_enabled_sites - - ### check if another site already listens to port 80 - fluidd_port_check - - ### download fluidd - download_fluidd - - ### ask user to install the recommended webinterface macros - install_fluidd_macros - - ### create /etc/nginx/conf.d/upstreams.conf - set_upstream_nginx_cfg - ### create /etc/nginx/sites-available/ - set_nginx_cfg "fluidd" - ### nginx on ubuntu 21 and above needs special permissions to access the files - set_nginx_permissions - - ### symlink nginx log - symlink_webui_nginx_log "fluidd" - - ### add fluidd to the update manager in moonraker.conf - patch_fluidd_update_manager - - fetch_webui_ports #WIP - - ### confirm message - print_confirm "Fluidd has been set up!" -} - -function install_fluidd_macros() { - local yn - while true; do - echo - top_border - echo -e "| It is recommended to use special macros in order to |" - echo -e "| have Fluidd fully functional and working. |" - blank_line - echo -e "| The recommended macros for Fluidd can be found here: |" - echo -e "| https://github.com/fluidd-core/fluidd-config |" - blank_line - echo -e "| If you already use these macros skip this step. |" - echo -e "| Otherwise you should consider to answer with 'yes' to |" - echo -e "| download the recommended macros. |" - bottom_border - read -p "${cyan}###### Download the recommended macros? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - download_fluidd_macros - break;; - N|n|No|no) - select_msg "No" - break;; - *) - print_error "Invalid command!";; - esac - done - return -} - -function download_fluidd_macros() { - local ms_cfg_repo path configs regex line gcode_dir - - ms_cfg_repo="https://github.com/fluidd-core/fluidd-config.git" - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" - configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -z ${configs} ]]; then - print_error "No printer.cfg found! Installation of Macros will be skipped ..." - log_error "execution stopped! reason: no printer.cfg found" - return - fi - - status_msg "Cloning fluidd-config ..." - [[ -d "${HOME}/fluidd-config" ]] && rm -rf "${HOME}/fluidd-config" - if git clone --recurse-submodules "${ms_cfg_repo}" "${HOME}/fluidd-config"; then - for config in ${configs}; do - path=$(echo "${config}" | rev | cut -d"/" -f2- | rev) - - if [[ -e "${path}/fluidd.cfg" && ! -h "${path}/fluidd.cfg" ]]; then - warn_msg "Attention! Existing fluidd.cfg detected!" - warn_msg "The file will be renamed to 'fluidd.bak.cfg' to be able to continue with the installation." - if ! mv "${path}/fluidd.cfg" "${path}/fluidd.bak.cfg"; then - error_msg "Renaming fluidd.cfg failed! Aborting installation ..." - return - fi - fi - - if [[ -h "${path}/fluidd.cfg" ]]; then - warn_msg "Recreating symlink in ${path} ..." - rm -rf "${path}/fluidd.cfg" - fi - - if ! ln -sf "${HOME}/fluidd-config/client.cfg" "${path}/fluidd.cfg"; then - error_msg "Creating symlink failed! Aborting installation ..." - return - fi - - if ! grep -Eq "^\[include fluidd.cfg\]$" "${path}/printer.cfg"; then - log_info "${path}/printer.cfg" - sed -i "1 i [include fluidd.cfg]" "${path}/printer.cfg" - fi - - line=$(($(grep -n "\[include fluidd.cfg\]" "${path}/printer.cfg" | tail -1 | cut -d: -f1) + 1)) - gcode_dir=${path/config/gcodes} - if ! grep -Eq "^\[virtual_sdcard\]$" "${path}/printer.cfg"; then - log_info "${path}/printer.cfg" - sed -i "${line} i \[virtual_sdcard]\npath: ${gcode_dir}\non_error_gcode: CANCEL_PRINT\n" "${path}/printer.cfg" - fi - done - else - print_error "Cloning failed! Aborting installation ..." - log_error "execution stopped! reason: cloning failed" - return - fi - - patch_fluidd_config_update_manager - - ok_msg "Done!" -} - -function download_fluidd() { - local url - url=$(get_fluidd_download_url) - - status_msg "Downloading Fluidd from ${url} ..." - - if [[ -d ${FLUIDD_DIR} ]]; then - rm -rf "${FLUIDD_DIR}" - fi - - mkdir "${FLUIDD_DIR}" && cd "${FLUIDD_DIR}" - - if wget "${url}"; then - ok_msg "Download complete!" - status_msg "Extracting archive ..." - unzip -q -o ./*.zip && ok_msg "Done!" - status_msg "Remove downloaded archive ..." - rm -rf ./*.zip && ok_msg "Done!" - else - print_error "Downloading Fluidd from\n ${url}\n failed!" - exit 1 - fi -} - -#===================================================# -#================== REMOVE FLUIDD ==================# -#===================================================# - -function remove_fluidd_dir() { - [[ ! -d ${FLUIDD_DIR} ]] && return - - status_msg "Removing Fluidd directory ..." - rm -rf "${FLUIDD_DIR}" && ok_msg "Directory removed!" -} - -function remove_fluidd_nginx_config() { - if [[ -e "/etc/nginx/sites-available/fluidd" ]]; then - status_msg "Removing Fluidd configuration for Nginx ..." - sudo rm "/etc/nginx/sites-available/fluidd" && ok_msg "File removed!" - fi - if [[ -L "/etc/nginx/sites-enabled/fluidd" ]]; then - status_msg "Removing Fluidd Symlink for Nginx ..." - sudo rm "/etc/nginx/sites-enabled/fluidd" && ok_msg "File removed!" - fi -} - -function remove_fluidd_logs() { - local files - files=$(find /var/log/nginx -name "fluidd*" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - sudo rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_fluidd_log_symlinks() { - local files regex - - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/fluidd-.*" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_legacy_fluidd_log_symlinks() { - local files - files=$(find "${HOME}/klipper_logs" -name "fluidd*" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_fluidd_config() { - if [[ -d "${HOME}/fluidd-config" ]]; then - status_msg "Removing ${HOME}/fluidd-config ..." - rm -rf "${HOME}/fluidd-config" - ok_msg "${HOME}/fluidd-config removed!" - print_confirm "Fluidd-Config successfully removed!" - fi -} - -function remove_fluidd() { - remove_fluidd_dir - remove_fluidd_nginx_config - remove_fluidd_logs - remove_fluidd_log_symlinks - remove_legacy_fluidd_log_symlinks - - ### remove fluidd_port from ~/.kiauh.ini - sed -i "/^fluidd_port=/d" "${INI_FILE}" - - print_confirm "Fluidd successfully removed!" -} - -#===================================================# -#================== UPDATE FLUIDD ==================# -#===================================================# - -function update_fluidd() { - backup_before_update "fluidd" - status_msg "Updating Fluidd ..." - download_fluidd - match_nginx_configs - symlink_webui_nginx_log "fluidd" - print_confirm "Fluidd successfully updated!" -} - -#===================================================# -#================== FLUIDD STATUS ==================# -#===================================================# - -function get_fluidd_status() { - local status - local data_arr=("${FLUIDD_DIR}" "${NGINX_SA}/fluidd" "${NGINX_SE}/fluidd") - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed!" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - echo "${status}" -} - -function get_local_fluidd_version() { - local versionfile="${FLUIDD_DIR}/.version" - local relinfofile="${FLUIDD_DIR}/release_info.json" - local version - - if [[ -f ${relinfofile} ]]; then - version=$(grep -o '"version":"[^"]*' "${relinfofile}" | grep -o '[^"]*$') - elif [[ -f ${versionfile} ]]; then - version=$(head -n 1 "${versionfile}") - fi - - echo "${version}" -} - -function get_remote_fluidd_version() { - [[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return - - local tags - tags=$(curl -s "https://api.github.com/repos/fluidd-core/fluidd/tags" | grep "name" | cut -d'"' -f4) - echo "${tags}" | head -1 -} - -function compare_fluidd_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_fluidd_version)" - remote_ver="$(get_remote_fluidd_version)" - - if [[ ${local_ver} != "${remote_ver}" && ${local_ver} != "" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "fluidd" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function get_fluidd_download_url() { - local releases_by_tag tags tag unstable_url url - - ### latest stable download url - url="https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" - - read_kiauh_ini "${FUNCNAME[0]}" - if [[ ${fluidd_install_unstable} == "true" ]]; then - releases_by_tag="https://api.github.com/repos/fluidd-core/fluidd/tags" - tags=$(curl -s "${releases_by_tag}" | grep "name" | cut -d'"' -f4) - tag=$(echo "${tags}" | head -1) - - ### latest unstable download url including pre-releases (alpha, beta, rc) - unstable_url="https://github.com/fluidd-core/fluidd/releases/download/${tag}/fluidd.zip" - - if [[ ${unstable_url} == *"download//"* ]]; then - warn_msg "Download URL broken! Falling back to URL of latest stable release!" - else - url=${unstable_url} - fi - fi - - echo "${url}" -} - -function fluidd_port_check() { - if [[ ${FLUIDD_ENABLED} == "false" ]]; then - - if [[ ${SITE_ENABLED} == "true" ]]; then - status_msg "Detected other enabled interfaces:" - - [[ ${MAINSAIL_ENABLED} == "true" ]] && \ - echo " ${cyan}● Mainsail - Port: ${MAINSAIL_PORT}${white}" - - if [[ ${MAINSAIL_PORT} == "80" ]]; then - PORT_80_BLOCKED="true" - select_fluidd_port - fi - else - DEFAULT_PORT=$(grep listen "${KIAUH_SRCDIR}/resources/fluidd" | head -1 | sed 's/^\s*//' | cut -d" " -f2 | cut -d";" -f1) - SET_LISTEN_PORT=${DEFAULT_PORT} - fi - SET_NGINX_CFG="true" - - else - SET_NGINX_CFG="false" - fi -} - -function select_fluidd_port() { - if [[ ${PORT_80_BLOCKED} == "true" ]]; then - echo - top_border - echo -e "| ${red}!!!WARNING!!!${white} |" - echo -e "| ${red}You need to choose a different port for Fluidd!${white} |" - echo -e "| ${red}The following web interface is listening at port 80:${white} |" - blank_line - [[ ${MAINSAIL_PORT} == "80" ]] && echo "| ● Mainsail |" - blank_line - echo -e "| Make sure you don't choose a port which was already |" - echo -e "| assigned to another webinterface! |" - blank_line - echo -e "| Be aware: there is ${red}NO${white} sanity check for the following |" - echo -e "| input. So make sure to choose a valid port! |" - bottom_border - - local new_port re="^[0-9]+$" - while true; do - read -p "${cyan}Please enter a new Port:${white} " new_port - if [[ ${new_port} =~ ${re} && ${new_port} != "${MAINSAIL_PORT}" ]]; then - select_msg "Setting port ${new_port} for Fluidd!" - SET_LISTEN_PORT=${new_port} - break - else - if [[ ! ${new_port} =~ ${re} ]]; then - error_msg "Invalid input!" - else - error_msg "Port already taken! Select a different one!" - fi - fi - done - fi -} - -function patch_fluidd_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager fluidd\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Fluidds update manager section to moonraker.conf - status_msg "Adding Fluidd to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager fluidd] -type: web -channel: stable -repo: fluidd-core/fluidd -path: ~/fluidd -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} - -function patch_fluidd_config_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager fluidd-config\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Fluidds update manager section to moonraker.conf - status_msg "Adding Fluidd-Config to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager fluidd-config] -type: git_repo -primary_branch: master -path: ~/fluidd-config -origin: https://github.com/fluidd-core/fluidd-config.git -managed_services: klipper -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} diff --git a/scripts/gcode_shell_command.sh b/scripts/gcode_shell_command.sh deleted file mode 100644 index ff804d3..0000000 --- a/scripts/gcode_shell_command.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#=================================================# -#======== INSTALL GCODE_SHELL_COMMAND.PY =========# -#=================================================# - -function setup_gcode_shell_command() { - top_border - echo -e "| You are about to install the 'G-Code Shell Command' |" - echo -e "| extension. Please make sure to read the instructions |" - echo -e "| before you continue and remember that potential risks |" - echo -e "| can be involved after installing this extension! |" - blank_line - echo -e "| ${red}You accept that you are doing this on your own risk!${white} |" - bottom_border - - local yn - while true; do - read -p "${cyan}###### Do you want to continue? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - - if [[ ! -d "${KLIPPER_DIR}/klippy/extras" ]]; then - print_error "Folder ~/klipper/klippy/extras not found!\n Klipper not installed yet?" - return - fi - - status_msg "Installing gcode shell command extension ..." - - if [[ ! -f "${KLIPPER_DIR}/klippy/extras/gcode_shell_command.py" ]]; then - install_gcode_shell_command - else - echo; warn_msg "File 'gcode_shell_command.py' already exists in the destination location!" - - while true; do - read -p "${cyan}###### Do you want to overwrite it? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - rm -f "${KLIPPER_DIR}/klippy/extras/gcode_shell_command.py" - install_gcode_shell_command - break;; - N|n|No|no) - select_msg "No" - break;; - *) - error_msg "Invalid Input!";; - esac - done - fi - return;; - N|n|No|no) - select_msg "No" - return;; - *) - error_msg "Invalid Input!";; - esac - done -} - -function install_gcode_shell_command() { - do_action_service "stop" "klipper" - status_msg "Copy 'gcode_shell_command.py' to '${KLIPPER_DIR}/klippy/extras' ..." - - if cp "${KIAUH_SRCDIR}/resources/gcode_shell_command.py" "${KLIPPER_DIR}/klippy/extras"; then - ok_msg "Done!" - else - error_msg "Cannot copy file to target destination...Exiting!" - return 1 - fi - - local yn - while true; do - echo - read -p "${cyan}###### Create an example shell command? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - create_example_shell_command - break;; - N|n|No|no) - select_msg "No" - break;; - esac - done - - do_action_service "restart" "klipper" - print_confirm "Shell command extension installed!" - return -} - -function create_example_shell_command() { - ### create a backup of the config folder - backup_config_dir - - local configs regex path - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" - configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - for cfg in ${configs}; do - path=$(echo "${cfg}" | rev | cut -d"/" -f2- | rev) - - if [[ ! -f "${path}/shell_command.cfg" ]]; then - status_msg "Copy shell_command.cfg to ${path} ..." - cp "${KIAUH_SRCDIR}/resources/shell_command.cfg" "${path}" - ok_msg "${path}/shell_command.cfg created!" - ### write include to the very first line of the printer.cfg - sed -i "1 i [include shell_command.cfg]" "${cfg}" - fi - done -} diff --git a/scripts/globals.sh b/scripts/globals.sh deleted file mode 100644 index 8690469..0000000 --- a/scripts/globals.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -# shellcheck disable=SC2034 -set -e - -function set_globals() { - #=================== SYSTEM ===================# - SYSTEMD="/etc/systemd/system" - INITD="/etc/init.d" - ETCDEF="/etc/default" - - #=================== KIAUH ====================# - green=$(echo -en "\e[92m") - yellow=$(echo -en "\e[93m") - magenta=$(echo -en "\e[35m") - red=$(echo -en "\e[91m") - cyan=$(echo -en "\e[96m") - white=$(echo -en "\e[39m") - INI_FILE="${HOME}/.kiauh.ini" - LOGFILE="/tmp/kiauh.log" - RESOURCES="${KIAUH_SRCDIR}/resources" - BACKUP_DIR="${HOME}/kiauh-backups" - - #================== KLIPPER ===================# - KLIPPY_ENV="${HOME}/klippy-env" - KLIPPER_DIR="${HOME}/klipper" - KLIPPER_REPO="https://github.com/Klipper3d/klipper.git" - - #================= MOONRAKER ==================# - MOONRAKER_ENV="${HOME}/moonraker-env" - MOONRAKER_DIR="${HOME}/moonraker" - MOONRAKER_REPO="https://github.com/Arksine/moonraker.git" - - #================= MAINSAIL ===================# - MAINSAIL_DIR="${HOME}/mainsail" - - #================== FLUIDD ====================# - FLUIDD_DIR="${HOME}/fluidd" - - #=============== KLIPPERSCREEN ================# - KLIPPERSCREEN_ENV="${HOME}/.KlipperScreen-env" - KLIPPERSCREEN_DIR="${HOME}/KlipperScreen" - KLIPPERSCREEN_REPO="https://github.com/jordanruthe/KlipperScreen.git" - - #========== MOONRAKER-TELEGRAM-BOT ============# - TELEGRAM_BOT_ENV="${HOME}/moonraker-telegram-bot-env" - TELEGRAM_BOT_DIR="${HOME}/moonraker-telegram-bot" - TELEGRAM_BOT_REPO="https://github.com/nlef/moonraker-telegram-bot.git" - - #=============== PRETTY-GCODE =================# - PGC_DIR="${HOME}/pgcode" - PGC_REPO="https://github.com/Kragrathea/pgcode" - - #================== NGINX =====================# - NGINX_SA="/etc/nginx/sites-available" - NGINX_SE="/etc/nginx/sites-enabled" - NGINX_CONFD="/etc/nginx/conf.d" - - #=============== MOONRAKER-OBICO ================# - MOONRAKER_OBICO_DIR="${HOME}/moonraker-obico" - MOONRAKER_OBICO_REPO="https://github.com/TheSpaghettiDetective/moonraker-obico.git" - - #=============== OCTOEVERYWHERE ================# - OCTOEVERYWHERE_ENV="${HOME}/octoeverywhere-env" - OCTOEVERYWHERE_DIR="${HOME}/octoeverywhere" - OCTOEVERYWHERE_REPO="https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" - - #=============== Crowsnest ================# - CROWSNEST_DIR="${HOME}/crowsnest" - CROWSNEST_REPO="https://github.com/mainsail-crew/crowsnest.git" - - #=============== Mobileraker ================# - MOBILERAKER_ENV="${HOME}/mobileraker-env" - MOBILERAKER_DIR="${HOME}/mobileraker_companion" - MOBILERAKER_REPO="https://github.com/Clon1998/mobileraker_companion.git" - - #=============== OCTOAPP ================# - OCTOAPP_ENV="${HOME}/octoapp-env" - OCTOAPP_DIR="${HOME}/octoapp" - OCTOAPP_REPO="https://github.com/crysxd/OctoApp-Plugin.git" - - #=============== Spoolman ================# - SPOOLMAN_DIR="${HOME}/Spoolman" - SPOOLMAN_DB_DIR="${HOME}/.local/share/spoolman" - SPOOLMAN_REPO="https://api.github.com/repos/Donkie/Spoolman/releases/latest" -} diff --git a/scripts/klipper.sh b/scripts/klipper.sh deleted file mode 100644 index fb205a1..0000000 --- a/scripts/klipper.sh +++ /dev/null @@ -1,665 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#TODO (multi instance): -# if the klipper installer is started another time while other klipper -# instances are detected, ask if new instances should be added - -#=================================================# -#================ INSTALL KLIPPER ================# -#=================================================# - -### -# this function detects all installed klipper -# systemd instances and returns their absolute path -function klipper_systemd() { - local services - local blacklist - local ignore - local match - - ### - # any service that uses "klipper" in its own name but isn't a full klipper service must be blacklisted using - # this variable, otherwise they will be falsely recognized as klipper instances. E.g. "klipper-mcu.service" - # is not a klipper service, but related to klippers linux mcu, which also requires its own service file, hence - # it must be blacklisted. - blacklist="mcu" - - ignore="${SYSTEMD}/klipper-(${blacklist}).service" - match="${SYSTEMD}/klipper(-[0-9a-zA-Z]+)?.service" - - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype awk ! -regex "${ignore}" -regex "${match}" | sort) - echo "${services}" -} - -function start_klipper_setup() { - local klipper_systemd_services - local python_version - local instance_count - local instance_names - local use_custom_names - local input - local regex - local blacklist - local error - - status_msg "Initializing Klipper installation ...\n" - - ### return early if klipper already exists - klipper_systemd_services=$(klipper_systemd) - - if [[ -n ${klipper_systemd_services} ]]; then - error="At least one Klipper service is already installed:" - - for s in ${klipper_systemd_services}; do - log_info "Found Klipper service: ${s}" - error="${error}\n βž” ${s}" - done - fi - [[ -n ${error} ]] && print_error "${error}" && return - - ### user selection for python version - print_dialog_user_select_python_version - while true; do - read -p "${cyan}###### Select Python version:${white} " -i "1" -e input - case "${input}" in - 1) - select_msg "Python 3.x\n" - python_version=3 - break;; - 2) - select_msg "Python 2.7\n" - python_version=2 - break;; - B|b) - clear; install_menu; break;; - *) - error_msg "Invalid Input!\n";; - esac - done && input="" - - ### user selection for instance count - print_dialog_user_select_instance_count - regex="^[1-9][0-9]*$" - while [[ ! ${input} =~ ${regex} ]]; do - read -p "${cyan}###### Number of Klipper instances to set up:${white} " -i "1" -e input - - if [[ ${input} =~ ${regex} ]]; then - instance_count="${input}" - select_msg "Instance count: ${instance_count}\n" - break - elif [[ ${input} == "B" || ${input} == "b" ]]; then - install_menu - else - error_msg "Invalid Input!\n" - fi - done && input="" - - ### user selection for custom names - use_custom_names="false" - if (( instance_count > 1 )); then - print_dialog_user_select_custom_name_bool - while true; do - read -p "${cyan}###### Assign custom names? (y/N):${white} " input - case "${input}" in - Y|y|Yes|yes) - select_msg "Yes\n" - use_custom_names="true" - break;; - N|n|No|no|"") - select_msg "No\n" - break;; - B|b) - clear; install_menu; break;; - *) - error_msg "Invalid Input!\n";; - esac - done && input="" - else - instance_names+=("printer") - fi - - ### user selection for setting the actual custom names - shopt -s nocasematch - if (( instance_count > 1 )) && [[ ${use_custom_names} == "true" ]]; then - local i - - i=1 - regex="^[0-9a-zA-Z]+$" - blacklist="mcu" - while [[ ! ${input} =~ ${regex} || ${input} =~ ${blacklist} || ${i} -le ${instance_count} ]]; do - read -p "${cyan}###### Name for instance #${i}:${white} " input - - if [[ ${input} =~ ${blacklist} ]]; then - error_msg "Name not allowed! You are trying to use a reserved name." - elif [[ ${input} =~ ${regex} && ! ${input} =~ ${blacklist} ]]; then - select_msg "Name: ${input}\n" - if [[ ${input} =~ ^[0-9]+$ ]]; then - instance_names+=("printer_${input}") - else - instance_names+=("${input}") - fi - i=$(( i + 1 )) - else - error_msg "Invalid Input!\n" - fi - done && input="" - elif (( instance_count > 1 )) && [[ ${use_custom_names} == "false" ]]; then - for (( i=1; i <= instance_count; i++ )); do - instance_names+=("printer_${i}") - done - fi - shopt -u nocasematch - - (( instance_count > 1 )) && status_msg "Installing ${instance_count} Klipper instances ..." - (( instance_count == 1 )) && status_msg "Installing single Klipper instance ..." - - run_klipper_setup "${python_version}" "${instance_names[@]}" -} - -function print_dialog_user_select_python_version() { - top_border - echo -e "| Please select your preferred Python version. | " - echo -e "| The recommended version is Python 3.x. | " - hr - echo -e "| 1) [Python 3.x] (recommended) | " - echo -e "| 2) [Python 2.7] ${yellow}(legacy)${white} | " - back_footer -} - -function print_dialog_user_select_instance_count() { - top_border - echo -e "| Please select the number of Klipper instances to set |" - echo -e "| up. The number of Klipper instances will determine |" - echo -e "| the amount of printers you can run from this host. |" - blank_line - echo -e "| ${yellow}WARNING:${white} |" - echo -e "| ${yellow}Setting up too many instances may crash your system.${white} |" - back_footer -} - -function print_dialog_user_select_custom_name_bool() { - top_border - echo -e "| You can now assign a custom name to each instance. |" - echo -e "| If skipped, each instance will get an index assigned |" - echo -e "| in ascending order, starting at index '1'. |" - blank_line - echo -e "| Info: |" - echo -e "| Only alphanumeric characters for names are allowed! |" - back_footer -} - -function run_klipper_setup() { - read_kiauh_ini "${FUNCNAME[0]}" - - local python_version=${1} - local instance_names - local confirm - local custom_repo - local custom_branch - local dep - - shift 1 - read -r -a instance_names <<< "${@}" - - custom_repo="${custom_klipper_repo}" - custom_branch="${custom_klipper_repo_branch}" - dep=(git) - - ### checking dependencies - dependency_check "${dep[@]}" - - ### step 1: clone klipper - clone_klipper "${custom_repo}" "${custom_branch}" - - ### step 2: install klipper dependencies and create python virtualenv - install_klipper_packages "${python_version}" - create_klipper_virtualenv "${python_version}" - - ### step 3: create klipper instances - for instance in "${instance_names[@]}"; do - create_klipper_service "${instance}" - done - - ### step 4: enable and start all instances - do_action_service "enable" "klipper" - do_action_service "start" "klipper" - - ### step 5: check for dialout group membership - check_usergroups - - ### confirm message - (( ${#instance_names[@]} == 1 )) && confirm="Klipper has been set up!" - (( ${#instance_names[@]} > 1 )) && confirm="${#instance_names[@]} Klipper instances have been set up!" - - ### finalizing the setup with writing instance names to the kiauh.ini - set_multi_instance_names - mask_disrupting_services - - print_confirm "${confirm}" && return -} - -function clone_klipper() { - local repo=${1} branch=${2} - - [[ -z ${repo} ]] && repo="${KLIPPER_REPO}" - repo=$(echo "${repo}" | sed -r "s/^(http|https):\/\/github\.com\///i; s/\.git$//") - repo="https://github.com/${repo}" - - [[ -z ${branch} ]] && branch="master" - - ### force remove existing klipper dir and clone into fresh klipper dir - [[ -d ${KLIPPER_DIR} ]] && rm -rf "${KLIPPER_DIR}" - - status_msg "Cloning Klipper from ${repo} ..." - - cd "${HOME}" || exit 1 - if git clone "${repo}" "${KLIPPER_DIR}"; then - cd "${KLIPPER_DIR}" && git checkout "${branch}" - else - print_error "Cloning Klipper from\n ${repo}\n failed!" - exit 1 - fi -} - -function create_klipper_virtualenv() { - local python_version="${1}" - - [[ -d ${KLIPPY_ENV} ]] && rm -rf "${KLIPPY_ENV}" - - status_msg "Installing $("python${python_version}" -V) virtual environment..." - - if virtualenv -p "python${python_version}" "${KLIPPY_ENV}"; then - (( python_version == 3 )) && "${KLIPPY_ENV}"/bin/pip install -U pip - "${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt - else - log_error "failure while creating python3 klippy-env" - error_msg "Creation of Klipper virtualenv failed!" - exit 1 - fi -} - -### -# extracts the required packages from the -# install-debian.sh script and installs them -# -# @param {string}: python_version - klipper-env python version -# -function install_klipper_packages() { - local packages log_name="Klipper" python_version="${1}" - local install_script="${KLIPPER_DIR}/scripts/install-debian.sh" - - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages=$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n') - ### add dfu-util for octopi-images - packages+=" dfu-util" - ### add pkg-config for rp2040 build - packages+=" pkg-config" - ### add dbus requirement for DietPi distro - [[ -e "/boot/dietpi/.version" ]] && packages+=" dbus" - - if (( python_version == 3 )); then - ### replace python-dev with python3-dev if python3 was selected - packages="${packages//python-dev/python3-dev}" - elif (( python_version == 2 )); then - ### package name 'python-dev' is deprecated (-> no installation candidate) on more modern linux distros - packages="${packages//python-dev/python2-dev}" - else - log_error "Internal Error: missing parameter 'python_version' during function call of ${FUNCNAME[0]}" - error_msg "Internal Error: missing parameter 'python_version' during function call of ${FUNCNAME[0]}" - exit 1 - fi - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -function create_klipper_service() { - local instance_name=${1} - - local printer_data - local cfg_dir - local gcodes_dir - local cfg - local log - local klippy_serial - local klippy_socket - local env_file - local service - local service_template - local env_template - local suffix - - printer_data="${HOME}/${instance_name}_data" - cfg_dir="${printer_data}/config" - gcodes_dir="${printer_data}/gcodes" - cfg="${cfg_dir}/printer.cfg" - log="${printer_data}/logs/klippy.log" - klippy_serial="${printer_data}/comms/klippy.serial" - klippy_socket="${printer_data}/comms/klippy.sock" - env_file="${printer_data}/systemd/klipper.env" - - if [[ ${instance_name} == "printer" ]]; then - suffix="${instance_name//printer/}" - else - suffix="-${instance_name//printer_/}" - fi - - create_required_folders "${printer_data}" - - service_template="${KIAUH_SRCDIR}/resources/klipper.service" - env_template="${KIAUH_SRCDIR}/resources/klipper.env" - service="${SYSTEMD}/klipper${suffix}.service" - - if [[ ! -f ${service} ]]; then - status_msg "Create Klipper service file ..." - - sudo cp "${service_template}" "${service}" - sudo cp "${env_template}" "${env_file}" - sudo sed -i "s|%USER%|${USER}|g; s|%KLIPPER_DIR%|${KLIPPER_DIR}|; s|%ENV%|${KLIPPY_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}" - sudo sed -i "s|%USER%|${USER}|; s|%KLIPPER_DIR%|${KLIPPER_DIR}|; s|%LOG%|${log}|; s|%CFG%|${cfg}|; s|%PRINTER%|${klippy_serial}|; s|%UDS%|${klippy_socket}|" "${env_file}" - - ok_msg "Klipper service file created!" - fi - - if [[ ! -f ${cfg} ]]; then - write_example_printer_cfg "${cfg}" "${gcodes_dir}" - fi -} - -function write_example_printer_cfg() { - local cfg=${1} - local gcodes_dir=${2} - local cfg_template - - cfg_template="${KIAUH_SRCDIR}/resources/example.printer.cfg" - - status_msg "Creating minimal example printer.cfg ..." - if cp "${cfg_template}" "${cfg}"; then - sed -i "s|%GCODES_DIR%|${gcodes_dir}|" "${cfg}" - ok_msg "Minimal example printer.cfg created!" - else - error_msg "Couldn't create minimal example printer.cfg!" - fi -} - -#================================================# -#================ REMOVE KLIPPER ================# -#================================================# - -function remove_klipper_service() { - [[ -z $(klipper_systemd) ]] && return - - status_msg "Removing Klipper services ..." - - for service in $(klipper_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - sudo systemctl daemon-reload - sudo systemctl reset-failed - done - - ok_msg "All Klipper services removed!" -} - -function find_instance_files() { - local target_folder=${1} - local target_name=${2} - local files - - readarray -t files < <(find "${HOME}" -regex "${HOME}/[A-Za-z0-9_]+_data/${target_folder}/${target_name}" | sort) - - echo -e "${files[@]}" -} - -function find_legacy_klipper_logs() { - local files - local regex="klippy(-[0-9a-zA-Z]+)?\.log(.*)?" - - readarray -t files < <(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort) - echo -e "${files[@]}" -} - -function find_legacy_klipper_uds() { - local files - - readarray -t files < <(find /tmp -maxdepth 1 -regextype posix-extended -regex "/tmp/klippy_uds(-[0-9a-zA-Z]+)?" | sort) - echo -e "${files[@]}" -} - -function find_legacy_klipper_printer() { - local files - - readarray -t files < <(find /tmp -maxdepth 1 -regextype posix-extended -regex "/tmp/printer(-[0-9a-zA-Z]+)?" | sort) - echo -e "${files[@]}" -} - -function remove_klipper_dir() { - [[ ! -d ${KLIPPER_DIR} ]] && return - - status_msg "Removing Klipper directory ..." - rm -rf "${KLIPPER_DIR}" - ok_msg "Directory removed!" -} - -function remove_klipper_env() { - [[ ! -d ${KLIPPY_ENV} ]] && return - - status_msg "Removing klippy-env directory ..." - rm -rf "${KLIPPY_ENV}" - ok_msg "Directory removed!" -} - -### -# takes in a string of space separated absolute -# filepaths and removes those files one after another -# -function remove_files() { - local files - read -r -a files <<< "${@}" - - if (( ${#files[@]} > 0 )); then - for file in "${files[@]}"; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_klipper() { - remove_klipper_service - remove_files "$(find_instance_files "systemd" "klipper.env")" - remove_files "$(find_instance_files "logs" "klippy.log.*")" - remove_files "$(find_instance_files "comms" "klippy.sock")" - remove_files "$(find_instance_files "comms" "klippy.serial")" - - remove_files "$(find_legacy_klipper_logs)" - remove_files "$(find_legacy_klipper_uds)" - remove_files "$(find_legacy_klipper_printer)" - - remove_klipper_dir - remove_klipper_env - - print_confirm "Klipper was successfully removed!" && return -} - -#================================================# -#================ UPDATE KLIPPER ================# -#================================================# - -### -# stops klipper, performs a git pull, installs -# possible new dependencies, then restarts klipper -# -function update_klipper() { - read_kiauh_ini "${FUNCNAME[0]}" - - local py_ver - local custom_repo="${custom_klipper_repo}" - local custom_branch="${custom_klipper_repo_branch}" - - py_ver=$(get_klipper_python_ver) - - do_action_service "stop" "klipper" - - if [[ ! -d ${KLIPPER_DIR} ]]; then - clone_klipper "${custom_repo}" "${custom_branch}" - else - backup_before_update "klipper" - - status_msg "Updating Klipper ..." - cd "${KLIPPER_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_klipper_packages "${py_ver}" - ### install possible new python dependencies - "${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}/scripts/klippy-requirements.txt" - fi - - ok_msg "Update complete!" - do_action_service "restart" "klipper" -} - -#================================================# -#================ KLIPPER STATUS ================# -#================================================# - -function get_klipper_status() { - local sf_count status py_ver - sf_count="$(klipper_systemd | wc -w)" - - py_ver=$(get_klipper_python_ver) - - ### remove the "SERVICE" entry from the data array if a klipper service is installed - local data_arr=(SERVICE "${KLIPPER_DIR}" "${KLIPPY_ENV}") - (( sf_count > 0 )) && unset "data_arr[0]" - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - if (( py_ver == 3 )); then - status="Installed: ${sf_count}(py${py_ver})" - else - status="Installed: ${sf_count}" - fi - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - - echo "${status}" -} - -function get_local_klipper_commit() { - [[ ! -d ${KLIPPER_DIR} || ! -d "${KLIPPER_DIR}/.git" ]] && return - - local commit - cd "${KLIPPER_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_klipper_commit() { - [[ ! -d ${KLIPPER_DIR} || ! -d "${KLIPPER_DIR}/.git" ]] && return - - local commit - local branch - - read_kiauh_ini "${FUNCNAME[0]}" - branch="${custom_klipper_repo_branch}" - [[ -z ${branch} ]] && branch="master" - - cd "${KLIPPER_DIR}" && git fetch origin -q - commit=$(git describe "origin/${branch}" --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_klipper_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_klipper_commit)" - remote_ver="$(get_remote_klipper_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add klipper to application_updates_available in kiauh.ini - add_to_application_updates "klipper" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -### -# reads the python version from the klipper virtual environment -# -# @output: writes the python major version to STDOUT -# -function get_klipper_python_ver() { - [[ ! -d ${KLIPPY_ENV} ]] && return - - local version - version=$("${KLIPPY_ENV}"/bin/python --version 2>&1 | cut -d" " -f2 | cut -d"." -f1) - echo "${version}" -} - -function mask_disrupting_services() { - local brltty="false" - local brltty_udev="false" - local modem_manager="false" - - [[ $(dpkg -s brltty 2>/dev/null | grep "Status") = *\ installed ]] && brltty="true" - [[ $(dpkg -s brltty-udev 2>/dev/null | grep "Status") = *\ installed ]] && brltty_udev="true" - [[ $(dpkg -s ModemManager 2>/dev/null | grep "Status") = *\ installed ]] && modem_manager="true" - - status_msg "Installed brltty package detected, masking brltty service ..." - if [[ ${brltty} == "true" ]]; then - sudo systemctl stop brltty - sudo systemctl mask brltty - fi - ok_msg "brltty service masked!" - - status_msg "Installed brltty-udev package detected, masking brltty-udev service ..." - if [[ ${brltty_udev} == "true" ]]; then - sudo systemctl stop brltty-udev - sudo systemctl mask brltty-udev - fi - ok_msg "brltty-udev service masked!" - - status_msg "Installed ModemManager package detected, masking ModemManager service ..." - if [[ ${modem_manager} == "true" ]]; then - sudo systemctl stop ModemManager - sudo systemctl mask ModemManager - fi - ok_msg "ModemManager service masked!" -} diff --git a/scripts/klipperscreen.sh b/scripts/klipperscreen.sh deleted file mode 100644 index 9701e7c..0000000 --- a/scripts/klipperscreen.sh +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#============== INSTALL KLIPPERSCREEN ==============# -#===================================================# - -function klipperscreen_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/KlipperScreen.service") - echo "${services}" -} - -function install_klipperscreen() { - ### return early if python version check fails - if [[ $(python3_check) == "false" ]]; then - local error="Versioncheck failed! Python 3.7 or newer required!\n" - error="${error} Please upgrade Python." - print_error "${error}" && return - fi - - ### first, we create a backup of the full klipper_config dir - safety first! - backup_config_dir - - ### install KlipperScreen - klipperscreen_setup - - ### add klipperscreen to the update manager in moonraker.conf - patch_klipperscreen_update_manager - - do_action_service "restart" "KlipperScreen" -} - -function klipperscreen_setup() { - local dep=(wget curl unzip dfu-util) - dependency_check "${dep[@]}" - status_msg "Cloning KlipperScreen from ${KLIPPERSCREEN_REPO} ..." - - # force remove existing KlipperScreen dir - [[ -d ${KLIPPERSCREEN_DIR} ]] && rm -rf "${KLIPPERSCREEN_DIR}" - - # clone into fresh KlipperScreen dir - cd "${HOME}" || exit 1 - if ! git clone "${KLIPPERSCREEN_REPO}" "${KLIPPERSCREEN_DIR}"; then - print_error "Cloning KlipperScreen from\n ${KLIPPERSCREEN_REPO}\n failed!" - exit 1 - fi - - status_msg "Installing KlipperScreen ..." - if "${KLIPPERSCREEN_DIR}"/scripts/KlipperScreen-install.sh; then - ok_msg "KlipperScreen successfully installed!" - else - print_error "KlipperScreen installation failed!" - exit 1 - fi -} - -#===================================================# -#=============== REMOVE KLIPPERSCREEN ==============# -#===================================================# - -function remove_klipperscreen() { - ### remove KlipperScreen dir - if [[ -d ${KLIPPERSCREEN_DIR} ]]; then - status_msg "Removing KlipperScreen directory ..." - rm -rf "${KLIPPERSCREEN_DIR}" && ok_msg "Directory removed!" - fi - - ### remove KlipperScreen VENV dir - if [[ -d ${KLIPPERSCREEN_ENV} ]]; then - status_msg "Removing KlipperScreen VENV directory ..." - rm -rf "${KLIPPERSCREEN_ENV}" && ok_msg "Directory removed!" - fi - - ### remove KlipperScreen service - if [[ -e "${SYSTEMD}/KlipperScreen.service" ]]; then - status_msg "Removing KlipperScreen service ..." - do_action_service "stop" "KlipperScreen" - do_action_service "disable" "KlipperScreen" - sudo rm -f "${SYSTEMD}/KlipperScreen.service" - - ###reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "KlipperScreen Service removed!" - fi - - ### remove KlipperScreen log - if [[ -e "/tmp/KlipperScreen.log" ]]; then - status_msg "Removing KlipperScreen log file ..." - rm -f "/tmp/KlipperScreen.log" && ok_msg "File removed!" - fi - - ### remove KlipperScreen log symlink in config dir - if [[ -e "${KLIPPER_CONFIG}/KlipperScreen.log" ]]; then - status_msg "Removing KlipperScreen log symlink ..." - rm -f "${KLIPPER_CONFIG}/KlipperScreen.log" && ok_msg "File removed!" - fi - - print_confirm "KlipperScreen successfully removed!" -} - -#===================================================# -#=============== UPDATE KLIPPERSCREEN ==============# -#===================================================# - -function update_klipperscreen() { - local old_md5 - old_md5=$(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) - - do_action_service "stop" "KlipperScreen" - backup_before_update "klipperscreen" - - cd "${KLIPPERSCREEN_DIR}" - git pull origin master -q && ok_msg "Fetch successfull!" - git checkout -f master && ok_msg "Checkout successfull" - - if [[ $(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then - status_msg "New dependecies detected..." - "${KLIPPERSCREEN_ENV}"/bin/pip install -r "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" - ok_msg "Dependencies have been installed!" - fi - - ok_msg "Update complete!" - do_action_service "start" "KlipperScreen" -} - -#===================================================# -#=============== KLIPPERSCREEN STATUS ==============# -#===================================================# - -function get_klipperscreen_status() { - local sf_count status - sf_count="$(klipperscreen_systemd | wc -w)" - - ### remove the "SERVICE" entry from the data array if a moonraker service is installed - local data_arr=(SERVICE "${KLIPPERSCREEN_DIR}" "${KLIPPERSCREEN_ENV}") - (( sf_count > 0 )) && unset "data_arr[0]" - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed!" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - echo "${status}" -} - -function get_local_klipperscreen_commit() { - [[ ! -d ${KLIPPERSCREEN_DIR} || ! -d "${KLIPPERSCREEN_DIR}/.git" ]] && return - - local commit - cd "${KLIPPERSCREEN_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_klipperscreen_commit() { - [[ ! -d ${KLIPPERSCREEN_DIR} || ! -d "${KLIPPERSCREEN_DIR}/.git" ]] && return - - local commit - cd "${KLIPPERSCREEN_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_klipperscreen_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_klipperscreen_commit)" - remote_ver="$(get_remote_klipperscreen_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "klipperscreen" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function patch_klipperscreen_update_manager() { - local patched="false" - local moonraker_configs - moonraker_configs=$(find "${KLIPPER_CONFIG}" -type f -name "moonraker.conf" | sort) - - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager KlipperScreen\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add KlipperScreens update manager section to moonraker.conf - status_msg "Adding KlipperScreen to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager KlipperScreen] -type: git_repo -path: ${HOME}/KlipperScreen -origin: https://github.com/jordanruthe/KlipperScreen.git -env: ${HOME}/.KlipperScreen-env/bin/python -requirements: scripts/KlipperScreen-requirements.txt -install_script: scripts/KlipperScreen-install.sh -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} diff --git a/scripts/mainsail.sh b/scripts/mainsail.sh deleted file mode 100644 index 73ea7a5..0000000 --- a/scripts/mainsail.sh +++ /dev/null @@ -1,690 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#================= INSTALL MAINSAIL ================# -#===================================================# - -function install_mainsail() { - if [[ -z $(moonraker_systemd) ]]; then - local error="Moonraker not installed! It's recommended to install Moonraker first!" - print_error "${error}" - while true; do - local yn - read -p "${cyan}###### Proceed to install Mainsail without installing Moonraker? (y/N):${white} " yn - case "${yn}" in - Y|y|Yes|yes) - select_msg "Yes" - break;; - N|n|No|no|"") - select_msg "No" - abort_msg "Exiting Mainsail setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - fi - - ### checking dependencies - local dep=(wget nginx unzip) - dependency_check "${dep[@]}" - ### detect conflicting Haproxy and Apache2 installations - detect_conflicting_packages - - status_msg "Initializing Mainsail installation ..." - ### first, we create a backup of the full klipper_config dir - safety first! - backup_config_dir - - ### check for other enabled web interfaces - unset SET_LISTEN_PORT - detect_enabled_sites - - ### check if another site already listens to port 80 - mainsail_port_check - - ### download mainsail - download_mainsail - - ### ask user to install the recommended webinterface macros - install_mainsail_macros - - ### create /etc/nginx/conf.d/upstreams.conf - set_upstream_nginx_cfg - ### create /etc/nginx/sites-available/ - set_nginx_cfg "mainsail" - ### nginx on ubuntu 21 and above needs special permissions to access the files - set_nginx_permissions - - ### symlink nginx log - symlink_webui_nginx_log "mainsail" - - ### add mainsail to the update manager in moonraker.conf - patch_mainsail_update_manager - - fetch_webui_ports #WIP - - ### confirm message - print_confirm "Mainsail has been set up!" -} - -function install_mainsail_macros() { - local yn - while true; do - echo - top_border - echo -e "| It is recommended to use special macros in order to |" - echo -e "| have Mainsail fully functional and working. |" - blank_line - echo -e "| The recommended macros for Mainsail can be seen here: |" - echo -e "| https://github.com/mainsail-crew/mainsail-config |" - blank_line - echo -e "| If you already use these macros skip this step. |" - echo -e "| Otherwise you should consider to answer with 'yes' to |" - echo -e "| download the recommended macros. |" - bottom_border - read -p "${cyan}###### Download the recommended macros? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - download_mainsail_macros - break;; - N|n|No|no) - select_msg "No" - break;; - *) - print_error "Invalid command!";; - esac - done - return -} - -function download_mainsail_macros() { - local ms_cfg_repo path configs regex line gcode_dir - - ms_cfg_repo="https://github.com/mainsail-crew/mainsail-config.git" - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/printer\.cfg" - configs=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -z ${configs} ]]; then - print_error "No printer.cfg found! Installation of Macros will be skipped ..." - log_error "execution stopped! reason: no printer.cfg found" - return - fi - - status_msg "Cloning mainsail-config ..." - [[ -d "${HOME}/mainsail-config" ]] && rm -rf "${HOME}/mainsail-config" - if git clone "${ms_cfg_repo}" "${HOME}/mainsail-config"; then - for config in ${configs}; do - path=$(echo "${config}" | rev | cut -d"/" -f2- | rev) - - if [[ -e "${path}/mainsail.cfg" && ! -h "${path}/mainsail.cfg" ]]; then - warn_msg "Attention! Existing mainsail.cfg detected!" - warn_msg "The file will be renamed to 'mainsail.bak.cfg' to be able to continue with the installation." - if ! mv "${path}/mainsail.cfg" "${path}/mainsail.bak.cfg"; then - error_msg "Renaming mainsail.cfg failed! Aborting installation ..." - return - fi - fi - - if [[ -h "${path}/mainsail.cfg" ]]; then - warn_msg "Recreating symlink in ${path} ..." - rm -rf "${path}/mainsail.cfg" - fi - - if ! ln -sf "${HOME}/mainsail-config/client.cfg" "${path}/mainsail.cfg"; then - error_msg "Creating symlink failed! Aborting installation ..." - return - fi - - if ! grep -Eq "^\[include mainsail.cfg\]$" "${path}/printer.cfg"; then - log_info "${path}/printer.cfg" - sed -i "1 i [include mainsail.cfg]" "${path}/printer.cfg" - fi - - line=$(($(grep -n "\[include mainsail.cfg\]" "${path}/printer.cfg" | tail -1 | cut -d: -f1) + 1)) - gcode_dir=${path/config/gcodes} - if ! grep -Eq "^\[virtual_sdcard\]$" "${path}/printer.cfg"; then - log_info "${path}/printer.cfg" - sed -i "${line} i \[virtual_sdcard]\npath: ${gcode_dir}\non_error_gcode: CANCEL_PRINT\n" "${path}/printer.cfg" - fi - done - else - print_error "Cloning failed! Aborting installation ..." - log_error "execution stopped! reason: cloning failed" - return - fi - - patch_mainsail_config_update_manager - - ok_msg "Done!" -} - -function download_mainsail() { - local services - local url - url=$(get_mainsail_download_url) - - status_msg "Downloading Mainsail from ${url} ..." - - if [[ -d ${MAINSAIL_DIR} ]]; then - rm -rf "${MAINSAIL_DIR}" - fi - - mkdir "${MAINSAIL_DIR}" && cd "${MAINSAIL_DIR}" - - if wget "${url}"; then - ok_msg "Download complete!" - status_msg "Extracting archive ..." - unzip -q -o ./*.zip && ok_msg "Done!" - status_msg "Remove downloaded archive ..." - rm -rf ./*.zip && ok_msg "Done!" - else - print_error "Downloading Mainsail from\n ${url}\n failed!" - exit 1 - fi - - ### check for moonraker multi-instance and if no-instance or multi-instance was found, enable mainsails remoteMode - services=$(moonraker_systemd) - if [[ ( -z "${services}" ) || ( $(echo "${services}" | wc -w) -gt 1 ) ]]; then - enable_mainsail_remotemode - fi -} - -#===================================================# -#================= REMOVE MAINSAIL =================# -#===================================================# - -function remove_mainsail_dir() { - [[ ! -d ${MAINSAIL_DIR} ]] && return - - status_msg "Removing Mainsail directory ..." - rm -rf "${MAINSAIL_DIR}" && ok_msg "Directory removed!" -} - -function remove_mainsail_nginx_config() { - if [[ -e "/etc/nginx/sites-available/mainsail" ]]; then - status_msg "Removing Mainsail configuration for Nginx ..." - sudo rm "/etc/nginx/sites-available/mainsail" && ok_msg "File removed!" - fi - if [[ -L "/etc/nginx/sites-enabled/mainsail" ]]; then - status_msg "Removing Mainsail Symlink for Nginx ..." - sudo rm "/etc/nginx/sites-enabled/mainsail" && ok_msg "File removed!" - fi -} - -function remove_mainsail_logs() { - local files - files=$(find /var/log/nginx -name "mainsail*" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - sudo rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_mainsail_log_symlinks() { - local files regex - - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/mainsail-.*" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_legacy_mainsail_log_symlinks() { - local files - files=$(find "${HOME}/klipper_logs" -name "mainsail*" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_mainsail_config() { - if [[ -d "${HOME}/mainsail-config" ]]; then - status_msg "Removing ${HOME}/mainsail-config ..." - rm -rf "${HOME}/mainsail-config" - ok_msg "${HOME}/mainsail-config removed!" - print_confirm "Mainsail-Config successfully removed!" - fi -} - -function remove_mainsail() { - remove_mainsail_dir - remove_mainsail_nginx_config - remove_mainsail_logs - remove_mainsail_log_symlinks - remove_legacy_mainsail_log_symlinks - - ### remove mainsail_port from ~/.kiauh.ini - sed -i "/^mainsail_port=/d" "${INI_FILE}" - - print_confirm "Mainsail successfully removed!" -} - -#===================================================# -#================= UPDATE MAINSAIL =================# -#===================================================# - -function update_mainsail() { - backup_before_update "mainsail" - status_msg "Updating Mainsail ..." - download_mainsail - match_nginx_configs - symlink_webui_nginx_log "mainsail" - print_confirm "Mainsail successfully updated!" -} - -#===================================================# -#================= MAINSAIL STATUS =================# -#===================================================# - -function get_mainsail_status() { - local status - local data_arr=("${MAINSAIL_DIR}" "${NGINX_SA}/mainsail" "${NGINX_SE}/mainsail") - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed!" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - echo "${status}" -} - -function get_local_mainsail_version() { - local versionfile="${MAINSAIL_DIR}/.version" - local relinfofile="${MAINSAIL_DIR}/release_info.json" - local version - - if [[ -f ${relinfofile} ]]; then - version=$(grep -o '"version":"[^"]*' "${relinfofile}" | grep -o '[^"]*$') - elif [[ -f ${versionfile} ]]; then - version=$(head -n 1 "${versionfile}") - fi - - echo "${version}" -} - -function get_remote_mainsail_version() { - [[ ! $(dpkg-query -f'${Status}' --show curl 2>/dev/null) = *\ installed ]] && return - - local tags - tags=$(curl -s "https://api.github.com/repos/mainsail-crew/mainsail/tags" | grep "name" | cut -d'"' -f4) - echo "${tags}" | head -1 -} - -function compare_mainsail_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_mainsail_version)" - remote_ver="$(get_remote_mainsail_version)" - - if [[ ${local_ver} != "${remote_ver}" && ${local_ver} != "" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "mainsail" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=========== MAINSAIL THEME INSTALLER ===========# -#================================================# - -function print_theme_list() { - local i=0 theme_list=${1} - - while IFS="," read -r col1 col2 col3 col4; do - if [[ ${col1} != "name" ]]; then - printf "| ${i}) %-51s|\n" "[${col1}]" - fi - i=$(( i + 1 )) - done <<< "${theme_list}" -} - -function ms_theme_installer_menu() { - local theme_list theme_author theme_repo theme_name theme_note theme_url - local theme_csv_url="https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" - theme_list=$(curl -s -L "${theme_csv_url}") - - top_border - echo -e "| ${red}~~~~~~~~ [ Mainsail Theme Installer ] ~~~~~~~${white} |" - hr - echo -e "| ${cyan}A preview of each Mainsail theme can be found here:${white} |" - echo -e "| https://docs.mainsail.xyz/theming/themes |" - blank_line - echo -e "| ${yellow}Important note:${white} |" - echo -e "| Installing a theme from this menu will overwrite an |" - echo -e "| already installed theme or modified custom.css file! |" - hr - print_theme_list "${theme_list}" - echo -e "| |" - echo -e "| R) [Remove Theme] |" - back_footer - - while IFS="," read -r col1 col2 col3 col4; do - theme_name+=("${col1}") - theme_note+=("${col2}") - theme_author+=("${col3}") - theme_repo+=("${col4}") - done <<< "${theme_list}" - - local option - while true; do - read -p "${cyan}Install theme:${white} " option - if (( option > 0 && option < ${#theme_name[@]} )); then - theme_url="https://github.com/${theme_author[${option}]}/${theme_repo[${option}]}" - ms_theme_install "${theme_url}" "${theme_name[${option}]}" "${theme_note[${option}]}" - break - elif [[ ${option} == "R" || ${option} == "r" ]]; then - ms_theme_delete - break - elif [[ ${option} == "B" || ${option} == "b" ]]; then - advanced_menu - break - else - error_msg "Invalid command!" - fi - done - ms_theme_installer_menu -} - -function ms_theme_install() { - read_kiauh_ini "${FUNCNAME[0]}" - - local theme_url - local theme_name - local theme_note - theme_url=${1} - theme_name=${2} - theme_note=${3} - - local folder_arr - local folder_names="${multi_instance_names}" - local target_folders=() - - IFS=',' read -r -a folder_arr <<< "${folder_names}" - - ### build theme target folder array - if (( ${#folder_arr[@]} > 1 )); then - for folder in "${folder_arr[@]}"; do - ### instance names/identifier of only numbers need to be prefixed with 'printer_' - if [[ ${folder} =~ ^[0-9]+$ ]]; then - target_folders+=("${HOME}/printer_${folder}_data/config") - else - target_folders+=("${HOME}/${folder}_data/config") - fi - done - else - target_folders+=("${HOME}/printer_data/config") - fi - - if (( ${#target_folders[@]} > 1 )); then - top_border - echo -e "| Please select the printer you want to apply the theme |" - echo -e "| installation to: |" - for (( i=0; i < ${#target_folders[@]}; i++ )); do - folder=$(echo "${target_folders[${i}]}" | rev | cut -d "/" -f2 | cut -d"_" -f2- | rev) - printf "|${cyan}%-55s${white}|\n" " ${i}) ${folder}" - done - bottom_border - - local target re="^[0-9]*$" - while true; do - read -p "${cyan}###### Select printer:${white} " target - ### break while loop if input is valid, else display error - [[ ${target} =~ ${re} && ${target} -lt ${#target_folders[@]} ]] && break - error_msg "Invalid command!" - done - fi - - [[ -d "${target_folders[${target}]}/.theme" ]] && rm -rf "${target_folders[${target}]}/.theme" - - status_msg "Installing '${theme_name}' to ${target_folders[${target}]} ..." - cd "${target_folders[${target}]}" - - if git clone "${theme_url}" ".theme"; then - ok_msg "Theme installation complete!" - [[ -n ${theme_note} ]] && echo "${yellow}###### Theme Info: ${theme_note}${white}" - ok_msg "Please remember to delete your browser cache!\n" - else - error_msg "Theme installation failed!\n" - fi -} - -function ms_theme_delete() { - local regex theme_folders target_folders=() - - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/\.theme" - theme_folders=$(find "${HOME}" -maxdepth 3 -type d -regextype posix-extended -regex "${regex}" | sort) -# theme_folders=$(find "${KLIPPER_CONFIG}" -mindepth 1 -type d -name ".theme" | sort) - - ### build target folder array - for folder in ${theme_folders}; do - target_folders+=("${folder}") - done - - if (( ${#target_folders[@]} == 0 )); then - status_msg "No Themes installed!\n" - return - elif (( ${#target_folders[@]} > 1 )); then - top_border - echo -e "| Please select the printer you want to remove the |" - echo -e "| theme installation from. |" - for (( i=0; i < ${#target_folders[@]}; i++ )); do - folder=$(echo "${target_folders[${i}]}" | rev | cut -d "/" -f2 | rev) - printf "|${cyan}%-55s${white}|\n" " ${i}) ${folder}" - done - bottom_border - - local target re="^[0-9]*$" - while true; do - read -p "${cyan}###### Select printer:${white} " target - ### break while loop if input is valid, else display error - [[ ${target} =~ ${re} && ${target} -lt ${#target_folders[@]} ]] && break - error_msg "Invalid command!" - done - fi - - status_msg "Removing ${target_folders[${target}]} ..." - rm -rf "${target_folders[${target}]}" && ok_msg "Theme removed!\n" - - return -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function get_mainsail_download_url() { - local releases_by_tag tags tag unstable_url url - - ### latest stable download url - url="https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" - - read_kiauh_ini "${FUNCNAME[0]}" - if [[ ${mainsail_install_unstable} == "true" ]]; then - releases_by_tag="https://api.github.com/repos/mainsail-crew/mainsail/tags" - tags=$(curl -s "${releases_by_tag}" | grep "name" | cut -d'"' -f4) - tag=$(echo "${tags}" | head -1) - - ### latest unstable download url including pre-releases (alpha, beta, rc) - unstable_url="https://github.com/mainsail-crew/mainsail/releases/download/${tag}/mainsail.zip" - - if [[ ${unstable_url} == *"download//"* ]]; then - warn_msg "Download URL broken! Falling back to URL of latest stable release!" - else - url=${unstable_url} - fi - fi - - echo "${url}" -} - -function mainsail_port_check() { - if [[ ${MAINSAIL_ENABLED} == "false" ]]; then - - if [[ ${SITE_ENABLED} == "true" ]]; then - status_msg "Detected other enabled interfaces:" - - [[ ${FLUIDD_ENABLED} == "true" ]] && \ - echo -e " ${cyan}● Fluidd - Port: ${FLUIDD_PORT}${white}" - - if [[ ${FLUIDD_PORT} == "80" ]]; then - PORT_80_BLOCKED="true" - select_mainsail_port - fi - else - DEFAULT_PORT=$(grep listen "${KIAUH_SRCDIR}/resources/mainsail" | head -1 | sed 's/^\s*//' | cut -d" " -f2 | cut -d";" -f1) - SET_LISTEN_PORT=${DEFAULT_PORT} - fi - SET_NGINX_CFG="true" - - else - SET_NGINX_CFG="false" - fi -} - -function select_mainsail_port() { - if [[ ${PORT_80_BLOCKED} == "true" ]]; then - echo - top_border - echo -e "| ${red}!!!WARNING!!!${white} |" - echo -e "| ${red}You need to choose a different port for Mainsail!${white} |" - echo -e "| ${red}The following web interface is listening at port 80:${white} |" - blank_line - [[ ${FLUIDD_PORT} == "80" ]] && echo "| ● Fluidd |" - blank_line - echo -e "| Make sure you don't choose a port which was already |" - echo -e "| assigned to another webinterface! |" - blank_line - echo -e "| Be aware: there is ${red}NO${white} sanity check for the following |" - echo -e "| input. So make sure to choose a valid port! |" - bottom_border - - local new_port re="^[0-9]+$" - while true; do - read -p "${cyan}Please enter a new Port:${white} " new_port - if [[ ${new_port} =~ ${re} && ${new_port} != "${FLUIDD_PORT}" ]]; then - select_msg "Setting port ${new_port} for Mainsail!" - SET_LISTEN_PORT=${new_port} - break - else - if [[ ! ${new_port} =~ ${re} ]]; then - error_msg "Invalid input!" - else - error_msg "Port already taken! Select a different one!" - fi - fi - done - fi -} - -function enable_mainsail_remotemode() { - [[ ! -f "${MAINSAIL_DIR}/config.json" ]] && return - - status_msg "Setting instance storage location to 'browser' ..." - sed -i 's|"instancesDB": "moonraker"|"instancesDB": "browser"|' "${MAINSAIL_DIR}/config.json" - ok_msg "Done!" -} - -function patch_mainsail_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager mainsail\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Mainsails update manager section to moonraker.conf - status_msg "Adding Mainsail to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager mainsail] -type: web -channel: stable -repo: mainsail-crew/mainsail -path: ~/mainsail -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} - -function patch_mainsail_config_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager mainsail-config\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Mainsails update manager section to moonraker.conf - status_msg "Adding Mainsail-Config to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager mainsail-config] -type: git_repo -primary_branch: master -path: ~/mainsail-config -origin: https://github.com/mainsail-crew/mainsail-config.git -managed_services: klipper -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} diff --git a/scripts/mjpg-streamer.sh b/scripts/mjpg-streamer.sh deleted file mode 100644 index 26168dc..0000000 --- a/scripts/mjpg-streamer.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#=================================================# -#============= INSTALL MJPG-STREAMER =============# -#=================================================# - -function install_mjpg-streamer() { - local webcamd="${KIAUH_SRCDIR}/resources/mjpg-streamer/webcamd" - local webcam_txt="${KIAUH_SRCDIR}/resources/mjpg-streamer/webcam.txt" - local service="${KIAUH_SRCDIR}/resources/mjpg-streamer/webcamd.service" - local repo="https://github.com/jacksonliam/mjpg-streamer.git" - - ### return early if webcamd.service already exists - if [[ -f "${SYSTEMD}/webcamd.service" ]]; then - print_error "Looks like MJPG-streamer is already installed!\n Please remove it first before you try to re-install it!" - return - fi - - status_msg "Initializing MJPG-Streamer installation ..." - - ### check and install dependencies if missing - local dep=(git cmake build-essential imagemagick libv4l-dev ffmpeg) - if apt-cache search libjpeg62-turbo-dev | grep -Eq "^libjpeg62-turbo-dev "; then - dep+=(libjpeg62-turbo-dev) - elif apt-cache search libjpeg8-dev | grep -Eq "^libjpeg8-dev "; then - dep+=(libjpeg8-dev) - fi - - dependency_check "${dep[@]}" - - ### step 1: clone mjpg-streamer - status_msg "Cloning MJPG-Streamer from ${repo} ..." - [[ -d "${HOME}/mjpg-streamer" ]] && rm -rf "${HOME}/mjpg-streamer" - - cd "${HOME}" || exit 1 - if ! git clone "${repo}" "${HOME}/mjpg-streamer"; then - print_error "Cloning MJPG-Streamer from\n ${repo}\n failed!" - exit 1 - fi - ok_msg "Cloning complete!" - - ### step 2: compiling mjpg-streamer - status_msg "Compiling MJPG-Streamer ..." - cd "${HOME}/mjpg-streamer/mjpg-streamer-experimental" - if ! make; then - print_error "Compiling MJPG-Streamer failed!" - exit 1 - fi - ok_msg "Compiling complete!" - - #step 3: install mjpg-streamer - status_msg "Installing MJPG-Streamer ..." - cd "${HOME}/mjpg-streamer" && mv mjpg-streamer-experimental/* . - mkdir www-mjpgstreamer - - cat <> ./www-mjpgstreamer/index.html - -mjpg_streamer test page - -

Snapshot

-

Refresh the page to refresh the snapshot

-Snapshot -

Stream

-Stream - - -EOT - - sudo cp "${webcamd}" "/usr/local/bin/webcamd" - sudo sed -i "/^config_dir=/ s|=.*|=${KLIPPER_CONFIG}|" /usr/local/bin/webcamd - sudo sed -i "/MJPGSTREAMER_HOME/ s/pi/${USER}/" /usr/local/bin/webcamd - sudo chmod +x /usr/local/bin/webcamd - - ### step 4: create webcam.txt config file - [[ ! -d ${KLIPPER_CONFIG} ]] && mkdir -p "${KLIPPER_CONFIG}" - if [[ ! -f "${KLIPPER_CONFIG}/webcam.txt" ]]; then - status_msg "Creating webcam.txt config file ..." - cp "${webcam_txt}" "${KLIPPER_CONFIG}/webcam.txt" - ok_msg "Done!" - fi - - ### step 5: create systemd service - status_msg "Creating MJPG-Streamer service ..." - sudo cp "${service}" "${SYSTEMD}/webcamd.service" - sudo sed -i "s|%USER%|${USER}|" "${SYSTEMD}/webcamd.service" - ok_msg "MJPG-Streamer service created!" - - ### step 6: enabling and starting mjpg-streamer service - status_msg "Starting MJPG-Streamer service, please wait ..." - sudo systemctl enable webcamd.service - if sudo systemctl start webcamd.service; then - ok_msg "MJPG-Streamer service started!" - else - status_msg "MJPG-Streamer service couldn't be started! No webcam connected?\n###### You need to manually restart the service once your webcam is set up correctly." - fi - - ### step 6.1: create webcamd.log symlink - [[ ! -d ${KLIPPER_LOGS} ]] && mkdir -p "${KLIPPER_LOGS}" - if [[ -f "/var/log/webcamd.log" && ! -L "${KLIPPER_LOGS}/webcamd.log" ]]; then - ln -s "/var/log/webcamd.log" "${KLIPPER_LOGS}/webcamd.log" - fi - - ### step 6.2: add webcamd.log logrotate - if [[ ! -f "/etc/logrotate.d/webcamd" ]]; then - status_msg "Create logrotate rule ..." - sudo /bin/sh -c "cat > /etc/logrotate.d/webcamd" << EOF -/var/log/webcamd.log -{ - rotate 2 - weekly - maxsize 32M - missingok - notifempty - compress - delaycompress - sharedscripts -} -EOF - ok_msg "Done!" - fi - - ### step 7: check if user is in group "video" - local usergroup_changed="false" - if ! groups "${USER}" | grep -q "video"; then - status_msg "Adding user '${USER}' to group 'video' ..." - sudo usermod -a -G video "${USER}" && ok_msg "Done!" - usergroup_changed="true" - fi - - ### confirm message - local confirm_msg="MJPG-Streamer has been set up!" - if [[ ${usergroup_changed} == "true" ]]; then - confirm_msg="${confirm_msg}\n ${yellow}INFO: Your User was added to a new group!${green}" - confirm_msg="${confirm_msg}\n ${yellow}You need to relog/restart for the group to be applied!${green}" - fi - - print_confirm "${confirm_msg}" - - ### print webcam ip adress/url - local ip - ip=$(hostname -I | cut -d" " -f1) - local cam_url="http://${ip}:8080/?action=stream" - local cam_url_alt="http://${ip}/webcam/?action=stream" - echo -e " ${cyan}● Webcam URL:${white} ${cam_url}" - echo -e " ${cyan}● Webcam URL:${white} ${cam_url_alt}" - echo -} - -#=================================================# -#============== REMOVE MJPG-STREAMER =============# -#=================================================# - -function remove_mjpg-streamer() { - ### remove MJPG-Streamer service - if [[ -e "${SYSTEMD}/webcamd.service" ]]; then - status_msg "Removing MJPG-Streamer service ..." - sudo systemctl stop webcamd && sudo systemctl disable webcamd - sudo rm -f "${SYSTEMD}/webcamd.service" - ###reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "MJPG-Streamer Service removed!" - fi - - ### remove webcamd from /usr/local/bin - if [[ -e "/usr/local/bin/webcamd" ]]; then - sudo rm -f "/usr/local/bin/webcamd" - fi - - ### remove MJPG-Streamer directory - if [[ -d "${HOME}/mjpg-streamer" ]]; then - status_msg "Removing MJPG-Streamer directory ..." - rm -rf "${HOME}/mjpg-streamer" - ok_msg "MJPG-Streamer directory removed!" - fi - - ### remove webcamd log and symlink - [[ -f "/var/log/webcamd.log" ]] && sudo rm -f "/var/log/webcamd.log" - [[ -L "${KLIPPER_LOGS}/webcamd.log" ]] && rm -f "${KLIPPER_LOGS}/webcamd.log" - - print_confirm "MJPG-Streamer successfully removed!" -} diff --git a/scripts/mobileraker.sh b/scripts/mobileraker.sh deleted file mode 100644 index 39445a0..0000000 --- a/scripts/mobileraker.sh +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -# -# This file is written and maintained by Patrick Schmidt author of Mobileraker -# It is based of the kliperscreen.sh install script! - - -set -e - -#===================================================# -#========== INSTALL MOBILERAKER COMPANION ==========# -#===================================================# - -function mobileraker_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/mobileraker.service") - echo "${services}" -} - -function install_mobileraker() { - ### return early if python version check fails - if [[ $(python3_check) == "false" ]]; then - local error="Versioncheck failed! Python 3.7 or newer required!\n" - error="${error} Please upgrade Python." - print_error "${error}" && return - fi - - ### first, we create a backup of the full klipper_config dir - safety first! - backup_config_dir - - ### install Mobileraker's Companion - mobileraker_setup - - ### add Mobileraker's Companion to the update manager in moonraker.conf - patch_mobileraker_update_manager - - do_action_service "restart" "mobileraker" -} - -function mobileraker_setup() { - local dep=(wget curl unzip dfu-util) - dependency_check "${dep[@]}" - status_msg "Cloning Mobileraker's companion from ${MOBILERAKER_REPO} ..." - - # force remove existing Mobileraker's companion dir - [[ -d ${MOBILERAKER_DIR} ]] && rm -rf "${MOBILERAKER_DIR}" - - # clone into fresh Mobileraker's companion dir - cd "${HOME}" || exit 1 - if ! git clone "${MOBILERAKER_REPO}" "${MOBILERAKER_DIR}"; then - print_error "Cloning mobileraker's companion from\n ${MOBILERAKER_REPO}\n failed!" - exit 1 - fi - - status_msg "Starting installer of Mobileraker's companion ..." - if "${MOBILERAKER_DIR}"/scripts/install.sh; then - ok_msg "Mobileraker's companion successfully installed!" - else - print_error "Mobileraker's companion installation failed!" - exit 1 - fi -} - -#===================================================# -#=========== REMOVE MOBILERAKER COMPANION ==========# -#===================================================# - -function remove_mobileraker() { - ### remove Mobileraker's companion dir - if [[ -d ${MOBILERAKER_DIR} ]]; then - status_msg "Removing Mobileraker's companion directory ..." - rm -rf "${MOBILERAKER_DIR}" && ok_msg "Directory removed!" - fi - - ### remove Mobileraker's companion VENV dir - if [[ -d ${MOBILERAKER_ENV} ]]; then - status_msg "Removing Mobileraker's companion VENV directory ..." - rm -rf "${MOBILERAKER_ENV}" && ok_msg "Directory removed!" - fi - - ### remove Mobileraker's companion service - if [[ -e "${SYSTEMD}/mobileraker.service" ]]; then - status_msg "Removing mobileraker service ..." - do_action_service "stop" "mobileraker" - do_action_service "disable" "mobileraker" - sudo rm -f "${SYSTEMD}/mobileraker.service" - - ###reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "Mobileraker's companion Service removed!" - fi - - - remove_mobileraker_logs - - print_confirm "Mobileraker's companion successfully removed!" -} - -function remove_mobileraker_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/mobileraker\.log.*" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -#===================================================# -#=========== UPDATE MOBILERAKER COMPANION ==========# -#===================================================# - -function update_mobileraker() { - local old_md5 - old_md5=$(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) - - do_action_service "stop" "mobileraker" - cd "${MOBILERAKER_DIR}" - git pull origin main -q && ok_msg "Fetch successfull!" - git checkout -f main && ok_msg "Checkout successfull" - - if [[ $(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then - status_msg "New dependecies detected..." - "${MOBILERAKER_ENV}"/bin/pip install -r "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" - ok_msg "Dependencies have been installed!" - fi - - ok_msg "Update complete!" - do_action_service "start" "mobileraker" -} - -#===================================================# -#=========== MOBILERAKER COMPANION STATUS ==========# -#===================================================# - -function get_mobileraker_status() { - local sf_count status - sf_count="$(mobileraker_systemd | wc -w)" - - ### remove the "SERVICE" entry from the data array if a moonraker service is installed - local data_arr=(SERVICE "${MOBILERAKER_DIR}" "${MOBILERAKER_ENV}") - (( sf_count > 0 )) && unset "data_arr[0]" - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed!" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - echo "${status}" -} - -function get_local_mobileraker_commit() { - [[ ! -d ${MOBILERAKER_DIR} || ! -d "${MOBILERAKER_DIR}/.git" ]] && return - - local commit - cd "${MOBILERAKER_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_mobileraker_commit() { - [[ ! -d ${MOBILERAKER_DIR} || ! -d "${MOBILERAKER_DIR}/.git" ]] && return - - local commit - cd "${MOBILERAKER_DIR}" && git fetch origin -q - commit=$(git describe origin/main --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_mobileraker_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_mobileraker_commit)" - remote_ver="$(get_remote_mobileraker_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "mobileraker" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function patch_mobileraker_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager mobileraker\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Mobileraker's Companion update manager section to moonraker.conf - status_msg "Adding Mobileraker's Companion to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager mobileraker] -type: git_repo -path: ${HOME}/mobileraker_companion -origin: https://github.com/Clon1998/mobileraker_companion.git -primary_branch:main -managed_services: mobileraker -env: ${HOME}/mobileraker-env/bin/python -requirements: scripts/mobileraker-requirements.txt -install_script: scripts/install.sh -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} diff --git a/scripts/moonraker-telegram-bot.sh b/scripts/moonraker-telegram-bot.sh deleted file mode 100644 index 06cf22b..0000000 --- a/scripts/moonraker-telegram-bot.sh +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#========== INSTALL MOONRAKERTELEGRAMBOT ===========# -#===================================================# - -function telegram_bot_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/moonraker-telegram-bot(-[0-9a-zA-Z]+)?.service" | sort) - echo "${services}" -} - -function telegram_bot_setup_dialog() { - ### return early if moonraker is not installed - local moonraker_services - moonraker_services=$(moonraker_systemd) - - if [[ -z ${moonraker_services} ]]; then - local error="Moonraker not installed! Please install Moonraker first!" - print_error "${error}" && return - fi - - status_msg "Initializing Telegram Bot installation ..." - ### first, we create a backup of the full klipper_config dir - safety first! - backup_config_dir - - local moonraker_count user_input=() moonraker_names=() - moonraker_count=$(echo "${moonraker_services}" | wc -w ) - for service in ${moonraker_services}; do - moonraker_names+=( "$(get_instance_name "${service}")" ) - done - - local telegram_bot_count - if (( moonraker_count == 1 )); then - ok_msg "Moonraker installation found!\n" - telegram_bot_count=1 - elif (( moonraker_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${moonraker_count} Moonraker instances found!" - for name in "${moonraker_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● ${name}" - done - blank_line - echo -e "| The setup will apply the same names to Telegram Bot! |" - blank_line - echo -e "| Please select the number of Telegram Bot instances to |" - echo -e "| install. Usually one Telegram Bot instance per |" - echo -e "| Moonraker instance is required, but you may not |" - echo -e "| install more Telegram Bot instances than available |" - echo -e "| Moonraker instances. |" - bottom_border - - ### ask for amount of instances - local re="^[1-9][0-9]*$" - while [[ ! ${telegram_bot_count} =~ ${re} || ${telegram_bot_count} -gt ${moonraker_count} ]]; do - read -p "${cyan}###### Number of Telegram Bot instances to set up:${white} " -i "${moonraker_count}" -e telegram_bot_count - ### break if input is valid - [[ ${telegram_bot_count} =~ ${re} && ${telegram_bot_count} -le ${moonraker_count} ]] && break - ### conditional error messages - [[ ! ${telegram_bot_count} =~ ${re} ]] && error_msg "Input not a number" - (( telegram_bot_count > moonraker_count )) && error_msg "Number of Telegram Bot instances larger than existing Moonraker instances" - done && select_msg "${telegram_bot_count}" - else - log_error "Internal error. moonraker_count of '${moonraker_count}' not equal or grather than one!" - return 1 - fi - - user_input+=("${telegram_bot_count}") - - ### confirm instance amount - local yn - while true; do - (( telegram_bot_count == 1 )) && local question="Install Telegram Bot?" - (( telegram_bot_count > 1 )) && local question="Install ${telegram_bot_count} Telegram Bot instances?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting Telegram Bot setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - - ### write existing klipper names into user_input array to use them as names for moonraker - if (( moonraker_count > 1 )); then - for name in "${moonraker_names[@]}"; do - user_input+=("${name}") - done - fi - - (( telegram_bot_count > 1 )) && status_msg "Installing ${telegram_bot_count} Telegram Bot instances ..." - (( telegram_bot_count == 1 )) && status_msg "Installing Telegram Bot ..." - telegram_bot_setup "${user_input[@]}" -} - -function install_telegram_bot_dependencies() { - local packages log_name="Telegram Bot" - local install_script="${TELEGRAM_BOT_DIR}/scripts/install.sh" - - ### read PKGLIST from official install-script - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages="$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n')" - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -function create_telegram_bot_virtualenv() { - status_msg "Installing python virtual environment..." - ### always create a clean virtualenv - [[ -d ${TELEGRAM_BOT_ENV} ]] && rm -rf "${TELEGRAM_BOT_ENV}" - if virtualenv -p /usr/bin/python3 --system-site-packages "${TELEGRAM_BOT_ENV}"; then - "${TELEGRAM_BOT_ENV}"/bin/pip install -r "${TELEGRAM_BOT_DIR}/scripts/requirements.txt" - else - log_error "failure while creating python3 moonraker-telegram-bot-env" - error_msg "Creation of Moonraker Telegram Bot virtualenv failed!" - exit 1 - fi -} - -function telegram_bot_setup() { - local instance_arr=("${@}") - ### checking dependencies - local dep=(git virtualenv) - dependency_check "${dep[@]}" - - ### step 1: clone telegram bot - clone_telegram_bot "${TELEGRAM_BOT_REPO}" - - ### step 2: install telegram bot dependencies and create python virtualenv - status_msg "Installing dependencies ..." - install_telegram_bot_dependencies - create_telegram_bot_virtualenv - - ### step 3: create telegram.conf - create_telegram_conf "${instance_arr[@]}" - - ### step 4: create telegram bot instances - create_telegram_bot_service "${instance_arr[@]}" - - ### step 5: add telegram-bot to the update manager in moonraker.conf - patch_telegram_bot_update_manager - - ### step 6: enable and start all instances - do_action_service "enable" "moonraker-telegram-bot" - do_action_service "start" "moonraker-telegram-bot" - - ### confirm message - local confirm="" - (( instance_arr[0] == 1 )) && confirm="Telegram Bot has been set up!" - (( instance_arr[0] > 1 )) && confirm="${instance_arr[0]} Telegram Bot instances have been set up!" - print_confirm "${confirm}" && return -} - -function clone_telegram_bot() { - local repo=${1} - - status_msg "Cloning Moonraker-Telegram-Bot from ${repo} ..." - ### force remove existing Moonraker-Telegram-Bot dir - [[ -d ${repo} ]] && rm -rf "${TELEGRAM_BOT_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${repo}" "${TELEGRAM_BOT_DIR}"; then - print_error "Cloning Moonraker-Telegram-Bot from\n ${repo}\n failed!" - exit 1 - fi -} - -function create_telegram_conf() { - local input=("${@}") - local telegram_bot_count=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local printer_data log_dir cfg cfg_dir - - if (( telegram_bot_count == 1 )); then - printer_data="${HOME}/printer_data" - log_dir="${printer_data}/logs" - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/telegram.conf" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write single instance config - write_telegram_conf "${cfg_dir}" "${cfg}" - - elif (( telegram_bot_count > 1 )); then - local j=0 re="^[1-9][0-9]*$" - - for (( i=1; i <= telegram_bot_count; i++ )); do - - printer_data="${HOME}/${names[${j}]}_data" - ### prefix instance name with "printer_" if it is only a number - [[ ${names[j]} =~ ${re} ]] && printer_data="${HOME}/printer_${names[${j}]}_data" - - - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/telegram.conf" - log_dir="${printer_data}/logs" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write multi instance config - write_telegram_conf "${cfg_dir}" "${cfg}" - - j=$(( j + 1 )) - done && unset j - - else - return 1 - fi -} - -function write_telegram_conf() { - local cfg_dir=${1} cfg=${2} - local conf_template="${TELEGRAM_BOT_DIR}/scripts/base_install_template" - - if [[ ! -f ${cfg} ]]; then - status_msg "Creating telegram.conf in ${cfg_dir} ..." - cp "${conf_template}" "${cfg}" - ok_msg "telegram.conf created!" - else - ok_msg "File '${cfg}' already exists! Skipping..." - fi -} - -function create_telegram_bot_service() { - local input=("${@}") - local instances=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local printer_data cfg_dir cfg log service env_file - - if (( instances == 1 )); then - printer_data="${HOME}/printer_data" - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/telegram.conf" - log="${printer_data}/logs/telegram.log" - service="${SYSTEMD}/moonraker-telegram-bot.service" - env_file="${printer_data}/systemd/moonraker-telegram-bot.env" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write single instance service - write_telegram_bot_service "" "${cfg}" "${log}" "${service}" "${env_file}" - ok_msg "Telegram Bot instance created!" - - elif (( instances > 1 )); then - local j=0 re="^[1-9][0-9]*$" - - for (( i=1; i <= instances; i++ )); do - ### overwrite config folder if name is only a number - if [[ ${names[j]} =~ ${re} ]]; then - printer_data="${HOME}/printer_${names[${j}]}_data" - else - printer_data="${HOME}/${names[${j}]}_data" - fi - - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/telegram.conf" - log="${printer_data}/logs/telegram.log" - service="${SYSTEMD}/moonraker-telegram-bot-${names[${j}]}.service" - env_file="${printer_data}/systemd/moonraker-telegram-bot.env" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write multi instance service - if write_telegram_bot_service "${names[${j}]}" "${cfg}" "${log}" "${service}" "${env_file}"; then - ok_msg "Telegram Bot instance moonraker-telegram-bot-${names[${j}]} created!" - else - error_msg "An error occured during creation of instance moonraker-telegram-bot-${names[${j}]}!" - fi - - j=$(( j + 1 )) - done && unset j - - else - return 1 - fi -} - -function write_telegram_bot_service() { - local i=${1} cfg=${2} log=${3} service=${4} env_file=${5} - local service_template="${KIAUH_SRCDIR}/resources/moonraker-telegram-bot.service" - local env_template="${KIAUH_SRCDIR}/resources/moonraker-telegram-bot.env" - - ### replace all placeholders - if [[ ! -f ${service} ]]; then - status_msg "Creating service file for instance ${i} ..." - sudo cp "${service_template}" "${service}" - if [[ -z ${i} ]]; then - sudo sed -i "s| %INST%||" "${service}" - else - sudo sed -i "s|%INST%|${i}|" "${service}" - fi - sudo sed -i "s|%USER%|${USER}|g; s|%TELEGRAM_BOT_DIR%|${TELEGRAM_BOT_DIR}|; s|%ENV%|${TELEGRAM_BOT_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}" - - status_msg "Creating environment file for instance ${i} ..." - cp "${env_template}" "${env_file}" - sed -i "s|%USER%|${USER}|; s|%TELEGRAM_BOT_DIR%|${TELEGRAM_BOT_DIR}|; s|%CFG%|${cfg}|; s|%LOG%|${log}|" "${env_file}" - fi -} - - -#===================================================# -#=========== REMOVE MOONRAKERTELEGRAMBOT ===========# -#===================================================# - -function remove_telegram_bot_systemd() { - [[ -z $(telegram_bot_systemd) ]] && return - - status_msg "Removing Telegram Bot Systemd Services ..." - for service in $(telegram_bot_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "Telegram Bot Services removed!" -} - -function remove_telegram_bot_dir() { - [[ ! -d ${TELEGRAM_BOT_DIR} ]] && return - - status_msg "Removing Moonraker-Telegram-Bot directory ..." - rm -rf "${TELEGRAM_BOT_DIR}" - ok_msg "Directory removed!" -} - -function remove_telegram_bot_env() { - [[ ! -d ${TELEGRAM_BOT_ENV} ]] && return - - status_msg "Removing moonraker-telegram-bot-env directory ..." - rm -rf "${TELEGRAM_BOT_ENV}" - ok_msg "Directory removed!" -} - -function remove_telegram_bot_env_file() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/systemd\/moonraker-telegram-bot\.env" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_telegram_bot_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/telegram\.log.*" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_legacy_telegram_bot_logs() { - local files regex="telegram(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_telegram_bot() { - remove_telegram_bot_systemd - remove_telegram_bot_dir - remove_telegram_bot_env - remove_telegram_bot_env_file - remove_telegram_bot_logs - remove_legacy_telegram_bot_logs - - local confirm="Moonraker-Telegram-Bot was successfully removed!" - print_confirm "${confirm}" && return -} - -#===================================================# -#=========== UPDATE MOONRAKERTELEGRAMBOT ===========# -#===================================================# - -function update_telegram_bot() { - do_action_service "stop" "moonraker-telegram-bot" - - if [[ ! -d ${TELEGRAM_BOT_DIR} ]]; then - clone_telegram_bot "${TELEGRAM_BOT_REPO}" - else - backup_before_update "moonraker-telegram-bot" - status_msg "Updating Moonraker ..." - cd "${TELEGRAM_BOT_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_telegram_bot_dependencies - ### install possible new python dependencies - "${TELEGRAM_BOT_ENV}"/bin/pip install -r "${TELEGRAM_BOT_DIR}/scripts/requirements.txt" - fi - - ok_msg "Update complete!" - do_action_service "start" "moonraker-telegram-bot" -} - -#===================================================# -#=========== MOONRAKERTELEGRAMBOT STATUS ===========# -#===================================================# - -function get_telegram_bot_status() { - local sf_count status - sf_count="$(telegram_bot_systemd | wc -w)" - - ### remove the "SERVICE" entry from the data array if a moonraker service is installed - local data_arr=(SERVICE "${TELEGRAM_BOT_DIR}" "${TELEGRAM_BOT_ENV}") - (( sf_count > 0 )) && unset "data_arr[0]" - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed: ${sf_count}" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - echo "${status}" -} - -function get_local_telegram_bot_commit() { - [[ ! -d ${TELEGRAM_BOT_DIR} || ! -d "${TELEGRAM_BOT_DIR}/.git" ]] && return - - local commit - cd "${TELEGRAM_BOT_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_telegram_bot_commit() { - [[ ! -d ${TELEGRAM_BOT_DIR} || ! -d "${TELEGRAM_BOT_DIR}/.git" ]] && return - - local commit - cd "${TELEGRAM_BOT_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_telegram_bot_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_telegram_bot_commit)" - remote_ver="$(get_remote_telegram_bot_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "telegram_bot" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function patch_telegram_bot_update_manager() { - local patched moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - patched="false" - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager moonraker-telegram-bot\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - - ### add Moonraker-Telegram-Bot update manager section to moonraker.conf - status_msg "Adding Moonraker-Telegram-Bot to update manager in file:\n ${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF - -[update_manager moonraker-telegram-bot] -type: git_repo -path: ~/moonraker-telegram-bot -origin: https://github.com/nlef/moonraker-telegram-bot.git -env: ~/moonraker-telegram-bot-env/bin/python -requirements: scripts/requirements.txt -install_script: scripts/install.sh -MOONRAKER_CONF - - fi - - patched="true" - done - - if [[ ${patched} == "true" ]]; then - do_action_service "restart" "moonraker" - fi -} diff --git a/scripts/moonraker.sh b/scripts/moonraker.sh deleted file mode 100644 index 9752697..0000000 --- a/scripts/moonraker.sh +++ /dev/null @@ -1,810 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#================ INSTALL MOONRAKER ================# -#===================================================# - -### -# this function detects all installed moonraker -# systemd instances and returns their absolute path -function moonraker_systemd() { - local services - local blacklist - local ignore - local match - - ### - # any moonraker client that uses "moonraker" in its own name must be blacklisted using - # this variable, otherwise they will be falsely recognized as moonraker instances - blacklist="obico" - - ignore="${SYSTEMD}/moonraker-(${blacklist}).service" - match="${SYSTEMD}/moonraker(-[0-9a-zA-Z]+)?.service" - - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype awk ! -regex "${ignore}" -regex "${match}" | sort) - echo "${services}" -} - -function moonraker_setup_dialog() { - status_msg "Initializing Moonraker installation ..." - - ### return early if python version check fails - if [[ $(python3_check) == "false" ]]; then - local error="Versioncheck failed! Python 3.7 or newer required!\n" - error="${error} Please upgrade Python." - print_error "${error}" && return - fi - - ### return early if moonraker already exists - local moonraker_services - moonraker_services=$(moonraker_systemd) - if [[ -n ${moonraker_services} ]]; then - local error="At least one Moonraker service is already installed:" - for s in ${moonraker_services}; do - log_info "Found Moonraker service: ${s}" - error="${error}\n βž” ${s}" - done - print_error "${error}" && return - fi - - ### return early if klipper is not installed - local klipper_services - klipper_services=$(klipper_systemd) - if [[ -z ${klipper_services} ]]; then - local error="Klipper not installed! Please install Klipper first!" - log_error "Moonraker setup started without Klipper being installed. Aborting setup." - print_error "${error}" && return - fi - - local klipper_count user_input=() klipper_names=() - klipper_count=$(echo "${klipper_services}" | wc -w ) - for service in ${klipper_services}; do - klipper_names+=( "$(get_instance_name "${service}")" ) - done - - local moonraker_count - if (( klipper_count == 1 )); then - ok_msg "Klipper installation found!\n" - moonraker_count=1 - elif (( klipper_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${klipper_count} Klipper instances found!" - for name in "${klipper_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● klipper-${name}" - done - blank_line - echo -e "| The setup will apply the same names to Moonraker! |" - blank_line - echo -e "| Please select the number of Moonraker instances to |" - echo -e "| install. Usually one Moonraker instance per Klipper |" - echo -e "| instance is required, but you may not install more |" - echo -e "| Moonraker instances than available Klipper instances. |" - bottom_border - - ### ask for amount of instances - local re="^[1-9][0-9]*$" - while [[ ! ${moonraker_count} =~ ${re} || ${moonraker_count} -gt ${klipper_count} ]]; do - read -p "${cyan}###### Number of Moonraker instances to set up:${white} " -i "${klipper_count}" -e moonraker_count - ### break if input is valid - [[ ${moonraker_count} =~ ${re} && ${moonraker_count} -le ${klipper_count} ]] && break - ### conditional error messages - [[ ! ${moonraker_count} =~ ${re} ]] && error_msg "Input not a number" - (( moonraker_count > klipper_count )) && error_msg "Number of Moonraker instances larger than installed Klipper instances" - done && select_msg "${moonraker_count}" - else - log_error "Internal error. klipper_count of '${klipper_count}' not equal or grather than one!" - return 1 - fi - - user_input+=("${moonraker_count}") - - ### confirm instance amount - local yn - while true; do - (( moonraker_count == 1 )) && local question="Install Moonraker?" - (( moonraker_count > 1 )) && local question="Install ${moonraker_count} Moonraker instances?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting Moonraker setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - - ### write existing klipper names into user_input array to use them as names for moonraker - if (( klipper_count > 1 )); then - for name in "${klipper_names[@]}"; do - user_input+=("${name}") - done - fi - - (( moonraker_count > 1 )) && status_msg "Installing ${moonraker_count} Moonraker instances ..." - (( moonraker_count == 1 )) && status_msg "Installing Moonraker ..." - moonraker_setup "${user_input[@]}" -} - -function install_moonraker_dependencies() { - local packages log_name="Moonraker" - local package_json="${MOONRAKER_DIR}/scripts/system-dependencies.json" - - ### read PKGLIST from official install-script - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages=$(python3 - << EOF -from __future__ import annotations -import shlex -import re -import pathlib -import logging -import json - -from typing import Tuple, Dict, List, Any - -def _get_distro_info() -> Dict[str, Any]: - release_file = pathlib.Path("/etc/os-release") - release_info: Dict[str, str] = {} - with release_file.open("r") as f: - lexer = shlex.shlex(f, posix=True) - lexer.whitespace_split = True - for item in list(lexer): - if "=" in item: - key, val = item.split("=", maxsplit=1) - release_info[key] = val - return dict( - distro_id=release_info.get("ID", ""), - distro_version=release_info.get("VERSION_ID", ""), - aliases=release_info.get("ID_LIKE", "").split() - ) - -def _convert_version(version: str) -> Tuple[str | int, ...]: - version = version.strip() - ver_match = re.match(r"\d+(\.\d+)*((?:-|\.).+)?", version) - if ver_match is not None: - return tuple([ - int(part) if part.isdigit() else part - for part in re.split(r"\.|-", version) - ]) - return (version,) - -class SysDepsParser: - def __init__(self, distro_info: Dict[str, Any] | None = None) -> None: - if distro_info is None: - distro_info = _get_distro_info() - self.distro_id: str = distro_info.get("distro_id", "") - self.aliases: List[str] = distro_info.get("aliases", []) - self.distro_version: Tuple[int | str, ...] = tuple() - version = distro_info.get("distro_version") - if version: - self.distro_version = _convert_version(version) - - def _parse_spec(self, full_spec: str) -> str | None: - parts = full_spec.split(";", maxsplit=1) - if len(parts) == 1: - return full_spec - pkg_name = parts[0].strip() - expressions = re.split(r"( and | or )", parts[1].strip()) - if not len(expressions) & 1: - logging.info( - f"Requirement specifier is missing an expression " - f"between logical operators : {full_spec}" - ) - return None - last_result: bool = True - last_logical_op: str | None = "and" - for idx, exp in enumerate(expressions): - if idx & 1: - if last_logical_op is not None: - logging.info( - "Requirement specifier contains sequential logical " - f"operators: {full_spec}" - ) - return None - logical_op = exp.strip() - if logical_op not in ("and", "or"): - logging.info( - f"Invalid logical operator {logical_op} in requirement " - f"specifier: {full_spec}") - return None - last_logical_op = logical_op - continue - elif last_logical_op is None: - logging.info( - f"Requirement specifier contains two seqential expressions " - f"without a logical operator: {full_spec}") - return None - dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip()) - req_var = dep_parts[0].strip().lower() - if len(dep_parts) != 3: - logging.info(f"Invalid comparison, must be 3 parts: {full_spec}") - return None - elif req_var == "distro_id": - left_op: str | Tuple[int | str, ...] = self.distro_id - right_op = dep_parts[2].strip().strip("\"'") - elif req_var == "distro_version": - if not self.distro_version: - logging.info( - "Distro Version not detected, cannot satisfy requirement: " - f"{full_spec}" - ) - return None - left_op = self.distro_version - right_op = _convert_version(dep_parts[2].strip().strip("\"'")) - else: - logging.info(f"Invalid requirement specifier: {full_spec}") - return None - operator = dep_parts[1].strip() - try: - compfunc = { - "<": lambda x, y: x < y, - ">": lambda x, y: x > y, - "==": lambda x, y: x == y, - "!=": lambda x, y: x != y, - ">=": lambda x, y: x >= y, - "<=": lambda x, y: x <= y - }.get(operator, lambda x, y: False) - result = compfunc(left_op, right_op) - if last_logical_op == "and": - last_result &= result - else: - last_result |= result - last_logical_op = None - except Exception: - logging.exception(f"Error comparing requirements: {full_spec}") - return None - if last_result: - return pkg_name - return None - - def parse_dependencies(self, sys_deps: Dict[str, List[str]]) -> List[str]: - if not self.distro_id: - logging.info( - "Failed to detect current distro ID, cannot parse dependencies" - ) - return [] - all_ids = [self.distro_id] + self.aliases - for distro_id in all_ids: - if distro_id in sys_deps: - if not sys_deps[distro_id]: - logging.info( - f"Dependency data contains an empty package definition " - f"for linux distro '{distro_id}'" - ) - continue - processed_deps: List[str] = [] - for dep in sys_deps[distro_id]: - parsed_dep = self._parse_spec(dep) - if parsed_dep is not None: - processed_deps.append(parsed_dep) - return processed_deps - else: - logging.info( - f"Dependency data has no package definition for linux " - f"distro '{self.distro_id}'" - ) - return [] -# *** SYSTEM DEPENDENCIES START *** -system_deps = { - "debian": [ - "python3-virtualenv", "python3-dev", "libopenjp2-7", "libsodium-dev", - "zlib1g-dev", "libjpeg-dev", "packagekit", - "wireless-tools; distro_id != 'ubuntu' or distro_version <= '24.04'", - "iw; distro_id == 'ubuntu' and distro_version >= '24.10'", "curl", - "build-essential" - ], -} -system_deps_json = pathlib.Path("$package_json") -system_deps = json.loads(system_deps_json.read_bytes()) -parser = SysDepsParser() -pkgs = parser.parse_dependencies(system_deps) -if pkgs: - print(' '.join(pkgs), end="") -exit(0) -EOF -) - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -function create_moonraker_virtualenv() { - status_msg "Installing python virtual environment..." - - ### always create a clean virtualenv - [[ -d ${MOONRAKER_ENV} ]] && rm -rf "${MOONRAKER_ENV}" - - if virtualenv -p /usr/bin/python3 "${MOONRAKER_ENV}"; then - "${MOONRAKER_ENV}"/bin/pip install -U pip - "${MOONRAKER_ENV}"/bin/pip install -r "${MOONRAKER_DIR}/scripts/moonraker-requirements.txt" - else - log_error "failure while creating python3 moonraker-env" - error_msg "Creation of Moonraker virtualenv failed!" - exit 1 - fi -} - -function moonraker_setup() { - local instance_arr=("${@}") - ### checking dependencies - local dep=(git wget curl unzip dfu-util virtualenv) - ### additional required dependencies on armbian - dep+=(libjpeg-dev zlib1g-dev) - dependency_check "${dep[@]}" - - ### step 1: clone moonraker - clone_moonraker "${MOONRAKER_REPO}" - - ### step 2: install moonraker dependencies and create python virtualenv - status_msg "Installing dependencies ..." - install_moonraker_dependencies - create_moonraker_virtualenv - - ### step 3: create moonraker.conf - create_moonraker_conf "${instance_arr[@]}" - - ### step 4: create moonraker instances - configure_moonraker_service "${instance_arr[@]}" - - ### step 5: create polkit rules for moonraker - install_moonraker_polkit || true - - ### step 6: enable and start all instances - do_action_service "enable" "moonraker" - do_action_service "start" "moonraker" - - ### confirm message - local confirm="" - (( instance_arr[0] == 1 )) && confirm="Moonraker has been set up!" - (( instance_arr[0] > 1 )) && confirm="${instance_arr[0]} Moonraker instances have been set up!" - print_confirm "${confirm}" && print_mr_ip_list "${instance_arr[0]}" && return -} - -function clone_moonraker() { - local repo=${1} - - status_msg "Cloning Moonraker from ${repo} ..." - - ### force remove existing moonraker dir and clone into fresh moonraker dir - [[ -d ${MOONRAKER_DIR} ]] && rm -rf "${MOONRAKER_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${MOONRAKER_REPO}" "${MOONRAKER_DIR}"; then - print_error "Cloning Moonraker from\n ${repo}\n failed!" - exit 1 - fi -} - -function create_moonraker_conf() { - local input=("${@}") - local moonraker_count=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local port lan printer_data cfg_dir cfg uds - - port=7125 - lan="$(hostname -I | cut -d" " -f1 | cut -d"." -f1-2).0.0/16" - - if (( moonraker_count == 1 )); then - printer_data="${HOME}/printer_data" - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/moonraker.conf" - uds="${printer_data}/comms/klippy.sock" - - ### write single instance config - write_moonraker_conf "${cfg_dir}" "${cfg}" "${port}" "${uds}" "${lan}" - - elif (( moonraker_count > 1 )); then - local j=0 re="^[1-9][0-9]*$" - - for (( i=1; i <= moonraker_count; i++ )); do - ### overwrite config folder if name is only a number - if [[ ${names[j]} =~ ${re} ]]; then - printer_data="${HOME}/printer_${names[${j}]}_data" - else - printer_data="${HOME}/${names[${j}]}_data" - fi - - cfg_dir="${printer_data}/config" - cfg="${cfg_dir}/moonraker.conf" - uds="${printer_data}/comms/klippy.sock" - - ### write multi instance config - write_moonraker_conf "${cfg_dir}" "${cfg}" "${port}" "${uds}" "${lan}" - port=$(( port + 1 )) - j=$(( j + 1 )) - done && unset j - - else - return 1 - fi -} - -function write_moonraker_conf() { - local cfg_dir=${1} cfg=${2} port=${3} uds=${4} lan=${5} - local conf_template="${KIAUH_SRCDIR}/resources/moonraker.conf" - - [[ ! -d ${cfg_dir} ]] && mkdir -p "${cfg_dir}" - - if [[ ! -f ${cfg} ]]; then - status_msg "Creating moonraker.conf in ${cfg_dir} ..." - cp "${conf_template}" "${cfg}" - sed -i "s|%USER%|${USER}|g; s|%PORT%|${port}|; s|%UDS%|${uds}|" "${cfg}" - # if host ip is not in the default ip ranges replace placeholder, - # otherwise remove placeholder from config - if ! grep -q "${lan}" "${cfg}"; then - sed -i "s|%LAN%|${lan}|" "${cfg}" - else - sed -i "/%LAN%/d" "${cfg}" - fi - ok_msg "moonraker.conf created!" - else - status_msg "File '${cfg_dir}/moonraker.conf' already exists!\nSkipping..." - fi -} - -function configure_moonraker_service() { - local input=("${@}") - local moonraker_count=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local printer_data cfg_dir service env_file - - if (( moonraker_count == 1 )) && [[ ${#names[@]} -eq 0 ]]; then - i="" - printer_data="${HOME}/printer_data" - cfg_dir="${printer_data}/config" - service="${SYSTEMD}/moonraker.service" - env_file="${printer_data}/systemd/moonraker.env" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write single instance service - write_moonraker_service "" "${printer_data}" "${service}" "${env_file}" - ok_msg "Moonraker instance created!" - - elif (( moonraker_count > 1 )) && [[ ${#names[@]} -gt 0 ]]; then - local j=0 re="^[1-9][0-9]*$" - - for (( i=1; i <= moonraker_count; i++ )); do - ### overwrite config folder if name is only a number - if [[ ${names[j]} =~ ${re} ]]; then - printer_data="${HOME}/printer_${names[${j}]}_data" - else - printer_data="${HOME}/${names[${j}]}_data" - fi - - cfg_dir="${printer_data}/config" - service="${SYSTEMD}/moonraker-${names[${j}]}.service" - env_file="${printer_data}/systemd/moonraker.env" - - ### create required folder structure - create_required_folders "${printer_data}" - - ### write multi instance service - write_moonraker_service "${names[${j}]}" "${printer_data}" "${service}" "${env_file}" - ok_msg "Moonraker instance 'moonraker-${names[${j}]}' created!" - j=$(( j + 1 )) - done && unset i - - ### enable mainsails remoteMode if mainsail is found - if [[ -d ${MAINSAIL_DIR} ]]; then - enable_mainsail_remotemode - fi - - else - return 1 - fi -} - -function write_moonraker_service() { - local i=${1} printer_data=${2} service=${3} env_file=${4} - local service_template="${KIAUH_SRCDIR}/resources/moonraker.service" - local env_template="${KIAUH_SRCDIR}/resources/moonraker.env" - - ### replace all placeholders - if [[ ! -f ${service} ]]; then - status_msg "Creating Moonraker Service ${i} ..." - sudo cp "${service_template}" "${service}" - sudo cp "${env_template}" "${env_file}" - - [[ -z ${i} ]] && sudo sed -i "s| %INST%||" "${service}" - [[ -n ${i} ]] && sudo sed -i "s|%INST%|${i}|" "${service}" - sudo sed -i "s|%USER%|${USER}|g; s|%MOONRAKER_DIR%|${MOONRAKER_DIR}|; s|%ENV%|${MOONRAKER_ENV}|; s|%ENV_FILE%|${env_file}|" "${service}" - sudo sed -i "s|%USER%|${USER}|; s|%MOONRAKER_DIR%|${MOONRAKER_DIR}|; s|%PRINTER_DATA%|${printer_data}|" "${env_file}" - fi -} - -function print_mr_ip_list() { - local ip count=${1} port=7125 - ip=$(hostname -I | cut -d" " -f1) - - for (( i=1; i <= count; i++ )); do - echo -e " ${cyan}● Instance ${i}:${white} ${ip}:${port}" - port=$(( port + 1 )) - done && echo -} - -### introduced due to -### https://github.com/Arksine/moonraker/issues/349 -### https://github.com/Arksine/moonraker/pull/346 -function install_moonraker_polkit() { - local POLKIT_LEGACY_FILE="/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla" - local POLKIT_FILE="/etc/polkit-1/rules.d/moonraker.rules" - local POLKIT_USR_FILE="/usr/share/polkit-1/rules.d/moonraker.rules" - local legacy_file_exists - local file_exists - local usr_file_exists - - local has_sup - local require_daemon_reload="false" - - legacy_file_exists=$(sudo find "${POLKIT_LEGACY_FILE}" 2> /dev/null) - file_exists=$(sudo find "${POLKIT_FILE}" 2> /dev/null) - usr_file_exists=$(sudo find "${POLKIT_USR_FILE}" 2> /dev/null) - - ### check for required SupplementaryGroups entry in service files - ### write it to the service if it doesn't exist - for service in $(moonraker_systemd); do - has_sup="$(grep "SupplementaryGroups=moonraker-admin" "${service}")" - if [[ -z ${has_sup} ]]; then - status_msg "Adding moonraker-admin supplementary group to ${service} ..." - sudo sed -i "/^Type=simple$/a SupplementaryGroups=moonraker-admin" "${service}" - require_daemon_reload="true" - ok_msg "Adding moonraker-admin supplementary group successfull!" - fi - done - - if [[ ${require_daemon_reload} == "true" ]]; then - status_msg "Reloading unit files ..." - sudo systemctl daemon-reload - ok_msg "Unit files reloaded!" - fi - - ### execute moonrakers policykit-rules script only if rule files do not already exist - if [[ -z ${legacy_file_exists} && ( -z ${file_exists} || -z ${usr_file_exists} ) ]]; then - status_msg "Installing Moonraker policykit rules ..." - "${HOME}"/moonraker/scripts/set-policykit-rules.sh - ok_msg "Moonraker policykit rules installed!" - fi - - return -} - -#==================================================# -#================ REMOVE MOONRAKER ================# -#==================================================# - -function remove_moonraker_sysvinit() { - [[ ! -e "${INITD}/moonraker" ]] && return - - status_msg "Removing Moonraker SysVinit service ..." - sudo systemctl stop moonraker - sudo update-rc.d -f moonraker remove - sudo rm -f "${INITD}/moonraker" "${ETCDEF}/moonraker" - ok_msg "Moonraker SysVinit service removed!" -} - -function remove_moonraker_systemd() { - [[ -z $(moonraker_systemd) ]] && return - - status_msg "Removing Moonraker Systemd Services ..." - - for service in $(moonraker_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "Moonraker Services removed!" -} - -function remove_moonraker_env_file() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/systemd\/moonraker\.env" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_moonraker_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/moonraker\.log.*" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_legacy_moonraker_logs() { - local files regex="moonraker(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_moonraker_api_key() { - ### remove legacy api key - if [[ -e "${HOME}/.klippy_api_key" ]]; then - status_msg "Removing legacy API Key ..." - rm "${HOME}/.klippy_api_key" - ok_msg "Done!" - fi - - ### remove api key - if [[ -e "${HOME}/.moonraker_api_key" ]]; then - status_msg "Removing API Key ..." - rm "${HOME}/.moonraker_api_key" - ok_msg "Done!" - fi -} - -function remove_moonraker_dir() { - [[ ! -d ${MOONRAKER_DIR} ]] && return - - status_msg "Removing Moonraker directory ..." - rm -rf "${MOONRAKER_DIR}" - ok_msg "Directory removed!" -} - -function remove_moonraker_env() { - [[ ! -d ${MOONRAKER_ENV} ]] && return - - status_msg "Removing moonraker-env directory ..." - rm -rf "${MOONRAKER_ENV}" - ok_msg "Directory removed!" -} - -function remove_moonraker_polkit() { - [[ ! -d ${MOONRAKER_DIR} ]] && return - - status_msg "Removing all Moonraker PolicyKit rules ..." - "${MOONRAKER_DIR}"/scripts/set-policykit-rules.sh --clear - ok_msg "Done!" -} - -function remove_moonraker() { - remove_moonraker_sysvinit - remove_moonraker_systemd - remove_moonraker_env_file - remove_moonraker_logs - remove_legacy_moonraker_logs - remove_moonraker_api_key - remove_moonraker_polkit - remove_moonraker_dir - remove_moonraker_env - - print_confirm "Moonraker was successfully removed!" - return -} - -#==================================================# -#================ UPDATE MOONRAKER ================# -#==================================================# - -function update_moonraker() { - do_action_service "stop" "moonraker" - - if [[ ! -d ${MOONRAKER_DIR} ]]; then - clone_moonraker "${MOONRAKER_REPO}" - else - backup_before_update "moonraker" - status_msg "Updating Moonraker ..." - cd "${MOONRAKER_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_moonraker_dependencies - ### install possible new python dependencies - "${MOONRAKER_ENV}"/bin/pip install -r "${MOONRAKER_DIR}/scripts/moonraker-requirements.txt" - fi - - ### required due to https://github.com/Arksine/moonraker/issues/349 - install_moonraker_polkit || true - - ok_msg "Update complete!" - do_action_service "restart" "moonraker" -} - -#==================================================# -#================ MOONRAKER STATUS ================# -#==================================================# - -function get_moonraker_status() { - local sf_count status - sf_count="$(moonraker_systemd | wc -w)" - - ### remove the "SERVICE" entry from the data array if a moonraker service is installed - local data_arr=(SERVICE "${MOONRAKER_DIR}" "${MOONRAKER_ENV}") - (( sf_count > 0 )) && unset "data_arr[0]" - - ### count+1 for each found data-item from array - local filecount=0 - for data in "${data_arr[@]}"; do - [[ -e ${data} ]] && filecount=$(( filecount + 1 )) - done - - if (( filecount == ${#data_arr[*]} )); then - status="Installed: ${sf_count}" - elif (( filecount == 0 )); then - status="Not installed!" - else - status="Incomplete!" - fi - - echo "${status}" -} - -function get_local_moonraker_commit() { - [[ ! -d ${MOONRAKER_DIR} || ! -d "${MOONRAKER_DIR}/.git" ]] && return - - local commit - cd "${MOONRAKER_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_moonraker_commit() { - [[ ! -d ${MOONRAKER_DIR} || ! -d "${MOONRAKER_DIR}/.git" ]] && return - - local commit - cd "${MOONRAKER_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_moonraker_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_moonraker_commit)" - remote_ver="$(get_remote_moonraker_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "moonraker" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} diff --git a/scripts/nginx.sh b/scripts/nginx.sh deleted file mode 100644 index 9b80d68..0000000 --- a/scripts/nginx.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#=================== REMOVE NGINX ==================# -#===================================================# - -function remove_nginx() { - if [[ $(dpkg -s nginx 2>/dev/null | grep "Status") = *\ installed ]]; then - status_msg "Stopping NGINX service ..." - if systemctl is-active nginx -q; then - sudo systemctl stop nginx && ok_msg "Service stopped!" - else - warn_msg "NGINX service not active!" - fi - - status_msg "Removing NGINX from system ..." - if sudo apt-get remove nginx -y && sudo update-rc.d -f nginx remove; then - ok_msg "NGINX removed!" - else - error_msg "Removing NGINX from system failed!" - fi - else - print_error "Looks like Nginx was already removed!\n Skipping..." - fi -} - -#===================================================# -#===================== HELPERS =====================# -#===================================================# - -function set_upstream_nginx_cfg() { - local current_date - local upstreams="${NGINX_CONFD}/upstreams.conf" - local common_vars="${NGINX_CONFD}/common_vars.conf" - - current_date=$(get_date) - - ### backup existing nginx configs - [[ ! -d "${BACKUP_DIR}/nginx_cfg" ]] && mkdir -p "${BACKUP_DIR}/nginx_cfg" - - if [[ -f ${upstreams} ]]; then - sudo mv "${upstreams}" "${BACKUP_DIR}/nginx_cfg/${current_date}_upstreams.conf" - fi - - if [[ -f ${common_vars} ]]; then - sudo mv "${common_vars}" "${BACKUP_DIR}/nginx_cfg/${current_date}_common_vars.conf" - fi - - ### transfer ownership of backed up files from root to ${USER} - local files - files=$(find "${BACKUP_DIR}/nginx_cfg") - - for file in ${files}; do - if [[ $(stat -c "%U" "${file}") != "${USER}" ]]; then - log_info "chown for user: ${USER} on file: ${file}" - sudo chown "${USER}" "${file}" - fi - done - - ### copy nginx configs to target destination - [[ ! -f ${upstreams} ]] && sudo cp "${RESOURCES}/upstreams.conf" "${upstreams}" - [[ ! -f ${common_vars} ]] && sudo cp "${RESOURCES}/common_vars.conf" "${common_vars}" -} - -function symlink_webui_nginx_log() { - local interface path access_log error_log regex logpaths - - interface=${1} - access_log="/var/log/nginx/${interface}-access.log" - error_log="/var/log/nginx/${interface}-error.log" - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs" - logpaths=$(find "${HOME}" -maxdepth 2 -type d -regextype posix-extended -regex "${regex}" | sort) - - for path in ${logpaths}; do - [[ ! -d ${path} ]] && mkdir -p "${path}" - - if [[ -f ${access_log} && ! -L "${path}/${interface}-access.log" ]]; then - status_msg "Creating symlink for ${access_log} ..." - ln -s "${access_log}" "${path}" - ok_msg "Symlink created: ${path}/${interface}-access.log" - fi - - if [[ -f ${error_log} && ! -L "${path}/${interface}-error.log" ]]; then - status_msg "Creating symlink for ${error_log} ..." - ln -s "${error_log}" "${path}" - ok_msg "Symlink created: ${path}/${interface}-error.log" - fi - done -} - -function match_nginx_configs() { - read_kiauh_ini "${FUNCNAME[0]}" - local require_service_restart="false" - local upstreams="${NGINX_CONFD}/upstreams.conf" - local common_vars="${NGINX_CONFD}/common_vars.conf" - local mainsail_nginx_cfg="/etc/nginx/sites-available/mainsail" - local fluidd_nginx_cfg="/etc/nginx/sites-available/fluidd" - local upstreams_webcams - local mainsail_webcams - local fluidd_webcams - - ### reinstall nginx configs if the amount of upstreams don't match anymore - upstreams_webcams=$(grep -Ec "mjpgstreamer" "/etc/nginx/conf.d/upstreams.conf") - mainsail_webcams=$(grep -Ec "mjpgstreamer" "${mainsail_nginx_cfg}" 2>/dev/null || echo "0") - fluidd_webcams=$(grep -Ec "mjpgstreamer" "${fluidd_nginx_cfg}" 2>/dev/null || echo "0") - - status_msg "Checking validity of NGINX configurations ..." - - ### check for outdated upstreams.conf - if (( upstreams_webcams < mainsail_webcams || upstreams_webcams < fluidd_webcams )); then - status_msg "Outdated upstreams.conf found! Updating ..." - - sudo rm -f "${upstreams}" "${common_vars}" - set_upstream_nginx_cfg - - require_service_restart="true" - fi - - ### check for outdated mainsail config - if [[ -e ${mainsail_nginx_cfg} ]] && (( upstreams_webcams > mainsail_webcams )); then - status_msg "Outdated Mainsail config found! Updating ..." - - sudo rm -f "${mainsail_nginx_cfg}" - sudo cp "${RESOURCES}/mainsail" "${mainsail_nginx_cfg}" - sudo sed -i "s/<>/mainsail/g" "${mainsail_nginx_cfg}" - sudo sed -i "/root/s/pi/${USER}/" "${mainsail_nginx_cfg}" - sudo sed -i "s/listen\s[0-9]*;/listen ${mainsail_port};/" "${mainsail_nginx_cfg}" - sudo sed -i "s/listen\s\[\:*\]\:[0-9]*;/listen \[::\]\:${mainsail_port};/" "${mainsail_nginx_cfg}" - - require_service_restart="true" - fi - - ### check for outdated fluidd config - if [[ -e ${fluidd_nginx_cfg} ]] && (( upstreams_webcams > fluidd_webcams )); then - status_msg "Outdated Fluidd config found! Updating ..." - - sudo rm -f "${fluidd_nginx_cfg}" - sudo cp "${RESOURCES}/fluidd" "${fluidd_nginx_cfg}" - sudo sed -i "s/<>/fluidd/g" "${fluidd_nginx_cfg}" - sudo sed -i "/root/s/pi/${USER}/" "${fluidd_nginx_cfg}" - sudo sed -i "s/listen\s[0-9]*;/listen ${fluidd_port};/" "${fluidd_nginx_cfg}" - sudo sed -i "s/listen\s\[\:*\]\:[0-9]*;/listen \[::\]\:${fluidd_port};/" "${fluidd_nginx_cfg}" - - require_service_restart="true" - fi - - ### only restart nginx if configs were updated - if [[ ${require_service_restart} == "true" ]]; then - sudo systemctl restart nginx.service - fi - - ok_msg "Done!" -} - -function remove_conflicting_packages() { - local apache=${1} haproxy=${2} - - ### disable services before removing them - disable_conflicting_packages "${apache}" "${haproxy}" - - if [[ ${apache} == "true" ]]; then - status_msg "Removing Apache2 from system ..." - if sudo apt-get remove apache2 -y && sudo update-rc.d -f apache2 remove; then - ok_msg "Apache2 removed!" - else - error_msg "Removing Apache2 from system failed!" - fi - fi - - if [[ ${haproxy} == "true" ]]; then - status_msg "Removing haproxy from system ..." - if sudo apt-get remove haproxy -y && sudo update-rc.d -f haproxy remove; then - ok_msg "Haproxy removed!" - else - error_msg "Removing Haproxy from system failed!" - fi - fi -} - -function disable_conflicting_packages() { - local apache=${1} haproxy=${2} - - if [[ ${apache} == "true" ]]; then - status_msg "Stopping Apache2 service ..." - if systemctl is-active apache2 -q; then - sudo systemctl stop apache2 && ok_msg "Service stopped!" - else - warn_msg "Apache2 service not active!" - fi - - status_msg "Disabling Apache2 service ..." - if sudo systemctl disable apache2; then - ok_msg "Apache2 service disabled!" - else - error_msg "Disabling Apache2 service failed!" - fi - fi - - if [[ ${haproxy} == "true" ]]; then - status_msg "Stopping Haproxy service ..." - if systemctl is-active haproxy -q; then - sudo systemctl stop haproxy && ok_msg "Service stopped!" - else - warn_msg "Haproxy service not active!" - fi - - status_msg "Disabling Haproxy service ..." - if sudo systemctl disable haproxy; then - ok_msg "Haproxy service disabled!" - else - error_msg "Disabling Haproxy service failed!" - fi - fi -} - -function detect_conflicting_packages() { - local apache="false" haproxy="false" - - ### check system for an installed apache2 service - [[ $(dpkg -s apache2 2>/dev/null | grep "Status") = *\ installed ]] && apache="true" - ### check system for an installed haproxy service - [[ $(dpkg -s haproxy 2>/dev/null | grep "Status") = *\ installed ]] && haproxy="true" - - #notify user about haproxy or apache2 services found and possible issues - if [[ ${haproxy} == "false" && ${apache} == "false" ]]; then - return - else - while true; do - echo - top_border - echo -e "| ${red}Conflicting package installations found:${white} |" - [[ ${apache} == "true" ]] && \ - echo -e "| ${red}● apache2${white} |" - [[ ${haproxy} == "true" ]] && \ - echo -e "| ${red}● haproxy${white} |" - blank_line - echo -e "| Having those packages installed can lead to unwanted |" - echo -e "| behaviour. It's recommended to remove those packages. |" - echo -e "| |" - echo -e "| ${green}1) Remove packages (recommend)${white} |" - echo -e "| 2) Disable only (may still cause issues) |" - echo -e "| ${red}3) Skip this step (not recommended)${white} |" - bottom_border - - local action - read -p "${cyan}###### Please choose:${white} " action - case "${action}" in - 1) - echo -e "###### > Remove packages" - remove_conflicting_packages "${apache}" "${haproxy}" - break;; - 2) - echo -e "###### > Disable only" - disable_conflicting_packages "${apache}" "${haproxy}" - break;; - 3) - echo -e "###### > Skip" - break;; - *) - error_msg "Invalid command!";; - esac - done - fi -} - -function set_nginx_cfg() { - local interface=${1} - - if [[ ${SET_NGINX_CFG} == "true" ]]; then - #check for dependencies - local dep=(nginx) - dependency_check "${dep[@]}" - - local cfg_src="${RESOURCES}/${interface}" - local cfg_dest="/etc/nginx/sites-available/${interface}" - - status_msg "Creating NGINX configuration for ${interface^} ..." - - # copy config to destination and set correct username - [[ -f ${cfg_dest} ]] && sudo rm -f "${cfg_dest}" - sudo cp "${cfg_src}" "${cfg_dest}" - sudo sed -i "/root/s/pi/${USER}/" "${cfg_dest}" - - if [[ ${SET_LISTEN_PORT} != "${DEFAULT_PORT}" ]]; then - sudo sed -i "s/listen\s[0-9]*;/listen ${SET_LISTEN_PORT};/" "${cfg_dest}" - sudo sed -i "s/listen\s\[\:*\]\:[0-9]*;/listen \[::\]\:${SET_LISTEN_PORT};/" "${cfg_dest}" - fi - - #remove nginx default config - if [[ -e "/etc/nginx/sites-enabled/default" ]]; then - sudo rm "/etc/nginx/sites-enabled/default" - fi - - #create symlink for own sites - if [[ ! -e "/etc/nginx/sites-enabled/${interface}" ]]; then - sudo ln -s "/etc/nginx/sites-available/${interface}" "/etc/nginx/sites-enabled/" - fi - - if [[ -n ${SET_LISTEN_PORT} ]]; then - ok_msg "${interface^} configured for port ${SET_LISTEN_PORT}!" - else - ok_msg "${interface^} configured for default port ${DEFAULT_PORT}!" - fi - - sudo systemctl restart nginx.service - - ok_msg "NGINX configuration for ${interface^} was set!" - fi -} - -### -# check if permissions of the users home directory -# grant execution rights to group and other which is -# required for NGINX to be able to serve Mainsail/Fluidd -# -function set_nginx_permissions() { - local homedir_perm - local exec_perms_count - - homedir_perm=$(ls -ld "${HOME}") - exec_perms_count=$(echo "${homedir_perm}" | cut -d" " -f1 | grep -c "x") - - if (( exec_perms_count < 3 )); then - status_msg "Granting NGINX the required permissions ..." - chmod og+x "${HOME}" && ok_msg "Done!" - fi - - return -} - -function read_listen_port() { - local port interface=${1} - port=$(grep listen "/etc/nginx/sites-enabled/${interface}" | head -1 | sed 's/^\s*//' | cut -d" " -f2 | cut -d";" -f1) - echo "${port}" -} - -function detect_enabled_sites() { - MAINSAIL_ENABLED="false" FLUIDD_ENABLED="false" - #check if there is another UI config already installed and reads the port they are listening on - if [[ -e "/etc/nginx/sites-enabled/mainsail" ]]; then - SITE_ENABLED="true" && MAINSAIL_ENABLED="true" - MAINSAIL_PORT=$(read_listen_port "mainsail") - fi - if [[ -e "/etc/nginx/sites-enabled/fluidd" ]]; then - SITE_ENABLED="true" && FLUIDD_ENABLED="true" - FLUIDD_PORT=$(read_listen_port "fluidd") - - fi -} diff --git a/scripts/obico.sh b/scripts/obico.sh deleted file mode 100644 index bd6a1cd..0000000 --- a/scripts/obico.sh +++ /dev/null @@ -1,489 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#===================================================# -#============== INSTALL MOONRAKER-OBICO ============# -#===================================================# - -function moonraker_obico_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/moonraker-obico(-[0-9a-zA-Z]+)?.service") - echo "${services}" -} - -function moonraker_obico_config() { - local moonraker_cfg_dirs - - read -r -a moonraker_cfg_dirs <<< "$(get_instance_folder_path "config")" - - if (( ${#moonraker_cfg_dirs[@]} > 0 )); then - echo "${moonraker_cfg_dirs[${1}]}/moonraker-obico.cfg" - else - echo "" - fi -} - -function moonraker_obico_needs_linking() { - local moonraker_obico_cfg=${1} - if [[ ! -f "${moonraker_obico_cfg}" ]]; then - return 1 - fi - if grep -s -E "^[^#]" "${moonraker_obico_cfg}" | grep -q 'auth_token'; then - return 1 - else - return 0 - fi -} - -function obico_server_url_prompt() { - top_border - printf "|${green}%-55s${white}|\n" " Obico Server URL" - blank_line - echo -e "| You can use a self-hosted Obico Server or the Obico |" - echo -e "| Cloud. For more information, please visit: |" - echo -e "| https://obico.io. |" - blank_line - echo -e "| For the Obico Cloud, leave it as the default: |" - printf "|${cyan}%-55s${white}|\n" " https://app.obico.io" - blank_line - echo -e "| For self-hosted server, specify: |" - printf "|${cyan}%-55s${white}|\n" " http://server_ip:port" - echo -e "| For instance, 'http://192.168.0.5:3334'. |" - bottom_border -} - -function moonraker_obico_setup_dialog() { - status_msg "Initializing Obico installation ..." - - local moonraker_count - local moonraker_names - - moonraker_count=$(moonraker_systemd | wc -w) - - if (( moonraker_count == 0 )); then - ### return early if moonraker is not installed - local error="Moonraker not installed! Please install Moonraker first!" - log_error "Obico setup started without Moonraker being installed. Aborting setup." - print_error "${error}" && return - elif (( moonraker_count > 1 )); then - # moonraker_names is valid only in case of multi-instance - read -r -a moonraker_names <<< "$(get_multi_instance_names)" - fi - - local moonraker_obico_services - local existing_moonraker_obico_count - moonraker_obico_services=$(moonraker_obico_systemd) - existing_moonraker_obico_count=$(echo "${moonraker_obico_services}" | wc -w ) - local allowed_moonraker_obico_count=$(( moonraker_count - existing_moonraker_obico_count )) - - # Allow user to reinstall an incomplete installation. - if (( allowed_moonraker_obico_count == 0 && moonraker_count > 0 )) && [[ $(get_moonraker_obico_status) != "Not linked!" ]]; then - local yn - while true; do - echo "${yellow}Obico is already installed.${white}" - echo "It is safe to run the install again to repair any issues." - echo "" - local question="Do you want to reinstall Obico?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting Obico installation...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - # The user responded yes, allow the install to run again. - allowed_moonraker_obico_count=1 - fi - - if (( allowed_moonraker_obico_count > 0 )); then - local new_moonraker_obico_count - - ### Step 1: Ask for the number of moonraker-obico instances to install - if (( moonraker_count == 1 )); then - ok_msg "Moonraker installation found!\n" - new_moonraker_obico_count=1 - elif (( moonraker_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${moonraker_count} Moonraker instances found!" - for name in "${moonraker_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● moonraker-${name}" - done - blank_line - if (( existing_moonraker_obico_count > 0 )); then - printf "|${green}%-55s${white}|\n" " ${existing_moonraker_obico_count} Obico instances already installed!" - for svc in ${moonraker_obico_services}; do - printf "|${cyan}%-57s${white}|\n" " ● moonraker-obco-$(get_instance_name "${svc}")" - done - fi - blank_line - echo -e "| The setup will apply the same names to Obico! |" - blank_line - echo -e "| Please select the number of Obico instances |" - echo -e "| to install. Usually one Obico instance per |" - echo -e "| Moonraker instance is required, but you may not |" - echo -e "| install more Obico instances than available |" - echo -e "| Moonraker instances. |" - bottom_border - - ### ask for amount of instances - local re="^[1-9][0-9]*$" - while [[ ! ${new_moonraker_obico_count} =~ ${re} || ${new_moonraker_obico_count} -gt ${allowed_moonraker_obico_count} ]]; do - read -p "${cyan}###### Number of new Obico instances to set up:${white} " -i "${allowed_moonraker_obico_count}" -e new_moonraker_obico_count - ### break if input is valid - [[ ${new_moonraker_obico_count} =~ ${re} && ${new_moonraker_obico_count} -le ${allowed_moonraker_obico_count} ]] && break - ### conditional error messages - [[ ! ${new_moonraker_obico_count} =~ ${re} ]] && error_msg "Input not a number" - (( new_moonraker_obico_count > allowed_moonraker_obico_count )) && error_msg "Number of Obico instances larger than installed Moonraker instances" - done && select_msg "${new_moonraker_obico_count}" - else - log_error "Internal error. moonraker_count of '${moonraker_count}' not equal or grather than one!" - return 1 - fi # (( moonraker_count == 1 )) - - ### Step 2: Confirm instance amount - local yn - while true; do - (( new_moonraker_obico_count == 1 )) && local question="Install Obico?" - (( new_moonraker_obico_count > 1 )) && local question="Install ${new_moonraker_obico_count} Obico instances?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting Obico setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - fi # (( allowed_moonraker_obico_count > 0 )) - - if (( new_moonraker_obico_count > 0 )); then - - ### Step 3: Ask for the Obico server URL - obico_server_url_prompt - local obico_server_url - while true; do - read -p "${cyan}###### Obico Server URL:${white} " -i "https://app.obico.io" -e obico_server_url - if echo "${obico_server_url}" | grep -qE "^(http|https)://[a-zA-Z0-9./?=_%:-]*"; then - break - else - error_msg "Invalid server URL!" - fi - done - - (( new_moonraker_obico_count > 1 )) && status_msg "Installing ${new_moonraker_obico_count} Obico instances ..." - (( new_moonraker_obico_count == 1 )) && status_msg "Installing Obico ..." - - ### Step 5: Clone the moonraker-obico repo - clone_moonraker_obico "${MOONRAKER_OBICO_REPO}" - - ### step 6: call moonrake-obico/install.sh with the correct params - local port=7125 - local instance_cfg_dirs - local instance_log_dirs - - read -r -a instance_cfg_dirs <<< "$(get_instance_folder_path "config")" - read -r -a instance_log_dirs <<< "$(get_instance_folder_path "logs")" - - if (( moonraker_count == 1 )); then - "${MOONRAKER_OBICO_DIR}/install.sh"\ - -C "${instance_cfg_dirs[0]}/moonraker.conf"\ - -p "${port}" -H 127.0.0.1 -l\ - "${instance_log_dirs[0]}"\ - -L -S "${obico_server_url}" - elif (( moonraker_count > 1 )); then - local j=${existing_moonraker_obico_count} - - for (( i=1; i <= new_moonraker_obico_count; i++ )); do - "${MOONRAKER_OBICO_DIR}/install.sh"\ - -n "${moonraker_names[${j}]}"\ - -C "${instance_cfg_dirs[${j}]}/moonraker.conf"\ - -p $((port+j))\ - -H 127.0.0.1\ - -l "${instance_log_dirs[${j}]}"\ - -L -S "${obico_server_url}" - j=$(( j + 1 )) - done && unset j - fi # (( moonraker_count == 1 )) - fi # (( new_moonraker_obico_count > 0 )) - - ### Step 7: Link to the Obico server if necessary - local not_linked_instances=() - if (( moonraker_count == 1 )); then - if moonraker_obico_needs_linking "$(moonraker_obico_config 0)"; then - not_linked_instances+=("0") - fi - elif (( moonraker_count > 1 )); then - for (( i=0; i <= moonraker_count; i++ )); do - if moonraker_obico_needs_linking "$(moonraker_obico_config "${i}")"; then - not_linked_instances+=("${i}") - fi - done - fi # (( moonraker_count == 1 )) - - if (( ${#not_linked_instances[@]} > 0 )); then - top_border - if (( moonraker_count == 1 )); then - printf "|${green}%-55s${white}|\n" " Obico not linked to the server!" - else - printf "|${green}%-55s${white}|\n" " ${#not_linked_instances[@]} Obico instances not linked to the server!" - for i in "${not_linked_instances[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● moonraker-obico-${moonraker_names[${i}]}" - done - fi - blank_line - echo -e "| It will take only 10 seconds to link printer to Obico.|" - echo -e "| For more information, visit: |" - echo -e "| https://www.obico.io/docs/user-guides/klipper-setup/ |" - blank_line - echo -e "| If you don't want to link the printer now, you can |" - echo -e "| restart the linking process later by: |" - echo -e "| 1. 'cd ~/kiauh && ./kiauh.sh' to launch KIAUH. |" - echo -e "| 2. Select ${green}[Install]${white} |" - echo -e "| 3. Select ${green}[Link to Obico Server]${white} |" - bottom_border - - while true; do - read -p "${cyan}###### Link to your Obico Server account now? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting Obico setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - - if (( moonraker_count == 1 )); then - status_msg "Link moonraker-obico to the Obico Server..." - "${MOONRAKER_OBICO_DIR}/scripts/link.sh" -q -c "$(moonraker_obico_config 0)" - elif (( moonraker_count > 1 )); then - for i in "${not_linked_instances[@]}"; do - local name="${moonraker_names[i]}" - status_msg "Link moonraker-obico-${name} to the Obico Server..." - "${MOONRAKER_OBICO_DIR}/scripts/link.sh" -q -n "${name}" -c "$(moonraker_obico_config "${i}")" - done - fi # (( moonraker_count == 1 )) - fi # (( ${#not_linked_instances[@]} > 0 )) -} - -function clone_moonraker_obico() { - local repo=${1} - - status_msg "Cloning Obico from ${repo} ..." - ### force remove existing Obico dir - [[ -d "${MOONRAKER_OBICO_DIR}" ]] && rm -rf "${MOONRAKER_OBICO_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${repo}" "${MOONRAKER_OBICO_DIR}"; then - print_error "Cloning Obico from\n ${repo}\n failed!" - exit 1 - fi -} - -function moonraker_obico_install() { - "${MOONRAKER_OBICO_DIR}/install.sh" "$@" -} - -#===================================================# -#============= REMOVE MOONRAKER-OBICO ==============# -#===================================================# - -function remove_moonraker_obico_systemd() { - [[ -z $(moonraker_obico_systemd) ]] && return - status_msg "Removing Obico Systemd Services ..." - - for service in $(moonraker_obico_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "Obico Services removed!" -} - -function remove_moonraker_obico_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/moonraker-obico(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_legacy_moonraker_obico_logs() { - local files regex="moonraker-obico(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}/klipper_logs" -maxdepth 1 -regextype posix-extended -regex "${HOME}/klipper_logs/${regex}" 2> /dev/null | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_moonraker_obico_dir() { - [[ ! -d ${MOONRAKER_OBICO_DIR} ]] && return - - status_msg "Removing Obico directory ..." - rm -rf "${MOONRAKER_OBICO_DIR}" - ok_msg "Directory removed!" -} - -function remove_moonraker_obico_env() { - [[ ! -d "${HOME}/moonraker-obico-env" ]] && return - - status_msg "Removing moonraker-obico-env directory ..." - rm -rf "${HOME}/moonraker-obico-env" - ok_msg "Directory removed!" -} - -function remove_moonraker_obico() { - remove_moonraker_obico_systemd - remove_moonraker_obico_logs - remove_moonraker_obico_dir - remove_moonraker_obico_env - - print_confirm "Obico was successfully removed!" - return -} - -#===================================================# -#============= UPDATE MOONRAKER-OBICO ==============# -#===================================================# - -function update_moonraker_obico() { - do_action_service "stop" "moonraker-obico" - - if [[ ! -d ${MOONRAKER_OBICO_DIR} ]]; then - clone_moonraker_obico "${MOONRAKER_OBICO_REPO}" - else - status_msg "Updating Obico ..." - cd "${MOONRAKER_OBICO_DIR}" && git pull - fi - - "${MOONRAKER_OBICO_DIR}/install.sh" -U - - ok_msg "Update complete!" - do_action_service "restart" "moonraker-obico" -} - -#===================================================# -#============= MOONRAKER-OBICO STATUS ==============# -#===================================================# - -function get_moonraker_obico_status() { - local status - local service_count - local is_linked - local moonraker_obico_services - - moonraker_obico_services=$(moonraker_obico_systemd) - service_count=$(echo "${moonraker_obico_services}" | wc -w ) - - is_linked="true" - if [[ -n ${moonraker_obico_services} ]]; then - for cfg_dir in $(get_instance_folder_path "config"); do - if moonraker_obico_needs_linking "${cfg_dir}/moonraker-obico.cfg"; then - is_linked="false" - fi - done - fi - - if (( service_count == 0 )); then - status="Not installed!" - elif [[ ! -d "${MOONRAKER_OBICO_DIR}" ]]; then - status="Incomplete!" - elif [[ ${is_linked} == "false" ]]; then - status="Not linked!" - else - status="Installed!" - fi - - echo "${status}" -} - -function get_local_moonraker_obico_commit() { - [[ ! -d ${MOONRAKER_OBICO_DIR} || ! -d "${MOONRAKER_OBICO_DIR}/.git" ]] && return - - local commit - cd "${MOONRAKER_OBICO_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_moonraker_obico_commit() { - [[ ! -d ${MOONRAKER_OBICO_DIR} || ! -d "${MOONRAKER_OBICO_DIR}/.git" ]] && return - - local commit - cd "${MOONRAKER_OBICO_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_moonraker_obico_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_moonraker_obico_commit)" - remote_ver="$(get_remote_moonraker_obico_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "moonraker_obico" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} - -### -# it is possible, that moonraker_obico is installed in a so called -# "non-linked" state. the linking can be achieved by running the -# installation script again. this function will check the obico -# installation status and returns the correctly formulated menu title -# -function obico_install_title() { - if [[ $(get_moonraker_obico_status) == "Not linked!" ]]; then - echo "[Link to Obico Server]" - else - echo "[Obico for Klipper] " - fi -} - diff --git a/scripts/octoapp.sh b/scripts/octoapp.sh deleted file mode 100644 index dea6918..0000000 --- a/scripts/octoapp.sh +++ /dev/null @@ -1,369 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -# -# This file is written and maintained by Christian WΓΌrthner from OctoApp -# Please contact me if you need any help! -# hello@octoapp.eu -# - -set -e - -#===================================================# -#============== Install ============# -#===================================================# - -function octoapp_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/octoapp(-[0-9a-zA-Z]+)?.service") - echo "${services}" -} - -function octoapp_setup_dialog() { - status_msg "Initializing OctoApp for Klipper installation ..." - - # First, check for moonraker service instances. - local moonraker_count - local moonraker_names - moonraker_count=$(moonraker_systemd | wc -w) - if (( moonraker_count == 0 )); then - ### return early if moonraker is not installed - local error="Moonraker not installed! Please install Moonraker first!" - log_error "OctoApp setup started without Moonraker being installed. Aborting setup." - print_error "${error}" && return - elif (( moonraker_count > 1 )); then - # moonraker_names is valid only in case of multi-instance - read -r -a moonraker_names <<< "$(get_multi_instance_names)" - fi - - # Next, check for any existing OctoApp services. - local octoapp_services - local existing_octoapp_count - octoapp_services=$(octoapp_systemd) - existing_octoapp_count=$(echo "${octoapp_services}" | wc -w ) - - # We need to make the moonraker instance count to the OctoApp service count. - local allowed_octoapp_count=$(( moonraker_count - existing_octoapp_count )) - if (( allowed_octoapp_count > 0 )); then - local new_octoapp_count - - ### Step 1: Ask for the number of OctoApp instances to install - if (( moonraker_count == 1 )); then - ok_msg "Moonraker installation found!\n" - new_octoapp_count=1 - elif (( moonraker_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${moonraker_count} Moonraker instances found!" - for name in "${moonraker_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● moonraker-${name}" - done - blank_line - if (( existing_octoapp_count > 0 )); then - printf "|${green}%-55s${white}|\n" " ${existing_octoapp_count} OctoApp instances already installed!" - for svc in ${octoapp_services}; do - printf "|${cyan}%-57s${white}|\n" " ● octoapp-$(get_instance_name "${svc}")" - done - fi - blank_line - echo -e "| The setup will apply the same names to OctoApp |" - blank_line - echo -e "| Please select the number of OctoApp instances to |" - echo -e "| install. Usually one OctoApp instance per Moonraker |" - echo -e "| instance is required, but you may not install more |" - echo -e "| OctoApp instances than available Moonraker instances. |" - bottom_border - - ### ask for amount of instances - local re="^[1-9][0-9]*$" - while [[ ! ${new_octoapp_count} =~ ${re} || ${new_octoapp_count} -gt ${allowed_octoapp_count} ]]; do - read -p "${cyan}###### Number of new OctoApp instances to set up:${white} " -i "${allowed_octoapp_count}" -e new_octoapp_count - ### break if input is valid - [[ ${new_octoapp_count} =~ ${re} && ${new_octoapp_count} -le ${allowed_octoapp_count} ]] && break - ### conditional error messages - [[ ! ${new_octoapp_count} =~ ${re} ]] && error_msg "Input not a number" - (( new_octoapp_count > allowed_octoapp_count )) && error_msg "Number of OctoApp instances larger than installed Moonraker instances" - done && select_msg "${new_octoapp_count}" - else - log_error "Internal error. moonraker_count of '${moonraker_count}' not equal or grater than one!" - return 1 - fi # (( moonraker_count == 1 )) - fi # (( allowed_octoapp_count > 0 )) - - # Special case for one moonraker instance with OctoApp already installed. - # If the user selects the install option again, they might be trying to recover the install - # or complete a printer link they didn't finish in the past. - # So in this case, we will allow them to run the install script again, since it's safe to run - # if the service is already installed, it will repair any missing issues. - if (( allowed_octoapp_count == 0 && moonraker_count == 1 )); then - local yn - while true; do - echo "${yellow}OctoApp is already installed.${white}" - echo "It is safe to run the install again to repair any issues or if the printer isn't linked, run the printer linking logic again." - echo "" - local question="Do you want to run the OctoApp recovery or linking logic again?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting OctoApp setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - # The user responded yes, allow the install to run again. - allowed_octoapp_count=1 - fi - - # If there's something to install, do it! - if (( allowed_octoapp_count > 0 )); then - - (( new_octoapp_count > 1 )) && status_msg "Installing ${new_octoapp_count} OctoApp instances ..." - (( new_octoapp_count == 1 )) && status_msg "Installing OctoApp ..." - - # Ensure the basic system dependencies are installed. - local dep=(git dfu-util virtualenv python3 python3-pip python3-venv) - dependency_check "${dep[@]}" - - # Close the repo - clone_octoapp "${OCTOAPP_REPO}" - - # Call install with the correct args. - local instance_cfg_dirs - read -r -a instance_cfg_dirs <<< "$(get_instance_folder_path "config")" - echo instance_cfg_dirs[0] - - if (( moonraker_count == 1 )); then - "${OCTOAPP_DIR}/install.sh" "${instance_cfg_dirs[0]}/moonraker.conf" - elif (( moonraker_count > 1 )); then - local j=${existing_octoapp_count} - - for (( i=1; i <= new_octoapp_count; i++ )); do - "${OCTOAPP_DIR}/install.sh" "${instance_cfg_dirs[${j}]}/moonraker.conf" - j=$(( j + 1 )) - done && unset j - fi # (( moonraker_count == 1 )) - fi # (( allowed_octoapp_count > 0 )) -} - -function octoapp_install() { - "${OCTOAPP_DIR}/install.sh" "$@" -} - -#===================================================# -#============= Remove ==============# -#===================================================# - -function remove_octoapp_systemd() { - [[ -z $(octoapp_systemd) ]] && return - status_msg "Removing OctoApp Systemd Services ..." - - for service in $(octoapp_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "OctoApp Services removed!" -} - -function remove_octoapp_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/octoapp(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoapp_dir() { - [[ ! -d ${OCTOAPP_DIR} ]] && return - - status_msg "Removing OctoApp directory ..." - rm -rf "${OCTOAPP_DIR}" - ok_msg "Directory removed!" -} - -function remove_octoapp_config() { - # Remove the system config but not the main config, so the printer id doesn't get lost. - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/octoapp-system(-[0-9a-zA-Z]+)?\.cfg(.*)?" - files=$(find "${HOME}" -maxdepth 4 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoapp_store_dir() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/octoapp-store" - files=$(find "${HOME}" -maxdepth 2 -type d -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -rf "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoapp_env() { - [[ ! -d "${HOME}/octoapp-env" ]] && return - - status_msg "Removing octoapp-env directory ..." - rm -rf "${HOME}/octoapp-env" - ok_msg "Directory removed!" -} - -function remove_octoapp() -{ - remove_octoapp_systemd - remove_octoapp_logs - remove_octoapp_dir - remove_octoapp_env - remove_octoapp_config - remove_octoapp_store_dir - - print_confirm "OctoApp was successfully removed!" - return -} - -#===================================================# -#============= UPDATE ==============# -#===================================================# - -function update_octoapp() { - do_action_service "stop" "octoapp" - - if [[ ! -d ${OCTOAPP_DIR} ]]; then - clone_octoapp "${OCTOAPP_REPO}" - else - backup_before_update "octoapp" - status_msg "Updating OctoApp for Klipper ..." - cd "${OCTOAPP_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_octoapp_dependencies - ### install possible new python dependencies - "${OCTOAPP_ENV}"/bin/pip install -r "${OCTOAPP_DIR}/requirements.txt" - fi - - ok_msg "Update complete!" - do_action_service "restart" "octoapp" -} - -function clone_octoapp() { - local repo=${1} - - status_msg "Cloning OctoApp from ${repo} ..." - - ### force remove existing octoapp dir and clone into fresh octoapp dir - [[ -d ${OCTOAPP_DIR} ]] && rm -rf "${OCTOAPP_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${OCTOAPP_REPO}" "${OCTOAPP_DIR}"; then - print_error "Cloning OctoApp from\n ${repo}\n failed!" - exit 1 - fi -} - -function install_octoapp_dependencies() { - local packages log_name="OctoApp" - local install_script="${OCTOAPP_DIR}/install.sh" - - ### read PKGLIST from official install-script - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages="$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n')" - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -#===================================================# -#============= STATUS ==============# -#===================================================# - -function get_octoapp_status() { - local status - local service_count - local octoapp_services - - octoapp_services=$(octoapp_systemd) - service_count=$(echo "${octoapp_services}" | wc -w ) - - if (( service_count == 0 )); then - status="Not installed!" - elif [[ ! -d "${OCTOAPP_DIR}" ]]; then - status="Incomplete!" - else - status="Installed!" - fi - - echo "${status}" -} - -function get_local_octoapp_commit() { - [[ ! -d ${OCTOAPP_DIR} || ! -d "${OCTOAPP_DIR}/.git" ]] && return - - local commit - cd "${OCTOAPP_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_octoapp_commit() { - [[ ! -d ${OCTOAPP_DIR} || ! -d "${OCTOAPP_DIR}/.git" ]] && return - - local commit - cd "${OCTOAPP_DIR}" && git fetch origin -q - commit=$(git describe origin/release --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_octoapp_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_octoapp_commit)" - remote_ver="$(get_remote_octoapp_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # Add us to the update file, so if the user selects "update all" it includes us. - add_to_application_updates "octoapp" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} diff --git a/scripts/octoeverywhere.sh b/scripts/octoeverywhere.sh deleted file mode 100644 index 49a0f96..0000000 --- a/scripts/octoeverywhere.sh +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -# -# This file is written and maintained by Quinn Damerell from OctoEverywhere -# Please contact our support team if you need any help! -# https://octoeverywhere.com/support -# - -set -e - -#===================================================# -#============== Install ============# -#===================================================# - -function octoeverywhere_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/octoeverywhere(-[0-9a-zA-Z]+)?.service") - echo "${services}" -} - -function octoeverywhere_setup_dialog() { - status_msg "Initializing OctoEverywhere for Klipper installation ..." - - # First, check for moonraker service instances. - local moonraker_count - local moonraker_names - moonraker_count=$(moonraker_systemd | wc -w) - if (( moonraker_count == 0 )); then - ### return early if moonraker is not installed - local error="Moonraker not installed! Please install Moonraker first!" - log_error "OctoEverywhere setup started without Moonraker being installed. Aborting setup." - print_error "${error}" && return - elif (( moonraker_count > 1 )); then - # moonraker_names is valid only in case of multi-instance - read -r -a moonraker_names <<< "$(get_multi_instance_names)" - fi - - # Next, check for any existing OctoEverywhere services. - local octoeverywhere_services - local existing_octoeverywhere_count - octoeverywhere_services=$(octoeverywhere_systemd) - existing_octoeverywhere_count=$(echo "${octoeverywhere_services}" | wc -w ) - - # We need to make the moonraker instance count to the OctoEverywhere service count. - local allowed_octoeverywhere_count=$(( moonraker_count - existing_octoeverywhere_count )) - if (( allowed_octoeverywhere_count > 0 )); then - local new_octoeverywhere_count - - ### Step 1: Ask for the number of OctoEverywhere instances to install - if (( moonraker_count == 1 )); then - ok_msg "Moonraker installation found!\n" - new_octoeverywhere_count=1 - elif (( moonraker_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${moonraker_count} Moonraker instances found!" - for name in "${moonraker_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● moonraker-${name}" - done - blank_line - if (( existing_octoeverywhere_count > 0 )); then - printf "|${green}%-55s${white}|\n" " ${existing_octoeverywhere_count} OctoEverywhere instances already installed!" - for svc in ${octoeverywhere_services}; do - printf "|${cyan}%-57s${white}|\n" " ● octoeverywhere-$(get_instance_name "${svc}")" - done - fi - blank_line - echo -e "| The setup will apply the same names to |" - echo -e "| OctoEverywhere |" - blank_line - echo -e "| Please select the number of OctoEverywhere instances |" - echo -e "| to install. Usually one OctoEverywhere instance per |" - echo -e "| Moonraker instance is required, but you may not |" - echo -e "| install more OctoEverywhere instances than available |" - echo -e "| Moonraker instances. |" - bottom_border - - ### ask for amount of instances - local re="^[1-9][0-9]*$" - while [[ ! ${new_octoeverywhere_count} =~ ${re} || ${new_octoeverywhere_count} -gt ${allowed_octoeverywhere_count} ]]; do - read -p "${cyan}###### Number of new OctoEverywhere instances to set up:${white} " -i "${allowed_octoeverywhere_count}" -e new_octoeverywhere_count - ### break if input is valid - [[ ${new_octoeverywhere_count} =~ ${re} && ${new_octoeverywhere_count} -le ${allowed_octoeverywhere_count} ]] && break - ### conditional error messages - [[ ! ${new_octoeverywhere_count} =~ ${re} ]] && error_msg "Input not a number" - (( new_octoeverywhere_count > allowed_octoeverywhere_count )) && error_msg "Number of OctoEverywhere instances larger than installed Moonraker instances" - done && select_msg "${new_octoeverywhere_count}" - else - log_error "Internal error. moonraker_count of '${moonraker_count}' not equal or grater than one!" - return 1 - fi # (( moonraker_count == 1 )) - fi # (( allowed_octoeverywhere_count > 0 )) - - # Special case for one moonraker instance with OctoEverywhere already installed. - # If the user selects the install option again, they might be trying to recover the install - # or complete a printer link they didn't finish in the past. - # So in this case, we will allow them to run the install script again, since it's safe to run - # if the service is already installed, it will repair any missing issues. - if (( allowed_octoeverywhere_count == 0 && moonraker_count == 1 )); then - local yn - while true; do - echo "${yellow}OctoEverywhere is already installed.${white}" - echo "It is safe to run the install again to repair any issues or if the printer isn't linked, run the printer linking logic again." - echo "" - local question="Do you want to run the OctoEverywhere recovery or linking logic again?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting OctoEverywhere setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - # The user responded yes, allow the install to run again. - allowed_octoeverywhere_count=1 - fi - - # If there's something to install, do it! - if (( allowed_octoeverywhere_count > 0 )); then - - (( new_octoeverywhere_count > 1 )) && status_msg "Installing ${new_octoeverywhere_count} OctoEverywhere instances ..." - (( new_octoeverywhere_count == 1 )) && status_msg "Installing OctoEverywhere ..." - - # Ensure the basic system dependencies are installed. - local dep=(git dfu-util virtualenv python3 python3-pip python3-venv) - dependency_check "${dep[@]}" - - # Close the repo - clone_octoeverywhere "${OCTOEVERYWHERE_REPO}" - - # Call install with the correct args. - local instance_cfg_dirs - read -r -a instance_cfg_dirs <<< "$(get_instance_folder_path "config")" - echo instance_cfg_dirs[0] - - if (( moonraker_count == 1 )); then - "${OCTOEVERYWHERE_DIR}/install.sh" "${instance_cfg_dirs[0]}/moonraker.conf" - elif (( moonraker_count > 1 )); then - local j=${existing_octoeverywhere_count} - - for (( i=1; i <= new_octoeverywhere_count; i++ )); do - "${OCTOEVERYWHERE_DIR}/install.sh" "${instance_cfg_dirs[${j}]}/moonraker.conf" - j=$(( j + 1 )) - done && unset j - fi # (( moonraker_count == 1 )) - fi # (( allowed_octoeverywhere_count > 0 )) -} - -function clone_octoeverywhere() { - local repo=${1} - - status_msg "Cloning OctoEverywhere..." - ### force remove existing repos - [[ -d "${OCTOEVERYWHERE_DIR}" ]] && rm -rf "${OCTOEVERYWHERE_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${repo}" "${OCTOEVERYWHERE_DIR}"; then - print_error "Cloning OctoEverywhere from\n ${repo}\n failed!" - exit 1 - fi -} - -function octoeverywhere_install() { - "${OCTOEVERYWHERE_DIR}/install.sh" "$@" -} - -#===================================================# -#============= Remove ==============# -#===================================================# - -function remove_octoeverywhere_systemd() { - [[ -z $(octoeverywhere_systemd) ]] && return - status_msg "Removing OctoEverywhere Systemd Services ..." - - for service in $(octoeverywhere_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "OctoEverywhere Services removed!" -} - -function remove_octoeverywhere_logs() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/logs\/octoeverywhere(-[0-9a-zA-Z]+)?\.log(.*)?" - files=$(find "${HOME}" -maxdepth 3 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoeverywhere_dir() { - [[ ! -d ${OCTOEVERYWHERE_DIR} ]] && return - - status_msg "Removing OctoEverywhere directory ..." - rm -rf "${OCTOEVERYWHERE_DIR}" - ok_msg "Directory removed!" -} - -function remove_octoeverywhere_config() { - # Remove the system config but not the main config, so the printer id doesn't get lost. - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/octoeverywhere-system(-[0-9a-zA-Z]+)?\.cfg(.*)?" - files=$(find "${HOME}" -maxdepth 4 -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -f "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoeverywhere_store_dir() { - local files regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/octoeverywhere-store" - files=$(find "${HOME}" -maxdepth 2 -type d -regextype posix-extended -regex "${regex}" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -rf "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoeverywhere_env() { - [[ ! -d "${HOME}/octoeverywhere-env" ]] && return - - status_msg "Removing octoeverywhere-env directory ..." - rm -rf "${HOME}/octoeverywhere-env" - ok_msg "Directory removed!" -} - -function remove_octoeverywhere() -{ - remove_octoeverywhere_systemd - remove_octoeverywhere_logs - remove_octoeverywhere_dir - remove_octoeverywhere_env - remove_octoeverywhere_config - remove_octoeverywhere_store_dir - - print_confirm "OctoEverywhere was successfully removed!" - return -} - -#===================================================# -#============= UPDATE ==============# -#===================================================# - -function update_octoeverywhere() { - do_action_service "stop" "octoeverywhere" - - if [[ ! -d ${OCTOEVERYWHERE_DIR} ]]; then - clone_octoeverywhere "${OCTOEVERYWHERE_REPO}" - else - backup_before_update "octoeverywhere" - status_msg "Updating OctoEverywhere for Klipper ..." - cd "${OCTOEVERYWHERE_DIR}" && git pull - ### read PKGLIST and install possible new dependencies - install_octoeverywhere_dependencies - ### install possible new python dependencies - "${OCTOEVERYWHERE_ENV}"/bin/pip install -r "${OCTOEVERYWHERE_DIR}/requirements.txt" - fi - - ok_msg "Update complete!" - do_action_service "restart" "octoeverywhere" -} - -function clone_octoeverywhere() { - local repo=${1} - - status_msg "Cloning OctoEverywhere from ${repo} ..." - - ### force remove existing octoeverywhere dir and clone into fresh octoeverywhere dir - [[ -d ${OCTOEVERYWHERE_DIR} ]] && rm -rf "${OCTOEVERYWHERE_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${OCTOEVERYWHERE_REPO}" "${OCTOEVERYWHERE_DIR}"; then - print_error "Cloning OctoEverywhere from\n ${repo}\n failed!" - exit 1 - fi -} - -function install_octoeverywhere_dependencies() { - local packages log_name="OctoEverywhere" - local install_script="${OCTOEVERYWHERE_DIR}/install.sh" - - ### read PKGLIST from official install-script - status_msg "Reading dependencies..." - # shellcheck disable=SC2016 - packages="$(grep "PKGLIST=" "${install_script}" | cut -d'"' -f2 | sed 's/\${PKGLIST}//g' | tr -d '\n')" - - echo "${cyan}${packages}${white}" | tr '[:space:]' '\n' - read -r -a packages <<< "${packages}" - - ### Update system package lists if stale - update_system_package_lists - - ### Install required packages - install_system_packages "${log_name}" "packages[@]" -} - -#===================================================# -#============= STATUS ==============# -#===================================================# - -function get_octoeverywhere_status() { - local status - local service_count - local octoeverywhere_services - - octoeverywhere_services=$(octoeverywhere_systemd) - service_count=$(echo "${octoeverywhere_services}" | wc -w ) - - if (( service_count == 0 )); then - status="Not installed!" - elif [[ ! -d "${OCTOEVERYWHERE_DIR}" ]]; then - status="Incomplete!" - else - status="Installed!" - fi - - echo "${status}" -} - -function get_local_octoeverywhere_commit() { - [[ ! -d ${OCTOEVERYWHERE_DIR} || ! -d "${OCTOEVERYWHERE_DIR}/.git" ]] && return - - local commit - cd "${OCTOEVERYWHERE_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_octoeverywhere_commit() { - [[ ! -d ${OCTOEVERYWHERE_DIR} || ! -d "${OCTOEVERYWHERE_DIR}/.git" ]] && return - - local commit - cd "${OCTOEVERYWHERE_DIR}" && git fetch origin -q - commit=$(git describe origin/master --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_octoeverywhere_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_octoeverywhere_commit)" - remote_ver="$(get_remote_octoeverywhere_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # Add us to the update file, so if the user selects "update all" it includes us. - add_to_application_updates "octoeverywhere" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} diff --git a/scripts/octoprint.sh b/scripts/octoprint.sh deleted file mode 100644 index 40640ff..0000000 --- a/scripts/octoprint.sh +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#=================================================# -#=============== INSTALL OCTOPRINT ===============# -#=================================================# - -function octoprint_systemd() { - local services - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/octoprint(-[0-9a-zA-Z]+)?.service" | sort) - echo "${services}" -} - -function octoprint_setup_dialog() { - status_msg "Initializing OctoPrint installation ..." - - local klipper_services - klipper_services=$(klipper_systemd) - if [[ -z ${klipper_services} ]]; then - local error="Klipper not installed! Please install Klipper first!" - log_error "OctoPrint setup started without Klipper being installed. Aborting setup." - print_error "${error}" && return - fi - - local klipper_count user_input=() klipper_names=() - klipper_count=$(echo "${klipper_services}" | wc -w ) - for service in ${klipper_services}; do - klipper_names+=( "$(get_instance_name "${service}")" ) - done - - local octoprint_count - if (( klipper_count == 1 )); then - ok_msg "Klipper installation found!\n" - octoprint_count=1 - - elif (( klipper_count > 1 )); then - top_border - printf "|${green}%-55s${white}|\n" " ${klipper_count} Klipper instances found!" - for name in "${klipper_names[@]}"; do - printf "|${cyan}%-57s${white}|\n" " ● ${name}" - done - blank_line - echo -e "| The setup will apply the same names to OctoPrint! |" - blank_line - echo -e "| Please select the number of OctoPrint instances to |" - echo -e "| install. Usually one OctoPrint instance per Klipper |" - echo -e "| instance is required, but you may not install more |" - echo -e "| OctoPrint instances than available Klipper instances. |" - bottom_border - - local re="^[1-9][0-9]*$" - while [[ ! ${octoprint_count} =~ ${re} || ${octoprint_count} -gt ${klipper_count} ]]; do - read -p "${cyan}###### Number of OctoPrint instances to set up:${white} " -i "${klipper_count}" -e octoprint_count - ### break if input is valid - [[ ${octoprint_count} =~ ${re} ]] && break - ### conditional error messages - [[ ! ${octoprint_count} =~ ${re} ]] && error_msg "Input not a number" - (( octoprint_count > klipper_count )) && error_msg "Number of OctoPrint instances larger than existing Klipper instances" - done && select_msg "${octoprint_count}" - - else - log_error "Internal error. klipper_count of '${klipper_count}' not equal or grather than one!" - return 1 - fi - - user_input+=("${octoprint_count}") - - ### confirm instance amount - local yn - while true; do - (( octoprint_count == 1 )) && local question="Install OctoPrint?" - (( octoprint_count > 1 )) && local question="Install ${octoprint_count} OctoPrint instances?" - read -p "${cyan}###### ${question} (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Exiting OctoPrint setup ...\n" - return;; - *) - error_msg "Invalid Input!";; - esac - done - - ### write existing klipper names into user_input array to use them as names for octoprint - if (( klipper_count > 1 )); then - for name in "${klipper_names[@]}"; do - user_input+=("${name}") - done - fi - - (( octoprint_count > 1 )) && status_msg "Installing ${octoprint_count} OctoPrint instances ..." - (( octoprint_count == 1 )) && status_msg "Installing OctoPrint ..." - octoprint_setup "${user_input[@]}" -} - -function octoprint_setup() { - local instance_arr=("${@}") - ### check and install all dependencies - local dep=( - git - wget - python3-pip - python3-dev - libyaml-dev - build-essential - python3-setuptools - python3-virtualenv - ) - dependency_check "${dep[@]}" - - ### step 1: check for tty and dialout usergroups and add reboot permissions - check_usergroups - add_reboot_permission - - ### step 2: install octoprint - install_octoprint "${instance_arr[@]}" - - ### step 3: set up service - create_octoprint_service "${instance_arr[@]}" - - ### step 4: enable and start all instances - do_action_service "enable" "octoprint" - do_action_service "start" "octoprint" - - ### confirm message - local confirm="" - (( instance_arr[0] == 1 )) && confirm="OctoPrint has been set up!" - (( instance_arr[0] > 1 )) && confirm="${instance_arr[0]} OctoPrint instances have been set up!" - print_confirm "${confirm}" && print_op_ip_list "${instance_arr[0]}" && return -} - -function install_octoprint() { - - function install_octoprint_python_env() { - local tmp="${1}" - ### create and activate the virtualenv - status_msg "Installing python virtual environment..." - - if [[ ! -d ${tmp} ]]; then - mkdir -p "${tmp}" - else - error_msg "Cannot create temporary directory in ${HOME}!" - error_msg "Folder 'TMP_OCTO_ENV' exists and may not be empty!" - error_msg "Please remove/rename that folder and start again." - return 1 - fi - - cd "${tmp}" - - if virtualenv --python=python3 venv; then - ### activate virtualenv - source venv/bin/activate - pip install pip --upgrade - pip install --no-cache-dir octoprint - ### leave virtualenv - deactivate - else - log_error "failure while creating python3 OctoPrint env" - error_msg "Creation of OctoPrint virtualenv failed!" - exit 1 - fi - - cd "${HOME}" - } - - local input=("${@}") - local octoprint_count=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local j=0 octo_env - local tmp="${HOME}/TMP_OCTO_ENV" - - ### handle single instance installs - if (( octoprint_count == 1 )); then - if install_octoprint_python_env "${tmp}"; then - status_msg "Installing OctoPrint ..." - octo_env="${HOME}/OctoPrint" - - ### rename the temporary directory to the correct name - [[ -d ${octo_env} ]] && rm -rf "${octo_env}" - mv "${tmp}" "${octo_env}" - - ### replace the temporary directory name with the actual one in ${octo_env}/venv/bin/python/octoprint - sed -i "s|${tmp}|${octo_env}|" "${octo_env}/venv/bin/octoprint" - else - error_msg "OctoPrint installation failed!" - return 1 - fi - fi - - ### handle multi instance installs - if (( octoprint_count > 1 )); then - if install_octoprint_python_env "${tmp}"; then - for (( i=1; i <= octoprint_count; i++ )); do - status_msg "Installing OctoPrint instance ${i}(${names[${j}]}) ..." - octo_env="${HOME}/OctoPrint_${names[${j}]}" - - ### rename the temporary directory to the correct name - [[ -d ${octo_env} ]] && rm -rf "${octo_env}" - cp -r "${tmp}" "${octo_env}" - - ### replace the temporary directory name with the actual one in ${octo_env}/venv/bin/python/octoprint - sed -i "s|${tmp}|${octo_env}|" "${octo_env}/venv/bin/octoprint" - j=$(( j + 1 )) - done && rm -rf "${tmp}" - else - error_msg "OctoPrint installation failed!" - return 1 - fi - fi -} - -function create_octoprint_service() { - local input=("${@}") - local octoprint_count=${input[0]} && unset "input[0]" - local names=("${input[@]}") && unset "input[@]" - local j=0 port=5000 - local printer_data octo_env service basedir printer config_yaml restart_cmd - - for (( i=1; i <= octoprint_count; i++ )); do - if (( octoprint_count == 1 )); then - printer_data="${HOME}/printer_data" - octo_env="${HOME}/OctoPrint" - service="${SYSTEMD}/octoprint.service" - basedir="${HOME}/.octoprint" - printer="${printer_data}/comms/klippy.serial" - config_yaml="${basedir}/config.yaml" - restart_cmd="sudo service octoprint restart" - elif (( octoprint_count > 1 )); then - - local re="^[1-9][0-9]*$" - if [[ ${names[j]} =~ ${re} ]]; then - printer_data="${HOME}/printer_${names[${j}]}_data" - else - printer_data="${HOME}/${names[${j}]}_data" - fi - - octo_env="${HOME}/OctoPrint_${names[${j}]}" - service="${SYSTEMD}/octoprint-${names[${j}]}.service" - basedir="${HOME}/.octoprint_${names[${j}]}" - printer="${printer_data}/comms/klippy.serial" - config_yaml="${basedir}/config.yaml" - restart_cmd="sudo service octoprint-${names[${j}]} restart" - fi - - (( octoprint_count == 1 )) && status_msg "Creating OctoPrint Service ..." - (( octoprint_count > 1 )) && status_msg "Creating OctoPrint Service ${i}(${names[${j}]}) ..." - - sudo /bin/sh -c "cat > ${service}" << OCTOPRINT -[Unit] -Description=Starts OctoPrint on startup -After=network-online.target -Wants=network-online.target - -[Service] -Environment="LC_ALL=C.UTF-8" -Environment="LANG=C.UTF-8" -Type=simple -User=${USER} -ExecStart=${octo_env}/venv/bin/octoprint --basedir ${basedir} --config ${config_yaml} --port=${port} serve - -[Install] -WantedBy=multi-user.target -OCTOPRINT - - port=$(( port + 1 )) - j=$(( j + 1 )) - ok_msg "Ok!" - - ### create config.yaml - if [[ ! -f ${basedir}/config.yaml ]]; then - [[ ! -d ${basedir} ]] && mkdir "${basedir}" - - (( octoprint_count == 1 )) && status_msg "Creating config.yaml ..." - (( octoprint_count > 1 )) && status_msg "Creating config.yaml for instance ${i}(${names[${j}]}) ..." - - /bin/sh -c "cat > ${basedir}/config.yaml" << CONFIGYAML -serial: - additionalPorts: - - ${printer} - disconnectOnErrors: false - port: ${printer} -server: - commands: - serverRestartCommand: ${restart_cmd} - systemRestartCommand: sudo shutdown -r now - systemShutdownCommand: sudo shutdown -h now -CONFIGYAML - ok_msg "Ok!" - fi - done -} - -function add_reboot_permission() { - #create a backup if file already exists - if [[ -f /etc/sudoers.d/octoprint-shutdown ]]; then - sudo mv /etc/sudoers.d/octoprint-shutdown /etc/sudoers.d/octoprint-shutdown.old - fi - - #create new permission file - status_msg "Add reboot permission to user '${USER}' ..." - cd "${HOME}" && echo "${USER} ALL=NOPASSWD: /sbin/shutdown" > octoprint-shutdown - sudo chown 0 octoprint-shutdown - sudo mv octoprint-shutdown /etc/sudoers.d/octoprint-shutdown - ok_msg "Permission set!" -} - -function print_op_ip_list() { - local ip octoprint_count="${1}" port=5000 - ip=$(hostname -I | cut -d" " -f1) - - for (( i=1; i <= octoprint_count; i++ )); do - echo -e " ${cyan}● Instance ${i}:${white} ${ip}:${port}" - port=$(( port + 1 )) - done && echo -} - -#=================================================# -#=============== REMOVE OCTOPRINT ================# -#=================================================# - -function remove_octoprint_service() { - [[ -z $(octoprint_systemd) ]] && return - - ###remove all octoprint services - status_msg "Removing OctoPrint Systemd Services ..." - - for service in $(octoprint_systemd | cut -d"/" -f5); do - status_msg "Removing ${service} ..." - sudo systemctl stop "${service}" - sudo systemctl disable "${service}" - sudo rm -f "${SYSTEMD}/${service}" - ok_msg "Done!" - done - - ### reloading units - sudo systemctl daemon-reload - sudo systemctl reset-failed -} - -function remove_octoprint_sudoers() { - [[ ! -f /etc/sudoers.d/octoprint-shutdown ]] && return - - ### remove sudoers file - sudo rm -f /etc/sudoers.d/octoprint-shutdown -} - -function remove_octoprint_env() { - local files - files=$(find "${HOME}" -maxdepth 1 -regextype posix-extended -regex "${HOME}/OctoPrint(_[0-9a-zA-Z]+)?" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -rf "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoprint_dir() { - local files - files=$(find "${HOME}" -maxdepth 1 -regextype posix-extended -regex "${HOME}/.octoprint(_[0-9a-zA-Z]+)?" | sort) - - if [[ -n ${files} ]]; then - for file in ${files}; do - status_msg "Removing ${file} ..." - rm -rf "${file}" - ok_msg "${file} removed!" - done - fi -} - -function remove_octoprint() { - remove_octoprint_service - remove_octoprint_sudoers - remove_octoprint_env - remove_octoprint_dir - - local confirm="OctoPrint was successfully removed!" - - print_confirm "${confirm}" && return -} - -#=================================================# -#=============== OCTOPRINT STATUS ================# -#=================================================# - -function get_octoprint_status() { - local sf_count env_count dir_count status - sf_count="$(octoprint_systemd | wc -w)" - env_count=$(find "${HOME}" -maxdepth 1 -regextype posix-extended -regex "${HOME}/OctoPrint(_[0-9a-zA-Z]+)?" | wc -w) - dir_count=$(find "${HOME}" -maxdepth 1 -regextype posix-extended -regex "${HOME}/.octoprint(_[0-9a-zA-Z]+)?" | wc -w) - - if (( sf_count == 0 )) && (( env_count == 0 )) && (( dir_count == 0 )); then - status="Not installed!" - elif (( sf_count == env_count )) && (( sf_count == dir_count )); then - status="Installed: ${sf_count}" - else - status="Incomplete!" - fi - - echo "${status}" -} diff --git a/scripts/pretty_gcode.sh b/scripts/pretty_gcode.sh deleted file mode 100644 index a3e1130..0000000 --- a/scripts/pretty_gcode.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#=================================================# -#================== INSTALL PGC ==================# -#=================================================# - -function install_pgc_for_klipper() { - local pgconfsrc="${PGC_DIR}/pgcode.local.conf" - local pgconf="/etc/nginx/sites-available/pgcode.local.conf" - local pgconfsl="/etc/nginx/sites-enabled/pgcode.local.conf" - local pgc_uri pgc_custom_port pgc_default_port="7136" - - status_msg "Installing PrettyGCode for Klipper ..." - echo -e "${cyan}\n###### On which port should PrettyGCode run? (Default: ${pgc_default_port})${white} " - read -e -p "${cyan}###### Port:${white} " -i "${pgc_default_port}" pgc_custom_port - - ### check nginx dependency - local dep=(nginx) - dependency_check "${dep[@]}" - - [[ -d ${PGC_DIR} ]] && rm -rf "${PGC_DIR}" - - cd "${HOME}" || exit 1 - if ! git clone "${PGC_REPO}" "${PGC_DIR}"; then - print_error "Cloning PrettyGCode for Klipper from\n ${PGC_REPO}\n failed!" - exit 1 - fi - - sudo cp "${pgconfsrc}" "${pgconf}" - sudo sed -i "s|/home/pi/pgcode;|${HOME}/pgcode;|" "${pgconf}" - - ### replace default port - if (( pgc_custom_port != pgc_default_port )); then - sudo sed -i "s|listen ${pgc_default_port};|listen ${pgc_custom_port};|" "${pgconf}" - sudo sed -i "s|listen \[::\]:${pgc_default_port};|listen \[::\]:${pgc_custom_port};|" "${pgconf}" - fi - - [[ ! -L ${pgconfsl} ]] && sudo ln -s "${pgconf}" "${pgconfsl}" - sudo systemctl restart nginx - - pgc_uri="http://$(hostname -I | cut -d" " -f1):${pgc_custom_port}" - echo -e "${cyan}\n● Accessible via:${white} ${pgc_uri}" - ok_msg "PrettyGCode for Klipper installed!\n" -} - -#=================================================# -#=================== REMOVE PGC ==================# -#=================================================# - -function remove_prettygcode() { - local pgconf="/etc/nginx/sites-available/pgcode.local.conf" - local pgconfsl="/etc/nginx/sites-enabled/pgcode.local.conf" - - if [[ -d ${PGC_DIR} || -f ${pgconf} || -L ${pgconfsl} ]]; then - status_msg "Removing PrettyGCode for Klipper ..." - rm -rf "${PGC_DIR}" - sudo rm -f "${pgconf}" - sudo rm -f "${pgconfsl}" - sudo systemctl restart nginx - print_confirm "PrettyGCode for Klipper successfully removed!" - else - print_error "PrettyGCode for Klipper not found!\n Skipping..." - fi -} - -#=================================================# -#=================== UPDATE PGC ==================# -#=================================================# - -function update_pgc_for_klipper() { - [[ ! -d ${PGC_DIR} ]] && return - - status_msg "Updating PrettyGCode for Klipper ..." - cd "${PGC_DIR}" && git pull - ok_msg "Update complete!" -} - -#=================================================# -#=================== PGC STATUS ==================# -#=================================================# - -function get_local_prettygcode_commit() { - local commit - - [[ ! -d ${PGC_DIR} || ! -d "${PGC_DIR}/.git" ]] && return - cd "${PGC_DIR}" - commit="$(git describe HEAD --always --tags | cut -d "-" -f 1,2)" - echo "${commit}" -} - -function get_remote_prettygcode_commit() { - local commit - - [[ ! -d ${PGC_DIR} || ! -d "${PGC_DIR}/.git" ]] && return - cd "${PGC_DIR}" && git fetch origin -q - commit=$(git describe origin/main --always --tags | cut -d "-" -f 1,2) - echo "${commit}" -} - -function compare_prettygcode_versions() { - local versions local_ver remote_ver - local_ver="$(get_local_prettygcode_commit)" - remote_ver="$(get_remote_prettygcode_commit)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add moonraker to application_updates_available in kiauh.ini - add_to_application_updates "pgc_for_klipper" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} diff --git a/scripts/rollback.sh b/scripts/rollback.sh deleted file mode 100755 index 8cf837d..0000000 --- a/scripts/rollback.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function rollback_menu() { - top_border - echo -e "| $(title_msg "~~~~~~~~~~~~~ [ Rollback Menu ] ~~~~~~~~~~~~~") |" - hr - echo -e "| If serious errors occured after updating Klipper or |" - echo -e "| Moonraker, you can use this menu to try and reset the |" - echo -e "| repository to an earlier state. |" - hr - echo -e "| 1) Rollback Klipper |" - echo -e "| 2) Rollback Moonraker |" - back_footer - - local action - while true; do - read -p "${cyan}###### Perform action:${white} " action - case "${action}" in - 1) - select_msg "Klipper" - rollback_component "klipper" - break;; - 2) - select_msg "Moonraker" - rollback_component "moonraker" - break;; - B|b) - clear; advanced_menu; break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function rollback_component() { - local component=${1} - - if [[ ! -d "${HOME}/${component}" ]]; then - print_error "Rollback not possible! Missing installation?" - return - fi - - echo - top_border - echo -e "| Please select how many commits you want to revert. |" - echo -e "| Consider using the information provided by the GitHub |" - echo -e "| commit history to decide how many commits to revert. |" - blank_line - echo -e "| ${red}Warning:${white} |" - echo -e "| ${red}Do not proceed if you are currently in the progress${white} |" - echo -e "| ${red}of printing! Proceeding WILL terminate that print!${white} |" - back_footer - - local count - while true; do - read -p "${cyan}###### Revert this amount of commits:${white} " count - if [[ -n ${count} ]] && (( count > 0 )); then - status_msg "Revert ${component^} by ${count} commits ..." - cd "${HOME}/${component}" - if git reset --hard HEAD~"${count}"; then - do_action_service "restart" "${component}" - print_confirm "${component^} was successfully reset!" - else - print_error "Reverting ${component^} failed! Please see the console output above." - fi - break - elif [[ ${count} == "B" || ${count} == "b" ]]; then - clear && print_header && break - else - error_msg "Invalid command!" - fi - done - rollback_menu -} diff --git a/scripts/spoolman.sh b/scripts/spoolman.sh deleted file mode 100644 index fd29edd..0000000 --- a/scripts/spoolman.sh +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -# Error Handling -set -e - -function install_spoolman() { - - pushd "${HOME}" &> /dev/null || exit 1 - - dependency_check curl jq - - if [[ ! -d "${SPOOLMAN_DIR}" && -z "$(ls -A "${SPOOLMAN_DIR}" 2> /dev/null)" ]]; then - status_msg "Downloading spoolman..." - setup_spoolman_folder - status_msg "Downloading complete" - start_install_script - advanced_config_prompt - else - ### In case spoolman is "incomplete" rerun install script - if get_spoolman_status | grep -q "Incomplete!"; then - start_install_script - exit 1 - fi - - ok_msg "Spoolman already installed" - exit 1 - fi - - enable_moonraker_integration_prompt - patch_spoolman_update_manager - - do_action_service "restart" "moonraker" -} - -function update_spoolman() { - ### stop and disable old spoolman service - do_action_service "stop" "Spoolman" - do_action_service "disable" "Spoolman" - - mv "${SPOOLMAN_DIR}" "${SPOOLMAN_DIR}_old" - - setup_spoolman_folder - cp "${SPOOLMAN_DIR}_old/.env" "${SPOOLMAN_DIR}/.env" - - start_install_script - - rm -rf "${SPOOLMAN_DIR}_old" -} - -function remove_spoolman(){ - if [[ -d "${SPOOLMAN_DIR}" ]]; then - status_msg "Removing spoolman service..." - do_action_service "stop" "Spoolman" - do_action_service "disable" "Spoolman" - sudo rm -f "${SYSTEMD}/Spoolman.service" - sudo systemctl daemon-reload - sudo systemctl reset-failed - ok_msg "Spoolman service removed!" - - status_msg "Removing spoolman directory..." - rm -rf "${SPOOLMAN_DIR}" - ok_msg "Spoolman directory removed!" - fi - - print_confirm "Spoolman successfully removed!" -} - -function update_moonraker_configs() { - local moonraker_configs regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/config\/moonraker\.conf" - moonraker_configs=$(find "${HOME}" -maxdepth 3 -type f -regextype posix-extended -regex "${regex}" | sort) - - for conf in ${moonraker_configs}; do - if ! grep -Eq "^\[update_manager Spoolman\]\s*$" "${conf}"; then - ### add new line to conf if it doesn't end with one - [[ $(tail -c1 "${conf}" | wc -l) -eq 0 ]] && echo "" >> "${conf}" - /bin/sh -c "cat >> ${conf}" << MOONRAKER_CONF -${1} -MOONRAKER_CONF - fi - done -} - -function enable_moonraker_integration() { - local integration_str env_port - # get spoolman port from .env - env_port=$(grep "^SPOOLMAN_PORT=" "${SPOOLMAN_DIR}/.env" | cut -d"=" -f2) - - integration_str=" -[spoolman] -server: http://$(hostname -I | cut -d" " -f1):${env_port} -" - - status_msg "Adding Spoolman integration..." - update_moonraker_configs "${integration_str}" -} - -function patch_spoolman_update_manager() { - local updater_str - updater_str=" -[update_manager Spoolman] -type: zip -channel: stable -repo: Donkie/Spoolman -path: ${SPOOLMAN_DIR} -virtualenv: .venv -requirements: requirements.txt -persistent_files: - .venv - .env -managed_services: Spoolman -" - - update_moonraker_configs "${updater_str}" - - # add spoolman service to moonraker.asvc - local moonraker_asvc regex - regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/moonraker\.asvc" - moonraker_asvc=$(find "${HOME}" -maxdepth 2 -type f -regextype posix-extended -regex "${regex}" | sort) - - if ! grep -q "^Spoolman$" "${moonraker_asvc}"; then - status_msg "Adding Spoolman service to moonraker.asvc..." - sed -i '$a''Spoolman' "${moonraker_asvc}" - fi -} - -function advanced_config_prompt() { - local reply - while true; do - read -erp "${cyan}###### Continue with default configuration? (Y/n):${white} " reply - case "${reply}" in - Y|y|Yes|yes|"") - select_msg "Yes" - break;; - N|n|No|no) - select_msg "No" - advanced_config - break;; - *) - error_msg "Invalid Input!\n";; - esac - done - return 0 -} - -function enable_moonraker_integration_prompt() { - local reply - while true; do - read -erp "${cyan}###### Enable Moonraker integration? (Y/n):${white} " reply - case "${reply}" in - Y|y|Yes|yes|"") - select_msg "Yes" - enable_moonraker_integration - break;; - N|n|No|no) - select_msg "No" - break;; - *) - error_msg "Invalid Input!\n";; - esac - done - return 0 -} - -function advanced_config() { - status_msg "###### Advanced configuration" - - local reply - while true; do - read -erp "${cyan}###### Select spoolman port (7912):${white} " reply - ### set default - if [[ -z "${reply}" ]]; then - reply="7912" - fi - - select_msg "${reply}" - ### check if port is valid - if ! [[ "${reply}" =~ ^[0-9]+$ && "${reply}" -ge 1024 && "${reply}" -le 65535 ]]; then - error_msg "Invalid port number!\n" - continue - fi - - ### update .env - sed -i "s/^SPOOLMAN_PORT=.*$/SPOOLMAN_PORT=${reply}/" "${SPOOLMAN_DIR}/.env" - do_action_service "restart" "Spoolman" - break - done - return 0 -} - -function setup_spoolman_folder() { - local source_url - ### get latest spoolman release url - source_url="$(curl -s "${SPOOLMAN_REPO}" | jq -r '.assets[] | select(.name == "spoolman.zip").browser_download_url')" - - mkdir -p "${SPOOLMAN_DIR}" - curl -sSL "${source_url}" -o /tmp/temp.zip - unzip /tmp/temp.zip -d "${SPOOLMAN_DIR}" &> /dev/null - rm /tmp/temp.zip - - chmod +x "${SPOOLMAN_DIR}"/scripts/install.sh -} - -function start_install_script() { - - pushd "${SPOOLMAN_DIR}" &> /dev/null || exit 1 - - if bash ./scripts/install.sh; then - ok_msg "Spoolman successfully installed!" - else - print_error "Spoolman installation failed!" - exit 1 - fi -} - -function get_spoolman_status() { - local -a files - files=( - "${SPOOLMAN_DIR}" - "${SYSTEMD}/Spoolman.service" - "${SPOOLMAN_DB_DIR}" - ) - - local count - count=0 - - for file in "${files[@]}"; do - [[ -e "${file}" ]] && count=$(( count +1 )) - done - - if [[ "${count}" -eq "${#files[*]}" ]]; then - echo "Installed" - elif [[ "${count}" -gt 0 ]]; then - echo "Incomplete!" - else - echo "Not installed!" - fi -} - -function get_local_spoolman_version() { - [[ ! -d "${SPOOLMAN_DIR}" ]] && return - - local version - version=$(grep -o '"version":\s*"[^"]*' "${SPOOLMAN_DIR}"/release_info.json | cut -d'"' -f4) - echo "${version}" -} - -function get_remote_spoolman_version() { - [[ ! -d "${SPOOLMAN_DIR}" ]] && return - - local version - version=$(curl -s "${SPOOLMAN_REPO}" | grep -o '"tag_name":\s*"v[^"]*"' | cut -d'"' -f4) - echo "${version}" -} - -function compare_spoolman_versions() { - local local_ver remote_ver - local_ver="$(get_local_spoolman_version)" - remote_ver="$(get_remote_spoolman_version)" - - if [[ ${local_ver} != "${remote_ver}" ]]; then - versions="${yellow}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - # add spoolman to application_updates_available in kiauh.ini - add_to_application_updates "spoolman" - else - versions="${green}$(printf " %-14s" "${local_ver}")${white}" - versions+="|${green}$(printf " %-13s" "${remote_ver}")${white}" - fi - - echo "${versions}" -} diff --git a/scripts/switch_klipper_repo.sh b/scripts/switch_klipper_repo.sh deleted file mode 100644 index 7f224c2..0000000 --- a/scripts/switch_klipper_repo.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function change_klipper_repo_menu() { - local repo_file="${KIAUH_SRCDIR}/klipper_repos.txt" - local repo branch repos=() branches=() - - if [[ ! -f ${repo_file} ]]; then - print_error "File not found:\n '${KIAUH_SRCDIR}/klipper_repos.txt'" - return - fi - - ### generate the repolist from the klipper_repos.txt textfile - while IFS="," read -r repo branch; do - repo=$(echo "${repo}" | sed -r "s/^http(s)?:\/\/github.com\///" | sed "s/\.git$//" ) - repos+=("${repo}") - ### if branch is not given, default to 'master' - [[ -z ${branch} ]] && branch="master" - branches+=("${branch}") - done < <(grep -E "^[^#]" "${repo_file}") - - top_border - echo -e "| ~~~~~~~~ [ Set custom Klipper repo ] ~~~~~~~~ | " - hr - blank_line - ### print repolist - local i=0 - for _ in "${repos[@]}"; do - printf "| %s) %-63s|\n" "${i}" "${yellow}${repos[${i}]}${white} β†’ ${branches[${i}]}" - i=$(( i + 1 )) - done - blank_line - back_help_footer - - local option - local num="^[0-9]+$" - local back="^(B|b)$" - local help="^(H|h)$" - - while true; do - read -p "${cyan}###### Perform action:${white} " option - - if [[ ${option} =~ ${num} && ${option} -lt ${#repos[@]} ]]; then - select_msg "Repo: ${repos[option]} Branch: ${branches[option]}" - - if [[ -d ${KLIPPER_DIR} ]]; then - top_border - echo -e "| ${red}!!! ATTENTION !!!${white} |" - echo -e "| Existing Klipper folder found! Proceeding will remove | " - echo -e "| the existing Klipper folder and replace it with a | " - echo -e "| clean copy of the previously selected source repo! | " - bottom_border - - local yn - while true; do - read -p "${cyan}###### Proceed? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - switch_klipper_repo "${repos[${option}]}" "${branches[${option}]}" - set_custom_klipper_repo "${repos[${option}]}" "${branches[${option}]}" - break;; - N|n|No|no) - select_msg "No" - break;; - *) - error_msg "Invalid command!";; - esac - done - break - else - status_msg "Set custom Klipper repository to:\n ● Repository: ${repos[${option}]}\n ● Branch: ${branches[${option}]}" - set_custom_klipper_repo "${repos[${option}]}" "${branches[${option}]}" - ok_msg "This repo will now be used for new Klipper installations!\n" - break - fi - - elif [[ ${option} =~ ${back} ]]; then - clear && print_header - settings_menu - elif [[ ${option} =~ ${help} ]]; then - clear && print_header - show_custom_klipper_repo_help - else - error_msg "Invalid command!" - fi - done - - change_klipper_repo_menu -} - -#================================================# -#=================== HELPERS ====================# -#================================================# - -function switch_klipper_repo() { - local repo=${1} branch=${2} - - status_msg "Switching Klipper repository..." - do_action_service "stop" "klipper" - - [[ -d ${KLIPPER_DIR} ]] && rm -rf "${KLIPPER_DIR}" - clone_klipper "${repo}" "${branch}" - - do_action_service "start" "klipper" -} - -function show_custom_klipper_repo_help() { - top_border - echo -e "| ~~~~ < ? > Help: Custom Klipper repo < ? > ~~~~ |" - hr - echo -e "| With this setting, it is possible to install Klipper |" - echo -e "| from a custom repository. It will also switch an |" - echo -e "| existing Klipper installation to the newly selected |" - echo -e "| source repository. |" - echo -e "| A list of repositories is automatically generated by |" - echo -e "| a 'klipper_repos.txt' textfile in KIAUHs root folder. |" - echo -e "| An example file is provided at the same location. |" - blank_line - back_footer - - local choice - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - B|b) - clear && print_header - change_klipper_repo_menu - break;; - *) - deny_action "show_settings_help";; - esac - done -} diff --git a/scripts/ui/advanced_menu.sh b/scripts/ui/advanced_menu.sh deleted file mode 100755 index 12923e5..0000000 --- a/scripts/ui/advanced_menu.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function advanced_ui() { - top_border - echo -e "| ${yellow}~~~~~~~~~~~~~ [ Advanced Menu ] ~~~~~~~~~~~~~${white} |" - hr - echo -e "| Klipper & API: | Mainsail: |" - echo -e "| 1) [Rollback] | 6) [Theme installer] |" - echo -e "| | |" - echo -e "| Firmware: | System: |" - echo -e "| 2) [Build only] | 7) [Change hostname] |" - echo -e "| 3) [Flash only] | |" - echo -e "| 4) [Build + Flash] | Extras: |" - echo -e "| 5) [Get MCU ID] | 8) [G-Code Shell Command] |" - back_footer -} - -function advanced_menu() { - do_action "" "advanced_ui" - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 1) - do_action "rollback_menu" "advanced_menu";; - 2) - do_action "build_fw" "advanced_ui";; - 3) - clear && print_header - do_action "init_flash_process" "advanced_ui";; - 4) - clear && print_header - status_msg "Please wait..." - build_fw && init_flash_process - advanced_ui;; - 5) - clear && print_header - select_mcu_connection - print_detected_mcu_to_screen - advanced_ui;; - 6) - do_action "ms_theme_installer_menu";; - 7) - clear - print_header - set_custom_hostname - advanced_ui;; - 8) - do_action "setup_gcode_shell_command" "advanced_ui";; - B|b) - clear; main_menu; break;; - *) - deny_action "advanced_ui";; - esac - done - advanced_menu -} diff --git a/scripts/ui/backup_menu.sh b/scripts/ui/backup_menu.sh deleted file mode 100755 index 783476c..0000000 --- a/scripts/ui/backup_menu.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function backup_ui() { - top_border - echo -e "| $(title_msg "~~~~~~~~~~~~~~ [ Backup Menu ] ~~~~~~~~~~~~~~") |" - hr - echo -e "| ${yellow}INFO: Backups are located in '~/kiauh-backups'${white} |" - hr - echo -e "| Klipper & API: | Touchscreen GUI: |" - echo -e "| 1) [Klipper] | 7) [KlipperScreen] |" - echo -e "| 2) [Moonraker] | |" - echo -e "| 3) [Config Folder] | 3rd Party Webinterface: |" - echo -e "| 4) [Moonraker Database] | 8) [OctoPrint] |" - echo -e "| | |" - echo -e "| Klipper Webinterface: | Other: |" - echo -e "| 5) [Mainsail] | 9) [Telegram Bot] |" - echo -e "| 6) [Fluidd] | 10) [OctoEverywhere] |" - echo -e "| | 11) [Spoolman] |" - back_footer -} - -function backup_menu() { - do_action "" "backup_ui" - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 1) - do_action "backup_klipper" "backup_ui";; - 2) - do_action "backup_moonraker" "backup_ui";; - 3) - do_action "backup_config_dir" "backup_ui";; - 4) - do_action "backup_moonraker_database" "backup_ui";; - 5) - do_action "backup_mainsail" "backup_ui";; - 6) - do_action "backup_fluidd" "backup_ui";; - 7) - do_action "backup_klipperscreen" "backup_ui";; - 8) - do_action "backup_octoprint" "backup_ui";; - 9) - do_action "backup_telegram_bot" "backup_ui";; - 10) - do_action "backup_octoeverywhere" "backup_ui";; - 11) - do_action "backup_spoolman" "backup_ui";; - B|b) - clear; main_menu; break;; - *) - deny_action "backup_ui";; - esac - done - backup_menu -} diff --git a/scripts/ui/general_ui.sh b/scripts/ui/general_ui.sh deleted file mode 100755 index 2abf794..0000000 --- a/scripts/ui/general_ui.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#ui total width = 57 chars -function top_border() { - echo -e "/=======================================================\\" -} - -function bottom_border() { - echo -e "\=======================================================/" -} - -function blank_line() { - echo -e "| |" -} - -function hr() { - echo -e "|-------------------------------------------------------|" -} - -function quit_footer() { - hr - echo -e "| ${red}Q) Quit${white} |" - bottom_border -} - -function back_footer() { - hr - echo -e "| ${green}B) Β« Back${white} |" - bottom_border -} - -function back_help_footer() { - hr - echo -e "| ${green}B) Β« Back${white} | ${yellow}H) Help [?]${white} |" - bottom_border -} - -function print_header() { - top_border - echo -e "| $(title_msg "~~~~~~~~~~~~~~~~~ [ KIAUH ] ~~~~~~~~~~~~~~~~~") |" - echo -e "| $(title_msg " Klipper Installation And Update Helper ") |" - echo -e "| $(title_msg "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") |" - bottom_border -} - -function do_action() { - clear && print_header - ### $1 is the action the user wants to fire - $1 -# print_msg && clear_msg - ### $2 is the menu the user usually gets directed back to after an action is completed - $2 -} - -function deny_action() { - clear && print_header - print_error "Invalid command!" - $1 -} diff --git a/scripts/ui/install_menu.sh b/scripts/ui/install_menu.sh deleted file mode 100755 index 3501e0a..0000000 --- a/scripts/ui/install_menu.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function install_ui() { - top_border - echo -e "| ${green}~~~~~~~~~~~ [ Installation Menu ] ~~~~~~~~~~~${white} |" - hr - echo -e "| You need this menu usually only for installing |" - echo -e "| all necessary dependencies for the various |" - echo -e "| functions on a completely fresh system. |" - hr - echo -e "| Firmware & API: | Other: |" - echo -e "| 1) [Klipper] | 7) [PrettyGCode] |" - echo -e "| 2) [Moonraker] | 8) [Telegram Bot] |" - echo -e "| | 9) $(obico_install_title) |" - echo -e "| Klipper Webinterface: | 10) [OctoEverywhere] |" - echo -e "| 3) [Mainsail] | 11) [Mobileraker] |" - echo -e "| 4) [Fluidd] | 12) [OctoApp for Klipper] |" - echo -e "| | 13) [Spoolman] |" - echo -e "| Touchscreen GUI: | |" - echo -e "| 5) [KlipperScreen] | Webcam Streamer: |" - echo -e "| | 14) [Crowsnest] |" - echo -e "| 3rd Party Webinterface: | |" - echo -e "| 6) [OctoPrint] | |" - back_footer -} - -function install_menu() { - clear -x && sudo true && clear -x # (re)cache sudo credentials so password prompt doesn't bork ui - print_header - install_ui - - ### save all installed webinterface ports to the ini file - fetch_webui_ports - - ### save all klipper multi-instance names to the ini file - set_multi_instance_names - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 1) - do_action "start_klipper_setup" "install_ui";; - 2) - do_action "moonraker_setup_dialog" "install_ui";; - 3) - do_action "install_mainsail" "install_ui";; - 4) - do_action "install_fluidd" "install_ui";; - 5) - do_action "install_klipperscreen" "install_ui";; - 6) - do_action "octoprint_setup_dialog" "install_ui";; - 7) - do_action "install_pgc_for_klipper" "install_ui";; - 8) - do_action "telegram_bot_setup_dialog" "install_ui";; - 9) - do_action "moonraker_obico_setup_dialog" "install_ui";; - 10) - do_action "octoeverywhere_setup_dialog" "install_ui";; - 11) - do_action "install_mobileraker" "install_ui";; - 12) - do_action "octoapp_setup_dialog" "install_ui";; - 13) - do_action "install_spoolman" "install_ui";; - 14) - do_action "install_crowsnest" "install_ui";; - B|b) - clear; main_menu; break;; - *) - deny_action "install_ui";; - esac - done - install_menu -} diff --git a/scripts/ui/main_menu.sh b/scripts/ui/main_menu.sh deleted file mode 100755 index 14cc1a4..0000000 --- a/scripts/ui/main_menu.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function main_ui() { - top_border - echo -e "| $(title_msg "~~~~~~~~~~~~~~~ [ Main Menu ] ~~~~~~~~~~~~~~~") |" - hr - echo -e "| 0) [Log-Upload] | Klipper: $(print_status "klipper")|" - echo -e "| | Repo: $(print_klipper_repo)|" - echo -e "| 1) [Install] | |" - echo -e "| 2) [Update] | Moonraker: $(print_status "moonraker")|" - echo -e "| 3) [Remove] | |" - echo -e "| 4) [Advanced] | Mainsail: $(print_status "mainsail")|" - echo -e "| 5) [Backup] | Fluidd: $(print_status "fluidd")|" - echo -e "| | KlipperScreen: $(print_status "klipperscreen")|" - echo -e "| 6) [Settings] | Telegram Bot: $(print_status "telegram_bot")|" - echo -e "| | Crowsnest: $(print_status "crowsnest")|" - echo -e "| | Obico: $(print_status "moonraker_obico")|" - echo -e "| | OctoEverywhere: $(print_status "octoeverywhere")|" - echo -e "| | Mobileraker: $(print_status "mobileraker")|" - echo -e "| | OctoApp: $(print_status "octoapp")|" - echo -e "| | Spoolman: $(print_status "spoolman")|" - echo -e "| | |" - echo -e "| | Octoprint: $(print_status "octoprint")|" - hr - echo -e "| $(print_kiauh_version)| Changelog: ${magenta}https://git.io/JnmlX${white} |" - quit_footer -} - -function get_kiauh_version() { - local version - cd "${KIAUH_SRCDIR}" - version="$(git tag -l 'v5*' | tail -1)" - echo "${version}" -} - -function print_kiauh_version() { - local version - version="$(printf "%-16s" "$(get_kiauh_version)")" - echo "${cyan}${version}${white}" -} - -function print_status() { - local status component="${1}" - status=$(get_"${component}"_status) - - if [[ ${status} == "Not installed!" ]]; then - status="${red}${status}${white}" - elif [[ ${status} == "Incomplete!" ]]; then - status="${yellow}${status}${white}" - elif [[ ${status} == "Not linked!" ]]; then - ### "Not linked!" is only required for Obico for Klipper - status="${yellow}${status}${white}" - else - status="${green}${status}${white}" - fi - - printf "%-28s" "${status}" -} - -function print_klipper_repo() { - read_kiauh_ini - - local repo klipper_status - klipper_status=$(get_klipper_status) - repo=$(echo "${custom_klipper_repo}" | sed "s/https:\/\/github\.com\///" | sed "s/\.git$//") - repo="${repo^^}" - - if [[ ${klipper_status} == "Not installed!" ]]; then - repo="${red}-${white}" - elif [[ -n ${repo} && ${repo} != "KLIPPER3D/KLIPPER" ]]; then - repo="${cyan}custom${white}" - else - repo="${cyan}Klipper3d/klipper${white}" - fi - - printf "%-28s" "${repo}" -} - - - -function main_menu() { - clear && print_header - main_ui - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - "start klipper") do_action_service "start" "klipper"; main_ui;; - "stop klipper") do_action_service "stop" "klipper"; main_ui;; - "restart klipper") do_action_service "restart" "klipper"; main_ui;; - "start moonraker") do_action_service "start" "moonraker"; main_ui;; - "stop moonraker") do_action_service "stop" "moonraker"; main_ui;; - "restart moonraker")do_action_service "restart" "moonraker"; main_ui;; - "start octoprint") do_action_service "start" "octoprint"; main_ui;; - "stop octoprint") do_action_service "stop" "octoprint"; main_ui;; - "restart octoprint") do_action_service "restart" "octoprint"; main_ui;; - "start crowsnest") do_action_service "start" "crowsnest"; main_ui;; - "stop crowsnest") do_action_service "stop" "crowsnest"; main_ui;; - "restart crowsnest") do_action_service "restart" "crowsnest"; main_ui;; - update) do_action "update_kiauh" "main_ui";; - 0)clear && print_header - upload_selection - main_ui;; - 1)clear && print_header - install_menu - break;; - 2) clear && print_header - update_menu - break;; - 3) clear && print_header - remove_menu - break;; - 4)clear && print_header - advanced_menu - break;; - 5)clear && print_header - backup_menu - main_ui;; - 6)clear && print_header - settings_menu - break;; - Q|q) - echo -e "${green}###### Happy printing! ######${white}"; echo - exit 0;; - *) - deny_action "main_ui";; - esac - done - main_menu -} diff --git a/scripts/ui/remove_menu.sh b/scripts/ui/remove_menu.sh deleted file mode 100755 index a24d1c9..0000000 --- a/scripts/ui/remove_menu.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function remove_ui() { - top_border - echo -e "| ${red}~~~~~~~~~~~~~~ [ Remove Menu ] ~~~~~~~~~~~~~~${white} |" - hr - echo -e "| ${yellow}INFO: Configurations and/or any backups will be kept!${white} |" - hr - echo -e "| Firmware & API: | Webcam Streamer: |" - echo -e "| 1) [Klipper] | 9) [Crowsnest] |" - echo -e "| 2) [Moonraker] | 10) [MJPG-Streamer] |" - echo -e "| | |" - echo -e "| Klipper Webinterface: | Other: |" - echo -e "| 3) [Mainsail] | 11) [PrettyGCode] |" - echo -e "| 4) [Mainsail-Config] | 12) [Telegram Bot] |" - echo -e "| 5) [Fluidd] | 13) [Obico for Klipper] |" - echo -e "| 6) [Fluidd-Config] | 14) [OctoEverywhere] |" - echo -e "| | 15) [Mobileraker] |" - echo -e "| Touchscreen GUI: | 16) [NGINX] |" - echo -e "| 7) [KlipperScreen] | 17) [OctoApp] |" - echo -e "| | 18) [Spoolman] |" - echo -e "| 3rd Party Webinterface: | |" - echo -e "| 8) [OctoPrint] | |" - back_footer -} - -function remove_menu() { - do_action "" "remove_ui" - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 1) - do_action "remove_klipper" "remove_ui";; - 2) - do_action "remove_moonraker" "remove_ui";; - 3) - do_action "remove_mainsail" "remove_ui";; - 4) - do_action "remove_mainsail_config" "remove_ui";; - 5) - do_action "remove_fluidd" "remove_ui";; - 6) - do_action "remove_fluidd_config" "remove_ui";; - 7) - do_action "remove_klipperscreen" "remove_ui";; - 8) - do_action "remove_octoprint" "remove_ui";; - 9) - do_action "remove_crowsnest" "remove_ui";; - 10) - do_action "remove_mjpg-streamer" "remove_ui";; - 11) - do_action "remove_prettygcode" "remove_ui";; - 12) - do_action "remove_telegram_bot" "remove_ui";; - 13) - do_action "remove_moonraker_obico" "remove_ui";; - 14) - do_action "remove_octoeverywhere" "remove_ui";; - 15) - do_action "remove_mobileraker" "remove_ui";; - 16) - do_action "remove_nginx" "remove_ui";; - 17) - do_action "remove_octoapp" "remove_ui";; - 18) - do_action "remove_spoolman" "remove_ui";; - B|b) - clear; main_menu; break;; - *) - deny_action "remove_ui";; - esac - done - remove_menu -} diff --git a/scripts/ui/settings_menu.sh b/scripts/ui/settings_menu.sh deleted file mode 100755 index 0cf4835..0000000 --- a/scripts/ui/settings_menu.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function settings_ui() { - read_kiauh_ini "${FUNCNAME[0]}" - - local custom_repo="${custom_klipper_repo}" - local custom_branch="${custom_klipper_repo_branch}" - local ms_pre_rls="${mainsail_install_unstable}" - local fl_pre_rls="${fluidd_install_unstable}" - local bbu="${backup_before_update}" - - ### custom repository - custom_repo=$(echo "${custom_repo}" | sed "s/https:\/\/github\.com\///" | sed "s/\.git$//" ) - if [[ -z ${custom_repo} ]]; then - custom_repo="${cyan}Klipper3D/klipper${white}" - else - custom_repo="${cyan}${custom_repo}${white}" - fi - - ### custom repository branch - if [[ -z ${custom_branch} ]]; then - custom_branch="${cyan}master${white}" - else - custom_branch="${cyan}${custom_branch}${white}" - fi - - ### webinterface stable toggle - if [[ ${ms_pre_rls} == "false" ]]; then - ms_pre_rls="${red}● ${ms_pre_rls}${white}" - else - ms_pre_rls="${green}● ${ms_pre_rls}${white}" - fi - - if [[ ${fl_pre_rls} == "false" ]]; then - fl_pre_rls="${red}● ${fl_pre_rls}${white}" - else - fl_pre_rls="${green}● ${fl_pre_rls}${white}" - fi - - ### backup before update toggle - if [[ "${bbu}" == "false" ]]; then - bbu="${red}● ${bbu}${white}" - else - bbu="${green}● ${bbu}${white}" - fi - - top_border - echo -e "| $(title_msg "~~~~~~~~~~~~ [ KIAUH Settings ] ~~~~~~~~~~~~~") |" - hr - echo -e "| Klipper: |" - echo -e "| ● Repository: |" - printf "| %-70s|\n" "${custom_repo} (${custom_branch})" - hr - echo -e "| Install unstable releases: |" - printf "| Mainsail: %-29sFluidd: %-27s|\n" "${ms_pre_rls}" "${fl_pre_rls}" - hr - printf "| Backup before updating: %-42s|\n" "${bbu}" - hr - echo -e "| 1) Set custom Klipper repository |" - blank_line - if [[ ${mainsail_install_unstable} == "false" ]]; then - echo -e "| 2) ${green}Allow${white} unstable Mainsail releases |" - else - echo -e "| 2) ${red}Disallow${white} unstable Mainsail releases |" - fi - if [[ ${fluidd_install_unstable} == "false" ]]; then - echo -e "| 3) ${green}Allow${white} unstable Fluidd releases |" - else - echo -e "| 3) ${red}Disallow${white} unstable Fluidd releases |" - fi - blank_line - if [[ ${backup_before_update} == "false" ]]; then - echo -e "| 4) ${green}Enable${white} automatic backups before updates |" - else - echo -e "| 4) ${red}Disable${white} automatic backups before updates |" - fi - back_help_footer -} - -function show_settings_help() { - local default_cfg="${cyan}${HOME}/klipper_config${white}" - - top_border - echo -e "| ~~~~~~ < ? > Help: KIAUH Settings < ? > ~~~~~~ |" - hr - echo -e "| ${cyan}Install unstable releases:${white} |" - echo -e "| If set to ${green}true${white}, KIAUH installs/updates the software |" - echo -e "| with the latest, currently available release. |" - echo -e "| ${yellow}This will include alpha, beta and rc releases!${white} |" - blank_line - echo -e "| If set to ${red}false${white}, KIAUH installs/updates the software |" - echo -e "| with the most recent stable release. |" - blank_line - echo -e "| Default: ${red}false${white} |" - blank_line - hr - echo -e "| ${cyan}Backup before updating:${white} |" - echo -e "| If set to true, KIAUH will automatically create a |" - echo -e "| backup from the corresponding component you are about |" - echo -e "| to update before actually updating it, preserving the |" - echo -e "| current state of the component in a safe location. |" - echo -e "| All backups are stored in '~/kiauh_backups'. |" - blank_line - echo -e "| Default: ${red}false${white} |" - blank_line - back_footer - - local choice - while true; do - read -p "${cyan}###### Please select:${white} " choice - case "${choice}" in - B|b) - clear && print_header - settings_menu - break;; - *) - deny_action "show_settings_help";; - esac - done -} - -function settings_menu() { - clear && print_header - settings_ui - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 1) - clear && print_header - change_klipper_repo_menu - settings_ui;; - 2) - switch_mainsail_releasetype - settings_menu;; - 3) - switch_fluidd_releasetype - settings_menu;; - 4) - toggle_backup_before_update - settings_menu;; - B|b) - clear - main_menu - break;; - H|h) - clear && print_header - show_settings_help - break;; - *) - deny_action "settings_ui";; - esac - done -} diff --git a/scripts/ui/update_menu.sh b/scripts/ui/update_menu.sh deleted file mode 100755 index 8e85599..0000000 --- a/scripts/ui/update_menu.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function update_ui() { - top_border - echo -e "| ${green}~~~~~~~~~~~~~~ [ Update Menu ] ~~~~~~~~~~~~~~${white} |" - hr - echo -e "| a) [Update all] | | |" - echo -e "| | Installed: | Latest: |" - echo -e "| Klipper & API: |---------------|--------------|" - echo -e "| 1) [Klipper] |$(compare_klipper_versions)|" - echo -e "| 2) [Moonraker] |$(compare_moonraker_versions)|" - echo -e "| | | |" - echo -e "| Klipper Webinterface: |---------------|--------------|" - echo -e "| 3) [Mainsail] |$(compare_mainsail_versions)|" - echo -e "| 4) [Fluidd] |$(compare_fluidd_versions)|" - echo -e "| | | |" - echo -e "| Touchscreen GUI: |---------------|--------------|" - echo -e "| 5) [KlipperScreen] |$(compare_klipperscreen_versions)|" - echo -e "| | | |" - echo -e "| Other: |---------------|--------------|" - echo -e "| 6) [PrettyGCode] |$(compare_prettygcode_versions)|" - echo -e "| 7) [Telegram Bot] |$(compare_telegram_bot_versions)|" - echo -e "| 8) [Obico for Klipper]|$(compare_moonraker_obico_versions)|" - echo -e "| 9) [OctoEverywhere] |$(compare_octoeverywhere_versions)|" - echo -e "| 10) [Mobileraker] |$(compare_mobileraker_versions)|" - echo -e "| 11) [Crowsnest] |$(compare_crowsnest_versions)|" - echo -e "| 12) [OctoApp] |$(compare_octoapp_versions)|" - echo -e "| 13) [Spoolman] |$(compare_spoolman_versions)|" - echo -e "| |------------------------------|" - echo -e "| 14) [System] | $(check_system_updates) |" - back_footer -} - -function update_menu() { - clear -x && sudo true && clear -x # (re)cache sudo credentials so password prompt doesn't bork ui - do_action "" "update_ui" - - local action - while true; do - read -p "${cyan}####### Perform action:${white} " action - case "${action}" in - 0) - do_action "toggle_backups" "update_ui";; - 1) - do_action "update_klipper" "update_ui";; - 2) - do_action "update_moonraker" "update_ui";; - 3) - do_action "update_mainsail" "update_ui";; - 4) - do_action "update_fluidd" "update_ui";; - 5) - do_action "update_klipperscreen" "update_ui";; - 6) - do_action "update_pgc_for_klipper" "update_ui";; - 7) - do_action "update_telegram_bot" "update_ui";; - 8) - do_action "update_moonraker_obico" "update_ui";; - 9) - do_action "update_octoeverywhere" "update_ui";; - 10) - do_action "update_mobileraker" "update_ui";; - 11) - do_action "update_crowsnest" "update_ui";; - 12) - do_action "update_octoapp" "update_ui";; - 13) - do_action "update_spoolman" "update_ui";; - 14) - do_action "upgrade_system_packages" "update_ui";; - a) - do_action "update_all" "update_ui";; - B|b) - clear; main_menu; break;; - *) - deny_action "update_ui";; - esac - done - update_menu -} - -function update_all() { - read_kiauh_ini "${FUNCNAME[0]}" - - local update_arr - local app_update_state="${application_updates_available}" - - IFS=', ' read -r -a update_arr <<< "${app_update_state}" - - while true; do - if (( ${#update_arr[@]} == 0 )); then - print_confirm "Everything is already up-to-date!" - echo; break - fi - - echo - top_border - echo -e "| The following installations will be updated: |" - - [[ "${update_arr[*]}" =~ "klipper" ]] && \ - echo -e "| ${cyan}● Klipper${white} |" - - [[ "${update_arr[*]}" =~ "moonraker" ]] && \ - echo -e "| ${cyan}● Moonraker${white} |" - - [[ "${update_arr[*]}" =~ "mainsail" ]] && \ - echo -e "| ${cyan}● Mainsail${white} |" - - [[ "${update_arr[*]}" =~ "fluidd" ]] && \ - echo -e "| ${cyan}● Fluidd${white} |" - - [[ "${update_arr[*]}" =~ "klipperscreen" ]] && \ - echo -e "| ${cyan}● KlipperScreen${white} |" - - [[ "${update_arr[*]}" =~ "spoolman" ]] && \ - echo -e "| ${cyan}● SpoolMan${white} |" - - [[ "${update_arr[*]}" =~ "pgc_for_klipper" ]] && \ - echo -e "| ${cyan}● PrettyGCode for Klipper${white} |" - - [[ "${update_arr[*]}" =~ "telegram_bot" ]] && \ - echo -e "| ${cyan}● MoonrakerTelegramBot${white} |" - - [[ "${update_arr[*]}" =~ "octoeverywhere" ]] && \ - echo -e "| ${cyan}● OctoEverywhere${white} |" - - [[ "${update_arr[*]}" =~ "mobileraker" ]] && \ - echo -e "| ${cyan}● Mobileraker${white} |" - - [[ "${update_arr[*]}" =~ "octoapp" ]] && \ - echo -e "| ${cyan}● OctoApp${white} |" - - [[ "${update_arr[*]}" =~ "system" ]] && \ - echo -e "| ${cyan}● System${white} |" - - bottom_border - - local yn - read -p "${cyan}###### Do you want to proceed? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - local component - local update - for component in "${update_arr[@]}"; do - if [[ ${component} == "system" ]]; then - update="upgrade_system_packages" - else - update="update_${component}" - fi - #shellcheck disable=SC2250 - $update - done - break;; - N|n|No|no) - break;; - *) - error_msg "Invalid command!";; - esac - done -} diff --git a/scripts/upload_log.sh b/scripts/upload_log.sh deleted file mode 100755 index 97f2e88..0000000 --- a/scripts/upload_log.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -function accept_upload_conditions() { - top_border - echo -e "| ${red}~~~~~~~~~~~ [ Upload Agreement ] ~~~~~~~~~~~~${white} |" - hr - echo -e "| The following function will help to quickly upload |" - echo -e "| logs for debugging purposes. With confirming this |" - echo -e "| dialog, you agree that during that process your logs |" - echo -e "| will be uploaded to: ${yellow}http://paste.c-net.org/${white} |" - hr - echo -e "| ${red}PLEASE NOTE:${white} |" - echo -e "| Be aware that logs can contain network information, |" - echo -e "| private data like usernames, filenames, or other |" - echo -e "| information you may not want to make public. |" - blank_line - echo -e "| Do ${red}NOT${white} use this function if you don't agree! |" - bottom_border - - local yn - while true; do - read -p "${cyan}Do you accept? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - sed -i "/logupload_accepted=/s/false/true/" "${INI_FILE}" - clear && print_header && upload_selection - ;; - N|n|No|no) - clear - main_menu - break - ;; - *) - error_msg "Invalid command!";; - esac - done -} - -function upload_selection() { - read_kiauh_ini "${FUNCNAME[0]}" - - local upload_agreed="${logupload_accepted}" - [[ ${upload_agreed} == "false" ]] && accept_upload_conditions - - local logfiles - local webif_logs="/var/log/nginx" - - function find_logfile() { - local name=${1} location=${2} - for log in $(find "${location}" -maxdepth 1 -regextype posix-extended -regex "${location}/${name}" | sort -g); do - logfiles+=("${log}") - done - } - - local logdir log_dirs - log_dirs=$(get_instance_folder_path "logs") - for logdir in ${log_dirs}; do - find_logfile "klippy(-[0-9a-zA-Z]+)?\.log" "${logdir}" - find_logfile "moonraker(-[0-9a-zA-Z]+)?\.log" "${logdir}" - find_logfile "telegram(-[0-9a-zA-Z]+)?\.log" "${logdir}" - done - find_logfile "mainsail.*" "${webif_logs}" - find_logfile "fluidd.*" "${webif_logs}" - find_logfile "KlipperScreen.log" "/tmp" - find_logfile "webcamd\.log(\.[0-9]+)?$" "/var/log" - find_logfile "kiauh\.log" "/tmp" - - ### draw interface - local i=0 - top_border - echo -e "| ${yellow}~~~~~~~~~~~~~~~ [ Log Upload ] ~~~~~~~~~~~~~~${white} |" - hr - echo -e "| You can choose the following logfiles for uploading: |" - blank_line - - for log in "${logfiles[@]}"; do - log=${log//${HOME}/"~"} - (( i < 10 )) && printf "| ${i}) %-50s|\n" "${log}" - (( i >= 10 )) && printf "| ${i}) %-50s|\n" "${log}" - i=$(( i + 1 )) - done - - blank_line - back_footer - - local option re="^[0-9]+$" - while true; do - read -p "${cyan}###### Please select:${white} " option - - if [[ ${option} =~ ${re} && ${option} -lt ${#logfiles[@]} ]]; then - upload_log "${logfiles[${option}]}" - upload_selection - elif [[ ${option} == "B" || ${option} == "b" ]]; then - return - else - error_msg "Invalid command!" - fi - done -} - -function upload_log() { - local link - clear && print_header - status_msg "Uploading ${1} ..." - link=$(curl -s -H "x-random;" --upload-file "${1}" 'http://paste.c-net.org/') - - if [[ -n ${link} ]]; then - ok_msg "${1} upload successfull!" - echo -e "\n${cyan}###### Here is your link:${white}" - echo -e ">>>>>> ${link}\n" - else - error_msg "Uploading failed!" - fi -} diff --git a/scripts/utilities.sh b/scripts/utilities.sh deleted file mode 100644 index aed70d5..0000000 --- a/scripts/utilities.sh +++ /dev/null @@ -1,778 +0,0 @@ -#!/usr/bin/env bash - -#=======================================================================# -# Copyright (C) 2020 - 2024 Dominik Willner # -# # -# 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 # -#=======================================================================# - -set -e - -#================================================# -#=================== STARTUP ====================# -#================================================# - -function check_euid() { - if [[ ${EUID} -eq 0 ]]; then - echo -e "${red}" - top_border - echo -e "| !!! THIS SCRIPT MUST NOT RUN AS ROOT !!! |" - echo -e "| |" - echo -e "| It will ask for credentials as needed. |" - bottom_border - echo -e "${white}" - exit 1 - fi -} - -function check_if_ratos() { - if [[ -n $(which ratos) ]]; then - echo -e "${red}" - top_border - echo -e "| !!! RatOS 2.1 or greater detected !!! |" - echo -e "| |" - echo -e "| KIAUH does currently not support RatOS. |" - echo -e "| If you have any questions, please ask for help on the |" - echo -e "| RatRig Community Discord: https://discord.gg/ratrig |" - bottom_border - echo -e "${white}" - exit 1 - fi -} - -#================================================# -#============= MESSAGE FORMATTING ===============# -#================================================# - -function select_msg() { - echo -e "${white} [βž”] ${1}" -} -function status_msg() { - echo -e "\n${magenta}###### ${1}${white}" -} -function ok_msg() { - echo -e "${green}[βœ“ OK] ${1}${white}" -} -function warn_msg() { - echo -e "${yellow}>>>>>> ${1}${white}" -} -function error_msg() { - echo -e "${red}>>>>>> ${1}${white}" -} -function abort_msg() { - echo -e "${red}<<<<<< ${1}${white}" -} -function title_msg() { - echo -e "${cyan}${1}${white}" -} - -function print_error() { - [[ -z ${1} ]] && return - - echo -e "${red}" - echo -e "#=======================================================#" - echo -e " ${1} " - echo -e "#=======================================================#" - echo -e "${white}" -} - -function print_confirm() { - [[ -z ${1} ]] && return - - echo -e "${green}" - echo -e "#=======================================================#" - echo -e " ${1} " - echo -e "#=======================================================#" - echo -e "${white}" -} - -#================================================# -#=================== LOGGING ====================# -#================================================# - -function timestamp() { - date +"[%F %T]" -} - -function init_logfile() { - local log="/tmp/kiauh.log" - { - echo -e "#================================================================#" - echo -e "# New KIAUH session started on: $(date) #" - echo -e "#================================================================#" - echo -e "KIAUH $(get_kiauh_version)" - echo -e "#================================================================#" - } >> "${log}" -} - -function log_info() { - local message="${1}" log="${LOGFILE}" - echo -e "$(timestamp) [INFO]: ${message}" | tr -s " " >> "${log}" -} - -function log_warning() { - local message="${1}" log="${LOGFILE}" - echo -e "$(timestamp) [WARN]: ${message}" | tr -s " " >> "${log}" -} - -function log_error() { - local message="${1}" log="${LOGFILE}" - echo -e "$(timestamp) [ERR]: ${message}" | tr -s " " >> "${log}" -} - -#================================================# -#=============== KIAUH SETTINGS =================# -#================================================# - -function read_kiauh_ini() { - local func=${1} - - if [[ ! -f ${INI_FILE} ]]; then - log_warning "Reading from .kiauh.ini failed! File not found! Creating default ini file." - init_ini - fi - - log_info "Reading from .kiauh.ini ... (${func})" - source "${INI_FILE}" -} - -function init_ini() { - ### remove pre-version 4 ini files - if [[ -f ${INI_FILE} ]] && ! grep -Eq "^# KIAUH v4\.0\.0$" "${INI_FILE}"; then - rm "${INI_FILE}" - fi - - ### initialize v4.0.0 ini file - if [[ ! -f ${INI_FILE} ]]; then - { - echo -e "# File creation date: $(date)" - echo -e "#=================================================#" - echo -e "# KIAUH - Klipper Installation And Update Helper #" - echo -e "# https://github.com/dw-0/kiauh #" - echo -e "# DO NOT edit this file! #" - echo -e "#=================================================#" - echo -e "# KIAUH v4.0.0" - echo -e "#" - } >> "${INI_FILE}" - fi - - if ! grep -Eq "^application_updates_available=" "${INI_FILE}"; then - echo -e "\napplication_updates_available=\c" >> "${INI_FILE}" - else - sed -i "/application_updates_available=/s/=.*/=/" "${INI_FILE}" - fi - - if ! grep -Eq "^backup_before_update=." "${INI_FILE}"; then - echo -e "\nbackup_before_update=false\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^logupload_accepted=." "${INI_FILE}"; then - echo -e "\nlogupload_accepted=false\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^custom_klipper_repo=" "${INI_FILE}"; then - echo -e "\ncustom_klipper_repo=\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^custom_klipper_repo_branch=" "${INI_FILE}"; then - echo -e "\ncustom_klipper_repo_branch=\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^mainsail_install_unstable=" "${INI_FILE}"; then - echo -e "\nmainsail_install_unstable=false\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^fluidd_install_unstable=" "${INI_FILE}"; then - echo -e "\nfluidd_install_unstable=false\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^multi_instance_names=" "${INI_FILE}"; then - echo -e "\nmulti_instance_names=\c" >> "${INI_FILE}" - fi - - if ! grep -Eq "^version_to_launch=" "${INI_FILE}"; then - echo -e "\nversion_to_launch=\n\c" >> "${INI_FILE}" - fi - - ### strip all empty lines out of the file - sed -i "/^[[:blank:]]*$/ d" "${INI_FILE}" -} - -function switch_mainsail_releasetype() { - read_kiauh_ini "${FUNCNAME[0]}" - local state="${mainsail_install_unstable}" - - if [[ ${state} == "false" ]]; then - sed -i '/mainsail_install_unstable=/s/false/true/' "${INI_FILE}" - log_info "mainsail_install_unstable changed (false -> true) " - else - sed -i '/mainsail_install_unstable=/s/true/false/' "${INI_FILE}" - log_info "mainsail_install_unstable changed (true -> false) " - fi -} - -function switch_fluidd_releasetype() { - read_kiauh_ini "${FUNCNAME[0]}" - local state="${fluidd_install_unstable}" - - if [[ ${state} == "false" ]]; then - sed -i '/fluidd_install_unstable=/s/false/true/' "${INI_FILE}" - log_info "fluidd_install_unstable changed (false -> true) " - else - sed -i '/fluidd_install_unstable=/s/true/false/' "${INI_FILE}" - log_info "fluidd_install_unstable changed (true -> false) " - fi -} - -function toggle_backup_before_update() { - read_kiauh_ini "${FUNCNAME[0]}" - local state="${backup_before_update}" - - if [[ ${state} = "false" ]]; then - sed -i '/backup_before_update=/s/false/true/' "${INI_FILE}" - else - sed -i '/backup_before_update=/s/true/false/' "${INI_FILE}" - fi -} - -function set_custom_klipper_repo() { - read_kiauh_ini "${FUNCNAME[0]}" - local repo=${1} branch=${2} - - sed -i "/^custom_klipper_repo=/d" "${INI_FILE}" - sed -i '$a'"custom_klipper_repo=${repo}" "${INI_FILE}" - sed -i "/^custom_klipper_repo_branch=/d" "${INI_FILE}" - sed -i '$a'"custom_klipper_repo_branch=${branch}" "${INI_FILE}" -} - -function add_to_application_updates() { - read_kiauh_ini "${FUNCNAME[0]}" - - local application="${1}" - local app_update_state="${application_updates_available}" - - if ! grep -Eq "${application}" <<< "${app_update_state}"; then - app_update_state="${app_update_state}${application}," - sed -i "/application_updates_available=/s/=.*/=${app_update_state}/" "${INI_FILE}" - fi -} - -#================================================# -#=============== HANDLE SERVICES ================# -#================================================# - -function do_action_service() { - local services action=${1} service=${2} - services=$(find "${SYSTEMD}" -maxdepth 1 -regextype posix-extended -regex "${SYSTEMD}/${service}(-[0-9a-zA-Z]+)?.service" | sort) - - if [[ -n ${services} ]]; then - for service in ${services}; do - service=$(echo "${service}" | rev | cut -d"/" -f1 | rev) - status_msg "${action^} ${service} ..." - - if sudo systemctl "${action}" "${service}"; then - log_info "${service}: ${action} > success" - ok_msg "${action^} ${service} successfull!" - else - log_warning "${service}: ${action} > failed" - warn_msg "${action^} ${service} failed!" - fi - done - fi -} - -#================================================# -#================ DEPENDENCIES ==================# -#================================================# - -### returns 'true' if python version >= 3.7 -function python3_check() { - local major minor passed - - major=$(python3 --version | cut -d" " -f2 | cut -d"." -f1) - minor=$(python3 --version | cut -d"." -f2) - - if (( major >= 3 && minor >= 7 )); then - passed="true" - else - passed="false" - fi - - echo "${passed}" -} - -function dependency_check() { - local dep=( "${@}" ) - local packages log_name="dependencies" - status_msg "Checking for the following dependencies:" - - #check if package is installed, if not write its name into array - for pkg in "${dep[@]}"; do - echo -e "${cyan}● ${pkg} ${white}" - [[ ! $(dpkg-query -f'${Status}' --show "${pkg}" 2>/dev/null) = *\ installed ]] && \ - packages+=("${pkg}") - done - - #if array is not empty, install packages from array - if (( ${#packages[@]} > 0 )); then - status_msg "Installing the following dependencies:" - for package in "${packages[@]}"; do - echo -e "${cyan}● ${package} ${white}" - done - echo - - # update system package lists if stale - update_system_package_lists - - # install required packages - install_system_packages "${log_name}" "packages[@]" - - else - ok_msg "Dependencies already met!" - return - fi -} - -function fetch_webui_ports() { - local port interfaces=("mainsail" "fluidd" "octoprint") - - ### read ports from possible installed interfaces and write them to ~/.kiauh.ini - for interface in "${interfaces[@]}"; do - if [[ -f "/etc/nginx/sites-available/${interface}" ]]; then - port=$(grep -E "listen" "/etc/nginx/sites-available/${interface}" | head -1 | sed 's/^\s*//' | sed 's/;$//' | cut -d" " -f2) - if ! grep -Eq "${interface}_port" "${INI_FILE}"; then - sed -i '$a'"${interface}_port=${port}" "${INI_FILE}" - else - sed -i "/^${interface}_port/d" "${INI_FILE}" - sed -i '$a'"${interface}_port=${port}" "${INI_FILE}" - fi - else - sed -i "/^${interface}_port/d" "${INI_FILE}" - fi - done -} - -#================================================# -#=================== SYSTEM =====================# -#================================================# - -function create_required_folders() { - local printer_data=${1} folders - folders=("backup" "certs" "config" "database" "gcodes" "comms" "logs" "systemd") - - for folder in "${folders[@]}"; do - local dir="${printer_data}/${folder}" - - ### remove possible symlink created by moonraker - if [[ -L "${dir}" && -d "${dir}" ]]; then - rm "${dir}" - fi - - if [[ ! -d "${dir}" ]]; then - status_msg "Creating folder '${dir}' ..." - mkdir -p "${dir}" - ok_msg "Folder '${dir}' created!" - fi - done -} - -function update_system_package_lists() { - local cache_mtime update_age update_interval silent - - if [[ $1 == '--silent' ]]; then silent="true"; fi - - if [[ -e /var/lib/apt/periodic/update-success-stamp ]]; then - cache_mtime="$(stat -c %Y /var/lib/apt/periodic/update-success-stamp)" - elif [[ -e /var/lib/apt/lists ]]; then - cache_mtime="$(stat -c %Y /var/lib/apt/lists)" - else - log_warning "Failure determining package cache age, forcing update" - cache_mtime=0 - fi - - update_age="$(($(date +'%s') - cache_mtime))" - update_interval=$((48*60*60)) # 48hrs - - # update if cache is greater than update_interval - if (( update_age > update_interval )); then - if [[ ! ${silent} == "true" ]]; then status_msg "Updating package lists..."; fi - if ! sudo apt-get update --allow-releaseinfo-change &>/dev/null; then - log_error "Failure while updating package lists!" - if [[ ! ${silent} == "true" ]]; then error_msg "Updating package lists failed!"; fi - return 1 - else - log_info "Package lists updated successfully" - if [[ ! ${silent} == "true" ]]; then status_msg "Updated package lists."; fi - fi - else - log_info "Package lists updated recently, skipping update..." - fi -} - -function check_system_updates() { - local updates_avail status - if ! update_system_package_lists --silent; then - status="${red}Update check failed! ${white}" - else - updates_avail="$(apt list --upgradeable 2>/dev/null | sed "1d")" - - if [[ -n ${updates_avail} ]]; then - status="${yellow}System upgrade available!${white}" - # add system to application_updates_available in kiauh.ini - add_to_application_updates "system" - else - status="${green}System up to date! ${white}" - fi - fi - - echo "${status}" -} - -function upgrade_system_packages() { - status_msg "Upgrading System ..." - update_system_package_lists - if sudo apt-get upgrade -y; then - print_confirm "Upgrade complete! Check the log above!\n ${yellow}KIAUH will not install any dist-upgrades or\n any packages which have been held back!${green}" - else - print_error "System upgrade failed! Please look for any errors printed above!" - fi -} - -function install_system_packages() { - local log_name="$1" - local packages=("${!2}") - status_msg "Installing packages..." - if sudo apt-get install -y "${packages[@]}"; then - ok_msg "${log_name^} packages installed!" - else - log_error "Failure while installing ${log_name,,} packages" - error_msg "Installing ${log_name} packages failed!" - exit 1 # exit kiauh - fi -} - -function check_usergroups() { - local group_dialout group_tty - - if grep -q "dialout" .local' in the browser. |" - echo -e "| |" - echo -e "| E.g.: If you set the hostname to 'my-printer' you |" - echo -e "| can open Mainsail / Fluidd / Octoprint by |" - echo -e "| browsing to: http://my-printer.local |" - bottom_border - - local yn - while true; do - read -p "${cyan}###### Do you want to change the hostname? (y/N):${white} " yn - case "${yn}" in - Y|y|Yes|yes) - select_msg "Yes" - change_hostname - break;; - N|n|No|no|"") - select_msg "No" - break;; - *) - error_msg "Invalid command!";; - esac - done -} - -function change_hostname() { - local new_hostname regex="^[^\-\_]+([0-9a-z]\-{0,1})+[^\-\_]+$" - echo - top_border - echo -e "| ${green}Allowed characters: a-z, 0-9 and single '-'${white} |" - echo -e "| ${red}No special characters allowed!${white} |" - echo -e "| ${red}No leading or trailing '-' allowed!${white} |" - bottom_border - - while true; do - read -p "${cyan}###### Please set the new hostname:${white} " new_hostname - - if [[ ${new_hostname} =~ ${regex} ]]; then - local yn - while true; do - echo - read -p "${cyan}###### Do you want '${new_hostname}' to be the new hostname? (Y/n):${white} " yn - case "${yn}" in - Y|y|Yes|yes|"") - select_msg "Yes" - set_hostname "${new_hostname}" - break;; - N|n|No|no) - select_msg "No" - abort_msg "Skip hostname change ..." - break;; - *) - print_error "Invalid command!";; - esac - done - else - warn_msg "'${new_hostname}' is not a valid hostname!" - fi - break - done -} - -function set_hostname() { - local new_hostname=${1} current_date - #check for dependencies - local dep=(avahi-daemon) - dependency_check "${dep[@]}" - - #create host file if missing or create backup of existing one with current date&time - if [[ -f /etc/hosts ]]; then - current_date=$(get_date) - status_msg "Creating backup of hosts file ..." - sudo cp "/etc/hosts" "/etc/hosts.${current_date}.bak" - ok_msg "Backup done!" - ok_msg "File:'/etc/hosts.${current_date}.bak'" - else - sudo touch /etc/hosts - fi - - #set new hostname in /etc/hostname - status_msg "Setting hostname to '${new_hostname}' ..." - status_msg "Please wait ..." - sudo hostnamectl set-hostname "${new_hostname}" - - #write new hostname to /etc/hosts - status_msg "Writing new hostname to /etc/hosts ..." - echo "127.0.0.1 ${new_hostname}" | sudo tee -a /etc/hosts &>/dev/null - ok_msg "New hostname successfully configured!" - ok_msg "Remember to reboot for the changes to take effect!" -} - -#================================================# -#============ INSTANCE MANAGEMENT ===============# -#================================================# - -### -# takes in a systemd service files full path and -# returns the sub-string with the instance name -# -# @param {string}: service file absolute path -# (e.g. '/etc/systemd/system/klipper-.service') -# -# => return sub-string containing only the part of the full string -# -function get_instance_name() { - local instance=${1} - local name - - name=$(echo "${instance}" | rev | cut -d"/" -f1 | cut -d"." -f2 | cut -d"-" -f1 | rev) - - echo "${name}" -} - -### -# returns the instance name/identifier of the klipper service -# if the klipper service is part of a multi instance setup -# otherwise returns an emtpy string -# -# @param {string}: name - klipper service name (e.g. klipper-name.service) -# -function get_klipper_instance_name() { - local instance=${1} - local name - - name=$(echo "${instance}" | rev | cut -d"/" -f1 | cut -d"." -f2 | rev) - - local regex="^klipper-[0-9a-zA-Z]+$" - if [[ ${name} =~ ${regex} ]]; then - name=$(echo "${name}" | cut -d"-" -f2) - else - name="" - fi - - echo "${name}" -} - -### -# loops through all installed klipper services and saves -# each instances name in a comma separated format to the kiauh.ini -# -function set_multi_instance_names() { - read_kiauh_ini "${FUNCNAME[0]}" - - local name - local names="" - local services - - services=$(klipper_systemd) - - ### - # if value of 'multi_instance_names' is not an empty - # string, delete its value, so it can be re-written - if [[ -n ${multi_instance_names} ]]; then - sed -i "/multi_instance_names=/s/=.*/=/" "${INI_FILE}" - fi - - for svc in ${services}; do - name=$(get_klipper_instance_name "${svc}") - - if ! grep -Eq "${name}" <<<"${names}"; then - names="${names}${name}," - fi - - done - - # write up-to-date instance name string to kiauh.ini - sed -i "/multi_instance_names=/s/=.*/=${names}/" "${INI_FILE}" -} - -### -# Helper function that returns all configured instance names -# -# => return an empty string if 0 or 1 klipper instance is installed -# => return space-separated string for names of the configured instances -# if 2 or more klipper instances are installed -# -function get_multi_instance_names() { - read_kiauh_ini "${FUNCNAME[0]}" - local instance_names=() - - ### - # convert the comma separates string from the .kiauh.ini into - # an array of instance names. a single instance installation - # results in an empty instance_names array - IFS=',' read -r -a instance_names <<< "${multi_instance_names}" - - echo "${instance_names[@]}" -} - -### -# helper function that returns all possibly available absolute -# klipper config directory paths based on their instance name. -# -# => return an empty string if klipper is not installed -# => return space-separated string of absolute config directory paths -# -function get_config_folders() { - local cfg_dirs=() - local instance_names - instance_names=$(get_multi_instance_names) - - if [[ -n ${instance_names} ]]; then - for name in ${instance_names}; do - ### - # by KIAUH convention, all instance names of only numbers - # need to be prefixed with 'printer_' - if [[ ${name} =~ ^[0-9]+$ ]]; then - cfg_dirs+=("${HOME}/printer_${name}_data/config") - else - cfg_dirs+=("${HOME}/${name}_data/config") - fi - done - elif [[ -z ${instance_names} && $(klipper_systemd | wc -w) -gt 0 ]]; then - cfg_dirs+=("${HOME}/printer_data/config") - else - cfg_dirs=() - fi - - echo "${cfg_dirs[@]}" -} - -### -# helper function that returns all available absolute directory paths -# based on their instance name and specified target folder -# -# @param {string}: folder name - target instance folder name (e.g. config) -# -# => return an empty string if klipper is not installed -# => return space-separated string of absolute directory paths -# -function get_instance_folder_path() { - local folder_name=${1} - local folder_paths=() - local instance_names - local path - - instance_names=$(get_multi_instance_names) - - if [[ -n ${instance_names} ]]; then - for name in ${instance_names}; do - ### - # by KIAUH convention, all instance names of only numbers - # need to be prefixed with 'printer_' - if [[ ${name} =~ ^[0-9]+$ ]]; then - path="${HOME}/printer_${name}_data/${folder_name}" - if [[ -d ${path} ]]; then - folder_paths+=("${path}") - fi - else - path="${HOME}/${name}_data/${folder_name}" - if [[ -d ${path} ]]; then - folder_paths+=("${path}") - fi - fi - done - elif [[ -z ${instance_names} && $(klipper_systemd | wc -w) -gt 0 ]]; then - path="${HOME}/printer_data/${folder_name}" - if [[ -d ${path} ]]; then - folder_paths+=("${path}") - fi - fi - - echo "${folder_paths[@]}" -}