From a62abdff4126208f986a1c9f61db24e784665884 Mon Sep 17 00:00:00 2001 From: arcolinuxz Date: Mon, 20 May 2024 09:28:14 +0200 Subject: [PATCH] update --- LICENSE | 674 +++++++ README.md | 167 ++ setup-our-git-credentials.sh | 75 + up.sh | 65 + usr/bin/akm | 1 + usr/bin/archlinux-kernel-manager | 52 + .../archlinux-kernel-manager.desktop | 12 + usr/share/archlinux-kernel-manager/akm.css | 176 ++ .../archlinux-kernel-manager.py | 115 ++ .../defaults/config.toml | 33 + .../images/364x408/akm-tux-splash.png | Bin 0 -> 59648 bytes .../images/48x48/akm-community.png | Bin 0 -> 2728 bytes .../images/48x48/akm-install.png | Bin 0 -> 2576 bytes .../images/48x48/akm-new.png | Bin 0 -> 2013 bytes .../images/48x48/akm-progress.png | Bin 0 -> 1368 bytes .../images/48x48/akm-remove.png | Bin 0 -> 2410 bytes .../images/48x48/akm-settings.png | Bin 0 -> 2150 bytes .../images/48x48/akm-star.png | Bin 0 -> 1049 bytes .../images/48x48/akm-tux.png | Bin 0 -> 2610 bytes .../images/48x48/akm-update.png | Bin 0 -> 1469 bytes .../images/48x48/akm-verified.png | Bin 0 -> 909 bytes .../images/48x48/akm-warning.png | Bin 0 -> 1804 bytes .../images/96x96/akm-tux.png | Bin 0 -> 3128 bytes .../archlinux-kernel-manager/libs/Kernel.py | 51 + .../libs/functions.py | 1703 +++++++++++++++++ .../ui/AboutDialog.py | 68 + .../archlinux-kernel-manager/ui/FlowBox.py | 635 ++++++ .../ui/KernelStack.py | 631 ++++++ .../archlinux-kernel-manager/ui/ManagerGUI.py | 516 +++++ .../archlinux-kernel-manager/ui/MenuButton.py | 45 + .../ui/MessageWindow.py | 93 + .../ui/ProgressWindow.py | 630 ++++++ .../ui/SettingsWindow.py | 713 +++++++ .../ui/SplashScreen.py | 27 + .../archlinux-kernel-manager/ui/Stack.py | 30 + .../apps/archlinux-kernel-manager-tux.svg | 18 + .../polkit-1/actions/org.archlinux.akm.policy | 104 + 37 files changed, 6634 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 setup-our-git-credentials.sh create mode 100755 up.sh create mode 120000 usr/bin/akm create mode 100755 usr/bin/archlinux-kernel-manager create mode 100644 usr/share/applications/archlinux-kernel-manager.desktop create mode 100644 usr/share/archlinux-kernel-manager/akm.css create mode 100755 usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py create mode 100644 usr/share/archlinux-kernel-manager/defaults/config.toml create mode 100644 usr/share/archlinux-kernel-manager/images/364x408/akm-tux-splash.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-community.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-install.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-new.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-progress.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-remove.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-settings.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-star.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-tux.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-update.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-verified.png create mode 100644 usr/share/archlinux-kernel-manager/images/48x48/akm-warning.png create mode 100644 usr/share/archlinux-kernel-manager/images/96x96/akm-tux.png create mode 100644 usr/share/archlinux-kernel-manager/libs/Kernel.py create mode 100644 usr/share/archlinux-kernel-manager/libs/functions.py create mode 100644 usr/share/archlinux-kernel-manager/ui/AboutDialog.py create mode 100644 usr/share/archlinux-kernel-manager/ui/FlowBox.py create mode 100644 usr/share/archlinux-kernel-manager/ui/KernelStack.py create mode 100644 usr/share/archlinux-kernel-manager/ui/ManagerGUI.py create mode 100644 usr/share/archlinux-kernel-manager/ui/MenuButton.py create mode 100644 usr/share/archlinux-kernel-manager/ui/MessageWindow.py create mode 100644 usr/share/archlinux-kernel-manager/ui/ProgressWindow.py create mode 100644 usr/share/archlinux-kernel-manager/ui/SettingsWindow.py create mode 100644 usr/share/archlinux-kernel-manager/ui/SplashScreen.py create mode 100644 usr/share/archlinux-kernel-manager/ui/Stack.py create mode 100644 usr/share/icons/hicolor/scalable/apps/archlinux-kernel-manager-tux.svg create mode 100644 usr/share/polkit-1/actions/org.archlinux.akm.policy diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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 new file mode 100644 index 0000000..d8ed40d --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Arch Linux Kernel Manager + +A GTK4 Python application used to install and remove Linux Kernels on an Arch based system. + +Installation and removal of Kernels is carried out using Pacman. + +Kernel packages are sourced from the [Arch Linux Archive](https://wiki.archlinux.org/title/Arch_Linux_Archive) (ALA) + +Both official and un-official community based Kernels are supported. + +# Official Kernels + +- linux +- linux-lts +- linux-zen +- linux-hardened +- linux-rt +- linux-rt-lts + +Since the ALA has a lot of kernel packages any versions found older than 2 years or more are ignored. +These kernels are considerably out of date and have shown to fail to install properly with issues related to missing modules. + +![Official kernels](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/d3e0416d-5ba8-4906-bada-835f4d949130) + +## What happens if a kernel installation fails + +The application will show a message that it has encountered an issue, and the log inside the progress window, should have the information required to understand why. +In the event of a failure, the application will try to reinstall the kernel using the version previously installed. + +# Community based kernels + +As long as the necessary Pacman repositories are configured these are supported. + +- linux-xanmod +- linux-xanmod-lts +- linux-cachyos +- linux-lqx +- linux-clear +- linux-amd +- linux-nitrous + +Most of these are sourced from the [Chaotic AUR](https://aur.chaotic.cx) + +See updating the [configuration file](#Adding-new-community-based-kernels) for adding new kernels. + +![Community kernels](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/072bc9b2-cca4-4c5a-ae91-be9c0440feb3) + +# Installed kernels + +![Installed kernels](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/1120c9cc-adc1-4f2c-92c5-dff8d1d1c841) + +# Progress window + +![Progress window](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/2b7e97db-06f6-4152-bf13-b81fbc42b63c) + +# Bootloader + +Only Grub and systemd-boot bootloaders are supported. + +After a successful install/removal of a kernel the relevant bootloader entries are updated. +By default, the application will use `bootctl` to distinguish which bootloader (Grub/systemd-boot) is currently being used. + +## Grub + +`grub-mkconfig` is run to update the grub.cfg file. + +## systemd-boot + +`bootctl --no-variables ---graceful update` is run to update systemd-boot entries + +# Advanced settings + +## Bootloader settings + +The bootloader settings can be overridden using the Advanced settings window. +Or you can manually update the configuration file (see the custom bootloader example). + +![Advanced settings](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/827033b1-9250-4967-9431-e2b6205ed3a0) + +## Latest kernel versions + +Based on the latest information taken from the configured pacman mirrors. + +![Kernel versions](https://github.com/DeltaCopy/archlinux-kernel-manager/assets/121581829/43416b00-3759-4913-8d09-8f9083edc358) + +# Default configuration file + +This file can be found inside `$HOME/.config/archlinux-kernel-manager` + +```toml + +title = "ArchLinux Kernel Manager Settings" + +[kernels] + +# Kernels which are sourced from the ALA (Arch Linux Archive) https://archive.archlinux.org +official = [ + { name = "linux", description = "The Linux kernel and modules (Stable)", headers = "linux-headers" }, + { name = "linux-lts", description = "The LTS Linux kernel and modules (Longterm)", headers = "linux-lts-headers" }, + { name = "linux-zen", description = "The Linux ZEN kernel and modules (Zen)", headers = "linux-zen-headers" }, + { name = "linux-hardened", description = "The Security-Hardened Linux kernel and modules (Hardened)", headers = "linux-hardened-headers" }, + { name = "linux-rt", description = "The Linux RT kernel and modules (Realtime)", headers = "linux-rt-headers" }, + { name = "linux-rt-lts", description = "The Linux RT LTS kernel and modules (Realtime Longterm)", headers = "linux-rt-lts-headers" }, +] + +# Kernels which are sourced from unofficial repositories, these won't work if you haven't updated your pacman configuration +# https://wiki.archlinux.org/title/Unofficial_user_repositories +community = [ + { name = "linux-xanmod", description = "The Linux kernel and modules with Xanmod patches", headers = "linux-xanmod-headers", repository = "chaotic-aur" }, + { name = "linux-xanmod-lts", description = "The Linux kernel and modules with Xanmod patches", headers = "linux-xanmod-lts-headers", repository = "chaotic-aur" }, + { name = "linux-amd", description = "Linux kernel aimed at the ZNVER4/MZEN4 AMD Ryzen CPU based hardware", headers = "linux-amd-headers", repository = "chaotic-aur" }, + { name = "linux-cachyos", description = "The Linux EEVDF-BORE scheduler Kernel by CachyOS with other patches and improvements kernel and modules", headers = "linux-cachyos-headers", repository = "chaotic-aur" }, + { name = "linux-ck", description = "The Linux kernel and modules with ck's hrtimer patches", headers = "linux-ck-headers", repository = "repo-ck" }, + { name = "linux-clear", description = "The Clear Linux kernel and modules", headers = "linux-clear-headers", repository = "chaotic-aur" }, + { name = "linux-lts-tkg-bmq", description = "The Linux-tkg kernel and modules", headers = "linux-lts-tkg-bmq-headers", repository = "chaotic-aur" }, + { name = "linux-tkg-pds", description = "The Linux-tkg kernel and modules", headers = "linux-tkg-pds-headers", repository = "chaotic-aur" }, + { name = "linux-lqx", description = "The Linux Liquorix kernel and modules", headers = "linux-lqx-headers", repository = "chaotic-aur" }, + { name = "linux-nitrous", description = "Modified Linux kernel optimized for Skylake and newer, compiled using clang", headers = "linux-nitrous-headers", repository = "chaotic-aur" }, +] + +# custom bootloader example +#[bootloader] +#name = "grub" +#grub_config = "/boot/grub/grub.cfg" + +``` +## Adding new community based kernels + +Further Kernels can be added using the same format. + +When adding new community based un-official kernels, the repository name should match the one defined inside the pacman `/etc/pacman.conf` file under `[repo-name]`. +Further details on un-official kernels can be found on https://wiki.archlinux.org/title/Kernel#Unofficial_kernels + +# Cache + +Kernel data retrieved from the ALA is stored inside a toml based file inside `$HOME/.cache/archlinux-kernel-manager/kernels.toml` + +This cached file is updated automatically every 5 days to ensure the application is kept up to date with the latest kernels. +Using the Update switch inside Advanced Settings, will force the application to update the cache. + +This cache file may take a little while to generate since archived Arch kernel package data is being retrieved from the ALA. + +# Logs + +Logs can be found inside `/var/log/archlinux-kernel-manager` + +# Required Python modules + +- python-tomlkit + +- python-gobject + +- python-requests + +- python-distro + +- python-psutil + +# Installing the application + +`wget https://raw.githubusercontent.com/DeltaCopy/archlinux-kernel-manager/main/PKGBUILD` + +`makepkg -si` + +# Running the application + +Run `akm` or `archlinux-kernel-manager` to launch the application. diff --git a/setup-our-git-credentials.sh b/setup-our-git-credentials.sh new file mode 100755 index 0000000..d78471d --- /dev/null +++ b/setup-our-git-credentials.sh @@ -0,0 +1,75 @@ +#!/bin/bash +#set -e +################################################################################################################## +# Author : Erik Dubois +# Website : https://www.erikdubois.be +# Website : https://www.alci.online +# Website : https://www.arcolinux.info +# Website : https://www.arcolinux.com +# Website : https://www.arcolinuxd.com +# Website : https://www.arcolinuxb.com +# Website : https://www.arcolinuxiso.com +# Website : https://www.arcolinuxforum.com +################################################################################################################## +# +# DO NOT JUST RUN THIS. EXAMINE AND JUDGE. RUN AT YOUR OWN RISK. +# +################################################################################################################## +#tput setaf 0 = black +#tput setaf 1 = red +#tput setaf 2 = green +#tput setaf 3 = yellow +#tput setaf 4 = dark blue +#tput setaf 5 = purple +#tput setaf 6 = cyan +#tput setaf 7 = gray +#tput setaf 8 = light blue +################################################################################################################## + +echo +tput setaf 3 +echo "################################################################" +echo "################### Start" +echo "################################################################" +tput sgr0 +echo + +# Problem solving commands + +# Read before using it. +# https://www.atlassian.com/git/tutorials/undoing-changes/git-reset +# git reset --hard orgin/master +# ONLY if you are very sure and no coworkers are on your github. + +# Command that have helped in the past +# Force git to overwrite local files on pull - no merge +# git fetch all +# git push --set-upstream origin master +# git reset --hard orgin/master + + +#setting up git +#https://www.atlassian.com/git/tutorials/setting-up-a-repository/git-config +#https://medium.com/clarusway/how-to-use-git-github-without-asking-for-authentication-always-passwordless-usage-of-private-git-8c32489bc2e9 +#https://blog.nillsf.com/index.php/2021/05/27/github-sso-using-password-protected-ssh-keys + +project=$(basename `pwd`) +githubdir="arcolinux" +echo "-----------------------------------------------------------------------------" +echo "this is project https://github.com/$githubdir/$project" +echo "-----------------------------------------------------------------------------" + +git config --global pull.rebase false +git config --global push.default simple +git config --global user.name "arcolinuxz" +git config --global user.email "arcolinuxinfo@gmail.com" +sudo git config --system core.editor nano +git remote set-url origin git@github.com-arc:$githubdir/$project + +echo +tput setaf 3 +echo "################################################################" +echo "################### End" +echo "################################################################" +tput sgr0 +echo \ No newline at end of file diff --git a/up.sh b/up.sh new file mode 100755 index 0000000..fd0c831 --- /dev/null +++ b/up.sh @@ -0,0 +1,65 @@ +#!/bin/bash +#set -e +################################################################################################################## +# Author : Erik Dubois +# Website : https://www.erikdubois.be +# Website : https://www.alci.online +# Website : https://www.ariser.eu +# Website : https://www.arcolinux.info +# Website : https://www.arcolinux.com +# Website : https://www.arcolinuxd.com +# Website : https://www.arcolinuxb.com +# Website : https://www.arcolinuxiso.com +# Website : https://www.arcolinuxforum.com +################################################################################################################## +# +# DO NOT JUST RUN THIS. EXAMINE AND JUDGE. RUN AT YOUR OWN RISK. +# +################################################################################################################## +#tput setaf 0 = black +#tput setaf 1 = red +#tput setaf 2 = green +#tput setaf 3 = yellow +#tput setaf 4 = dark blue +#tput setaf 5 = purple +#tput setaf 6 = cyan +#tput setaf 7 = gray +#tput setaf 8 = light blue +################################################################################################################## + +# reset - commit your changes or stash them before you merge +# git reset --hard - personal alias - grh + +# checking if I have the latest files from github +echo "Checking for newer files online first" +git pull + +# Below command will backup everything inside the project folder +git add --all . + +# Give a comment to the commit if you want +echo "####################################" +echo "Write your commit comment!" +echo "####################################" + +read input + +# Committing to the local repository with a message containing the time details and commit text + +git commit -m "$input" + +# Push the local files to github + +if grep -q main .git/config; then + echo "Using main" + git push -u origin main +fi + +if grep -q master .git/config; then + echo "Using master" + git push -u origin master +fi + +echo "################################################################" +echo "################### Git Push Done ######################" +echo "################################################################" diff --git a/usr/bin/akm b/usr/bin/akm new file mode 120000 index 0000000..46a2982 --- /dev/null +++ b/usr/bin/akm @@ -0,0 +1 @@ +archlinux-kernel-manager \ No newline at end of file diff --git a/usr/bin/archlinux-kernel-manager b/usr/bin/archlinux-kernel-manager new file mode 100755 index 0000000..c46d158 --- /dev/null +++ b/usr/bin/archlinux-kernel-manager @@ -0,0 +1,52 @@ +#!/usr/bin/env sh + +# this script should not be run as root +# the polkit agent running on the desktop environment should prompt for root password + +echo "---------------------------------------------------------------------------" +echo "[INFO] Checking session" +test $(whoami) == "root" && echo "[ERROR] Do not run this script as root." && exit 1 +test -z $DISPLAY && echo "[ERROR] DISPLAY variable is not set." && exit 1 + +# check session is either one of X11, Wayland or TTY +session=$(loginctl show-session $(loginctl|grep $(whoami) | awk '{print $1}') -p Type | awk -F= '{print $2}' | grep "x11\|wayland\|tty") + +test -z "$session" && echo "[ERROR] Failed to verify session for user." && exit 1 + +xauth_file=$(xauth info | awk -F"Authority file:" '{print $2}' | tr -d ' ') +test -s "$xauth_file" || touch "$xauth_file" + +case "$session" in + "wayland") + # Wayland session, generate Xauth session cookie for $DISPLAY + xauth gen $DISPLAY &> /dev/null + echo "[INFO] Display = $DISPLAY" + echo "[INFO] Session = $session" + + test -z "$(xauth list)" || echo "[INFO] Xauth session = OK" + ;; + "x11") + # X11 session, don't do anything here + echo "[INFO] Display = $DISPLAY" + echo "[INFO] Session = $session" + + # just show msg on whether the Xauth session cookie is setup + test -z "$(xauth list)" || echo "[INFO] Xauth session = OK" + ;; + "tty") + # TTY session, as user may not use a display manager + echo "[INFO] Display = $DISPLAY" + echo "[INFO] Session = $session" + + test -z "$(xauth list)" || echo "[INFO] Xauth session = OK" + ;; + *) + # anything here is an unknown session, most likely AKM will fail to load + echo "[WARN] Continuing, but cannot verify session for user." + ;; +esac +echo "---------------------------------------------------------------------------" + +echo "[INFO] Starting Arch Linux Kernel Manager" +pkexec '/usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py' + diff --git a/usr/share/applications/archlinux-kernel-manager.desktop b/usr/share/applications/archlinux-kernel-manager.desktop new file mode 100644 index 0000000..b5a4d68 --- /dev/null +++ b/usr/share/applications/archlinux-kernel-manager.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Arch Linux Kernel Manager +GenericName=Arch Linux Kernel Manager +X-GNOME-FullName=Arch Linux Kernel Manager +Comment=Arch Linux Kernel Manager - Add/Remove Linux kernels +Exec=/usr/bin/archlinux-kernel-manager +Icon=archlinux-kernel-manager-tux +Terminal=false +Type=Application +Categories=GTK;GNOME;Utility;Settings;X-GNOME-Settings-Panel;X-GNOME-SystemSettings;X-Unity-Settings-Panel;X-XFCE-SettingsDialog;X-XFCE-SystemSettings; +Keywords=kernel;tool;system +StartupNotify=true diff --git a/usr/share/archlinux-kernel-manager/akm.css b/usr/share/archlinux-kernel-manager/akm.css new file mode 100644 index 0000000..5f2598d --- /dev/null +++ b/usr/share/archlinux-kernel-manager/akm.css @@ -0,0 +1,176 @@ +box#box{ + padding: 5px 5px 5px 5px; +} + +box#main{ + padding-left: 20px; + padding-right: 20px; + padding-bottom: 20px; + padding-top: 10px; +} + +box#box_padding_left{ + padding-left: 5px; +} + +box#vbox_flowbox_message { + padding: 10px 10px 10px 10px; +} + +box#vbox_active_kernel{ + padding: 5px 5px 5px 5px; +} + +box#hbox_notify_revealer{ + padding: 10px 10px 10px 10px; + border-radius: 10px; + border-spacing: 5px; + background-color: #3fe1b0; + color: #0e1313; + border-color: transparent; +} + +box#box_row{ + padding-left: 20px; + padding-top: 8px; + padding-bottom: 8px; +} + +box#hbox_kernel{ + background-color: @theme_base_color; + padding-bottom: 8px; + box-shadow: -6px 8px 10px rgba(81,41,10,0.1),0px 2px 2px rgba(81,41,10,0.2); +} + +box#vbox_header{ + background-color: @theme_base_color; + border-top: 1px solid @borders; + border-bottom: 1px solid @borders; + border-left: 1px solid @borders; + border-right: 1px solid @borders; + padding: 5px 5px 5px 5px; + border-radius: 100px; + border-spacing: 5px; + font-weight: bold; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + box-shadow: -6px 8px 10px rgba(81,41,10,0.1),0px 2px 2px rgba(81,41,10,0.2); +} + +box#hbox_warning{ + background-color: #4c8bf5; + color: white; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + padding: 5px 5px 5px 5px; +} + +label#label_notify_revealer{ + background-color: #3fe1b0; + color: #0e1313; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + font-weight: bold; +} + +label#label_active_kernel { + font-family: 'Noto Sans', 'Helvetica', sans-serif; + font-size: 11px; +} + +label#label_stack_kernel { + font-size: 20px; + font-weight: 600; + border-top: 1px solid @borders; + border-bottom: 1px solid @borders; + border-left: 1px solid @borders; + border-right: 1px solid @borders; + padding: 5px 5px 5px 5px; + border-radius: 100px; + border-spacing: 5px; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + color: #fcfcfc; + background-color: @theme_base_color; +} + +label#label_stack_desc { + font-size: 12.5px; + font-weight: 600; + padding: 5px 5px 5px 5px; + font-style: italic; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +label#label_community_warning { + font-size: 12.5px; + padding: 5px 5px 5px 5px; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +label#label_error{ + font-size: 12.5px; + font-weight: 600; + padding: 5px 5px 5px 5px; + color: #fcfcfc; + background-color: #E42217; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + border-top: 1px solid @borders; + border-bottom: 1px solid @borders; + border-left: 1px solid @borders; + border-right: 1px solid @borders; + padding: 5px 5px 5px 5px; + border-radius: 10px; + border-spacing: 5px; +} + +label#label_stack_count { + font-size: 12px; + font-weight: 500; + padding: 5px 5px 5px 5px; + font-style: italic; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +label#label_kernel_version { + padding: 5px 5px 5px 5px; + font-size: 12px; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +label#label_kernel_flowbox { + padding: 5px 5px 5px 5px; + font-size: 11px; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +label#label_about { + padding: 5px 5px 5px 5px; + font-size: 11px; + font-family: 'Noto Sans', 'Helvetica', sans-serif; +} + +button#button_uninstall_kernel{ + font-weight: 600; +} + +.sidebar label{ + font-size: 12.5px; + font-weight: 500; + padding: 10px 10px 10px 10px; +} + +.sidebar label:hover, +.sidebar label:focus{ + font-weight: bold; +} + +#hbox_stack_sidebar{ + box-shadow: rgba(33, 35, 38, 0.1) 0px 10px 10px -10px; +} + +#textview_log text{ + background-color: #232627; + color: #fcfcfc; + font-family: 'Noto Sans', 'Helvetica', sans-serif; + border-top: 1px solid @borders; + border-bottom: 1px solid @borders; + border-left: 1px solid @borders; + border-right: 1px solid @borders; +} diff --git a/usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py b/usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py new file mode 100755 index 0000000..eef0a83 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +import os + +import gi +import signal +import libs.functions as fn + +from ui.ManagerGUI import ManagerGUI + +gi.require_version("Gtk", "4.0") + +from gi.repository import Gtk, Gio, GLib, Gdk + +base_dir = fn.os.path.dirname(fn.os.path.realpath(__file__)) + + +app_name = "Arch Linux Kernel Manager" +app_version = "${app_version}" +app_name_dir = "archlinux-kernel-manager" +app_id = "com.deltacopy.kernelmanager" +lock_file = "/tmp/akm.lock" +pid_file = "/tmp/akm.pid" + + +class Main(Gtk.Application): + def __init__(self): + super().__init__(application_id=app_id, flags=Gio.ApplicationFlags.FLAGS_NONE) + + def do_activate(self): + default_context = GLib.MainContext.default() + + win = self.props.active_window + if not win: + win = ManagerGUI( + application=self, + app_name=app_name, + default_context=default_context, + app_version=app_version, + ) + + display = Gtk.Widget.get_display(win) + + # sourced from /usr/share/icons/hicolor/scalable/apps + win.set_icon_name("archlinux-kernel-manager-tux") + + provider = Gtk.CssProvider.new() + css_file = Gio.file_new_for_path(base_dir + "/akm.css") + provider.load_from_file(css_file) + Gtk.StyleContext.add_provider_for_display( + display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + + win.present() + + def do_startup(self): + Gtk.Application.do_startup(self) + + def do_shutdown(self): + Gtk.Application.do_shutdown(self) + if os.path.exists(lock_file): + os.remove(lock_file) + if os.path.exists(pid_file): + os.remove(pid_file) + + +def signal_handler(sig, frame): + Gtk.main_quit(0) + + +# These should be kept as it ensures that multiple installation instances can't be run concurrently. +if __name__ == "__main__": + try: + # signal.signal(signal.SIGINT, signal_handler) + + if not fn.os.path.isfile(lock_file): + with open(pid_file, "w") as f: + f.write(str(fn.os.getpid())) + + # splash = SplashScreen() + + app = Main() + app.run(None) + else: + md = Gtk.MessageDialog( + parent=Main(), + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.YES_NO, + text="%s Lock File Found" % app_name, + ) + md.format_secondary_markup( + "A %s lock file has been found. This indicates there is already an instance of %s running.\n\ + Click 'Yes' to remove the lock file and try running again" + % (lock_file, app_name) + ) # noqa + + result = md.run() + md.destroy() + + if result in (Gtk.ResponseType.OK, Gtk.ResponseType.YES): + pid = "" + if fn.os.path.exists(pid_file): + with open(pid_file, "r") as f: + line = f.read() + pid = line.rstrip().lstrip() + + else: + # in the rare event that the lock file is present, but the pid isn't + fn.os.unlink(lock_file) + fn.sys.exit(1) + else: + fn.sys.exit(1) + except Exception as e: + # fn.logger.error("Exception in __main__: %s" % e) + print("Exception in __main__: %s" % e) diff --git a/usr/share/archlinux-kernel-manager/defaults/config.toml b/usr/share/archlinux-kernel-manager/defaults/config.toml new file mode 100644 index 0000000..3ea002f --- /dev/null +++ b/usr/share/archlinux-kernel-manager/defaults/config.toml @@ -0,0 +1,33 @@ +title = "ArchLinux Kernel Manager Settings" + +[kernels] + +# Kernels which are sourced from https://archive.archlinux.org +official = [ + { name = "linux", description = "The Linux kernel and modules (Stable)", headers = "linux-headers" }, + { name = "linux-lts", description = "The LTS Linux kernel and modules (Longterm)", headers = "linux-lts-headers" }, + { name = "linux-zen", description = "The Linux ZEN kernel and modules (Zen)", headers = "linux-zen-headers" }, + { name = "linux-hardened", description = "The Security-Hardened Linux kernel and modules (Hardened)", headers = "linux-hardened-headers" }, + { name = "linux-rt", description = "The Linux RT kernel and modules (Realtime)", headers = "linux-rt-headers" }, + { name = "linux-rt-lts", description = "The Linux RT LTS kernel and modules (Realtime Longterm)", headers = "linux-rt-lts-headers" }, +] + +# Kernels which are sourced from unofficial repositories, these won't work if you haven't updated your pacman configuration +# https://wiki.archlinux.org/title/Unofficial_user_repositories +community = [ + { name = "linux-xanmod", description = "The Linux kernel and modules with Xanmod patches", headers = "linux-xanmod-headers", repository = "chaotic-aur" }, + { name = "linux-xanmod-lts", description = "The Linux kernel and modules with Xanmod patches", headers = "linux-xanmod-lts-headers", repository = "chaotic-aur" }, + { name = "linux-amd", description = "Linux kernel aimed at the ZNVER4/MZEN4 AMD Ryzen CPU based hardware", headers = "linux-amd-headers", repository = "chaotic-aur" }, + { name = "linux-cachyos", description = "The Linux EEVDF-BORE scheduler Kernel by CachyOS with other patches and improvements kernel and modules", headers = "linux-cachyos-headers", repository = "chaotic-aur" }, + { name = "linux-ck", description = "The Linux kernel and modules with ck's hrtimer patches", headers = "linux-ck-headers", repository = "repo-ck" }, + { name = "linux-clear", description = "The Clear Linux kernel and modules", headers = "linux-clear-headers", repository = "chaotic-aur" }, + { name = "linux-lqx", description = "The Linux Liquorix kernel and modules", headers = "linux-lqx-headers", repository = "chaotic-aur" }, + { name = "linux-nitrous", description = "Modified Linux kernel optimized for Skylake and newer, compiled using clang", headers = "linux-nitrous-headers", repository = "chaotic-aur" }, +] + +# Custom bootloader example +# Only systemd-boot or grub are valid names +# When using grub also set grub_config +#[bootloader] +#name = "grub" +#grub_config = "/boot/grub/grub.cfg" diff --git a/usr/share/archlinux-kernel-manager/images/364x408/akm-tux-splash.png b/usr/share/archlinux-kernel-manager/images/364x408/akm-tux-splash.png new file mode 100644 index 0000000000000000000000000000000000000000..c1479a3a929eb7bc5d63e6b0e4875c8026509443 GIT binary patch literal 59648 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≠Q;pP&EdV%``6If|2AlXc77Y(ooDWi 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 ig*nV&4s*a}gZ}^!0B~b+ZC%g+0000uqssm literal 0 HcmV?d00001 diff --git a/usr/share/archlinux-kernel-manager/images/48x48/akm-install.png b/usr/share/archlinux-kernel-manager/images/48x48/akm-install.png new file mode 100644 index 0000000000000000000000000000000000000000..7e86cf3e1b8081ffb3756c1f094c22b1928bfdbf GIT binary patch literal 2576 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 literal 0 HcmV?d00001 diff --git a/usr/share/archlinux-kernel-manager/images/48x48/akm-progress.png b/usr/share/archlinux-kernel-manager/images/48x48/akm-progress.png new file mode 100644 index 0000000000000000000000000000000000000000..623a39a32ee24679bb2518d8f6d2c966ec2a0a6a GIT binary patch literal 1368 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 literal 0 HcmV?d00001 diff --git a/usr/share/archlinux-kernel-manager/images/48x48/akm-star.png b/usr/share/archlinux-kernel-manager/images/48x48/akm-star.png new file mode 100644 index 0000000000000000000000000000000000000000..de1748cd364a5ba1ef3234f5b05ea90027058dd3 GIT binary patch literal 1049 zcmV+!1m^pRP)Sg5SUd3Vi4%@8A<}VQNZ$w=kXp2Pja-n;7lC0Ex0BVs9yNiUyW3 zfN>OPOGDnCi8iy2s;k;avu%8xGXDh#9M4GbcOh`he*qHreG?!N@|XNQ45viI#~J8V z&+t!xTD1O+J}nV*{ygy@N>s-bxR?h47tLn{QS$4AB}P%8lmZo$xJ!W^--*i-M$>+LC4(aq%RbgR^;A09zQ57F)-yW) zY@^PHuj_oxWKs?yqu(koTuwDC_ z4(fbQi_Sk?Pu>}$)1RNEn}K((7cnIkQQ~7tC-DLz9b|P$b^*$l+{c6Y+ShhT&!TP3 z6=dx~oo>63DU`lYV0m`(0j#sWo>Duo$@X@VjFEL)ybB19aO&4C?&r_W8)W@foo?Kh zZ7uXMfK8c3euw!Do2hX;+3+>;OFbEyo>@S=Jt}qlJ+H~WZnAL?8HiB4uLqeaz`&}k@t9w%^e#Z)TmcZLg(`34_3Lzd0T8E!4qnC(FQY%F z07x?d0nBzUK;p=N6P78cEbfnbfyRI5w@T6f*c#Bq9q_PNy&7*~Si;TApGU-(kxvjI z5%Mv7YJ4Sc3J^Hv;|v_l{}q!!Ck4)%yOz2>O1(CUN7%YzQord>P@px*zdApcd=d@T z7CSS|CEt{^-xI3Lz?76%7KxcwUk$2n6znytz!DUbv6wHqZKJ?Wh5my7_7~bumo^r6 Tiof-X00000NkvXXu0mjfD|_B^ literal 0 HcmV?d00001 diff --git a/usr/share/archlinux-kernel-manager/images/48x48/akm-tux.png b/usr/share/archlinux-kernel-manager/images/48x48/akm-tux.png new file mode 100644 index 0000000000000000000000000000000000000000..571927988baf267c6561002dab3eb4643692e5d2 GIT binary patch literal 2610 zcmV-23eEM2P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13BO51 zK~!jg&6#^}RMj2FKX>om-E3Z)&F+S5$dXN@BnqJ@j-Z1)LWTj9+Li&vDUR~;L5t3S zb)@x;ud#oWj;2!;wOYisB34l-5=Mfy)<=0N@`yZwK(c`(yV>X6y?gq{k~Ev_ZZ;&8 zerL|?-FtrL_xpX%`Q6|BopWL6aV_vVa1w|BK43TS6i|8z>)sca$=9S0&BU=8aAtKb5hT^DAv8A*~7I_-8lg@uKb zl$7+mEdi4UYj~&#n99q`sjI7F^5n_r^?Cp#Nn+u`g}nIUi@fyGOW15SWI5rnb0*a` z01OoYX~~ksj2v0Px^*uzapD93YHDg&yY{zuJRR8WR_?!l6`H2?9G5dh_6Lp6-h6Yt zuvjeOkw+dD&p!K%Fq_R{)vA?Z)24TX(P$7;r%n}aceBXN&F%RdtQ)N1p;`g8y}g~z z&Q5l0--=!LF%R4xpg8OAcssr)99G%6b0;2;r}qjBI*scGtKb3x2M&DAy5Fs4Y-|mc zqd}=P;ch=gVbK~I@6ltsVkP_b?I#!v_8gZzMD_=bzgCs%#p%~ALfwoA??R}1vPJCh zXdw<+h4@wy+CEu)`ifELq=Z!3Fj&Jw6+-Pxr$o_3+5wORNU{|P83+*c!=X<#%C#_t z#5J#GkYO}b1lqRmmvDE;@p%OhD$&RpP9BbO=5v8ffv-+j2}Piy;tIx$83SNpkne(_ zs)erY(5$uNr`$pFp)XNo=r|L=b0|n&SVNWU9L&8RG4dW78X7o%{yd8oEhZcebNck@ z9N_KCP2g6C!*Sh$#Vhf)1qmHH1VKS0s&LjYofhXq3?FqPqehKFmi27gwvAJ#PT_XD z@%#N%!23Y+#Tb_Q7wH#ez}|cBT~V-j@gm%AH(_rxu~0ioz7u1Pney^-oK7c_BoPP% zsI0t_vuDrt)bS7Ep%fda@xiV@fh0-wrKP1KHf(s4;lqc=b3F=5uASk|GODYqv0AO& z^^u||0Bql0-&4;BKm|S-q(1|K0jLMWhaYYh(Wok-QB|mFNHjN}5p8YfL?jaKzTdO8 zw44CGp;vn#rk1Z==_U4Ilsp{1n- zRaLtMR8{RJ6^%ym`TVrDwsQRV2^t$4>F8)@?%cUJ91iq)eeeB)1NhBF_?0nM0wO0T zM-&y=#leH$2vrRwgh(h9NEtqXfKMDh{+&2*V83|py}t=S6c!eWf`Wpcp%ewiUMd1R z91e#t7z|?Fx|h?&db-x$eqMb4{V8$NO|ym3XcRV^O%xRs^(67?rEuOXNm8hyqC!+v zRf%vo*vGN{9^jrW81#z|KG-arPN$eTbEc@QtQ0z(u8Tk?mte!QY}l}2LQxb^SNB3v zr~H23fL0<9@QLE$Vv(DhD@-PnC@(MXxeD|8b7&wBu?|=vNfJ7pjs*+mCl!iBA_Gtf z27>{Y%Z1P9BNz;lot-f0Gc$22Q~X$`(^X57#EmyjqM)E4sZcC7;A#MutE~H4uTLlr zZs>1prUYc*alKxT-|uJAq)90SrHiPp%w}`kh%6J0CTuyc08Ht|W=dclFve&!BFi$P zM~_Y^*!xT<6LgI!DJkLP$rC-h6recW*h~q`0dP8Xs9kah|!>sm6du`g%BzG`alan)Jtot8?)Jr!C=7YbmDTky245Y zPN!3mQ91qmVyfy^Q9FMcXsVZ8UmK~ZnTAf+mwz}M#%N4=NS)~ak>%)*-A<0w#2Z@z zxZQ5NUN6C5kn69%9)osz-#L#~mrh5&EAYvKSZc@eiwmcPET5yYf5%$p&0B!q@9(Qn zm)RCW4R|=PV?C0Ha{P>%&?iX}O-(0x?bqR|rdKFQp#n&8F7(;sEYRzyMxY3ZTMDfV z@9L9fcwq%hyl*qprcGzlrj3-AmUbJ?&Q6;&fOwciA`yIk4K$$FJcL3aYOg%X#8G4) z*~=Zk(-{-^*BWLN7xU*5C&aY)o>S=+5tS9NdOke($K70a-9+AcYXeiJ#19veg;j63 z-EK~uJWdQ5(3m|AHjaedC%~Qu%V)zcZ;Q(oyQoa3tB*BFFv4C0iUP(gFl6=3?Gmu; zCorxo?##@Yv)H)toqiqff~IL8Nk|v0mpI_Hd!gY;*!Be6JqPp`CM~~!`Cuv&pKt==(a zqJm9WIjWy@Cb zO<>K-%uY;$6Gvg+w;ay*;z>)uoXyV*3wz&94G9=u*39~+=Tcu^Pc$0sBh%E>#6u4~ z%+*(qXV0EJ02G+Oo@Sz=Vep)WBfDYKCd8Iqyc_T^WB%JDRC@_&^tI&_XX&}x*9wLl zP)zBUwF^249NJg>^hdKX=2#dzwi=twMmQYi=+Q=w968db#IWaqK8bD-1MgXAJ`SHB zL!9#Pp{(bL)qmm3WVRCsEc!hyf4ZBsvYtDN^0DM+$J3cU>%YIU(#xO#H3+f}vJG&q zy`6@JUHy`lB?z`b*an7d&{PO{p|cs9nqlkLG^!DnKC?CbH-z3VGI84}sY z*&w$f)+cXA&04$OJu1bK1CNw3AUkEJrdb z5Y^zlFW~LZc%%Lhw`ZacD7>MZ$E>-mn0^&p<%oOo%^BF+#JlcRZr$2QMmhPSPN(OM zXOb*)XJHQ0nmf?iJK50E$YTfNk7p0m26B<@CPwdfN4N+KqK* zYlp;m;lV&c6o_~U#>5AS0>(g$2}(jxd6C2iqP!Y4(U73L5O-a*D}r}m zOmGj33!c!Gk~{QW(ba#l7Oo z1S-A5AFmXAzRQEW$&H+$=W7t}J}4hP7&)@i@Oh&fc|#9!I@brX;Dz9@WjSk}|2KHO z3t7Fhn$c3UBzSl{k5mGYKcv8?>@=blAEFj7qGk`qyFEx*eRaX-^iJe-6tWsBT~Jup z*DX*9Mvh8>&r`jacz!SDrf2Y1=BB0*8+ZYEtG_;Yttj9?R&6^`7ktjM`^hpLE=Vbx zyrUPB`wrs%#9!QpoW)l^JZVQ(Wt+~bOwTM?piDF6WC4L5`d#V|%i9B(*u8g=K(uc+a%Qh2_>9s5uG10+LDX)mCGXHyIo&32 z3m`VIXOY0TCxpC2_>4jnFoW&v*4KXf{-Iapv`X;{MCrlB0yY{s8SvcBZlpU*Z`H1U z|KM3UeV(@k5OoIcrLR^oVsOfW=XP{~>o9&>TfjH?v)oN;w5#VH{<)bMjA|V+;Y~=l z8?M(bAukh`yTgtdU8vrkT@biaEMZ&}kOSXgM0&fiQd__?z{?#Eq6Qc42;~KV+i4Et zTDv^>b|cvBhAIeVO$~uiQSNLL)jKhp%`FJb#3m8d*kr+{w;RB2)1&5?UGjvIatE`R zfx@jD$pwL_>m!JX0y5#*=d{x`OQ_`TzbbdCoX}I49vNE@xOw#&CPc+BC zzJyeGPUpl}FpN9p%HR7yrGha(16Bl=3{MtgP1Z+i3e0~6DtJulF_6_!U{9Q0Gz+CZ z`~*pbSrUAzjYO(deTGXKG~|S-e^&E zE?Hn+!WQ^K${9AJr4UtFzEK9^CLEA_*xr&BHJ3pbj{+IKz6F4AsY$8~h|r+1i6aUG^# z*mh&_e)!ZDjlgcvTuE&vKTEW!hWO>G^G1d3?0h`EL%$)jjeMGGZ(rB22d`!|L__l@ X7qP1t5%o>D00000NkvXXu0mjfvNgi+ literal 0 HcmV?d00001 diff --git a/usr/share/archlinux-kernel-manager/images/48x48/akm-verified.png b/usr/share/archlinux-kernel-manager/images/48x48/akm-verified.png new file mode 100644 index 0000000000000000000000000000000000000000..653949780b2003387413a4c5d2908388d27ac0be GIT binary patch literal 909 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 SCCowq0000 datetime_value_self: + return datetime_value_other + + +class CommunityKernel: + def __init__(self, name, headers, repository, version, build_date, install_size): + self.name = name + self.headers = headers + self.repository = repository + self.version = version + self.build_date = build_date + self.install_size = install_size + + def __gt__(self, other): + if other.name > self.name: + return other + + +class InstalledKernel: + def __init__(self, name, version, date, size): + self.name = name + self.version = version + self.date = date + self.size = size diff --git a/usr/share/archlinux-kernel-manager/libs/functions.py b/usr/share/archlinux-kernel-manager/libs/functions.py new file mode 100644 index 0000000..fdb57c9 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/libs/functions.py @@ -0,0 +1,1703 @@ +import logging +import shutil +import sys +import os +import distro +from os import makedirs +import requests +import threading +import re +import time +import subprocess +import gi +import datetime +import psutil +import queue +import pathlib +import tomlkit +from tomlkit import dumps, load +from datetime import timedelta +from logging.handlers import TimedRotatingFileHandler +from threading import Thread +from queue import Queue +from ui.MessageWindow import MessageWindow +from libs.Kernel import Kernel, InstalledKernel, CommunityKernel + +gi.require_version("Gtk", "4.0") +from gi.repository import GLib + + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + +latest_archlinux_package_search_url = ( + "https://archlinux.org/packages/search/json?name=${PACKAGE_NAME}" +) +archlinux_mirror_archive_url = "https://archive.archlinux.org" +headers = {"Content-Type": "text/plain;charset=UTF-8"} + +dist_id = distro.id() +dist_name = distro.name() + +cache_days = 5 +fetched_kernels_dict = {} +cached_kernels_list = [] +community_kernels_list = [] +supported_kernels_dict = {} +community_kernels_dict = {} +pacman_repos_list = [] +process_timeout = 200 + +sudo_username = os.getlogin() +home = "/home/" + str(sudo_username) + +# pacman log file +pacman_logfile = "/var/log/pacman.log" + +# pacman lock file +pacman_lockfile = "/var/lib/pacman/db.lck" + +# pacman conf file +pacman_conf_file = "/etc/pacman.conf" + +# thread names +thread_get_kernels = "thread_get_kernels" +thread_get_community_kernels = "thread_get_community_kernels" +thread_install_community_kernel = "thread_install_community_kernel" +thread_install_archive_kernel = "thread_install_archive_kernel" +thread_check_kernel_state = "thread_check_kernel_state" +thread_uninstall_kernel = "thread_uninstall_kernel" +thread_monitor_messages = "thread_monitor_messages" +thread_refresh_cache = "thread_refresh_cache" +thread_refresh_ui = "thread_refresh_ui" + +cache_dir = "%s/.cache/archlinux-kernel-manager" % home +cache_file = "%s/kernels.toml" % cache_dir +cache_update = "%s/update" % cache_dir + +log_dir = "/var/log/archlinux-kernel-manager" +event_log_file = "%s/event.log" % log_dir + + +config_file_default = "%s/defaults/config.toml" % base_dir +config_dir = "%s/.config/archlinux-kernel-manager" % home +config_file = "%s/.config/archlinux-kernel-manager/config.toml" % home + +logger = logging.getLogger("logger") + +# create console handler and set level to debug +ch = logging.StreamHandler() + + +logger.setLevel(logging.DEBUG) +ch.setLevel(logging.DEBUG) + +# create formatter +formatter = logging.Formatter( + "%(asctime)s:%(levelname)s > %(message)s", "%Y-%m-%d %H:%M:%S" +) +# add formatter to ch +ch.setFormatter(formatter) + + +# add ch to logger +logger.addHandler(ch) + + +# ===================================================== +# CHECK FOR KERNEL UPDATES +# ===================================================== +def get_latest_kernel_updates(self): + logger.info("Getting latest kernel versions") + try: + last_update_check = None + fetch_update = False + cache_timestamp = None + + if os.path.exists(cache_file): + with open(cache_file, "r", encoding="utf-8") as f: + # data = tomlkit.load(f) + + data = f.readlines()[2] + + if len(data) == 0: + logger.error( + "%s is empty, delete it and open the app again" % cache_file + ) + + if len(data) > 0 and "timestamp" in data.strip(): + # cache_timestamp = data["timestamp"] + cache_timestamp = ( + data.split("timestamp = ")[1].replace('"', "").strip() + ) + + if not os.path.exists(cache_update): + last_update_check = datetime.datetime.now().strftime("%Y-%m-%d") + with open(cache_update, mode="w", encoding="utf-8") as f: + f.write("%s\n" % last_update_check) + + permissions(cache_dir) + + else: + with open(cache_update, mode="r", encoding="utf-8") as f: + last_update_check = f.read().strip() + + with open(cache_update, mode="w", encoding="utf-8") as f: + f.write("%s\n" % datetime.datetime.now().strftime("%Y-%m-%d")) + + permissions(cache_dir) + + logger.info( + "Linux package update last fetched on %s" + % datetime.datetime.strptime(last_update_check, "%Y-%m-%d").date() + ) + + if ( + datetime.datetime.strptime(last_update_check, "%Y-%m-%d").date() + < datetime.datetime.now().date() + ): + + logger.info("Fetching Linux package update data") + + response = requests.get( + latest_archlinux_package_search_url.replace( + "${PACKAGE_NAME}", "linux" + ), + headers=headers, + allow_redirects=True, + timeout=60, + stream=True, + ) + + if response.status_code == 200: + if response.json() is not None: + if len(response.json()["results"]) > 0: + if response.json()["results"][0]["last_update"]: + logger.info( + "Linux kernel package last update = %s" + % datetime.datetime.strptime( + response.json()["results"][0]["last_update"], + "%Y-%m-%dT%H:%M:%S.%f%z", + ).date() + ) + if ( + datetime.datetime.strptime( + response.json()["results"][0]["last_update"], + "%Y-%m-%dT%H:%M:%S.%f%z", + ).date() + ) > ( + datetime.datetime.strptime( + cache_timestamp, "%Y-%m-%d %H-%M-%S" + ).date() + ): + logger.info( + "Linux kernel package updated, cache refresh required" + ) + + refresh_cache(self) + + return True + + else: + logger.info( + "Linux kernel package not updated, cache refresh not required" + ) + + return False + + else: + logger.info("Kernel update check not required") + + return False + + else: + logger.info("No cache file present, refresh required") + if not os.path.exists(cache_update): + last_update_check = datetime.datetime.now().strftime("%Y-%m-%d") + with open(cache_update, mode="w", encoding="utf-8") as f: + f.write("%s\n" % last_update_check) + + permissions(cache_dir) + + return False + + except Exception as e: + logger.error("Exception in get_latest_kernel_updates(): %s" % e) + return True + + +# ===================================================== +# CACHE LAST MODIFIED +# ===================================================== +def get_cache_last_modified(): + try: + if os.path.exists(cache_file): + timestamp = datetime.datetime.fromtimestamp( + pathlib.Path(cache_file).stat().st_mtime, tz=datetime.timezone.utc + ) + + return "%s %s" % ( + timestamp.date(), + str(timestamp.time()).split(".")[0], + ) + + else: + return "Cache file does not exist" + except Exception as e: + logger.error("Exception in get_cache_last_modified(): %s" % e) + + +# ===================================================== +# LOG DIRECTORY +# ===================================================== + +try: + if not os.path.exists(log_dir): + makedirs(log_dir) +except Exception as e: + logger.error("Exception in make log directory(): %s" % e) + + +# rotate the events log every Friday +tfh = TimedRotatingFileHandler(event_log_file, encoding="utf-8", delay=False, when="W4") +tfh.setFormatter(formatter) +logger.addHandler(tfh) + +# ===================================================== +# PERMISSIONS +# ===================================================== + + +def permissions(dst): + try: + groups = subprocess.run( + ["sh", "-c", "id " + sudo_username], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + for x in groups.stdout.decode().split(" "): + if "gid" in x: + g = x.split("(")[1] + group = g.replace(")", "").strip() + subprocess.call(["chown", "-R", sudo_username + ":" + group, dst], shell=False) + + except Exception as e: + logger.error("Exception in permissions(): %s" % e) + + +def setup_config(self): + try: + if not os.path.exists(config_dir): + makedirs(config_dir) + + if not os.path.exists(config_file): + shutil.copy(config_file_default, config_dir) + permissions(config_dir) + + return read_config(self) + + except Exception as e: + logger.error("Exception in setup_config(): %s" % e) + + +def update_config(config_data, bootloader): + try: + logger.info("Updating config data") + + with open(config_file, "w") as f: + tomlkit.dump(config_data, f) + + return True + + except Exception as e: + logger.error("Exception in update_config(): %s" % e) + return False + + +def read_config(self): + try: + logger.debug("Config file = %s" % config_file) + logger.info("Reading in config file") + config_data = None + with open(config_file, "rb") as f: + config_data = tomlkit.load(f) + + for official_kernel in config_data["kernels"]["official"]: + supported_kernels_dict[official_kernel["name"]] = ( + official_kernel["description"], + official_kernel["headers"], + ) + + for community_kernel in config_data["kernels"]["community"]: + community_kernels_dict[community_kernel["name"]] = ( + community_kernel["description"], + community_kernel["headers"], + community_kernel["repository"], + ) + + return config_data + except Exception as e: + logger.error("Exception in read_config(): %s" % e) + sys.exit(1) + + +def create_cache_dir(): + try: + if not os.path.exists(cache_dir): + makedirs(cache_dir) + + logger.info("Cache directory = %s" % cache_dir) + + permissions(cache_dir) + except Exception as e: + logger.error("Exception in create_cache_dir(): %s" % e) + + +def create_log_dir(): + try: + if not os.path.exists(log_dir): + makedirs(log_dir) + + logger.info("Log directory = %s" % log_dir) + except Exception as e: + logger.error("Exception in create_log_dir(): %s" % e) + + +def write_cache(): + try: + if len(fetched_kernels_dict) > 0: + with open(cache_file, "w", encoding="utf-8") as f: + f.write('title = "Arch Linux Kernels"\n\n') + f.write( + 'timestamp = "%s"\n' + % datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ) + f.write('source = "%s"\n\n' % archlinux_mirror_archive_url) + + for kernel in fetched_kernels_dict.values(): + f.write("[[kernel]]\n") + f.write( + 'name = "%s"\nheaders = "%s"\nversion = "%s"\nsize = "%s"\nfile_format = "%s"\nlast_modified = "%s"\n\n' + % ( + kernel.name, + kernel.headers, + kernel.version, + kernel.size, + kernel.file_format, + kernel.last_modified, + ) + ) + permissions(cache_file) + except Exception as e: + logger.error("Exception in write_cache(): %s" % e) + + +# install from the ALA +def install_archive_kernel(self): + try: + for pkg_archive_url in self.official_kernels: + if self.errors_found is True: + break + + install_cmd_str = [ + "pacman", + "-U", + pkg_archive_url, + "--noconfirm", + "--needed", + ] + + wait_for_pacman_process() + + logger.info("Running %s" % install_cmd_str) + + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(install_cmd_str), + ) + + event_log = [] + self.messages_queue.put(event) + + with subprocess.Popen( + install_cmd_str, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + print(line.strip()) + self.messages_queue.put(line) + event_log.append(line.lower().strip()) + + time.sleep(0.3) + + error = None + + if "installation finished. no error reported." in event_log: + error = False + else: + # check errors and indicate to user install failed + for log in event_log: + # if "installation finished. no error reported." in log: + # error = False + # break + if "error" in log or "errors" in log: + + event = ( + "%s [ERROR]: Errors have been encountered during installation\n" + % (datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) + ) + + self.messages_queue.put(event) + + self.errors_found = True + + error = True + + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} failed\n" + f"There have been errors, please review the logs\n", + "images/48x48/akm-warning.png", + priority=GLib.PRIORITY_DEFAULT, + ) + + break + + # query to check if kernel installed + if "headers" in pkg_archive_url: + if ( + check_kernel_installed(self.kernel.name + "-headers") + and error is False + ): + + self.kernel_state_queue.put( + (0, "install", self.kernel.name + "-headers") + ) + + event = "%s [INFO]: Installation of %s-headers completed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + ) + + self.messages_queue.put(event) + + else: + self.kernel_state_queue.put( + (1, "install", self.kernel.name + "-headers") + ) + + event = "%s [ERROR]: Installation of %s-headers failed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + ) + + self.errors_found = True + self.messages_queue.put(event) + + else: + if check_kernel_installed(self.kernel.name) and error is False: + self.kernel_state_queue.put((0, "install", self.kernel.name)) + + event = "%s [INFO]: Installation of kernel %s completed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + ) + + self.messages_queue.put(event) + + else: + self.kernel_state_queue.put((1, "install", self.kernel.name)) + + event = "%s [ERROR]: Installation of kernel %s failed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + ) + + self.messages_queue.put(event) + + # signal to say end reached + self.kernel_state_queue.put(None) + + except Exception as e: + logger.error("Exception in install_archive_kernel(): %s" % e) + + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} failed\n" + f"There have been errors, please review the logs\n", + "images/48x48/akm-warning.png", + priority=GLib.PRIORITY_DEFAULT, + ) + + +def refresh_cache(self): + if os.path.exists(cache_file): + os.remove(cache_file) + get_official_kernels(self) + write_cache() + + +def read_cache(self): + try: + self.timestamp = None + with open(cache_file, "rb") as f: + data = tomlkit.load(f) + + if len(data) == 0: + logger.error( + "%s is empty, delete it and open the app again" % cache_file + ) + + name = None + headers = None + version = None + size = None + last_modified = None + file_format = None + + if len(data) > 0: + self.timestamp = data["timestamp"] + + self.cache_timestamp = data["timestamp"] + + # check date of cache, if it's older than 5 days - refresh + + if self.timestamp: + self.timestamp = datetime.datetime.strptime( + self.timestamp, "%Y-%m-%d %H-%M-%S" + ) + + delta = datetime.datetime.now() - self.timestamp + + if delta.days >= cache_days: + logger.info("Cache is older than 5 days, refreshing ..") + refresh_cache(self) + else: + + if delta.days > 0: + logger.debug("Cache is %s days old" % delta.days) + else: + logger.debug("Cache is newer than 5 days") + + kernels = data["kernel"] + + if len(kernels) > 1: + for k in kernels: + + # any kernels older than 2 years + # (currently linux v4.x or earlier) are deemed eol so ignore them + + # if ( + # datetime.datetime.now().year + # - datetime.datetime.strptime( + # k["last_modified"], "%d-%b-%Y %H:%M" + # ).year + # <= 2 + # ): + cached_kernels_list.append( + Kernel( + k["name"], + k["headers"], + k["version"], + k["size"], + k["last_modified"], + k["file_format"], + ) + ) + + name = None + headers = None + version = None + size = None + last_modified = None + file_format = None + + if len(cached_kernels_list) > 0: + sorted(cached_kernels_list) + logger.info("Kernels cache data processed") + else: + logger.error( + "Cached file is invalid, remove it and try again" + ) + + else: + logger.error("Failed to read cache file") + + except Exception as e: + logger.error("Exception in read_cache(): %s" % e) + + +# get latest versions of the official kernels +def get_latest_versions(self): + try: + kernel_versions = {} + for kernel in supported_kernels_dict: + check_cmd_str = ["pacman", "-Si", kernel] + + process_kernel_query = subprocess.Popen( + check_cmd_str, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + out, err = process_kernel_query.communicate(timeout=process_timeout) + + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if line.startswith("Version :"): + kernel_versions[kernel] = line.split("Version :")[1] + break + + self.kernel_versions_queue.put(kernel_versions) + + except Exception as e: + logger.error("Exception in get_latest_versions(): %s" % e) + + +def parse_archive_html(response, linux_kernel): + for line in response.splitlines(): + if " 0: + if "-x86_64" in files[0]: + version = files[0].split("-x86_64")[0] + file_format = files[0].split("-x86_64")[1] + + url = ( + "/packages/l/%s" % archlinux_mirror_archive_url + + "/%s" % linux_kernel + + "/%s" % files[0] + ) + + if ".sig" not in file_format: + if len(line.rstrip().split(" ")) > 0: + size = line.strip().split(" ").pop().strip() + + last_modified = line.strip().split("").pop() + for x in last_modified.split(" "): + if len(x.strip()) > 0 and ":" in x.strip(): + # 02-Mar-2023 21:12 + # %d-%b-Y %H:%M + last_modified = x.strip() + + headers = "%s%s" % ( + supported_kernels_dict[linux_kernel][1], + version.replace(linux_kernel, ""), + ) + + if ( + version is not None + and url is not None + and headers is not None + and datetime.datetime.now().year + - datetime.datetime.strptime( + last_modified, "%d-%b-%Y %H:%M" + ).year + <= 2 # ignore kernels <=2 years old + ): + ke = Kernel( + linux_kernel, + headers, + version, + size, + last_modified, + file_format, + ) + + fetched_kernels_dict[version] = ke + + version = None + file_format = None + url = None + size = None + last_modified = None + + +def wait_for_response(response_queue): + while True: + # time.sleep(0.1) + items = response_queue.get() + + # error break from loop + if items is None: + break + + # we have all kernel data break + if len(supported_kernels_dict) == len(items): + break + + +def get_response(session, linux_kernel, response_queue, response_content): + response = session.get( + "%s/packages/l/%s" % (archlinux_mirror_archive_url, linux_kernel), + headers=headers, + allow_redirects=True, + timeout=60, + stream=True, + ) + + if response.status_code == 200: + logger.debug("Response is 200") + if response.text is not None: + response_content[linux_kernel] = response.text + response_queue.put(response_content) + else: + logger.error("Something went wrong with the request") + logger.error(response.text) + response_queue.put(None) + + +def get_official_kernels(self): + try: + if not os.path.exists(cache_file) or self.refresh_cache is True: + session = requests.session() + response_queue = Queue() + response_content = {} + # loop through linux kernels + for linux_kernel in supported_kernels_dict: + logger.info( + "Fetching data from %s/packages/l/%s" + % (archlinux_mirror_archive_url, linux_kernel) + ) + Thread( + target=get_response, + args=( + session, + linux_kernel, + response_queue, + response_content, + ), + daemon=True, + ).start() + + wait_for_response(response_queue) + session.close() + + for kernel in response_content: + parse_archive_html(response_content[kernel], kernel) + + if len(fetched_kernels_dict) > 0: # and self.refresh_cache is True: + write_cache() + read_cache(self) + + self.queue_kernels.put(cached_kernels_list) + # elif self.refresh_cache is False: + # logger.info("Cache already processed") + # read_cache(self) + # self.queue_kernels.put(cached_kernels_list) + + else: + logger.error("Failed to retrieve Linux Kernel list") + self.queue_kernels.put(None) + else: + logger.debug("Reading cache file = %s" % cache_file) + # read cache file + read_cache(self) + self.queue_kernels.put(cached_kernels_list) + + except Exception as e: + logger.error("Exception in get_official_kernels(): %s" % e) + + +def wait_for_cache(self): + while True: + if not os.path.exists(cache_file): + time.sleep(0.2) + else: + read_cache(self) + break + + +# ===================================================== +# THREADING +# ===================================================== + + +# check if the named thread is running +def is_thread_alive(thread_name): + for thread in threading.enumerate(): + if thread.name == thread_name and thread.is_alive(): + return True + + return False + + +# print all threads +def print_all_threads(): + for thread in threading.enumerate(): + logger.info("Thread = %s and state is %s" % (thread.name, thread.is_alive())) + + +# ===================================================== +# UPDATE TEXTVIEW IN PROGRESS WINDOW +# ===================================================== + + +def update_progress_textview(self, line): + try: + if len(line) > 0: + self.textbuffer.insert_markup( + self.textbuffer.get_end_iter(), " %s" % line, len(" %s" % line) + ) + except Exception as e: + logger.error("Exception in update_progress_textview(): %s" % e) + finally: + self.messages_queue.task_done() + text_mark_end = self.textbuffer.create_mark( + "end", self.textbuffer.get_end_iter(), False + ) + # scroll to the end of the textview + self.textview.scroll_mark_onscreen(text_mark_end) + + +# ===================================================== +# MESSAGES QUEUE: MONITOR THEN UPDATE TEXTVIEW +# ===================================================== + + +def monitor_messages_queue(self): + try: + while True: + message = self.messages_queue.get() + + GLib.idle_add( + update_progress_textview, + self, + message, + priority=GLib.PRIORITY_DEFAULT, + ) + except Exception as e: + logger.error("Exception in monitor_messages_queue(): %s" % e) + + +# ===================================================== +# CHECK IF KERNEL INSTALLED +# ===================================================== + + +def check_kernel_installed(name): + try: + logger.info("Checking kernel package %s is installed" % name) + check_cmd_str = ["pacman", "-Q", name] + logger.debug("Running cmd = %s" % check_cmd_str) + process_kernel_query = subprocess.Popen( + check_cmd_str, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + out, err = process_kernel_query.communicate(timeout=process_timeout) + + logger.debug(out.decode("utf-8")) + + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if line.split(" ")[0] == name: + return True + else: + return False + + return False + except Exception as e: + logger.error("Exception in check_kernel_installed(): %s" % e) + + +def wait_for_pacman_process(): + + timeout = 120 + i = 0 + while check_pacman_lockfile(): + time.sleep(0.1) + logger.debug("Pacman lockfile found .. waiting") + i += 1 + if i == timeout: + logger.info("Timeout reached") + break + + +# ===================================================== +# REMOVE KERNEL +# ===================================================== + + +def uninstall(self): + try: + kernel_installed = check_kernel_installed(self.kernel.name) + logger.info("Kernel installed = %s" % kernel_installed) + kernel_headers_installed = check_kernel_installed(self.kernel.name + "-headers") + logger.info("Kernel headers installed = %s" % kernel_headers_installed) + + uninstall_cmd_str = None + event_log = [] + + if kernel_installed is True and kernel_headers_installed is True: + uninstall_cmd_str = [ + "pacman", + "-Rs", + self.kernel.name, + self.kernel.name + "-headers", + "--noconfirm", + ] + + if kernel_installed is True and kernel_headers_installed is False: + uninstall_cmd_str = ["pacman", "-Rs", self.kernel.name, "--noconfirm"] + + if kernel_installed == 0: + logger.info("Kernel is not installed, uninstall not required") + self.kernel_state_queue.put((0, "uninstall", self.kernel.name)) + + logger.debug("Uninstall cmd = %s" % uninstall_cmd_str) + + # check if kernel, and kernel header is actually installed + if uninstall_cmd_str is not None: + + wait_for_pacman_process() + + logger.info("Running %s" % uninstall_cmd_str) + + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(uninstall_cmd_str), + ) + self.messages_queue.put(event) + + with subprocess.Popen( + uninstall_cmd_str, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + self.messages_queue.put(line) + print(line.strip()) + event_log.append(line.lower().strip()) + + # self.pacmanlog_queue.put(line) + # process_stdout_lst.append(line) + + time.sleep(0.3) + + self.errors_found = False + for log in event_log: + if "error" in log: + self.errors_found = True + + # query to check if kernel installed + if "headers" in uninstall_cmd_str: + if check_kernel_installed(self.kernel.name + "-headers") is True: + self.kernel_state_queue.put( + (1, "uninstall", self.kernel.name + "-headers") + ) + + event = ( + "%s [ERROR]: Uninstall failed\n" + % datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ) + + self.messages_queue.put(event) + + else: + self.kernel_state_queue.put((0, "uninstall", self.kernel.name)) + + event = ( + "%s [INFO]: Uninstall completed\n" + % datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ) + + self.messages_queue.put(event) + + else: + if check_kernel_installed(self.kernel.name) is True: + self.kernel_state_queue.put((1, "uninstall", self.kernel.name)) + + event = ( + "%s [ERROR]: Uninstall failed\n" + % datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ) + + self.messages_queue.put(event) + + else: + self.kernel_state_queue.put((0, "uninstall", self.kernel.name)) + + event = ( + "%s [INFO]: Uninstall completed\n" + % datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ) + + self.messages_queue.put(event) + + # signal to say end reached + self.kernel_state_queue.put(None) + + except Exception as e: + logger.error("Exception in uninstall(): %s" % e) + + +# ===================================================== +# LIST COMMUNITY KERNELS +# ===================================================== + + +def get_community_kernels(self): + try: + logger.info("Fetching package information for community based kernels") + for community_kernel in sorted(community_kernels_dict): + if community_kernels_dict[community_kernel][2] in pacman_repos_list: + pacman_repo = community_kernels_dict[community_kernel][2] + headers = community_kernels_dict[community_kernel][1] + name = community_kernel + + # fetch kernel info + query_cmd_str = [ + "pacman", + "-Si", + "%s/%s" % (pacman_repo, name), + ] + + # logger.debug("Running %s" % query_cmd_str) + process_kernel_query = subprocess.Popen( + query_cmd_str, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + out, err = process_kernel_query.communicate(timeout=process_timeout) + version = None + install_size = None + build_date = None + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if line.startswith("Version :"): + version = line.split("Version :")[1].strip() + if line.startswith("Installed Size :"): + install_size = line.split("Installed Size :")[1].strip() + if "MiB" in install_size: + install_size = round( + float(install_size.replace("MiB", "").strip()) + * 1.048576, + ) + + if line.startswith("Build Date :"): + build_date = line.split("Build Date :")[1].strip() + + if name and version and install_size and build_date: + community_kernels_list.append( + CommunityKernel( + name, + headers, + pacman_repo, + version, + build_date, + install_size, + ) + ) + + self.queue_community_kernels.put(community_kernels_list) + + except Exception as e: + logger.error("Exception in get_community_kernels(): %s" % e) + + +# ===================================================== +# INSTALL COMMUNITY KERNELS +# ===================================================== +def install_community_kernel(self): + try: + for kernel in [self.kernel.name, "%s-headers" % self.kernel.name]: + error = False + + if self.errors_found is True: + break + + install_cmd_str = [ + "pacman", + "-S", + "%s/%s" % (self.kernel.repository, kernel), + "--noconfirm", + "--needed", + ] + + logger.info("Running %s" % install_cmd_str) + + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(install_cmd_str), + ) + + event_log = [] + + self.messages_queue.put(event) + + with subprocess.Popen( + install_cmd_str, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + print(line.strip()) + self.messages_queue.put(line) + event_log.append(line.lower().strip()) + + time.sleep(0.3) + + for log in event_log: + if "installation finished. no error reported." in log: + error = False + break + + if "error" in log: + error = True + + if check_kernel_installed(kernel) and error == False: + logger.info("Kernel = installed") + + self.kernel_state_queue.put((0, "install", kernel)) + + event = "%s [INFO]: Installation of %s completed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + kernel, + ) + + self.messages_queue.put(event) + + else: + logger.error("Kernel = install failed") + + self.kernel_state_queue.put((1, "install", kernel)) + + event = "%s [ERROR]: Installation of %s failed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + kernel, + ) + + self.errors_found = True + self.messages_queue.put(event) + + # signal to say end reached + self.kernel_state_queue.put(None) + except Exception as e: + logger.error("Exception in install_community_kernel(): %s " % e) + + +# ===================================================== +# CHECK PACMAN LOCK FILE EXISTS +# ===================================================== + + +# check pacman lockfile +def check_pacman_lockfile(): + return os.path.exists(pacman_lockfile) + + +# ====================================================================== +# GET PACMAN REPOS +# ====================================================================== + + +def get_pacman_repos(): + if os.path.exists(pacman_conf_file): + list_repos_cmd_str = ["pacman-conf", "-l"] + with subprocess.Popen( + list_repos_cmd_str, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + pacman_repos_list.append(line.strip()) + + else: + logger.error("Failed to locate %s, are you on an ArchLinux based system ?") + + +# ====================================================================== +# GET INSTALLED KERNEL INFO +# ====================================================================== + + +def get_installed_kernel_info(package_name): + query_str = ["pacman", "-Qi", package_name] + + try: + process_kernel_query = subprocess.Popen( + query_str, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out, err = process_kernel_query.communicate(timeout=process_timeout) + install_size = None + install_date = None + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if line.startswith("Installed Size :"): + install_size = line.split("Installed Size :")[1].strip() + if "MiB" in install_size: + install_size = round( + float(install_size.replace("MiB", "").strip()) * 1.048576, + ) + if line.startswith("Install Date :"): + install_date = line.split("Install Date :")[1].strip() + return install_size, install_date + except Exception as e: + logger.error("Exception in get_installed_kernel_info(): %s" % e) + + +# ====================================================================== +# GET INSTALLED KERNELS +# ====================================================================== + + +def get_installed_kernels(): + query_str = ["pacman", "-Q"] + installed_kernels = [] + + try: + process_kernel_query = subprocess.Popen( + query_str, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + out, err = process_kernel_query.communicate(timeout=process_timeout) + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if line.lower().strip().startswith("linux"): + package_name = line.split(" ")[0] + package_version = line.split(" ")[1] + + if ( + package_name in supported_kernels_dict + or package_name in community_kernels_dict + ): + install_size, install_date = get_installed_kernel_info( + package_name + ) + installed_kernel = InstalledKernel( + package_name, + package_version, + install_date, + install_size, + ) + + installed_kernels.append(installed_kernel) + + return installed_kernels + except Exception as e: + logger.error("Exception in get_installed_kernels(): %s" % e) + + +# ====================================================================== +# GET ACTIVE KERNEL +# ====================================================================== + + +def get_active_kernel(): + logger.info("Getting active Linux kernel") + query_str = ["uname", "-rs"] + + try: + process_kernel_query = subprocess.Popen( + query_str, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + out, err = process_kernel_query.communicate(timeout=process_timeout) + + if process_kernel_query.returncode == 0: + for line in out.decode("utf-8").splitlines(): + if len(line.strip()) > 0: + kernel = line.strip() + _version = "-".join(kernel.split("-")[:-1]) + _type = kernel.split("-")[-1] + + logger.info("Active kernel = %s" % kernel) + + return kernel + except Exception as e: + logger.error("Exception in get_active_kernel(): %s" % e) + + +# ===================================================== +# PACMAN SYNC PACKAGE DB +# ===================================================== +def sync_package_db(): + try: + sync_str = ["pacman", "-Sy"] + logger.info("Synchronizing pacman package databases") + process_sync = subprocess.run( + sync_str, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=process_timeout, + ) + + if process_sync.returncode == 0: + return None + else: + if process_sync.stdout: + out = str(process_sync.stdout.decode("utf-8")) + logger.error(out) + + return out + + except Exception as e: + logger.error("Exception in sync_package_db(): %s" % e) + + +def get_boot_loader(): + try: + logger.info("Getting bootloader") + cmd = ["bootctl", "status"] + logger.debug("Running %s" % " ".join(cmd)) + process = subprocess.run( + cmd, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=process_timeout, + universal_newlines=True, + bufsize=1, + ) + + if process.returncode == 0: + for line in process.stdout.splitlines(): + if line.strip().startswith("Product:"): + product = line.strip().split("Product:")[1].strip() + if "grub" in product.lower(): + logger.info("bootctl product reports booted with grub") + return "grub" + if "systemd-boot" in product.lower(): + logger.info("bootcl product reports booted with systemd-boot") + return "systemd-boot" + elif line.strip().startswith("Not booted with EFI"): # noqa + # bios + logger.info( + "bootctl - not booted with EFI, setting bootloader to grub" + ) + return "grub" + else: + logger.error("Failed to run %s" % " ".join(cmd)) + logger.error(process.stdout) + except Exception as e: + logger.error("Exception in get_boot_loader(): %s" % e) + + +# ====================================================================== +# GET INSTALLED KERNEL VERSION +# ====================================================================== + + +def get_kernel_version(kernel): + cmd = ["pacman", "-Q", kernel] + + try: + logger.debug("Running %s" % " ".join(cmd)) + process = subprocess.run( + cmd, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=process_timeout, + universal_newlines=True, + bufsize=1, + ) + + if process.returncode == 0: + for line in process.stdout.splitlines(): + print(line.strip()) + return line.split(" ")[1] + except Exception as e: + logger.error("Exception in get_kernel_version(): %s" % e) + + +# ====================================================================== +# UPDATE BOOTLOADER ENTRIES +# ====================================================================== + + +# grub - grub-mkconfig /boot/grub/grub.cfg +# systemd-boot - bootctl update +def update_bootloader(self): + cmd = None + + if self.action == "install": + image = "images/48x48/akm-install.png" + cmd = ["kernel-install", "add-all"] + else: + image = "images/48x48/akm-remove.png" + cmd = ["kernel-install", "remove", self.installed_kernel_version] + + try: + + """ + kernel-install -add-all + kernel-install remove $kernel_version + this is for systems which do not have any pacman hooks in place + useful for vanilla arch installs + """ + + self.label_notify_revealer.set_text("Updating bootloader %s" % self.bootloader) + self.reveal_notify() + + logger.info("Current bootloader = %s" % self.bootloader) + + if cmd is not None: + stdout_lines = [] + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(cmd), + ) + + self.messages_queue.put(event) + + logger.info("Running %s" % " ".join(cmd)) + + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + self.messages_queue.put(line) + stdout_lines.append(line.lower().strip()) + print(line.strip()) + + # time.sleep(0.3) + + error = False + + for log in stdout_lines: + if "error" in log or "errors" in log: + + # event = ( + # "%s [ERROR]: Errors have been encountered during installation\n" + # % (datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) + # ) + # + # self.messages_queue.put(event) + + self.errors_found = True + + error = True + + if error is True: + self.label_notify_revealer.set_text("%s failed" % " ".join(cmd)) + self.reveal_notify() + + logger.error("%s failed" % " ".join(cmd)) + else: + self.label_notify_revealer.set_text("%s completed" % " ".join(cmd)) + self.reveal_notify() + + logger.info("%s completed" % " ".join(cmd)) + + if self.bootloader == "grub": + if self.bootloader_grub_cfg is not None: + cmd = ["grub-mkconfig", "-o", self.bootloader_grub_cfg] + else: + logger.error("Bootloader grub config file not specified") + + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(cmd), + ) + self.messages_queue.put(event) + + elif self.bootloader == "systemd-boot": + # cmd = ["bootctl", "update"] + # graceful update systemd-boot + cmd = ["bootctl", "--no-variables", "--graceful", "update"] + event = "%s [INFO]: Running %s\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + " ".join(cmd), + ) + self.messages_queue.put(event) + else: + logger.error("Bootloader is empty / not supported") + + if cmd is not None: + stdout_lines = [] + logger.info("Running %s" % " ".join(cmd)) + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) as process: + while True: + if process.poll() is not None: + break + for line in process.stdout: + stdout_lines.append(line.strip()) + self.messages_queue.put(line) + print(line.strip()) + + # time.sleep(0.3) + + if process.returncode == 0: + self.label_notify_revealer.set_text( + "Bootloader %s updated" % self.bootloader + ) + self.reveal_notify() + + logger.info("%s update completed" % self.bootloader) + + event = "%s [INFO]: %s update completed\n" % ( + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.bootloader, + ) + self.messages_queue.put(event) + + logger.info( + "Linux packages have changed a reboot is recommended" + ) + event = "%s [INFO]: #### Linux packages have changed a reboot is recommended ####\n" % datetime.datetime.now().strftime( + "%Y-%m-%d-%H-%M-%S" + ) + self.messages_queue.put(event) + + if self.restore is False: + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} completed\n" + f"This window can now be closed\n", + image, + priority=GLib.PRIORITY_DEFAULT, + ) + else: + if ( + "Skipping" + or "same boot loader version in place already." + in stdout_lines + ): + logger.info("%s update completed" % self.bootloader) + + event = "%s [INFO]: %s update completed\n" % ( + datetime.datetime.now().strftime( + "%Y-%m-%d-%H-%M-%S" + ), + self.bootloader, + ) + self.messages_queue.put(event) + + if self.restore is False: + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} completed\n" + f"This window can now be closed\n", + image, + priority=GLib.PRIORITY_DEFAULT, + ) + + else: + self.label_notify_revealer.set_text( + "Bootloader %s update failed" % self.bootloader + ) + self.reveal_notify() + + event = "%s [ERROR]: %s update failed\n" % ( + datetime.datetime.now().strftime( + "%Y-%m-%d-%H-%M-%S" + ), + self.bootloader, + ) + + logger.error("%s update failed" % self.bootloader) + logger.error(str(stdout_lines)) + self.messages_queue.put(event) + + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} failed .. attempting kernel restore\n" + f"There have been errors, please review the logs\n", + image, + priority=GLib.PRIORITY_DEFAULT, + ) + + else: + logger.error("Bootloader update failed") + + GLib.idle_add( + show_mw, + self, + "System changes", + f"Kernel {self.action} failed\n" + f"There have been errors, please review the logs\n", + image, + priority=GLib.PRIORITY_DEFAULT, + ) + else: + logger.error("Bootloader update cannot continue, failed to set command.") + except Exception as e: + logger.error("Exception in update_bootloader(): %s" % e) + + +# ====================================================================== +# SHOW MESSAGE WINDOW AFTER BOOTLOADER UPDATE +# ====================================================================== +def show_mw(self, title, msg, image): + mw = MessageWindow( + title=title, + message=msg, + image_path=image, + detailed_message=False, + transient_for=self, + ) + + mw.present() + + +# ====================================================================== +# CHECKS PACMAN PROCESS +# ====================================================================== +def check_pacman_process(self): + try: + process_found = False + for proc in psutil.process_iter(): + try: + pinfo = proc.as_dict(attrs=["pid", "name", "create_time"]) + + if pinfo["name"] == "pacman": + process_found = True + + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if process_found is True: + logger.info("Pacman process is running") + return True + else: + return False + except Exception as e: + logger.error("Exception in check_pacman_process() : %s" % e) diff --git a/usr/share/archlinux-kernel-manager/ui/AboutDialog.py b/usr/share/archlinux-kernel-manager/ui/AboutDialog.py new file mode 100644 index 0000000..40600fd --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/AboutDialog.py @@ -0,0 +1,68 @@ +# This class stores static information about the app, and is displayed in the about window +import os +import gi +import libs.functions as fn + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, Gdk + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class AboutDialog(Gtk.AboutDialog): + def __init__(self, manager_gui, **kwargs): + super().__init__(**kwargs) + + website = "https://github.com/DeltaCopy/archlinux-kernel-manager" + authors = ["fennec (DeltaCopy)"] + program_name = "Arch Linux Kernel Manager" + comments = ( + f"Add/Remove Officially supported Linux kernels on Arch based systems\n" + f"Powered by the Arch Linux Archive (a.k.a ALA)\n" + f"Community based Linux kernels are also supported\n" + f"This application matches your system theme !\n" + f"Developed in Python with GTK 4\n" + ) + + icon_name = "akm-tux" + + self.set_transient_for(manager_gui) + self.set_modal(True) + self.set_authors(authors) + self.set_program_name(program_name) + self.set_comments(comments) + self.set_website(website) + + self.set_logo_icon_name(icon_name) + self.set_version("Version %s" % manager_gui.app_version) + + self.connect("activate-link", self.on_activate_link) + + tux_icon = Gdk.Texture.new_from_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/364x408/akm-tux-splash.png") + ) + ) + + self.set_logo(tux_icon) + + def on_activate_link(self, about_dialog, uri): + try: + cmd = ["sudo", "-u", fn.sudo_username, "xdg-open", uri] + + proc = fn.subprocess.Popen( + cmd, + shell=False, + stdout=fn.subprocess.PIPE, + stderr=fn.subprocess.STDOUT, + universal_newlines=True, + ) + + out, err = proc.communicate(timeout=50) + + fn.logger.warning(out) + + except Exception as e: + fn.logger.error("Exception in activate_link(): %s" % e) + + return True diff --git a/usr/share/archlinux-kernel-manager/ui/FlowBox.py b/usr/share/archlinux-kernel-manager/ui/FlowBox.py new file mode 100644 index 0000000..99fdfc6 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/FlowBox.py @@ -0,0 +1,635 @@ +import datetime + +import gi +import os +import libs.functions as fn +from ui.ProgressWindow import ProgressWindow +from ui.MessageWindow import MessageWindow + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, GLib + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class FlowBox(Gtk.FlowBox): + def __init__( + self, + kernel, + active_kernel, + manager_gui, + source, + ): + super(FlowBox, self).__init__() + + self.manager_gui = manager_gui + + # self.set_row_spacing(1) + # self.set_column_spacing(1) + # self.set_name("hbox_kernel") + # self.set_activate_on_single_click(True) + # self.connect("child-activated", self.on_child_activated) + self.set_valign(Gtk.Align.START) + + self.set_selection_mode(Gtk.SelectionMode.NONE) + + # self.set_homogeneous(True) + + self.set_max_children_per_line(2) + self.set_min_children_per_line(2) + self.kernel_count = 0 + + self.active_kernel_found = False + self.kernels = [] + + self.kernel = kernel + self.source = source + + if self.source == "official": + self.flowbox_official() + + if self.source == "community": + self.flowbox_community() + + def flowbox_community(self): + for community_kernel in self.kernel: + self.kernels.append(community_kernel) + + self.kernel_count += 1 + + if len(self.kernels) > 0: + installed = False + + for cache in self.kernels: + fb_child = Gtk.FlowBoxChild() + fb_child.set_name( + "%s %s %s" % (cache.name, cache.version, cache.repository) + ) + + vbox_kernel_widgets = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=0 + ) + vbox_kernel_widgets.set_name("vbox_kernel_widgets") + vbox_kernel_widgets.set_homogeneous(True) + + switch_kernel = Gtk.Switch() + switch_kernel.set_halign(Gtk.Align.START) + + hbox_kernel_switch = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=0 + ) + + hbox_kernel_switch.append(switch_kernel) + + label_kernel_size = Gtk.Label(xalign=0, yalign=0) + label_kernel_size.set_name("label_kernel_flowbox") + + label_kernel_name = Gtk.Label(xalign=0, yalign=0) + label_kernel_name.set_name("label_kernel_version") + label_kernel_name.set_markup( + "%s %s %s" + % (cache.name, cache.version, cache.repository) + ) + label_kernel_name.set_selectable(True) + + vbox_kernel_widgets.append(label_kernel_name) + + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/48x48/akm-tux.png") + ) + ) + tux_icon.set_can_shrink(True) + + for installed_kernel in self.manager_gui.installed_kernels: + if "{}-{}".format( + installed_kernel.name, installed_kernel.version + ) == "{}-{}".format(cache.name, cache.version): + installed = True + + if ( + cache.name == installed_kernel.name + and cache.version > installed_kernel.version + ): + fn.logger.info( + "Kernel upgrade available - %s %s" + % (cache.name, cache.version) + ) + + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/48x48/akm-update.png") + ) + ) + tux_icon.set_can_shrink(True) + + label_kernel_name.set_markup( + "*%s %s %s" + % (cache.name, cache.version, cache.repository) + ) + + if installed is True: + switch_kernel.set_state(True) + switch_kernel.set_active(True) + + else: + switch_kernel.set_state(False) + switch_kernel.set_active(False) + + tux_icon.set_content_fit(content_fit=Gtk.ContentFit.SCALE_DOWN) + tux_icon.set_halign(Gtk.Align.START) + + installed = False + switch_kernel.connect("state-set", self.kernel_toggle_state, cache) + + hbox_kernel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + hbox_kernel.set_name("hbox_kernel") + + label_kernel_size.set_text("%sM" % str(cache.install_size)) + + vbox_kernel_widgets.append(label_kernel_size) + + label_kernel_build_date = Gtk.Label(xalign=0, yalign=0) + label_kernel_build_date.set_name("label_kernel_flowbox") + label_kernel_build_date.set_text(cache.build_date) + + vbox_kernel_widgets.append(label_kernel_build_date) + + vbox_kernel_widgets.append(hbox_kernel_switch) + + hbox_kernel.append(tux_icon) + hbox_kernel.append(vbox_kernel_widgets) + + fb_child.set_child(hbox_kernel) + + self.append(fb_child) + + def flowbox_official(self): + for official_kernel in self.manager_gui.official_kernels: + if official_kernel.name == self.kernel: + self.kernels.append(official_kernel) + self.kernel_count += 1 + + if len(self.kernels) > 0: + installed = False + + latest = sorted(self.kernels)[:-1][0] + + for cache in sorted(self.kernels): + fb_child = Gtk.FlowBoxChild() + fb_child.set_name("%s %s" % (cache.name, cache.version)) + if cache == latest: + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/48x48/akm-new.png") + ) + ) + + else: + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/48x48/akm-tux.png") + ) + ) + + tux_icon.set_content_fit(content_fit=Gtk.ContentFit.SCALE_DOWN) + tux_icon.set_halign(Gtk.Align.START) + + vbox_kernel_widgets = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=0 + ) + vbox_kernel_widgets.set_homogeneous(True) + + hbox_kernel_switch = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=0 + ) + + switch_kernel = Gtk.Switch() + switch_kernel.set_halign(Gtk.Align.START) + + hbox_kernel_switch.append(switch_kernel) + + label_kernel_version = Gtk.Label(xalign=0, yalign=0) + label_kernel_version.set_name("label_kernel_version") + label_kernel_version.set_selectable(True) + + label_kernel_size = Gtk.Label(xalign=0, yalign=0) + label_kernel_size.set_name("label_kernel_flowbox") + + if self.manager_gui.installed_kernels is None: + self.manager_gui.installed_kernels = fn.get_installed_kernels() + + for installed_kernel in self.manager_gui.installed_kernels: + if ( + "{}-{}".format(installed_kernel.name, installed_kernel.version) + == cache.version + ): + installed = True + + if installed is True: + switch_kernel.set_state(True) + switch_kernel.set_active(True) + + else: + switch_kernel.set_state(False) + switch_kernel.set_active(False) + + installed = False + switch_kernel.connect("state-set", self.kernel_toggle_state, cache) + + label_kernel_version.set_markup("%s" % cache.version) + + hbox_kernel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + hbox_kernel.set_name("hbox_kernel") + + label_kernel_size.set_text(cache.size) + + vbox_kernel_widgets.append(label_kernel_version) + vbox_kernel_widgets.append(label_kernel_size) + + label_kernel_modified = Gtk.Label(xalign=0, yalign=0) + label_kernel_modified.set_name("label_kernel_flowbox") + label_kernel_modified.set_text(cache.last_modified) + + vbox_kernel_widgets.append(label_kernel_modified) + + vbox_kernel_widgets.append(hbox_kernel_switch) + + hbox_kernel.append(tux_icon) + hbox_kernel.append(vbox_kernel_widgets) + + fb_child.set_child(hbox_kernel) + + self.append(fb_child) + + else: + fn.logger.error("Failed to read in kernels.") + + def kernel_toggle_state(self, switch, data, kernel): + fn.logger.debug( + "Switch toggled, kernel selected = %s %s" % (kernel.name, kernel.version) + ) + message = None + title = None + + if fn.check_pacman_lockfile() is False: + # switch widget is currently toggled off + if switch.get_state() is False: # and switch.get_active() is True: + for inst_kernel in fn.get_installed_kernels(): + if inst_kernel.name == kernel.name: + if self.source == "official": + if ( + inst_kernel.version + > kernel.version.split("%s-" % inst_kernel.name)[1] + ): + title = "Downgrading %s kernel" % kernel.name + else: + title = "Upgrading %s kernel" % kernel.name + + break + + if title is None: + title = "Kernel install" + + if self.source == "community": + message = "This will install %s-%s - Is this ok ?" % ( + kernel.name, + kernel.version, + ) + elif self.source == "official": + message = ( + "This will install %s - Is this ok ?" % kernel.version + ) + + message_window = FlowBoxMessageWindow( + title=title, + message=message, + action="install", + kernel=kernel, + transient_for=self.manager_gui, + textview=self.manager_gui.textview, + textbuffer=self.manager_gui.textbuffer, + switch=switch, + source=self.source, + manager_gui=self.manager_gui, + ) + message_window.present() + return True + + # switch widget is currently toggled on + # if widget.get_state() == True and widget.get_active() == False: + if switch.get_state() is True: + # and switch.get_active() is False: + installed_kernels = fn.get_installed_kernels() + + if len(installed_kernels) > 1: + + if self.source == "community": + message = "This will remove %s-%s - Is this ok ?" % ( + kernel.name, + kernel.version, + ) + elif self.source == "official": + message = ( + "This will remove %s - Is this ok ?" % kernel.version + ) + + message_window = FlowBoxMessageWindow( + title="Kernel uninstall", + message=message, + action="uninstall", + kernel=kernel, + transient_for=self.manager_gui, + textview=self.manager_gui.textview, + textbuffer=self.manager_gui.textbuffer, + switch=switch, + source=self.source, + manager_gui=self.manager_gui, + ) + message_window.present() + return True + else: + switch.set_state(True) + # switch.set_active(False) + fn.logger.warn( + "You only have 1 kernel installed, and %s-%s is currently running, uninstall aborted." + % (kernel.name, kernel.version) + ) + msg_win = MessageWindow( + title="Warning: Uninstall aborted", + message=f"You only have 1 kernel installed\n" + f"{kernel.name} {kernel.version} is currently active\n", + image_path="images/48x48/akm-remove.png", + transient_for=self.manager_gui, + detailed_message=False, + ) + msg_win.present() + return True + + else: + fn.logger.error( + "Pacman lockfile found, is another pacman process running ?" + ) + + msg_win = MessageWindow( + title="Warning", + message="Pacman lockfile found, which indicates another pacman process is running", + transient_for=self.manager_gui, + detailed_message=False, + image_path="images/48x48/akm-warning.png", + ) + msg_win.present() + return True + + # while self.manager_gui.default_context.pending(): + # self.manager_gui.default_context.iteration(True) + + +class FlowBoxInstalled(Gtk.FlowBox): + def __init__(self, installed_kernels, manager_gui, **kwargs): + super().__init__(**kwargs) + + self.set_selection_mode(Gtk.SelectionMode.NONE) + + self.set_homogeneous(True) + self.set_max_children_per_line(2) + self.set_min_children_per_line(2) + + self.manager_gui = manager_gui + + for installed_kernel in installed_kernels: + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + os.path.join(base_dir, "images/48x48/akm-tux.png") + ) + ) + + fb_child = Gtk.FlowBoxChild() + fb_child.set_name( + "%s %s" % (installed_kernel.name, installed_kernel.version) + ) + + tux_icon.set_content_fit(content_fit=Gtk.ContentFit.SCALE_DOWN) + tux_icon.set_halign(Gtk.Align.START) + + label_installed_kernel_version = Gtk.Label(xalign=0, yalign=0) + label_installed_kernel_version.set_name("label_kernel_version") + label_installed_kernel_version.set_markup( + "%s %s" % (installed_kernel.name, installed_kernel.version) + ) + label_installed_kernel_version.set_selectable(True) + + hbox_installed_version = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=0 + ) + + hbox_installed_version.append(label_installed_kernel_version) + + label_installed_kernel_size = Gtk.Label(xalign=0, yalign=0) + label_installed_kernel_size.set_name("label_kernel_flowbox") + label_installed_kernel_size.set_text("%sM" % str(installed_kernel.size)) + + label_installed_kernel_date = Gtk.Label(xalign=0, yalign=0) + label_installed_kernel_date.set_name("label_kernel_flowbox") + label_installed_kernel_date.set_text("%s" % installed_kernel.date) + + btn_uninstall_kernel = Gtk.Button.new_with_label("Remove") + + btn_context = btn_uninstall_kernel.get_style_context() + btn_context.add_class("destructive-action") + + vbox_uninstall_button = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=0 + ) + vbox_uninstall_button.set_name("box_padding_left") + + btn_uninstall_kernel.set_hexpand(False) + btn_uninstall_kernel.set_halign(Gtk.Align.CENTER) + btn_uninstall_kernel.set_vexpand(False) + btn_uninstall_kernel.set_valign(Gtk.Align.CENTER) + + vbox_uninstall_button.append(btn_uninstall_kernel) + + btn_uninstall_kernel.connect( + "clicked", self.button_uninstall_kernel, installed_kernel + ) + + vbox_kernel_widgets = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=0 + ) + + vbox_kernel_widgets.append(hbox_installed_version) + vbox_kernel_widgets.append(label_installed_kernel_size) + vbox_kernel_widgets.append(label_installed_kernel_date) + vbox_kernel_widgets.append(vbox_uninstall_button) + + hbox_kernel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + hbox_kernel.set_name("hbox_kernel") + + hbox_kernel.append(tux_icon) + hbox_kernel.append(vbox_kernel_widgets) + + fb_child.set_child(hbox_kernel) + + self.append(fb_child) + + def button_uninstall_kernel(self, button, installed_kernel): + installed_kernels = fn.get_installed_kernels() + + if len(installed_kernels) > 1: + fn.logger.info( + "Selected kernel to remove = %s %s" + % (installed_kernel.name, installed_kernel.version) + ) + + message_window = FlowBoxMessageWindow( + title="Kernel uninstall", + message="This will remove %s-%s - Is this ok ?" + % (installed_kernel.name, installed_kernel.version), + action="uninstall", + kernel=installed_kernel, + transient_for=self.manager_gui, + textview=self.manager_gui.textview, + textbuffer=self.manager_gui.textbuffer, + switch=None, + source=None, + manager_gui=self.manager_gui, + ) + message_window.present() + else: + fn.logger.warn( + "You only have 1 kernel installed %s %s, uninstall aborted." + % (installed_kernel.name, installed_kernel.version) + ) + msg_win = MessageWindow( + title="Warning: Uninstall aborted", + message=f"You only have 1 kernel installed\n" + f"{installed_kernel.name} {installed_kernel.version}\n", + image_path="images/48x48/akm-remove.png", + transient_for=self.manager_gui, + detailed_message=False, + ) + msg_win.present() + + +class FlowBoxMessageWindow(Gtk.Window): + def __init__( + self, + title, + message, + action, + kernel, + textview, + textbuffer, + switch, + source, + manager_gui, + **kwargs, + ): + super().__init__(**kwargs) + + self.set_title(title=title) + self.set_modal(modal=True) + self.set_resizable(False) + self.set_icon_name("archlinux-kernel-manager-tux") + + header_bar = Gtk.HeaderBar() + header_bar.set_show_title_buttons(False) + + label_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_title.set_markup("%s" % title) + + self.set_titlebar(header_bar) + + header_bar.set_title_widget(label_title) + + self.textview = textview + self.textbuffer = textbuffer + self.manager_gui = manager_gui + self.kernel = kernel + self.action = action + self.switch = switch + self.source = source + + vbox_flowbox_message = Gtk.Box.new( + orientation=Gtk.Orientation.VERTICAL, spacing=10 + ) + vbox_flowbox_message.set_name("vbox_flowbox_message") + + self.set_child(child=vbox_flowbox_message) + + label_flowbox_message = Gtk.Label(xalign=0, yalign=0) + label_flowbox_message.set_markup("%s" % message) + label_flowbox_message.set_name("label_flowbox_message") + + vbox_flowbox_message.set_halign(Gtk.Align.CENTER) + + # Widgets. + button_yes = Gtk.Button.new_with_label("Yes") + button_yes.set_size_request(100, 30) + button_yes.set_halign(Gtk.Align.END) + button_yes_context = button_yes.get_style_context() + button_yes_context.add_class("destructive-action") + button_yes.connect("clicked", self.on_button_yes_clicked) + + button_no = Gtk.Button.new_with_label("No") + button_no.set_size_request(100, 30) + button_no.set_halign(Gtk.Align.END) + button_no.connect("clicked", self.on_button_no_clicked) + + hbox_buttons = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=15) + hbox_buttons.set_halign(Gtk.Align.CENTER) + hbox_buttons.append(button_yes) + hbox_buttons.append(button_no) + + vbox_flowbox_message.append(label_flowbox_message) + vbox_flowbox_message.append(hbox_buttons) + + def on_button_yes_clicked(self, button): + self.hide() + self.destroy() + progress_window = None + if fn.check_pacman_lockfile() is False: + if self.action == "uninstall": + progress_window = ProgressWindow( + title="Removing kernel", + action="uninstall", + textview=self.textview, + textbuffer=self.textbuffer, + kernel=self.kernel, + switch=self.switch, + source=self.source, + manager_gui=self.manager_gui, + transient_for=self.manager_gui, + ) + + if self.action == "install": + progress_window = ProgressWindow( + title="Installing kernel", + action="install", + textview=self.textview, + textbuffer=self.textbuffer, + kernel=self.kernel, + switch=self.switch, + source=self.source, + manager_gui=self.manager_gui, + transient_for=self.manager_gui, + ) + else: + fn.logger.error( + "Pacman lockfile found, is another pacman process running ?" + ) + + def on_button_no_clicked(self, button): + if self.action == "uninstall": + if self.switch is not None: + self.switch.set_state(True) + + elif self.action == "install": + if self.switch is not None: + self.switch.set_state(False) + + self.hide() + self.destroy() + + return True diff --git a/usr/share/archlinux-kernel-manager/ui/KernelStack.py b/usr/share/archlinux-kernel-manager/ui/KernelStack.py new file mode 100644 index 0000000..6760403 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/KernelStack.py @@ -0,0 +1,631 @@ +import gi +import os +import libs.functions as fn +from ui.FlowBox import FlowBox, FlowBoxInstalled +from ui.Stack import Stack +from libs.Kernel import Kernel, InstalledKernel, CommunityKernel + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, Gdk + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class KernelStack: + def __init__( + self, + manager_gui, + **kwargs, + ): + super().__init__(**kwargs) + self.manager_gui = manager_gui + self.flowbox_stacks = [] + self.search_entries = [] + + def add_installed_kernels_to_stack(self, reload): + vbox_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_header.set_name("vbox_header") + + lbl_heading = Gtk.Label(xalign=0.5, yalign=0.5) + lbl_heading.set_name("label_flowbox_message") + lbl_heading.set_text("%s" % "Installed kernels".upper()) + + lbl_padding = Gtk.Label(xalign=0.0, yalign=0.0) + lbl_padding.set_text(" ") + + grid_banner_img = Gtk.Grid() + + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-install.png") + ) + + image_settings.set_icon_size(Gtk.IconSize.LARGE) + image_settings.set_halign(Gtk.Align.START) + + grid_banner_img.attach(image_settings, 0, 1, 1, 1) + grid_banner_img.attach_next_to( + lbl_padding, + image_settings, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + grid_banner_img.attach_next_to( + lbl_heading, + lbl_padding, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + vbox_header.append(grid_banner_img) + + label_installed_desc = Gtk.Label(xalign=0, yalign=0) + label_installed_desc.set_text("Installed Linux kernel and modules") + label_installed_desc.set_name("label_stack_desc") + + label_installed_count = Gtk.Label(xalign=0, yalign=0) + + label_installed_count.set_name("label_stack_count") + + vbox_search_entry = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + search_entry_installed = Gtk.SearchEntry() + search_entry_installed.set_name("search_entry_installed") + search_entry_installed.set_placeholder_text("Search installed kernels...") + search_entry_installed.connect("search_changed", self.flowbox_filter_installed) + + vbox_search_entry.append(search_entry_installed) + + if reload is True: + if self.manager_gui.vbox_installed_kernels is not None: + for widget in self.manager_gui.vbox_installed_kernels: + if widget.get_name() == "label_stack_count": + widget.set_markup( + "%s Installed kernels" + % len(self.manager_gui.installed_kernels) + ) + + if widget.get_name() == "scrolled_window_installed": + self.manager_gui.vbox_installed_kernels.remove(widget) + + scrolled_window_installed = Gtk.ScrolledWindow() + scrolled_window_installed.set_name("scrolled_window_installed") + scrolled_window_installed.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + scrolled_window_installed.set_propagate_natural_height(True) + scrolled_window_installed.set_propagate_natural_width(True) + + self.flowbox_installed = FlowBoxInstalled( + installed_kernels=self.manager_gui.installed_kernels, + manager_gui=self.manager_gui, + ) + vbox_installed_flowbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=12 + ) + + # vbox_installed_flowbox.set_halign(align=Gtk.Align.FILL) + + vbox_installed_flowbox.append(self.flowbox_installed) + + scrolled_window_installed.set_child(vbox_installed_flowbox) + + self.manager_gui.vbox_installed_kernels.append(scrolled_window_installed) + + if self.manager_gui.vbox_active_installed_kernel is not None: + self.manager_gui.vbox_installed_kernels.reorder_child_after( + self.manager_gui.vbox_active_installed_kernel, + scrolled_window_installed, + ) + else: + self.manager_gui.vbox_installed_kernels = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + self.manager_gui.vbox_installed_kernels.set_name("vbox_installed_kernels") + + self.manager_gui.vbox_active_installed_kernel = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=5 + ) + self.manager_gui.vbox_active_installed_kernel.set_name("vbox_active_kernel") + + label_active_installed_kernel = Gtk.Label(xalign=0.5, yalign=0.5) + label_active_installed_kernel.set_name("label_active_kernel") + label_active_installed_kernel.set_selectable(True) + + label_active_installed_kernel.set_markup( + "Active kernel: %s" % self.manager_gui.active_kernel + ) + label_active_installed_kernel.set_halign(Gtk.Align.START) + self.manager_gui.vbox_active_installed_kernel.append( + label_active_installed_kernel + ) + + scrolled_window_installed = Gtk.ScrolledWindow() + scrolled_window_installed.set_name("scrolled_window_installed") + scrolled_window_installed.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + scrolled_window_installed.set_propagate_natural_height(True) + scrolled_window_installed.set_propagate_natural_width(True) + + label_installed_count.set_markup( + "%s Installed kernels" % len(self.manager_gui.installed_kernels) + ) + + self.flowbox_installed = FlowBoxInstalled( + installed_kernels=self.manager_gui.installed_kernels, + manager_gui=self.manager_gui, + ) + vbox_installed_flowbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=12 + ) + + # vbox_installed_flowbox.set_halign(align=Gtk.Align.FILL) + + vbox_installed_flowbox.append(self.flowbox_installed) + + scrolled_window_installed.set_child(vbox_installed_flowbox) + + # self.manager_gui.vbox_installed_kernels.append(label_installed_title) + self.manager_gui.vbox_installed_kernels.append(vbox_header) + self.manager_gui.vbox_installed_kernels.append(label_installed_desc) + self.manager_gui.vbox_installed_kernels.append(label_installed_count) + self.manager_gui.vbox_installed_kernels.append(vbox_search_entry) + self.manager_gui.vbox_installed_kernels.append(scrolled_window_installed) + self.manager_gui.vbox_installed_kernels.append( + self.manager_gui.vbox_active_installed_kernel + ) + + self.manager_gui.stack.add_titled( + self.manager_gui.vbox_installed_kernels, "Installed", "Installed" + ) + + def add_official_kernels_to_stack(self, reload): + if reload is True: + self.flowbox_stacks.clear() + for kernel in fn.supported_kernels_dict: + vbox_flowbox = None + stack_child = self.manager_gui.stack.get_child_by_name(kernel) + + if stack_child is not None: + for stack_widget in stack_child: + if stack_widget.get_name() == "scrolled_window_official": + scrolled_window_official = stack_widget + vbox_flowbox = ( + scrolled_window_official.get_child().get_child() + ) + + for widget in vbox_flowbox: + widget.remove_all() + + self.flowbox_official_kernel = FlowBox( + kernel, + self.manager_gui.active_kernel, + self.manager_gui, + "official", + ) + self.flowbox_stacks.append(self.flowbox_official_kernel) + + vbox_flowbox.append(self.flowbox_official_kernel) + + # while self.manager_gui.default_context.pending(): + # self.manager_gui.default_context.iteration(True) + else: + for kernel in fn.supported_kernels_dict: + self.manager_gui.vbox_kernels = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + + self.manager_gui.vbox_kernels.set_name("stack_%s" % kernel) + + hbox_sep_kernels = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=10 + ) + + hsep_kernels = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) + + vbox_active_kernel = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=5 + ) + vbox_active_kernel.set_name("vbox_active_kernel") + + label_active_kernel = Gtk.Label(xalign=0.5, yalign=0.5) + label_active_kernel.set_name("label_active_kernel") + label_active_kernel.set_selectable(True) + label_active_kernel.set_markup( + "Active kernel: %s" % self.manager_gui.active_kernel + ) + label_active_kernel.set_halign(Gtk.Align.START) + + label_bottom_padding = Gtk.Label(xalign=0, yalign=0) + label_bottom_padding.set_text(" ") + + hbox_sep_kernels.append(hsep_kernels) + + self.flowbox_official_kernel = FlowBox( + kernel, self.manager_gui.active_kernel, self.manager_gui, "official" + ) + + self.flowbox_stacks.append(self.flowbox_official_kernel) + + vbox_flowbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + vbox_flowbox.set_name("vbox_flowbox_%s" % kernel) + # vbox_flowbox.set_halign(align=Gtk.Align.FILL) + vbox_flowbox.append(self.flowbox_official_kernel) + + scrolled_window_official = Gtk.ScrolledWindow() + scrolled_window_official.set_name("scrolled_window_official") + scrolled_window_official.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + scrolled_window_official.set_propagate_natural_height(True) + scrolled_window_official.set_propagate_natural_width(True) + + label_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_title.set_text(kernel.upper()) + label_title.set_name("label_stack_kernel") + + label_desc = Gtk.Label(xalign=0, yalign=0) + label_desc.set_text(fn.supported_kernels_dict[kernel][0]) + label_desc.set_name("label_stack_desc") + + label_count = Gtk.Label(xalign=0, yalign=0) + label_count.set_markup( + "%s Available kernels" + % self.flowbox_official_kernel.kernel_count + ) + + label_count.set_name("label_stack_count") + + vbox_search_entry = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + + search_entry_official = Gtk.SearchEntry() + search_entry_official.set_name(kernel) + search_entry_official.set_placeholder_text( + "Search %s kernels..." % kernel + ) + search_entry_official.connect( + "search_changed", self.flowbox_filter_official + ) + + self.search_entries.append(search_entry_official) + + vbox_search_entry.append(search_entry_official) + + vbox_active_kernel.append(label_active_kernel) + + vbox_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_header.set_name("vbox_header") + + lbl_heading = Gtk.Label(xalign=0.5, yalign=0.5) + lbl_heading.set_name("label_flowbox_message") + lbl_heading.set_text( + "%s - Verified and official kernels" % kernel.upper() + ) + + lbl_padding = Gtk.Label(xalign=0.0, yalign=0.0) + lbl_padding.set_text(" ") + + grid_banner_img = Gtk.Grid() + + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-verified.png") + ) + + image_settings.set_icon_size(Gtk.IconSize.LARGE) + image_settings.set_halign(Gtk.Align.START) + + grid_banner_img.attach(image_settings, 0, 1, 1, 1) + grid_banner_img.attach_next_to( + lbl_padding, + image_settings, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + grid_banner_img.attach_next_to( + lbl_heading, + lbl_padding, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + vbox_header.append(grid_banner_img) + + # vbox_kernels.append(label_title) + self.manager_gui.vbox_kernels.append(vbox_header) + # self.manager_gui.vbox_kernels.append(label_title) + self.manager_gui.vbox_kernels.append(label_desc) + self.manager_gui.vbox_kernels.append(label_count) + self.manager_gui.vbox_kernels.append(vbox_search_entry) + self.manager_gui.vbox_kernels.append(hbox_sep_kernels) + + scrolled_window_official.set_child(vbox_flowbox) + self.manager_gui.vbox_kernels.append(scrolled_window_official) + self.manager_gui.vbox_kernels.append(vbox_active_kernel) + + kernel_sidebar_title = None + + if kernel == "linux": + kernel_sidebar_title = "Linux" + elif kernel == "linux-lts": + kernel_sidebar_title = "Linux-LTS" + elif kernel == "linux-zen": + kernel_sidebar_title = "Linux-ZEN" + elif kernel == "linux-hardened": + kernel_sidebar_title = "Linux-Hardened" + elif kernel == "linux-rt": + kernel_sidebar_title = "Linux-RT" + elif kernel == "linux-rt-lts": + kernel_sidebar_title = "Linux-RT-LTS" + + self.manager_gui.stack.add_titled( + self.manager_gui.vbox_kernels, kernel, kernel_sidebar_title + ) + + def flowbox_filter_official(self, search_entry): + def filter_func(fb_child, text): + if search_entry.get_name() == fb_child.get_name().split(" ")[0]: + if text in fb_child.get_name(): + return True + else: + return False + else: + return True + + text = search_entry.get_text() + + for flowbox in self.flowbox_stacks: + flowbox.set_filter_func(filter_func, text) + + def flowbox_filter_community(self, search_entry): + def filter_func(fb_child, text): + if search_entry.get_name() == "search_entry_community": + if text in fb_child.get_name(): + return True + else: + return False + else: + return True + + text = search_entry.get_text() + + self.flowbox_community.set_filter_func(filter_func, text) + + def flowbox_filter_installed(self, search_entry): + def filter_func(fb_child, text): + if search_entry.get_name() == "search_entry_installed": + if text in fb_child.get_name(): + return True + else: + return False + else: + return True + + text = search_entry.get_text() + + self.flowbox_installed.set_filter_func(filter_func, text) + + def add_community_kernels_to_stack(self, reload): + vbox_active_kernel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + vbox_active_kernel.set_name("vbox_active_kernel") + vbox_kernels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + hbox_sep_kernels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + hsep_kernels = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) + hbox_sep_kernels.append(hsep_kernels) + + label_active_kernel = Gtk.Label(xalign=0.5, yalign=0.5) + label_active_kernel.set_name("label_active_kernel") + label_active_kernel.set_selectable(True) + label_active_kernel.set_markup( + "Active kernel: %s" % self.manager_gui.active_kernel + ) + label_active_kernel.set_halign(Gtk.Align.START) + + label_count = Gtk.Label(xalign=0, yalign=0) + label_count.set_name("label_stack_count") + + vbox_search_entry = None + + search_entry_community = Gtk.SearchEntry() + search_entry_community.set_name("search_entry_community") + search_entry_community.set_placeholder_text( + "Search %s kernels..." % "community based" + ) + search_entry_community.connect("search_changed", self.flowbox_filter_community) + + hbox_warning_message = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=5 + ) + hbox_warning_message.set_name("hbox_warning_message") + + label_pacman_warning = Gtk.Label(xalign=0, yalign=0) + label_pacman_warning.set_name("label_community_warning") + + image_warning = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-warning.png") + ) + image_warning.set_name("image_warning") + + image_warning.set_icon_size(Gtk.IconSize.LARGE) + image_warning.set_halign(Gtk.Align.CENTER) + + hbox_warning = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_warning.set_name("hbox_warning") + + hbox_warning.append(image_warning) + # hbox_warning.append(label_pacman_warning) + + label_warning = Gtk.Label(xalign=0, yalign=0) + label_warning.set_name("label_community_warning") + label_warning.set_markup( + f"These are user produced content\n" + f"Any use of the provided files is at your own risk" + ) + + hbox_warning.append(label_warning) + + if reload is True: + vbox_flowbox = None + stack_child = self.manager_gui.stack.get_child_by_name("Community Kernels") + + if stack_child is not None: + for stack_widget in stack_child: + if stack_widget.get_name() == "label_stack_count": + stack_widget.set_markup( + "%s Available kernels" + % len(self.manager_gui.community_kernels) + ) + if stack_widget.get_name() == "vbox_search_entry": + if len(self.manager_gui.community_kernels) == 0: + for search_entry in stack_widget: + search_entry.set_visible(False) + else: + for search_entry in stack_widget: + search_entry.set_visible(True) + + if stack_widget.get_name() == "scrolled_window_community": + scrolled_window_community = stack_widget + vbox_flowbox = scrolled_window_community.get_child().get_child() + + for widget in vbox_flowbox: + if widget.get_name() != "vbox_no_community": + widget.remove_all() + else: + if len(self.manager_gui.community_kernels) > 0: + # widget.hide() + for box_widget in widget: + box_widget.hide() + + vbox_search_entry = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + + vbox_search_entry.append(search_entry_community) + # widget.append(hbox_warning) + widget.append(vbox_search_entry) + + self.flowbox_community = FlowBox( + self.manager_gui.community_kernels, + self.manager_gui.active_kernel, + self.manager_gui, + "community", + ) + vbox_flowbox.append(self.flowbox_community) + + while self.manager_gui.default_context.pending(): + # fn.time.sleep(0.1) + self.manager_gui.default_context.iteration(True) + else: + self.flowbox_community = None + + vbox_flowbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + # vbox_flowbox.set_halign(align=Gtk.Align.FILL) + + if len(self.manager_gui.community_kernels) == 0: + label_count.set_markup("%s Available kernels" % 0) + else: + self.flowbox_community = FlowBox( + self.manager_gui.community_kernels, + self.manager_gui.active_kernel, + self.manager_gui, + "community", + ) + + vbox_flowbox.append(self.flowbox_community) + + label_count.set_markup( + "%s Available kernels" % self.flowbox_community.kernel_count + ) + + vbox_search_entry = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + + vbox_search_entry.set_name("vbox_search_entry") + + vbox_search_entry.append(search_entry_community) + + if reload is False: + scrolled_window_community = Gtk.ScrolledWindow() + scrolled_window_community.set_name("scrolled_window_community") + scrolled_window_community.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + scrolled_window_community.set_propagate_natural_height(True) + scrolled_window_community.set_propagate_natural_width(True) + + label_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_title.set_text("Community Kernels") + label_title.set_name("label_stack_kernel") + + label_desc = Gtk.Label(xalign=0, yalign=0) + label_desc.set_text("Community Linux kernel and modules") + label_desc.set_name("label_stack_desc") + + vbox_active_kernel.append(label_active_kernel) + + vbox_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_header.set_name("vbox_header") + + lbl_heading = Gtk.Label(xalign=0.5, yalign=0.5) + lbl_heading.set_name("label_flowbox_message") + + lbl_heading.set_text( + "%s - Unofficial kernels" % "Community based".upper() + ) + + lbl_padding = Gtk.Label(xalign=0.0, yalign=0.0) + lbl_padding.set_text(" ") + + grid_banner_img = Gtk.Grid() + + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-community.png") + ) + + image_settings.set_icon_size(Gtk.IconSize.LARGE) + image_settings.set_halign(Gtk.Align.START) + + grid_banner_img.attach(image_settings, 0, 1, 1, 1) + grid_banner_img.attach_next_to( + lbl_padding, + image_settings, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + grid_banner_img.attach_next_to( + lbl_heading, + lbl_padding, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + vbox_header.append(grid_banner_img) + + vbox_kernels.append(vbox_header) + vbox_kernels.append(label_desc) + vbox_kernels.append(hbox_warning) + vbox_kernels.append(label_count) + + if vbox_search_entry is not None: + vbox_kernels.append(vbox_search_entry) + vbox_kernels.append(hbox_sep_kernels) + + scrolled_window_community.set_child(vbox_flowbox) + + vbox_kernels.append(scrolled_window_community) + vbox_kernels.append(vbox_active_kernel) + + self.manager_gui.stack.add_titled( + vbox_kernels, "Community Kernels", "Community" + ) diff --git a/usr/share/archlinux-kernel-manager/ui/ManagerGUI.py b/usr/share/archlinux-kernel-manager/ui/ManagerGUI.py new file mode 100644 index 0000000..a8120d5 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/ManagerGUI.py @@ -0,0 +1,516 @@ +import gi +import os +from ui.MenuButton import MenuButton +from ui.Stack import Stack +from ui.KernelStack import KernelStack +from ui.FlowBox import FlowBox, FlowBoxInstalled +from ui.AboutDialog import AboutDialog +from ui.SplashScreen import SplashScreen +from ui.MessageWindow import MessageWindow +from ui.SettingsWindow import SettingsWindow +import libs.functions as fn + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, Gdk, GLib + + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class ManagerGUI(Gtk.ApplicationWindow): + def __init__(self, app_name, default_context, app_version, **kwargs): + super().__init__(**kwargs) + + self.default_context = default_context + + self.app_version = app_version + + if self.app_version == "${app_version}": + self.app_version = "dev" + + fn.logger.info("Version = %s" % self.app_version) + + self.set_title(app_name) + self.set_resizable(True) + self.set_default_size(950, 650) + + # get list of kernels from the arch archive website, aur, then cache + self.official_kernels = [] + self.community_kernels = [] + + # splashscreen queue for threading + self.queue_load_progress = fn.Queue() + + # official kernels queue for threading + self.queue_kernels = fn.Queue() + + # community kernels queue for threading + self.queue_community_kernels = fn.Queue() + + hbox_notify_revealer = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=20 + ) + hbox_notify_revealer.set_name("hbox_notify_revealer") + hbox_notify_revealer.set_halign(Gtk.Align.CENTER) + + self.notify_revealer = Gtk.Revealer() + self.notify_revealer.set_reveal_child(False) + self.label_notify_revealer = Gtk.Label(xalign=0, yalign=0) + self.label_notify_revealer.set_name("label_notify_revealer") + + self.notify_revealer.set_child(hbox_notify_revealer) + + hbox_notify_revealer.append(self.label_notify_revealer) + + self.splash_screen = SplashScreen(app_name) + + try: + fn.Thread( + target=self.wait_for_gui_load, + daemon=True, + ).start() + except Exception as e: + fn.logger.error(e) + + while self.default_context.pending(): + fn.time.sleep(0.1) + self.default_context.iteration(True) + + self.bootloader = None + self.bootloader_grub_cfg = None + + # self.bootloader = fn.get_boot_loader() + + config_data = fn.setup_config(self) + + if "bootloader" in config_data.keys(): + if config_data["bootloader"]["name"] is not None: + self.bootloader = config_data["bootloader"]["name"].lower() + + if self.bootloader == "grub": + if config_data["bootloader"]["grub_config"] is not None: + self.bootloader_grub_cfg = config_data["bootloader"][ + "grub_config" + ] + elif self.bootloader != "systemd-boot" or self.bootloader != "grub": + fn.logger.warning( + "Invalid bootloader config found it should only be systemd-boot or grub" + ) + + fn.logger.warning("Using bootctl to determine current bootloader") + self.bootloader = None + + if self.bootloader is not None or self.bootloader_grub_cfg is not None: + fn.logger.info("User provided bootloader options read from config file") + fn.logger.info("User bootloader option = %s " % self.bootloader) + if self.bootloader_grub_cfg is not None: + fn.logger.info( + "User bootloader Grub config = %s " % self.bootloader_grub_cfg + ) + else: + # no config setting found for bootloader use default method + self.bootloader = fn.get_boot_loader() + if self.bootloader == "grub": + self.bootloader_grub_cfg = "/boot/grub/grub.cfg" + + if self.bootloader is not None: + fn.create_cache_dir() + fn.create_log_dir() + fn.get_pacman_repos() + + self.stack = Stack(transition_type="OVER_DOWN") + self.kernel_stack = KernelStack(self) + + header_bar = Gtk.HeaderBar() + + label_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_title.set_markup("%s" % app_name) + + header_bar.set_title_widget(label_title) + header_bar.set_show_title_buttons(True) + + self.set_titlebar(header_bar) + + menu_outerbox = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + header_bar.pack_end(menu_outerbox) + + menu_outerbox.show() + + menubutton = MenuButton() + + menu_outerbox.append(menubutton) + + menubutton.show() + + action_about = Gio.SimpleAction(name="about") + action_about.connect("activate", self.on_about) + + action_settings = Gio.SimpleAction(name="settings") + action_settings.connect("activate", self.on_settings, fn) + + self.add_action(action_settings) + + self.add_action(action_about) + + action_refresh = Gio.SimpleAction(name="refresh") + action_refresh.connect("activate", self.on_refresh) + + self.add_action(action_refresh) + + action_quit = Gio.SimpleAction(name="quit") + action_quit.connect("activate", self.on_quit) + + self.add_action(action_quit) + + # add shortcut keys + + event_controller_key = Gtk.EventControllerKey.new() + event_controller_key.connect("key-pressed", self.key_pressed) + + self.add_controller(event_controller_key) + + # overlay = Gtk.Overlay() + # self.set_child(child=overlay) + + self.vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.vbox.set_name("main") + + self.set_child(child=self.vbox) + + self.vbox.append(self.notify_revealer) + + self.installed_kernels = fn.get_installed_kernels() + + self.active_kernel = fn.get_active_kernel() + + fn.logger.info("Installed kernels = %s" % len(self.installed_kernels)) + + self.refresh_cache = False + + self.refresh_cache = fn.get_latest_kernel_updates(self) + + self.start_get_kernels_threads() + + self.load_kernels_gui() + + # validate bootloader + if self.bootloader_grub_cfg and not os.path.exists( + self.bootloader_grub_cfg + ): + mw = MessageWindow( + title="Grub config file not found", + message=f"The specified Grub config file: {self.bootloader_grub_cfg} does not exist\n" + f"This will cause an issue when updating the bootloader\n" + f"Update the configuration file/use the Advanced Settings to change this\n", + image_path="images/48x48/akm-error.png", + detailed_message=False, + transient_for=self, + ) + + mw.present() + if self.bootloader == "systemd-boot": + if not os.path.exists( + "/sys/firmware/efi/fw_platform_size" + ) or not os.path.exists("/sys/firmware/efi/efivars"): + mw = MessageWindow( + title="Legacy boot detected", + message=f"Cannot select systemd-boot, UEFI boot mode is not available\n" + f"Update the configuration file\n" + f"Or use the Advanced Settings to change this\n", + image_path="images/48x48/akm-warning.png", + detailed_message=False, + transient_for=self, + ) + + mw.present() + + else: + fn.logger.error("Failed to set bootloader, application closing") + fn.sys.exit(1) + + def key_pressed(self, keyval, keycode, state, userdata): + shortcut = Gtk.accelerator_get_label( + keycode, keyval.get_current_event().get_modifier_state() + ) + + # quit application + if shortcut in ("Ctrl+Q", "Ctrl+Mod2+Q"): + self.destroy() + + def open_settings(self, fn): + settings_win = SettingsWindow(fn, self) + settings_win.present() + + def timeout(self): + self.hide_notify() + + def hide_notify(self): + self.notify_revealer.set_reveal_child(False) + if self.timeout_id is not None: + GLib.source_remove(self.timeout_id) + self.timeout_id = None + + def reveal_notify(self): + # reveal = self.notify_revealer.get_reveal_child() + self.notify_revealer.set_reveal_child(True) + self.timeout_id = GLib.timeout_add(3000, self.timeout) + + def start_get_kernels_threads(self): + if self.refresh_cache is False: + fn.logger.info("Starting get official Linux kernels thread") + try: + fn.Thread( + name=fn.thread_get_kernels, + target=fn.get_official_kernels, + daemon=True, + args=(self,), + ).start() + + except Exception as e: + fn.logger.error("Exception in thread fn.get_official_kernels(): %s" % e) + finally: + self.official_kernels = self.queue_kernels.get() + self.queue_kernels.task_done() + + else: + self.official_kernels = self.queue_kernels.get() + self.queue_kernels.task_done() + + fn.logger.info("Starting pacman db synchronization thread") + self.queue_load_progress.put("Starting pacman db synchronization") + + self.pacman_db_sync() + + fn.logger.info("Starting get community kernels thread") + self.queue_load_progress.put("Getting community based Linux kernels") + + try: + thread_get_community_kernels = fn.Thread( + name=fn.thread_get_community_kernels, + target=fn.get_community_kernels, + daemon=True, + args=(self,), + ) + + thread_get_community_kernels.start() + + except Exception as e: + fn.logger.error("Exception in thread_get_community_kernels: %s" % e) + finally: + self.community_kernels = self.queue_community_kernels.get() + self.queue_community_kernels.task_done() + + # ===================================================== + # PACMAN DB SYNC + # ===================================================== + + def pacman_db_sync(self): + sync_err = fn.sync_package_db() + + if sync_err is not None: + fn.logger.error("Pacman db synchronization failed") + + print( + "---------------------------------------------------------------------------" + ) + + GLib.idle_add( + self.show_sync_db_message_dialog, + sync_err, + priority=GLib.PRIORITY_DEFAULT, + ) + + else: + fn.logger.info("Pacman DB synchronization completed") + + def show_sync_db_message_dialog(self, sync_err): + mw = MessageWindow( + title="Error - Pacman db synchronization", + message=f"Pacman db synchronization failed\n" + f"Failed to run 'pacman -Syu'\n" + f"{sync_err}\n", + image_path="images/48x48/akm-warning.png", + transient_for=self, + detailed_message=True, + ) + + mw.present() + + # keep splash screen open, until main gui is loaded + def wait_for_gui_load(self): + while True: + fn.time.sleep(0.2) + status = self.queue_load_progress.get() + if status == 1: + GLib.idle_add( + self.splash_screen.destroy, + priority=GLib.PRIORITY_DEFAULT, + ) + break + + def on_settings(self, action, param, fn): + self.open_settings(fn) + + def on_about(self, action, param): + about_dialog = AboutDialog(self) + about_dialog.present() + + def on_refresh(self, action, param): + if not fn.is_thread_alive(fn.thread_refresh_ui): + fn.Thread( + name=fn.thread_refresh_ui, + target=self.refresh_ui, + daemon=True, + ).start() + + def refresh_ui(self): + fn.logger.debug("Refreshing UI") + + self.label_notify_revealer.set_text("Refreshing UI started") + GLib.idle_add( + self.reveal_notify, + priority=GLib.PRIORITY_DEFAULT, + ) + fn.pacman_repos_list = [] + fn.get_pacman_repos() + + fn.cached_kernels_list = [] + fn.community_kernels_list = [] + + self.official_kernels = None + + self.community_kernels = None + + self.installed_kernels = None + + self.start_get_kernels_threads() + + self.installed_kernels = fn.get_installed_kernels() + + self.label_notify_revealer.set_text("Refreshing official kernels") + GLib.idle_add( + self.reveal_notify, + priority=GLib.PRIORITY_DEFAULT, + ) + + GLib.idle_add( + self.kernel_stack.add_official_kernels_to_stack, + True, + priority=GLib.PRIORITY_DEFAULT, + ) + + self.label_notify_revealer.set_text("Refreshing community kernels") + GLib.idle_add( + self.reveal_notify, + priority=GLib.PRIORITY_DEFAULT, + ) + + GLib.idle_add( + self.kernel_stack.add_community_kernels_to_stack, + True, + priority=GLib.PRIORITY_DEFAULT, + ) + + self.label_notify_revealer.set_text("Refreshing installed kernels") + GLib.idle_add( + self.reveal_notify, + priority=GLib.PRIORITY_DEFAULT, + ) + + GLib.idle_add( + self.kernel_stack.add_installed_kernels_to_stack, + True, + priority=GLib.PRIORITY_DEFAULT, + ) + + while self.default_context.pending(): + fn.time.sleep(0.3) + self.default_context.iteration(False) + + # fn.time.sleep(0.5) + + fn.logger.debug("Refresh UI completed") + + self.label_notify_revealer.set_text("Refreshing UI completed") + GLib.idle_add( + self.reveal_notify, + priority=GLib.PRIORITY_DEFAULT, + ) + + def on_quit(self, action, param): + self.destroy() + fn.logger.info("Application quit") + + def on_button_quit_response(self, widget): + self.destroy() + fn.logger.info("Application quit") + + def load_kernels_gui(self): + hbox_sep = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + hsep = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) + hbox_sep.append(hsep) + + # handle error here with message + if self.official_kernels is None: + fn.logger.error("Failed to retrieve kernel list") + + stack_sidebar = Gtk.StackSidebar() + stack_sidebar.set_name("stack_sidebar") + stack_sidebar.set_stack(self.stack) + + hbox_stack_sidebar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + hbox_stack_sidebar.set_name("hbox_stack_sidebar") + hbox_stack_sidebar.append(stack_sidebar) + hbox_stack_sidebar.append(self.stack) + + self.vbox.append(hbox_stack_sidebar) + + button_quit = Gtk.Button.new_with_label("Quit") + # button_quit.set_size_request(100, 30) + button_quit.connect( + "clicked", + self.on_button_quit_response, + ) + + btn_context = button_quit.get_style_context() + btn_context.add_class("destructive-action") + + grid_bottom_panel = Gtk.Grid() + grid_bottom_panel.set_halign(Gtk.Align.END) + grid_bottom_panel.set_row_homogeneous(True) + + grid_bottom_panel.attach(button_quit, 0, 1, 1, 1) + + self.vbox.append(grid_bottom_panel) + + self.textbuffer = Gtk.TextBuffer() + + self.textview = Gtk.TextView() + self.textview.set_property("editable", False) + self.textview.set_property("monospace", True) + + self.textview.set_vexpand(True) + self.textview.set_hexpand(True) + + self.textview.set_buffer(self.textbuffer) + + fn.logger.info("Creating kernel UI") + + # add official kernel flowbox + + fn.logger.debug("Adding official kernels to UI") + self.kernel_stack.add_official_kernels_to_stack(reload=False) + + fn.logger.debug("Adding community kernels to UI") + self.kernel_stack.add_community_kernels_to_stack(reload=False) + + fn.logger.debug("Adding installed kernels to UI") + self.kernel_stack.add_installed_kernels_to_stack(reload=False) + + while self.default_context.pending(): + self.default_context.iteration(True) + + fn.time.sleep(0.3) + + self.queue_load_progress.put(1) + fn.logger.info("Kernel manager UI loaded") diff --git a/usr/share/archlinux-kernel-manager/ui/MenuButton.py b/usr/share/archlinux-kernel-manager/ui/MenuButton.py new file mode 100644 index 0000000..e72732a --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/MenuButton.py @@ -0,0 +1,45 @@ +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk + +# Gtk.Builder xml for the application menu +APP_MENU = """ + + + +
+ + _About + win.about + + + _Settings + win.settings + + + _Refresh + win.refresh + + + _Quit + win.quit + +
+
+
+""" + + +class MenuButton(Gtk.MenuButton): + """ + Wrapper class for at Gtk.Menubutton with a menu defined + in a Gtk.Builder xml string + """ + + def __init__(self, icon_name="open-menu-symbolic"): + super(MenuButton, self).__init__() + builder = Gtk.Builder.new_from_string(APP_MENU, -1) + menu = builder.get_object("app-menu") + self.set_menu_model(menu) + self.set_icon_name(icon_name) diff --git a/usr/share/archlinux-kernel-manager/ui/MessageWindow.py b/usr/share/archlinux-kernel-manager/ui/MessageWindow.py new file mode 100644 index 0000000..6ed7915 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/MessageWindow.py @@ -0,0 +1,93 @@ +import gi +import os +import libs.functions as fn + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, GLib + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class MessageWindow(Gtk.Window): + def __init__(self, title, message, image_path, detailed_message, **kwargs): + super().__init__(**kwargs) + + # self.set_title(title=title) + self.set_modal(modal=True) + self.set_resizable(False) + + header_bar = Gtk.HeaderBar() + header_bar.set_show_title_buttons(True) + + hbox_title = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + + label_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_title.set_markup("%s" % title) + + hbox_title.append(label_title) + header_bar.set_title_widget(hbox_title) + + self.set_titlebar(header_bar) + + vbox_message = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=10) + vbox_message.set_name("vbox_flowbox_message") + + image = Gtk.Picture.new_for_filename(os.path.join(base_dir, image_path)) + + image.set_content_fit(content_fit=Gtk.ContentFit.SCALE_DOWN) + image.set_halign(Gtk.Align.START) + + hbox_image = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + + # hbox_image.append(image) + + self.set_child(child=vbox_message) + + if detailed_message is True: + scrolled_window = Gtk.ScrolledWindow() + + textview = Gtk.TextView() + textview.set_property("editable", False) + textview.set_property("monospace", True) + + textview.set_vexpand(True) + textview.set_hexpand(True) + + msg_buffer = textview.get_buffer() + msg_buffer.insert( + msg_buffer.get_end_iter(), + "Event timestamp = %s\n" + % fn.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ) + msg_buffer.insert(msg_buffer.get_end_iter(), "%s\n" % message) + + scrolled_window.set_child(textview) + + hbox_image.append(scrolled_window) + + self.set_size_request(700, 500) + self.set_resizable(True) + else: + label_message = Gtk.Label(xalign=0, yalign=0) + label_message.set_markup("%s" % message) + label_message.set_name("label_flowbox_message") + + hbox_image.append(image) + hbox_image.append(label_message) + + vbox_message.append(hbox_image) + + button_ok = Gtk.Button.new_with_label("OK") + button_ok.set_size_request(100, 30) + button_ok.set_halign(Gtk.Align.END) + button_ok.connect("clicked", self.on_button_ok_clicked) + + hbox_buttons = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + hbox_buttons.set_halign(Gtk.Align.END) + hbox_buttons.append(button_ok) + + vbox_message.append(hbox_buttons) + + def on_button_ok_clicked(self, button): + self.hide() + self.destroy() diff --git a/usr/share/archlinux-kernel-manager/ui/ProgressWindow.py b/usr/share/archlinux-kernel-manager/ui/ProgressWindow.py new file mode 100644 index 0000000..e09e0ad --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/ProgressWindow.py @@ -0,0 +1,630 @@ +import sys +import gi +import os +import libs.functions as fn +from ui.MessageWindow import MessageWindow +from gi.repository import Gtk, Gio, GLib + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class ProgressWindow(Gtk.Window): + def __init__( + self, + title, + action, + textview, + textbuffer, + kernel, + switch, + source, + manager_gui, + **kwargs, + ): + super().__init__(**kwargs) + + self.set_title(title=title) + self.set_modal(modal=True) + self.set_resizable(True) + self.set_size_request(700, 400) + self.connect("close-request", self.on_close) + + self.textview = textview + self.textbuffer = textbuffer + + self.kernel_state_queue = fn.Queue() + self.messages_queue = fn.Queue() + self.kernel = kernel + self.timeout_id = None + self.errors_found = False + + self.action = action + self.switch = switch + + self.restore = False + + self.source = source + self.manager_gui = manager_gui + + self.bootloader = self.manager_gui.bootloader + self.bootloader_grub_cfg = self.manager_gui.bootloader_grub_cfg + + vbox_progress = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_progress.set_name("main") + + self.set_child(child=vbox_progress) + + header_bar = Gtk.HeaderBar() + self.label_title = Gtk.Label(xalign=0.5, yalign=0.5) + + header_bar.set_show_title_buttons(True) + + self.set_titlebar(header_bar) + + self.label_title.set_markup("%s" % title) + header_bar.set_title_widget(self.label_title) + + vbox_icon_settings = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_icon_settings.set_name("vbox_icon_settings") + + lbl_heading = Gtk.Label(xalign=0.5, yalign=0.5) + lbl_heading.set_name("label_flowbox_message") + + lbl_padding = Gtk.Label(xalign=0.0, yalign=0.0) + lbl_padding.set_text(" ") + + grid_banner_img = Gtk.Grid() + + image_settings = None + + if action == "install": + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-install.png") + ) + lbl_heading.set_markup( + "Installing %s version %s " + % (self.kernel.name, self.kernel.version) + ) + if action == "uninstall": + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-remove.png") + ) + lbl_heading.set_markup( + "Removing %s version %s " + % (self.kernel.name, self.kernel.version) + ) + + # get kernel version from pacman + self.installed_kernel_version = fn.get_kernel_version(self.kernel.name) + + image_settings.set_halign(Gtk.Align.START) + image_settings.set_icon_size(Gtk.IconSize.LARGE) + + hbox_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_header.set_name("vbox_header") + + hbox_header.append(image_settings) + hbox_header.append(lbl_heading) + + vbox_progress.append(hbox_header) + + self.spinner = Gtk.Spinner() + self.spinner.set_spinning(True) + + image_warning = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-warning.png") + ) + + image_warning.set_icon_size(Gtk.IconSize.LARGE) + image_warning.set_halign(Gtk.Align.START) + + hbox_progress_warning = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=5 + ) + hbox_progress_warning.set_name("hbox_warning") + + hbox_progress_warning.append(image_warning) + + self.label_progress_window_desc = Gtk.Label(xalign=0, yalign=0) + + self.label_progress_window_desc.set_markup( + f"Do not close this window while a kernel {self.action} activity is in progress\n" + f"Progress can be monitored in the log below\n" + f"A reboot is recommended when Linux packages have changed" + ) + + hbox_progress_warning.append(self.label_progress_window_desc) + + self.label_status = Gtk.Label(xalign=0, yalign=0) + + button_close = Gtk.Button.new_with_label("Close") + button_close.set_size_request(100, 30) + button_close.set_halign(Gtk.Align.END) + + button_close.connect( + "clicked", + self.on_button_close_response, + ) + + self.label_spinner_progress = Gtk.Label(xalign=0, yalign=0) + if self.action == "install": + self.label_spinner_progress.set_markup( + "Please wait kernel %s is in progress" % "installation" + ) + elif self.action == "uninstall": + self.label_spinner_progress.set_markup( + "Please wait kernel %s is in progress" % "removal" + ) + + self.hbox_spinner = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + self.hbox_spinner.append(self.label_spinner_progress) + self.hbox_spinner.append(self.spinner) + + vbox_padding = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + vbox_padding.set_valign(Gtk.Align.END) + + label_padding = Gtk.Label(xalign=0, yalign=0) + label_padding.set_valign(Gtk.Align.END) + vbox_padding.append(label_padding) + + hbox_button_close = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=20) + + hbox_button_close.append(button_close) + hbox_button_close.set_halign(Gtk.Align.END) + + self.scrolled_window = Gtk.ScrolledWindow() + self.scrolled_window.set_propagate_natural_height(True) + self.scrolled_window.set_propagate_natural_width(True) + self.scrolled_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + + hbox_notify_revealer = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=20 + ) + + hbox_notify_revealer.set_name("hbox_notify_revealer") + hbox_notify_revealer.set_halign(Gtk.Align.CENTER) + + self.notify_revealer = Gtk.Revealer() + self.notify_revealer.set_reveal_child(False) + self.label_notify_revealer = Gtk.Label(xalign=0, yalign=0) + self.label_notify_revealer.set_name("label_notify_revealer") + + self.notify_revealer.set_child(hbox_notify_revealer) + + hbox_notify_revealer.append(self.label_notify_revealer) + + if self.textview.get_buffer() is not None: + self.textview = Gtk.TextView() + self.textview.set_property("editable", False) + self.textview.set_property("monospace", True) + + self.textview.set_vexpand(True) + self.textview.set_hexpand(True) + + self.textview.set_buffer(self.textbuffer) + self.scrolled_window.set_child(self.textview) + + self.scrolled_window.set_size_request(300, 300) + + vbox_progress.append(hbox_progress_warning) + vbox_progress.append(self.notify_revealer) + vbox_progress.append(self.scrolled_window) + vbox_progress.append(self.hbox_spinner) + vbox_progress.append(self.label_status) + vbox_progress.append(vbox_padding) + vbox_progress.append(hbox_button_close) + + self.present() + + self.linux_headers = None + + if ( + self.source == "official" + and action == "install" + or action == "uninstall" + and self.source == "official" + ): + if kernel.name == "linux": + self.linux_headers = "linux-headers" + if kernel.name == "linux-rt": + self.linux_headers = "linux-rt-headers" + if kernel.name == "linux-rt-lts": + self.linux_headers = "linux-rt-lts-headers" + if kernel.name == "linux-hardened": + self.linux_headers = "linux-hardened-headers" + if kernel.name == "linux-zen": + self.linux_headers = "linux-zen-headers" + if kernel.name == "linux-lts": + self.linux_headers = "linux-lts-headers" + + self.official_kernels = [ + "%s/packages/l/%s/%s-x86_64%s" + % ( + fn.archlinux_mirror_archive_url, + kernel.name, + kernel.version, + kernel.file_format, + ), + "%s/packages/l/%s/%s-x86_64%s" + % ( + fn.archlinux_mirror_archive_url, + self.linux_headers, + kernel.headers, + kernel.file_format, + ), + ] + + # in the event an install goes wrong, fallback and reinstall previous kernel + + if self.source == "official": + self.restore_kernel = None + + for inst_kernel in fn.get_installed_kernels(): + if inst_kernel.name == self.kernel.name: + + self.restore_kernel = inst_kernel + break + + if self.restore_kernel: + fn.logger.info("Restore kernel = %s" % self.restore_kernel.name) + fn.logger.info( + "Restore kernel version = %s" % self.restore_kernel.version + ) + else: + fn.logger.info("No previous %s kernel installed" % self.kernel.name) + else: + fn.logger.info("Community kernel, no kernel restore available") + + if fn.check_pacman_lockfile() is False: + th_monitor_messages_queue = fn.threading.Thread( + name=fn.thread_monitor_messages, + target=fn.monitor_messages_queue, + daemon=True, + args=(self,), + ) + + th_monitor_messages_queue.start() + + if fn.is_thread_alive(fn.thread_monitor_messages): + self.textbuffer.delete( + self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter() + ) + + if not fn.is_thread_alive(fn.thread_check_kernel_state): + th_check_kernel_state = fn.threading.Thread( + name=fn.thread_check_kernel_state, + target=self.check_kernel_state, + daemon=True, + ) + th_check_kernel_state.start() + + if action == "install" and self.source == "community": + self.label_notify_revealer.set_text( + "Installing from %s" % kernel.repository + ) + self.reveal_notify() + event = ( + "%s [INFO]: Installing kernel from repository %s, kernel = %s-%s\n" + % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.repository, + self.kernel.name, + self.kernel.version, + ) + ) + self.messages_queue.put(event) + + if not fn.is_thread_alive(fn.thread_install_community_kernel): + th_install_ch = fn.threading.Thread( + name=fn.thread_install_community_kernel, + target=fn.install_community_kernel, + args=(self,), + daemon=True, + ) + + th_install_ch.start() + + if action == "install" and self.source == "official": + self.label_notify_revealer.set_text("Installing kernel packages ...") + + self.reveal_notify() + + event = "%s [INFO]: Installing kernel = %s | version = %s\n" % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + self.kernel.version, + ) + self.messages_queue.put(event) + + if not fn.is_thread_alive(fn.thread_install_archive_kernel): + th_install = fn.threading.Thread( + name=fn.thread_install_archive_kernel, + target=fn.install_archive_kernel, + args=(self,), + daemon=True, + ) + + th_install.start() + + if action == "uninstall": + if fn.check_pacman_lockfile() is False: + self.label_notify_revealer.set_text("Removing kernel packages ...") + self.reveal_notify() + + event = "%s [INFO]: Uninstalling kernel %s %s\n" % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.name, + self.kernel.version, + ) + self.messages_queue.put(event) + + if not fn.is_thread_alive(fn.thread_uninstall_kernel): + th_uninstall_kernel = fn.threading.Thread( + name=fn.thread_uninstall_kernel, + target=self.uninstall_kernel, + daemon=True, + ) + + th_uninstall_kernel.start() + else: + self.label_notify_revealer.set_text( + "Pacman lockfile found cannot continue ..." + ) + + self.reveal_notify() + + fn.logger.error( + "Pacman lockfile found, is another pacman process running ?" + ) + + def timeout(self): + self.hide_notify() + + def hide_notify(self): + self.notify_revealer.set_reveal_child(False) + if self.timeout_id is not None: + GLib.source_remove(self.timeout_id) + self.timeout_id = None + + def reveal_notify(self): + # reveal = self.notify_revealer.get_reveal_child() + self.notify_revealer.set_reveal_child(True) + self.timeout_id = GLib.timeout_add(3000, self.timeout) + + def on_button_close_response(self, widget): + if fn.check_pacman_process(self): + mw = MessageWindow( + title="Pacman process running", + message="Pacman is busy processing a transaction .. please wait", + image_path="images/48x48/akm-progress.png", + transient_for=self, + detailed_message=False, + ) + + mw.present() + else: + self.destroy() + + def on_close(self, data): + if fn.check_pacman_process(self): + mw = MessageWindow( + title="Pacman process running", + message="Pacman is busy processing a transaction .. please wait", + image_path="images/48x48/akm-progress.png", + transient_for=self, + detailed_message=False, + ) + + mw.present() + else: + self.destroy() + + def check_kernel_state(self): + returncode = None + kernel = None + while True: + items = self.kernel_state_queue.get() + + try: + if items is not None: + returncode, action, kernel = items + + if returncode == 0: + self.label_notify_revealer.set_text( + "Kernel %s completed" % action + ) + self.reveal_notify() + + fn.logger.info("Kernel %s completed" % action) + + event = "%s [INFO]: Kernel %s completed\n" % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + action, + ) + self.messages_queue.put(event) + + if returncode == 1: + self.errors_found = True + + self.label_notify_revealer.set_text("Kernel %s failed" % action) + self.reveal_notify() + + fn.logger.error("Kernel %s failed" % action) + + event = "%s [ERROR]: Kernel %s failed\n" % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + action, + ) + self.messages_queue.put(event) + + self.label_status.set_markup( + "Kernel %s failed - see logs above" + % action + ) + + # undo action here if action == install + + event = ( + "%s [INFO]: Attempting to undo previous Linux package changes\n" + % ( + fn.datetime.datetime.now().strftime( + "%Y-%m-%d-%H-%M-%S" + ), + ) + ) + + self.messages_queue.put(event) + + if action == "install" and self.restore_kernel is not None: + self.restore = True + fn.logger.info( + "Installation failed, attempting removal of previous Linux package changes" + ) + self.set_title("Kernel installation failed") + + self.label_spinner_progress.set_markup( + "Please wait restoring kernel %s" + % self.restore_kernel.version + ) + + fn.uninstall(self) + + fn.logger.info( + "Restoring previously installed kernel %s" + % self.restore_kernel.version + ) + + self.official_kernels = [ + "%s/packages/l/%s/%s-%s-x86_64%s" + % ( + fn.archlinux_mirror_archive_url, + self.restore_kernel.name, + self.restore_kernel.name, + self.restore_kernel.version, + ".pkg.tar.zst", + ), + "%s/packages/l/%s/%s-%s-x86_64%s" + % ( + fn.archlinux_mirror_archive_url, + self.linux_headers, + self.linux_headers, + self.restore_kernel.version, + ".pkg.tar.zst", + ), + ] + self.errors_found = False + fn.install_archive_kernel(self) + self.set_title("Kernel installation failed") + self.label_status.set_markup( + f"Kernel %s failed - see logs above\n" + % action + ) + + # self.spinner.set_spinning(False) + # self.hbox_spinner.hide() + # + # self.label_progress_window_desc.set_markup( + # f"This window can be now closed\n" + # f"A reboot is recommended when Linux packages have changed" + # ) + + # break + else: + if ( + returncode == 0 + and "-headers" in kernel + or action == "uninstall" + or action == "install" + and self.errors_found is False + ): + + fn.update_bootloader(self) + self.update_installed_list() + self.update_official_list() + + if len(self.manager_gui.community_kernels) > 0: + self.update_community_list() + + if self.restore == False: + self.label_title.set_markup( + "Kernel %s completed" % action + ) + + self.label_status.set_markup( + "Kernel %s completed" + % action + ) + + self.spinner.set_spinning(False) + self.hbox_spinner.hide() + + self.label_progress_window_desc.set_markup( + f"This window can be now closed\n" + f"A reboot is recommended when Linux packages have changed" + ) + else: + self.label_title.set_markup( + "Kernel %s failed" % action + ) + + self.label_status.set_markup( + "Kernel %s failed" + % action + ) + + self.spinner.set_spinning(False) + self.hbox_spinner.hide() + + self.label_progress_window_desc.set_markup( + f"This window can be now closed\n" + f"Previous kernel restored due to failure\n" + f"A reboot is recommended when Linux packages have changed" + ) + + # # else: + # self.spinner.set_spinning(False) + # self.hbox_spinner.hide() + # + # self.label_progress_window_desc.set_markup( + # f"This window can be now closed\n" + # f"A reboot is recommended when Linux packages have changed" + # ) + + break + except Exception as e: + fn.logger.error("Exception in check_kernel_state(): %s" % e) + + finally: + self.kernel_state_queue.task_done() + + def update_installed_list(self): + self.manager_gui.installed_kernels = fn.get_installed_kernels() + GLib.idle_add( + self.manager_gui.kernel_stack.add_installed_kernels_to_stack, True + ) + + def update_official_list(self): + self.manager_gui.installed_kernels = fn.get_installed_kernels() + GLib.idle_add( + self.manager_gui.kernel_stack.add_official_kernels_to_stack, + True, + ) + + def update_community_list(self): + self.manager_gui.installed_kernels = fn.get_installed_kernels() + GLib.idle_add( + self.manager_gui.kernel_stack.add_community_kernels_to_stack, + True, + ) + + def uninstall_kernel(self): + event = "%s [INFO]: Uninstalling kernel %s\n" % ( + fn.datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), + self.kernel.version, + ) + + self.messages_queue.put(event) + + fn.uninstall(self) diff --git a/usr/share/archlinux-kernel-manager/ui/SettingsWindow.py b/usr/share/archlinux-kernel-manager/ui/SettingsWindow.py new file mode 100644 index 0000000..f5b827e --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/SettingsWindow.py @@ -0,0 +1,713 @@ +import gi +import os +from ui.Stack import Stack +from ui.MessageWindow import MessageWindow +import libs.functions as fn +from gi.repository import Gtk, Gio, GLib, GObject + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + + +class SettingsWindow(Gtk.Window): + def __init__(self, fn, manager_gui, **kwargs): + super().__init__(**kwargs) + + self.set_title("Arch Linux Kernel Manager - Settings") + self.set_resizable(False) + self.set_size_request(600, 600) + stack = Stack(transition_type="CROSSFADE") + + self.set_icon_name("akm-tux") + self.manager_gui = manager_gui + self.set_modal(True) + self.set_transient_for(self.manager_gui) + + self.queue_kernels = self.manager_gui.queue_kernels + + header_bar = Gtk.HeaderBar() + + header_bar.set_show_title_buttons(True) + + self.set_titlebar(header_bar) + + hbox_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + hbox_main.set_name("box") + self.set_child(child=hbox_main) + + vbox_header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_header.set_name("vbox_header") + + lbl_heading = Gtk.Label(xalign=0.5, yalign=0.5) + lbl_heading.set_name("label_flowbox_message") + lbl_heading.set_text("Preferences") + + lbl_padding = Gtk.Label(xalign=0.0, yalign=0.0) + lbl_padding.set_text(" ") + + grid_banner_img = Gtk.Grid() + + image_settings = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-settings.png") + ) + + image_settings.set_icon_size(Gtk.IconSize.LARGE) + image_settings.set_halign(Gtk.Align.START) + + grid_banner_img.attach(image_settings, 0, 1, 1, 1) + grid_banner_img.attach_next_to( + lbl_padding, + image_settings, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + grid_banner_img.attach_next_to( + lbl_heading, + lbl_padding, + Gtk.PositionType.RIGHT, + 1, + 1, + ) + + vbox_header.append(grid_banner_img) + + hbox_main.append(vbox_header) + + stack_switcher = Gtk.StackSwitcher() + stack_switcher.set_orientation(Gtk.Orientation.HORIZONTAL) + stack_switcher.set_stack(stack) + + button_close = Gtk.Button(label="Close") + button_close.connect("clicked", self.on_close_clicked) + + hbox_stack_sidebar = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + hbox_stack_sidebar.set_name("box") + + hbox_stack_sidebar.append(stack_switcher) + hbox_stack_sidebar.append(stack) + + hbox_main.append(hbox_stack_sidebar) + + vbox_button = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + vbox_button.set_halign(Gtk.Align.END) + vbox_button.set_name("box") + + vbox_button.append(button_close) + + hbox_stack_sidebar.append(vbox_button) + + vbox_settings = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + vbox_settings.set_name("box") + + label_official_kernels = Gtk.Label(xalign=0, yalign=0) + label_official_kernels.set_markup( + "Latest Official kernels (%s)" % len(fn.supported_kernels_dict) + ) + + label_community_kernels = Gtk.Label(xalign=0, yalign=0) + label_community_kernels.set_markup( + "Latest Community based kernels (%s)" + % len(self.manager_gui.community_kernels) + ) + + vbox_settings_listbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + self.listbox_official_kernels = Gtk.ListBox() + self.listbox_official_kernels.set_selection_mode(Gtk.SelectionMode.NONE) + + self.label_loading_kernels = Gtk.Label(xalign=0, yalign=0) + self.label_loading_kernels.set_text("Loading ...") + + self.listbox_official_kernels.append(self.label_loading_kernels) + + listbox_community_kernels = Gtk.ListBox() + listbox_community_kernels.set_selection_mode(Gtk.SelectionMode.NONE) + + scrolled_window_community = Gtk.ScrolledWindow() + scrolled_window_official = Gtk.ScrolledWindow() + + scrolled_window_community.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + + scrolled_window_official.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC + ) + + scrolled_window_community.set_size_request(0, 150) + scrolled_window_official.set_size_request(0, 150) + + scrolled_window_official.set_child(self.listbox_official_kernels) + vbox_community_warning = None + + self.kernel_versions_queue = fn.Queue() + fn.Thread( + target=fn.get_latest_versions, + args=(self,), + daemon=True, + ).start() + + fn.Thread(target=self.check_official_version_queue, daemon=True).start() + + if len(self.manager_gui.community_kernels) > 0: + for community_kernel in self.manager_gui.community_kernels: + row_community_kernel = Gtk.ListBoxRow() + hbox_community_kernel = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=5 + ) + hbox_community_kernel.set_name("box_row") + + hbox_row_official_kernel_row = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10 + ) + + label_community_kernel = Gtk.Label(xalign=0, yalign=0) + label_community_kernel.set_text("%s" % community_kernel.name) + + label_community_kernel_version = Gtk.Label(xalign=0, yalign=0) + label_community_kernel_version.set_text("%s" % community_kernel.version) + + hbox_row_official_kernel_row.append(label_community_kernel) + hbox_row_official_kernel_row.append(label_community_kernel_version) + + hbox_community_kernel.append(hbox_row_official_kernel_row) + + row_community_kernel.set_child(hbox_community_kernel) + listbox_community_kernels.append(row_community_kernel) + scrolled_window_community.set_child(listbox_community_kernels) + else: + vbox_community_warning = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=10 + ) + vbox_community_warning.set_name("box") + + image_warning = Gtk.Image.new_from_file( + os.path.join(base_dir, "images/48x48/akm-warning.png") + ) + + image_warning.set_icon_size(Gtk.IconSize.LARGE) + image_warning.set_halign(Gtk.Align.START) + + hbox_warning = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_warning.set_name("box") + + hbox_warning.append(image_warning) + + label_pacman_no_community = Gtk.Label(xalign=0, yalign=0) + label_pacman_no_community.set_markup( + f"Cannot find any supported unofficial pacman repository's\n" + f"Add unofficial pacman repository's to use community based kernels" + ) + + hbox_warning.append(label_pacman_no_community) + + vbox_community_warning.append(hbox_warning) + + vbox_settings_listbox.append(label_official_kernels) + vbox_settings_listbox.append(scrolled_window_official) + vbox_settings_listbox.append(label_community_kernels) + + if len(self.manager_gui.community_kernels) > 0: + vbox_settings_listbox.append(scrolled_window_community) + else: + vbox_settings_listbox.append(vbox_community_warning) + + vbox_settings.append(vbox_settings_listbox) + + vbox_settings_adv = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + vbox_settings_adv.set_name("box") + + self.listbox_settings_adv = Gtk.ListBox() + self.listbox_settings_adv.set_selection_mode(Gtk.SelectionMode.NONE) + + row_settings_adv = Gtk.ListBoxRow() + self.listbox_settings_adv.append(row_settings_adv) + + hbox_bootloader_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_bootloader_row.set_name("box_row") + hbox_bootloader_row.set_halign(Gtk.Align.START) + + self.hbox_bootloader_grub_row = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=5 + ) + self.hbox_bootloader_grub_row.set_name("box_row") + self.hbox_bootloader_grub_row.set_halign(Gtk.Align.START) + + self.text_entry_bootloader_file = Gtk.Entry() + + hbox_switch_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_switch_row.set_name("box_row") + hbox_switch_row.set_halign(Gtk.Align.START) + + hbox_log_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_log_row.set_name("box_row") + hbox_log_row.set_halign(Gtk.Align.START) + + label_bootloader = Gtk.Label(xalign=0, yalign=0) + label_bootloader.set_markup("Bootloader") + + hbox_warning = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + hbox_warning.set_name("hbox_warning") + + label_bootloader_warning = Gtk.Label(xalign=0, yalign=0) + label_bootloader_warning.set_markup( + f"Only change this setting if you know what you are doing\n" + f"The selected Grub/Systemd-boot bootloader entry will be updated\n" + f"This may break your system" + ) + + hbox_warning.append(label_bootloader_warning) + + label_settings_bootloader_title = Gtk.Label(xalign=0.5, yalign=0.5) + label_settings_bootloader_title.set_markup("Current Bootloader") + + self.label_settings_bootloader_file = Gtk.Label(xalign=0.5, yalign=0.5) + self.label_settings_bootloader_file.set_text("GRUB config file") + + self.button_override_bootloader = Gtk.Button( + label="Override bootloader settings" + ) + self.button_override_bootloader.connect("clicked", self.on_override_clicked) + self.hbox_bootloader_override_row = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=20 + ) + self.hbox_bootloader_override_row.set_name("box_row") + self.hbox_bootloader_override_row.append(self.button_override_bootloader) + + boot_loaders = {0: "grub", 1: "systemd-boot"} + + # Set up the factory + factory = Gtk.SignalListItemFactory() + factory.connect("setup", self._on_factory_setup) + factory.connect("bind", self._on_factory_bind) + + self.model = Gio.ListStore(item_type=Bootloader) + for bootloader_id in boot_loaders.keys(): + self.model.append( + Bootloader( + id=bootloader_id, + name=boot_loaders[bootloader_id], + ) + ) + + self.dropdown_bootloader = Gtk.DropDown( + model=self.model, factory=factory, hexpand=True + ) + + self.dropdown_bootloader.set_sensitive(False) + + self.selected_bootloader = None + + self._bootloader_grub_config = "/boot/grub/grub.cfg" + + row_settings_override_grub = Gtk.ListBoxRow() + row_settings_grub = Gtk.ListBoxRow() + self.listbox_settings_adv.append(row_settings_grub) + + self.listbox_settings_adv.append(row_settings_override_grub) + + self.text_entry_bootloader_file.connect("changed", self.on_entry_changed) + self.text_entry_bootloader_file.props.editable = False + text_entry_buffer_file = Gtk.EntryBuffer() + + if self.manager_gui.bootloader_grub_cfg is not None: + text_entry_buffer_file.set_text( + self.manager_gui.bootloader_grub_cfg, + len(self.manager_gui.bootloader_grub_cfg), + ) + else: + text_entry_buffer_file.set_text( + self._bootloader_grub_config, + len(self._bootloader_grub_config), + ) + + self.text_entry_bootloader_file.set_buffer(text_entry_buffer_file) + self.text_entry_bootloader_file.set_halign(Gtk.Align.END) + self.text_entry_bootloader_file.set_sensitive(False) + + label_grub_file_path = Gtk.Label(xalign=0.5, yalign=0.5) + label_grub_file_path.set_markup("Grub file path") + + self.hbox_bootloader_grub_row.append(label_grub_file_path) + self.hbox_bootloader_grub_row.append(self.text_entry_bootloader_file) + + row_settings_grub.set_child(self.hbox_bootloader_grub_row) + + if manager_gui.bootloader == "grub": + self.dropdown_bootloader.set_selected(0) + self.selected_bootloader = 0 + self.hbox_bootloader_grub_row.set_visible(True) + + row_settings_override_grub.set_child(self.hbox_bootloader_override_row) + + if manager_gui.bootloader == "systemd-boot": + + self.selected_bootloader = 1 + + self.dropdown_bootloader.set_selected(1) + row_settings_override_systemd = Gtk.ListBoxRow() + self.listbox_settings_adv.append(row_settings_override_systemd) + row_settings_override_systemd.set_child(self.hbox_bootloader_override_row) + + self.hbox_bootloader_grub_row.set_visible(False) + + self.dropdown_bootloader.connect( + "notify::selected-item", self._on_selected_item_notify + ) + + hbox_bootloader_row.append(label_settings_bootloader_title) + hbox_bootloader_row.append(self.dropdown_bootloader) + + row_settings_adv.set_child(hbox_bootloader_row) + + vbox_settings_adv.append(label_bootloader) + vbox_settings_adv.append(hbox_warning) + vbox_settings_adv.append(self.listbox_settings_adv) + + listbox_settings_cache = Gtk.ListBox() + listbox_settings_cache.set_selection_mode(Gtk.SelectionMode.NONE) + + row_settings_cache = Gtk.ListBoxRow() + listbox_settings_cache.append(row_settings_cache) + + label_cache = Gtk.Label(xalign=0, yalign=0) + label_cache.set_markup("Refresh data from Arch Linux Archive") + + label_cache_update = Gtk.Label(xalign=0.5, yalign=0.5) + label_cache_update.set_text("Update (this will take some time)") + + self.label_cache_update_status = Gtk.Label(xalign=0.5, yalign=0.5) + + switch_refresh_cache = Gtk.Switch() + switch_refresh_cache.connect("state-set", self.refresh_toggle) + + label_cache_file = Gtk.Label(xalign=0, yalign=0) + label_cache_file.set_text(fn.cache_file) + label_cache_file.set_selectable(True) + + self.label_cache_lastmodified = Gtk.Label(xalign=0, yalign=0) + self.label_cache_lastmodified.set_markup( + "Last modified date: %s" % fn.get_cache_last_modified() + ) + + hbox_switch_row.append(label_cache_update) + hbox_switch_row.append(switch_refresh_cache) + hbox_switch_row.append(self.label_cache_update_status) + + row_settings_cache.set_child(hbox_switch_row) + + label_logfile = Gtk.Label(xalign=0, yalign=0) + label_logfile.set_markup("Log file") + + button_logfile = Gtk.Button(label="Open event log file") + button_logfile.connect("clicked", self.on_button_logfile_clicked) + + label_logfile_location = Gtk.Label(xalign=0.5, yalign=0.5) + label_logfile_location.set_text(fn.event_log_file) + label_logfile_location.set_selectable(True) + hbox_log_row.append(button_logfile) + hbox_log_row.append(label_logfile_location) + + listbox_settings_log = Gtk.ListBox() + listbox_settings_log.set_selection_mode(Gtk.SelectionMode.NONE) + + row_settings_log = Gtk.ListBoxRow() + listbox_settings_log.append(row_settings_log) + + row_settings_log.set_child(hbox_log_row) + + vbox_settings_adv.append(label_cache) + vbox_settings_adv.append(self.label_cache_lastmodified) + vbox_settings_adv.append(label_cache_file) + vbox_settings_adv.append(listbox_settings_cache) + vbox_settings_adv.append(label_logfile) + vbox_settings_adv.append(listbox_settings_log) + + stack.add_titled(vbox_settings_adv, "Advanced Settings", "Advanced") + stack.add_titled(vbox_settings, "Kernels", "Kernel versions") + + def populate_official_kernels(self): + self.label_loading_kernels.hide() + for official_kernel in fn.supported_kernels_dict: + row_official_kernel = Gtk.ListBoxRow() + hbox_row_official = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + + hbox_row_official_kernel_row = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10 + ) + + hbox_row_official.set_name("box_row") + + label_kernel = Gtk.Label(xalign=0, yalign=0) + label_kernel.set_text("%s" % official_kernel) + + label_kernel_version = Gtk.Label(xalign=0, yalign=0) + label_kernel_version.set_text("%s" % self.kernel_versions[official_kernel]) + + hbox_row_official_kernel_row.append(label_kernel) + hbox_row_official_kernel_row.append(label_kernel_version) + + hbox_row_official.append(hbox_row_official_kernel_row) + + row_official_kernel.set_child(hbox_row_official) + + self.listbox_official_kernels.append(row_official_kernel) + + def check_official_version_queue(self): + while True: + self.kernel_versions = self.kernel_versions_queue.get() + + if self.kernel_versions is not None: + break + + self.kernel_versions_queue.task_done() + + GLib.idle_add(self.populate_official_kernels, priority=GLib.PRIORITY_DEFAULT) + + def on_entry_changed(self, entry): + if ( + len(entry.get_text()) > 0 + and entry.get_text() != self.manager_gui.bootloader_grub_cfg + ): + self.button_override_bootloader.get_child().set_text("Apply changes") + + def _on_factory_setup(self, factory, list_item): + label = Gtk.Label() + list_item.set_child(label) + + def _on_factory_bind(self, factory, list_item): + label = list_item.get_child() + bootloader = list_item.get_item() + label.set_text(bootloader.name) + + def on_override_clicked(self, widget): + if self.button_override_bootloader.get_child().get_text() == "Apply changes": + # validate bootloader + if self.dropdown_bootloader.get_selected() == 1: + if not os.path.exists( + "/sys/firmware/efi/fw_platform_size" + ) or not os.path.exists("/sys/firmware/efi/efivars"): + mw = MessageWindow( + title="Legacy boot detected", + message="Cannot select systemd-boot, UEFI boot mode is not available", + image_path="images/48x48/akm-warning.png", + transient_for=self, + detailed_message=False, + ) + + mw.present() + self.dropdown_bootloader.set_selected(0) + return + + config_data = fn.read_config(self) + + if config_data is not None: + # grub + + if ( + self.dropdown_bootloader.get_selected() == 0 + and len( + self.text_entry_bootloader_file.get_buffer().get_text().strip() + ) + > 0 + ): + if fn.os.path.exists( + self.text_entry_bootloader_file.get_buffer().get_text().strip() + ): + if "bootloader" in config_data.keys(): + config_data.remove("bootloader") + + bootloader = fn.tomlkit.table(True) + bootloader.update({"name": "grub"}) + bootloader.update( + { + "grub_config": self.text_entry_bootloader_file.get_buffer() + .get_text() + .strip() + } + ) + + config_data.append("bootloader", bootloader) + + if fn.update_config(config_data, "grub") is True: + self.manager_gui.bootloader = "grub" + self.manager_gui.bootloader_grub_cfg = ( + self.text_entry_bootloader_file.get_buffer() + .get_text() + .strip() + ) + else: + mw = MessageWindow( + title="Grub config file", + message="The specified Grub config file %s does not exist" + % self.text_entry_bootloader_file.get_buffer() + .get_text() + .strip(), + image_path="images/48x48/akm-warning.png", + transient_for=self, + detailed_message=False, + ) + + mw.present() + self.button_override_bootloader.get_child().set_text( + "Override bootloader settings" + ) + + elif ( + self.dropdown_bootloader.get_selected() == 1 + and self.selected_bootloader + != self.dropdown_bootloader.get_selected() + ): + if "bootloader" in config_data.keys(): + config_data.remove("bootloader") + + self.hbox_bootloader_grub_row.set_visible(True) + + bootloader = fn.tomlkit.table(True) + bootloader.update({"name": "systemd-boot"}) + + config_data.append("bootloader", bootloader) + + if fn.update_config(config_data, "systemd-boot") is True: + self.manager_gui.bootloader = "systemd-boot" + + else: + self.dropdown_bootloader.set_sensitive(True) + + if self.dropdown_bootloader.get_selected() == 0: + self.hbox_bootloader_grub_row.set_visible(True) + self.text_entry_bootloader_file.set_sensitive(True) + self.text_entry_bootloader_file.props.editable = True + elif self.dropdown_bootloader.get_selected() == 1: + self.hbox_bootloader_grub_row.set_visible(False) + + def _on_selected_item_notify(self, dd, _): + if self.dropdown_bootloader.get_selected() != self.selected_bootloader: + self.button_override_bootloader.get_child().set_text("Apply changes") + else: + self.button_override_bootloader.get_child().set_text( + "Override bootloader settings" + ) + if dd.get_selected() == 1: + if self.text_entry_bootloader_file is not None: + self.hbox_bootloader_grub_row.set_visible(False) + elif dd.get_selected() == 0: + if self.text_entry_bootloader_file is not None: + self.hbox_bootloader_grub_row.set_visible(True) + self.text_entry_bootloader_file.set_sensitive(True) + self.text_entry_bootloader_file.props.editable = True + + def monitor_kernels_queue(self, switch): + while True: + if len(fn.fetched_kernels_dict) > 0: + self.manager_gui.official_kernels = self.queue_kernels.get() + self.queue_kernels.task_done() + self.refreshed = True + if self.manager_gui.official_kernels is not None: + switch.set_sensitive(False) + self.update_official_list() + self.update_community_list() + self.update_timestamp() + self.label_cache_update_status.set_markup( + "Cache refresh completed" + ) + else: + self.label_cache_update_status.set_markup( + "Cache refresh failed" + ) + self.refreshed = False + self.update_timestamp() + break + else: + self.label_cache_update_status.set_markup( + "Cache refresh in progress" + ) + # fn.time.sleep(0.3) + + def refresh_toggle(self, switch, data): + if switch.get_active() is True: + # refresh cache + fn.logger.info("Refreshing cache file %s" % fn.cache_file) + switch.set_sensitive(False) + + try: + th_refresh_cache = fn.Thread( + name=fn.thread_refresh_cache, + target=fn.refresh_cache, + args=(self,), + daemon=True, + ) + + th_refresh_cache.start() + + # monitor queue + fn.Thread( + target=self.monitor_kernels_queue, daemon=True, args=(switch,) + ).start() + + except Exception as e: + fn.logger.error("Exception in refresh_toggle(): %s" % e) + self.label_cache_update_status.set_markup("Cache refresh failed") + + def update_timestamp(self): + if self.refreshed is True: + self.label_cache_lastmodified.set_markup( + "Last modified date: %s" + % fn.get_cache_last_modified() + ) + else: + self.label_cache_lastmodified.set_markup( + "Last modified date: %s" + % "Refresh failed" + ) + + def update_official_list(self): + self.manager_gui.installed_kernels = fn.get_installed_kernels() + GLib.idle_add( + self.manager_gui.kernel_stack.add_official_kernels_to_stack, + True, + ) + + def update_community_list(self): + self.manager_gui.installed_kernels = fn.get_installed_kernels() + + GLib.idle_add( + self.manager_gui.kernel_stack.add_community_kernels_to_stack, + True, + ) + + def on_close_clicked(self, widget): + self.destroy() + + def on_button_logfile_clicked(self, widget): + try: + cmd = ["sudo", "-u", fn.sudo_username, "xdg-open", fn.event_log_file] + fn.subprocess.Popen( + cmd, + shell=False, + stdout=fn.subprocess.PIPE, + stderr=fn.subprocess.STDOUT, + ) + + except Exception as e: + fn.logger.error("Exception in on_button_logfile_clicked(): %s" % e) + + +class Bootloader(GObject.Object): + __gtype_name__ = "Bootloader" + + def __init__(self, id, name): + super().__init__() + + self.id = id + self.name = name + + @GObject.Property + def bootloader_id(self): + return self.id + + @GObject.Property + def bootloader_name(self): + return self.name diff --git a/usr/share/archlinux-kernel-manager/ui/SplashScreen.py b/usr/share/archlinux-kernel-manager/ui/SplashScreen.py new file mode 100644 index 0000000..b945ca2 --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/SplashScreen.py @@ -0,0 +1,27 @@ +import gi +import libs.functions as fn + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, Gio, GLib, GdkPixbuf, GObject, Gdk + +base_dir = fn.os.path.abspath(fn.os.path.join(fn.os.path.dirname(__file__), "..")) + + +class SplashScreen(Gtk.Window): + def __init__(self, app_name, **kwargs): + super().__init__(**kwargs) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + self.set_title(app_name) + self.set_icon_name("archlinux-kernel-manager-tux") + + tux_icon = Gtk.Picture.new_for_file( + file=Gio.File.new_for_path( + fn.os.path.join(base_dir, "images/364x408/akm-tux-splash.png") + ) + ) + tux_icon.set_content_fit(content_fit=Gtk.ContentFit.FILL) + + self.set_child(child=tux_icon) + self.present() diff --git a/usr/share/archlinux-kernel-manager/ui/Stack.py b/usr/share/archlinux-kernel-manager/ui/Stack.py new file mode 100644 index 0000000..a9454ef --- /dev/null +++ b/usr/share/archlinux-kernel-manager/ui/Stack.py @@ -0,0 +1,30 @@ +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk + + +class Stack(Gtk.Stack): + def __init__(self, transition_type): + super(Stack, self).__init__() + + # self.set_transition_type(Gtk.StackTransitionType.ROTATE_LEFT) + if transition_type == "ROTATE_LEFT": + transition_type = Gtk.StackTransitionType.ROTATE_LEFT + if transition_type == "ROTATE_RIGHT": + transition_type = Gtk.StackTransitionType.ROTATE_RIGHT + if transition_type == "CROSSFADE": + transition_type = Gtk.StackTransitionType.CROSSFADE + if transition_type == "SLIDE_UP": + transition_type = Gtk.StackTransitionType.SLIDE_UP + if transition_type == "SLIDE_DOWN": + transition_type = Gtk.StackTransitionType.SLIDE_DOWN + if transition_type == "OVER_DOWN": + transition_type = Gtk.StackTransitionType.OVER_DOWN + + self.set_transition_type(transition_type) + self.set_hexpand(True) + self.set_vexpand(True) + self.set_transition_duration(250) + self.set_hhomogeneous(False) + self.set_vhomogeneous(False) diff --git a/usr/share/icons/hicolor/scalable/apps/archlinux-kernel-manager-tux.svg b/usr/share/icons/hicolor/scalable/apps/archlinux-kernel-manager-tux.svg new file mode 100644 index 0000000..7b1fae4 --- /dev/null +++ b/usr/share/icons/hicolor/scalable/apps/archlinux-kernel-manager-tux.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/usr/share/polkit-1/actions/org.archlinux.akm.policy b/usr/share/polkit-1/actions/org.archlinux.akm.policy new file mode 100644 index 0000000..4cfedd0 --- /dev/null +++ b/usr/share/polkit-1/actions/org.archlinux.akm.policy @@ -0,0 +1,104 @@ + + + + ArcoLinux + http://arcolinux.info/ + package-x-generic + + Add/Remove Linux kernels from Arch Linux based systems + 變更您自身的使用者資料 + 修改您的用户数据 + Зміна даних вашого користувача + Kullanıcı bilgilerinizi değiştirin + Ändra ditt egna användardata + Измените ваше личне корисничке податке + Izmenite vaše lične korisničke podatke + Spremenite lastne uporabniške podatke + Zmeniť svoje vlastné používateľské údaje + Изменить личные пользовательские данные + Alterar os seus próprios dados + Alterar dados do próprio usuário + Zmiana własnych danych + ਆਪਣਾ ਯੂਜ਼ਰ ਡਾਟਾ ਬਦਲੋ + Modificar sas pròprias donadas + Uw eigen gebruikersgegevens bewerken + Mainīt pašam savus lietotāja datus + Keisti savo naudotojo duomenis + 개인 사용자 데이터 변경 + Өзіңіздің пайдаланушы ақпаратыңызды өзгерту + თქვენი პირადი ინფორმაციის შეცვლა + 自身のユーザーデータを変更する + Cambia i propri dati utente + Ubah data penggunamu sendiri + Modificar tu proprie datos de usator + Saját felhasználói adatainak módosítása + Promijenite vlastite korisničke podatke + Cambiar os seus propios datos de usuario + Cambie i tiei dâts utent + Modifier ses propres données + Muuta omia käyttäjätietojasi + Aldatu zure erabiltzaile-datuak + Cambie sus propios datos de usuario + Ŝanĝi viajn proprajn uzantodatumojn + Change your own user data + Change your own user data + Αλλάξτε τα δεδομένα χρήστη σας + Ändern Sie Ihre eigenen Benutzerdaten + Skift data for din egen bruger + Změnit své vlastní údaje + Canvieu les dades d'usuari pròpies + Camudacia de los tos datos d'usuariu + غيّر بيانات المستخدم خاصتك + Authentication is required to change your own user data + 若要變更您自身的使用者資料需要核對身分 + 需要授权以修改您的用户数据 + Для зміни даних вашого користувача потрібно пройти розпізнавання + Kendi kullanıcı bilginizi değiştirmek için kimlik kanıtlaması gereklidir + Autentisering krävs för att ändra ditt egna användardata + Потребно је потврђивање идентитета за промену ваших личних корисничких података + Potrebno je potvrđivanje identiteta za promenu vaših ličnih korisničkih podataka + Za spremembo lastnih podatkov je zahtevana overitev + Na zmenu vlastných používateľských údajov je potrebné overenie totožnosti + Для изменения личных пользовательских данных требуется аутентификация + É necessária autenticação para alterar os seus próprios dados de utilizador + Autenticação é necessária para alterar dados do próprio usuário + Wymagane jest uwierzytelnienie, aby zmienić własne dane + ਤੁਹਾਡਾ ਯੂਜ਼ਰ ਡਾਟਾ ਬਦਲਣ ਲਈ ਪਰਮਾਣਿਤ ਹੋਣ ਦੀ ਲੋੜ ਹੈ + Vos cal vos autentificar per modificar vòstras pròprias donadas d'utilizaire + Authenticatie vereist om uw eigen gebruikersgegevens te kunnen veranderen + Nepieciešama autentifikācija, lai mainītu pats savus lietotāja datus + Norint keisti savo naudotojo duomenis, reikia patvirtinti tapatybę + 내 사용자 데이터를 변경하려면 인증해야 합니다 + Өзіңіздің пайдаланушы ақпаратыңызды өзгерту үшін аутентификация керек + თქვენი პირადი მონაცემების შესაცვლელად აუცილებელია ავთენტიფიკაციის გავლა. + 自分自身のユーザーデータを変更するには認証が必要です + È richiesto autenticarsi per cambiare i propri dati utente + Otentikasi diperlukan untuk mengubah data pengguna milikmu + Authentication es necessari pro modificar tu proprie datos de usator + Hitelesítés szükséges a saját felhasználói adatai módosításához + Potrebna je ovjera za promjenu vlastitih korisničkih podataka + Requírese autenticación para cambiar os seus propios datos de usuario + La autenticazion e je necssarie par cambiâ i propris dâts utent + Vous devez vous authentifier pour modifier vos propres données utilisateur + Omien käyttäjätietojen muuttaminen vaatii tunnistautumisen + Derrigorrezkoa da autentifikatzea zure erabiltzaile-datuak aldatzeko + Se necesita autenticación para cambiar sus datos de usuario + Aŭtentigo bezonatas por ŝanĝi viajn proprajn uzantodatumojn + Authentication is required to change your own user data + Authentication is required to change your own user data + Απαιτείται πιστοποίηση για αλλαγή των δικών σας δεδομένων χρήστη + Zur Änderung Ihrer eigenen Benutzerdaten ist eine Authentifizierung erforderlich + Der kræves autentifikation for at skifte data for din egen bruger + Pro změnu svých vlastních údajů je vyžadována autentizace + Es requereix autenticació per canviar les vostres dades d'usuari + Ríquese l'autenticación pa camudar los tos datos d'usuairu + الاستيثاق مطلوب لتغيير بيانات المستخدم التي تملكها + + auth_admin + auth_admin + auth_admin + + /usr/share/archlinux-kernel-manager/archlinux-kernel-manager.py + true + +