├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── LICENSES ├── CC0-1.0.txt ├── GPL-3.0-or-later.txt └── MIT.txt ├── README.md ├── REUSE.toml ├── regreet.sample.toml ├── src ├── cache │ ├── lru.rs │ └── mod.rs ├── client.rs ├── config.rs ├── constants.rs ├── gui │ ├── component.rs │ ├── messages.rs │ ├── mod.rs │ ├── model.rs │ ├── templates.rs │ └── widget │ │ └── clock.rs ├── main.rs ├── sysutil.rs └── tomlutils.rs └── systemd-tmpfiles.conf /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: Lint 6 | 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install GTK4 22 | run: sudo apt update && sudo apt install libgtk-4-dev build-essential 23 | - run: rustup toolchain install 1.75 --profile minimal 24 | - name: Restore build cache 25 | uses: Swatinem/rust-cache@v2.7.5 26 | 27 | - run: cargo build --verbose 28 | - run: cargo test --verbose 29 | 30 | - name: pre-commit 31 | uses: pre-commit/action@v3.0.1 32 | - uses: pre-commit-ci/lite-action@v1.1.0 33 | if: always() 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | target/ 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | repos: 6 | - repo: https://github.com/doublify/pre-commit-rust 7 | rev: v1.0 8 | hooks: 9 | - id: fmt 10 | - id: clippy 11 | - repo: https://github.com/DevinR528/cargo-sort 12 | rev: v1.0.9 13 | hooks: 14 | - id: cargo-sort 15 | - repo: https://github.com/fsfe/reuse-tool 16 | rev: v5.0.2 17 | hooks: 18 | - id: reuse 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | [package] 6 | name = "regreet" 7 | version = "0.1.3" 8 | authors = ["Harish Rajagopal "] 9 | edition = "2021" 10 | rust-version = "1.75" 11 | description = "Clean and customizable greeter for greetd" 12 | repository = "https://github.com/rharish101/ReGreet/" 13 | license = "GPL-3.0-or-later" 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | clap = { version = "4.5", features = ["derive"] } 18 | const_format = { version = "0.2.33", features = ["rust_1_64"] } 19 | educe = "0.6" 20 | file-rotate = "0.7" 21 | glob = "0.3" 22 | greetd_ipc = { version = "0.10", features = ["tokio-codec"] } 23 | gtk4 = "0.9" 24 | humantime-serde = "1.1.1" 25 | jiff = "0.1.14" 26 | lazy_static = "1.5.0" 27 | lru = "0.12" 28 | pwd = "1.4.0" 29 | regex = "1.10" 30 | relm4 = "0.9" 31 | serde = { version = "1.0", features = ["derive"] } 32 | shlex = "1.3" 33 | thiserror = "2.0" 34 | tokio = { version = "1.39", features = ["net", "time"] } 35 | toml = "0.8" 36 | tracing = "0.1" 37 | tracing-appender = "0.2" 38 | tracing-subscriber = { version = "0.3", features = ["local-time"] } 39 | tracker = "0.2" 40 | 41 | [features] 42 | gtk4_8 = ["gtk4/v4_8"] 43 | 44 | [dev-dependencies] 45 | test-case = "3.3.1" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright © 2007 Free Software Foundation, Inc. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for software and other kinds of works. 11 | 12 | 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. 13 | 14 | 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. 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | 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. 27 | 28 | The precise terms and conditions for copying, distribution and modification follow. 29 | 30 | TERMS AND CONDITIONS 31 | 32 | 0. Definitions. 33 | 34 | “This License” refers to version 3 of the GNU General Public License. 35 | 36 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 37 | 38 | “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. 39 | 40 | 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. 41 | 42 | A “covered work” means either the unmodified Program or a work based on the Program. 43 | 44 | 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. 45 | 46 | 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. 47 | 48 | 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. 49 | 50 | 1. Source Code. 51 | 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. 52 | 53 | 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. 54 | 55 | 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. 56 | 57 | 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. 58 | 59 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. 60 | 61 | The Corresponding Source for a work in source code form is that same work. 62 | 63 | 2. Basic Permissions. 64 | 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. 65 | 66 | 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. 67 | 68 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 69 | 70 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 71 | 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. 72 | 73 | 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. 74 | 75 | 4. Conveying Verbatim Copies. 76 | 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. 77 | 78 | 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. 79 | 80 | 5. Conveying Modified Source Versions. 81 | 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: 82 | 83 | a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 84 | 85 | 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”. 86 | 87 | 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. 88 | 89 | 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. 90 | 91 | 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. 92 | 93 | 6. Conveying Non-Source Forms. 94 | 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: 95 | 96 | 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. 97 | 98 | 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. 99 | 100 | 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. 101 | 102 | 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. 103 | 104 | 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. 105 | 106 | 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. 107 | 108 | 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. 109 | 110 | “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. 111 | 112 | 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). 113 | 114 | 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. 115 | 116 | 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. 117 | 118 | 7. Additional Terms. 119 | “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. 120 | 121 | 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. 122 | 123 | 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: 124 | 125 | a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 126 | 127 | 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 128 | 129 | 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 130 | 131 | d) Limiting the use for publicity purposes of names of licensors or authors of the material; or 132 | 133 | e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 134 | 135 | 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. 136 | 137 | 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. 138 | 139 | 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. 140 | 141 | 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. 142 | 143 | 8. Termination. 144 | 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). 145 | 146 | 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. 147 | 148 | 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. 149 | 150 | 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. 151 | 152 | 9. Acceptance Not Required for Having Copies. 153 | 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. 154 | 155 | 10. Automatic Licensing of Downstream Recipients. 156 | 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. 157 | 158 | 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. 159 | 160 | 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. 161 | 162 | 11. Patents. 163 | 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”. 164 | 165 | 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. 166 | 167 | 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. 168 | 169 | 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. 170 | 171 | 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. 172 | 173 | 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. 174 | 175 | 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. 176 | 177 | 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. 178 | 179 | 12. No Surrender of Others' Freedom. 180 | 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. 181 | 182 | 13. Use with the GNU Affero General Public License. 183 | 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. 184 | 185 | 14. Revised Versions of this License. 186 | 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. 187 | 188 | 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. 189 | 190 | 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. 191 | 192 | 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. 193 | 194 | 15. Disclaimer of Warranty. 195 | 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. 196 | 197 | 16. Limitation of Liability. 198 | 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. 199 | 200 | 17. Interpretation of Sections 15 and 16. 201 | 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. 202 | 203 | END OF TERMS AND CONDITIONS 204 | 205 | How to Apply These Terms to Your New Programs 206 | 207 | 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. 208 | 209 | 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. 210 | 211 | 212 | Copyright (C) 213 | 214 | 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. 215 | 216 | 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. 217 | 218 | You should have received a copy of the GNU General Public License along with this program. If not, see . 219 | 220 | Also add information on how to contact you by electronic and paper mail. 221 | 222 | If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: 223 | 224 | Copyright (C) 225 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 226 | This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. 227 | 228 | 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”. 229 | 230 | 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 . 231 | 232 | 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 . 233 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSES/GPL-3.0-or-later.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright © 2007 Free Software Foundation, Inc. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for software and other kinds of works. 11 | 12 | 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. 13 | 14 | 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. 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | 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. 27 | 28 | The precise terms and conditions for copying, distribution and modification follow. 29 | 30 | TERMS AND CONDITIONS 31 | 32 | 0. Definitions. 33 | 34 | “This License” refers to version 3 of the GNU General Public License. 35 | 36 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 37 | 38 | “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. 39 | 40 | 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. 41 | 42 | A “covered work” means either the unmodified Program or a work based on the Program. 43 | 44 | 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. 45 | 46 | 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. 47 | 48 | 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. 49 | 50 | 1. Source Code. 51 | 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. 52 | 53 | 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. 54 | 55 | 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. 56 | 57 | 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. 58 | 59 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. 60 | 61 | The Corresponding Source for a work in source code form is that same work. 62 | 63 | 2. Basic Permissions. 64 | 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. 65 | 66 | 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. 67 | 68 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 69 | 70 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 71 | 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. 72 | 73 | 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. 74 | 75 | 4. Conveying Verbatim Copies. 76 | 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. 77 | 78 | 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. 79 | 80 | 5. Conveying Modified Source Versions. 81 | 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: 82 | 83 | a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 84 | 85 | 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”. 86 | 87 | 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. 88 | 89 | 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. 90 | 91 | 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. 92 | 93 | 6. Conveying Non-Source Forms. 94 | 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: 95 | 96 | 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. 97 | 98 | 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. 99 | 100 | 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. 101 | 102 | 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. 103 | 104 | 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. 105 | 106 | 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. 107 | 108 | 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. 109 | 110 | “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. 111 | 112 | 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). 113 | 114 | 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. 115 | 116 | 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. 117 | 118 | 7. Additional Terms. 119 | “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. 120 | 121 | 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. 122 | 123 | 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: 124 | 125 | a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 126 | 127 | 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 128 | 129 | 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 130 | 131 | d) Limiting the use for publicity purposes of names of licensors or authors of the material; or 132 | 133 | e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 134 | 135 | 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. 136 | 137 | 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. 138 | 139 | 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. 140 | 141 | 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. 142 | 143 | 8. Termination. 144 | 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). 145 | 146 | 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. 147 | 148 | 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. 149 | 150 | 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. 151 | 152 | 9. Acceptance Not Required for Having Copies. 153 | 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. 154 | 155 | 10. Automatic Licensing of Downstream Recipients. 156 | 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. 157 | 158 | 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. 159 | 160 | 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. 161 | 162 | 11. Patents. 163 | 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”. 164 | 165 | 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. 166 | 167 | 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. 168 | 169 | 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. 170 | 171 | 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. 172 | 173 | 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. 174 | 175 | 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. 176 | 177 | 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. 178 | 179 | 12. No Surrender of Others' Freedom. 180 | 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. 181 | 182 | 13. Use with the GNU Affero General Public License. 183 | 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. 184 | 185 | 14. Revised Versions of this License. 186 | 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. 187 | 188 | 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. 189 | 190 | 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. 191 | 192 | 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. 193 | 194 | 15. Disclaimer of Warranty. 195 | 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. 196 | 197 | 16. Limitation of Liability. 198 | 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. 199 | 200 | 17. Interpretation of Sections 15 and 16. 201 | 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. 202 | 203 | END OF TERMS AND CONDITIONS 204 | 205 | How to Apply These Terms to Your New Programs 206 | 207 | 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. 208 | 209 | 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. 210 | 211 | 212 | Copyright (C) 213 | 214 | 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. 215 | 216 | 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. 217 | 218 | You should have received a copy of the GNU General Public License along with this program. If not, see . 219 | 220 | Also add information on how to contact you by electronic and paper mail. 221 | 222 | If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: 223 | 224 | Copyright (C) 225 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 226 | This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. 227 | 228 | 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”. 229 | 230 | 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 . 231 | 232 | 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 . 233 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | # ReGreet 14 | 15 | A clean and customizable GTK-based [greetd](https://git.sr.ht/~kennylevinsen/greetd) greeter written in Rust using [Relm4](https://relm4.org/). 16 | This is meant to be run under a Wayland compositor (like [Sway](https://github.com/swaywm/sway)). 17 | 18 | It is based on [Max Moser's LightDM Elephant greeter](https://github.com/max-moser/lightdm-elephant-greeter), which is based on [Matt ~~Shultz's~~ Fischer's example LightDM greeter](https://web.archive.org/web/20210923235052/https://www.mattfischer.com/blog/archives/5). 19 | 20 | ## Screenshots 21 | 22 | ![Welcome](https://user-images.githubusercontent.com/25344287/221668247-f5193c01-2202-4739-803b-6f163da3d03b.png) 23 | ![Dropdown session menu](https://user-images.githubusercontent.com/25344287/221668339-524a731f-509c-46a7-9ceb-e6c8e92a61a0.png) 24 | ![Manual session entry](https://user-images.githubusercontent.com/25344287/221668422-ab82d10b-3167-4a31-9705-c1d066ced252.png) 25 | ![Password entry with selected user](https://user-images.githubusercontent.com/25344287/221668490-cfd231a8-bcb9-426b-ba27-783b09c29e9c.png) 26 | ![Password entry with manual user](https://user-images.githubusercontent.com/25344287/221668537-dbc8ebda-b1ec-4f71-a521-77674e2ee992.png) 27 | ![Login fail](https://user-images.githubusercontent.com/25344287/226113001-a66e8303-6d1f-4f75-b362-baccb9e07f9f.png) 28 | 29 | These screenshots use the [Canta GTK theme](https://github.com/vinceliuice/Canta-theme) in dark mode with the Roboto font. All screenshots are provided under the [CC-BY-SA-4.0 license](https://creativecommons.org/licenses/by-sa/4.0/legalcode). 30 | 31 | ## Features 32 | * Shows a dropdown list of existing users and X11/Wayland sessions 33 | * Allows manual entry of username and session command 34 | * Remembers the last authenticated user 35 | * Automatically selects the last used session per user 36 | * Allows setting environment variables for created sessions 37 | * Supports customizing: 38 | - Background image 39 | - Clock 40 | - GTK theme 41 | - Dark mode 42 | - Icon theme 43 | - Cursor theme 44 | - Font 45 | * Allows changing reboot & poweroff commands for different init systems 46 | * Supports custom CSS files for further customizations 47 | * Respects `XDG_DATA_DIRS` environment variable 48 | * Respects fields `Hidden` and `NoDisplay` in session files 49 | * Picks up the first found session with the same name and in the same type (X11/Wayland). This allows for overriding system-provided session files. 50 | * Demo mode to run ReGreet without greetd for easier development. 51 | 52 | ## Requirements 53 | * Rust 1.75.0+ (for compilation only) 54 | * greetd 55 | * GTK 4.0+ 56 | * A Wayland compositor (such as [Cage](https://www.hjdskes.nl/projects/cage/) or [Sway](https://swaywm.org/) or [Hyprland](https://hyprland.org/)) 57 | 58 | **Note**: Please make sure you have all requirements installed, as having a greetd greeter constantly failing isn't as much fun as it sounds. 59 | 60 | ## Installation 61 | ### Arch Linux 62 | ReGreet is available as [greetd-regreet](https://archlinux.org/packages/extra/x86_64/greetd-regreet) in the official Arch Linux repositories, and as [greetd-regreet-git](https://aur.archlinux.org/packages/greetd-regreet-git) in the AUR. 63 | Note that I only maintain the AUR package, and the package in the Arch repos is maintained by someone else. 64 | 65 | Install the AUR package either by cloning the AUR repository and running `makepkg`, or by using your favourite AUR helper: 66 | ```sh 67 | paru -S greetd-regreet-git 68 | ``` 69 | 70 | Install the package in the Arch repos as follows: 71 | ```sh 72 | pacman -S greetd-regreet 73 | ``` 74 | 75 | ### Unofficial Packages 76 | #### NixOS 77 | For a minimal config, add `programs.regreet.enable = true;` in your NixOS configuration file. 78 | For users who want to configure more, they can see all the options of the module by searching for `regreet` on [NixOS Search](https://search.nixos.org/options?query=regreet). 79 | 80 | ### Manual 81 | First, the greeter must be compiled using Cargo: 82 | ```sh 83 | cargo build --release 84 | ``` 85 | 86 | The compilation process also configures the greeter to look for or use certain directories. 87 | These can be changed by setting the values of certain environment variables. 88 | These are: 89 | 90 | Environment Variable | Default | Use 91 | -- | -- | -- 92 | GREETD\_CONFIG\_DIR | `/etc/greetd` | The configuration directory used by greetd 93 | STATE\_DIR | `/var/lib/regreet` | The directory used to store the ReGreet state/cache 94 | LOG\_DIR | `/var/log/regreet` | The directory used to store logs 95 | SESSION\_DIRS | `/usr/share/xsessions:/usr/share/wayland-sessions` | A colon (:) separated list of directories where the greeter looks for session files 96 | X11\_CMD\_PREFIX | `startx /usr/bin/env` | The default command prefix for X11 sessions to launch the X server (see [this explanation on Reddit](https://web.archive.org/web/20240803120131/https://old.reddit.com/r/linux/comments/1c8zdcw/using_x11_window_managers_with_greetd_login/)) 97 | REBOOT\_CMD | `reboot` | The default command used to reboot the system 98 | POWEROFF\_CMD | `poweroff` | The default command used to shut down the system 99 | LOGIN\_DEFS\_PATHS | `/etc/login.defs:/usr/etc/login.defs` | A colon (:) separated list of `login.defs` file paths. First found is loaded. 100 | LOGIN\_DEFS\_UID\_MIN | 1000 | Override the assumed default if `login.defs` doesnt specify `UID_MIN`. 101 | LOGIN\_DEFS\_UID\_MAX | 60000 | Override the assumed default if `login.defs` doesnt specify `UID_MAX`. 102 | 103 | The greeter can be installed by copying the file `target/release/regreet` to `/usr/bin` (or similar directories like `/bin`). 104 | 105 | Optionally, to set up the log and state directories using systemd-tmpfiles, do either of the following: 106 | * Copy the configuration given in [systemd-tmpfiles.conf](./systemd-tmpfiles.conf) to `/etc/tmpfiles.d/regreet.conf` or `/usr/lib/tmpfiles.d/regreet.conf`. 107 | * Run the `systemd-tmpfiles` CLI: 108 | ```sh 109 | systemd-tmpfiles --create "$PWD/systemd-tmpfiles.conf" 110 | ``` 111 | 112 | #### GTK4 Versions 113 | ReGreet targets GTK version 4.0 or above. 114 | If you have higher versions of GTK, then you can enable additional features in ReGreet. 115 | Currently, the extra features enabled are: 116 | 117 | GTK Version | Feature Flag | Features 118 | -- | -- | -- 119 | 4.8 | `gtk4_8` |
  • Changing how the background image fits the screen
120 | 121 | To compile with support for a GTK version, pass the corresponding feature flag during building. 122 | For example, to compile with GTK 4.8+ support, run: 123 | ```sh 124 | cargo build -F gtk4_8 --release 125 | ``` 126 | 127 | To compile with full support, run: 128 | ```sh 129 | cargo build --all-features --release 130 | ``` 131 | 132 | ## Usage 133 | ### Set as Default Session 134 | Edit the greetd config file (`/etc/greetd/config.toml`) to set ReGreet with a Wayland compositor as the default session. 135 | For example, if using Cage: 136 | ```toml 137 | [default_session] 138 | command = "cage -s -mlast -- regreet" 139 | user = "greeter" 140 | ``` 141 | The `-s` argument enables VT switching in cage (0.1.2 and newer only), which is highly recommended to prevent locking yourself out. 142 | The `-mlast` argument tells Cage to use the last-connected monitor only, which is useful since ReGreet is a single-monitor application. 143 | 144 | If using Sway, create a Sway config file (in a path such as `/etc/greetd/sway-config`) as follows: 145 | ``` 146 | exec "regreet; swaymsg exit" 147 | include /etc/sway/config.d/* 148 | ``` 149 | 150 | Then, set Sway to use this config (whose path is shown here as `/path/to/custom/sway/config`) as the default greetd session: 151 | ```toml 152 | [default_session] 153 | command = "sway --config /path/to/custom/sway/config" 154 | user = "greeter" 155 | ``` 156 | 157 | If using Hyprland, create a Hyprland config file (in a path such as `/etc/greetd/hyprland.conf`) as follows: 158 | ``` 159 | exec-once = regreet; hyprctl dispatch exit 160 | misc { 161 | disable_hyprland_logo = true 162 | disable_splash_rendering = true 163 | disable_hyprland_qtutils_check = true 164 | } 165 | ``` 166 | 167 | Then, set Hyprland to use this config (whose path is shown here as `/path/to/custom/hyprland/config`) as the default greetd session: 168 | ```toml 169 | [default_session] 170 | command = "Hyprland --config /path/to/custom/hyprland/config" 171 | user = "greeter" 172 | ``` 173 | 174 | Restart greetd to use the new config. 175 | 176 | #### Startup delays 177 | If you find that ReGreet takes too much time to start up, you may be affected by this: [swaywm/sway/wiki#gtk-applications-take-20-seconds-to-start](https://github.com/swaywm/sway/wiki#gtk-applications-take-20-seconds-to-start). 178 | See this link for the fix. 179 | Alternatively, the solution proposed in [issue #34](https://github.com/rharish101/ReGreet/issues/34) may resolve it. 180 | 181 | As another option, you can disable portals by exporting environment variables for the Wayland compositor launched for ReGreet. 182 | Simply prepend `env GTK_USE_PORTAL=0 GDK_DEBUG=no-portals` to the start of the default session command in `greetd.toml`. 183 | For example, with Cage, the session command would be: 184 | ```toml 185 | [default_session] 186 | command = "env GTK_USE_PORTAL=0 GDK_DEBUG=no-portals cage -s -mlast -- regreet" 187 | ``` 188 | 189 | If using Hyprland, you can instead append the following lines to the Hyprland config for ReGreet: 190 | ``` 191 | env = GTK_USE_PORTAL,0 192 | env = GDK_DEBUG,no-portals 193 | ``` 194 | 195 | ### Configuration 196 | The configuration file must be in the [TOML](https://toml.io/) format. 197 | By default, it is named `regreet.toml`, and located in the greetd configuration directory specified during compilation (`/etc/greetd/` by default). 198 | You can use a config file in a different location with the `--config` argument as follows: 199 | ```sh 200 | regreet --config /path/to/custom/regreet/config.toml 201 | ``` 202 | 203 | A sample configuration is provided along with sample values for all available options in [`regreet.sample.toml`](regreet.sample.toml). 204 | Currently, the following can be configured: 205 | * Background image 206 | * How the background image fits the screen (needs GTK 4.8+ support compiled) 207 | * Environment variables for created sessions 208 | * Greeting message 209 | * Clock 210 | * GTK theme 211 | * Dark mode 212 | * Icon theme 213 | * Cursor theme 214 | * Font 215 | * Reboot command 216 | * Shut down command 217 | * X11 command prefix (see [this explanation on Reddit](https://web.archive.org/web/20240803120131/https://old.reddit.com/r/linux/comments/1c8zdcw/using_x11_window_managers_with_greetd_login/)) 218 | 219 | **NOTE:** For configuring other essential features, such as the keyboard layout/mapping, the choice of monitor to use, etc., please check out the configuration options for the wayland compositor that you are using to run ReGreet. 220 | For example, if you use Cage, check out the [Cage wiki](https://github.com/cage-kiosk/cage/wiki/Configuration). 221 | If you use Sway, check out the [Sway wiki](https://github.com/swaywm/sway/wiki#configuration). 222 | If you use Hyprland, check out the [Hyprland wiki](https://wiki.hyprland.org/). 223 | 224 | ### Custom CSS 225 | ReGreet supports loading CSS files to act as a custom global stylesheet. 226 | This enables one to do further customizations above what ReGreet supports through the config file. 227 | 228 | By default, the custom CSS file is named `regreet.css`, and located in the greetd configuration directory specified during compilation (`/etc/greetd/` by default). 229 | To load a custom CSS stylesheet from a different location, pass the `-s` or `--style` CLI argument as follows: 230 | ```sh 231 | regreet --style /path/to/custom.css 232 | ``` 233 | 234 | Please refer to the GTK4 docs on [CSS in GTK](https://docs.gtk.org/gtk4/css-overview.html) and [GTK CSS Properties](https://docs.gtk.org/gtk4/css-properties.html) to learn how to style a GTK4 app using CSS. 235 | For a general reference on CSS, please refer to the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax). 236 | 237 | **Tip:** You might want to use [demo mode](#demo-mode) to test out your CSS before making it permanent. 238 | 239 | ### Changing Reboot/Shut Down Commands 240 | The default reboot and shut down commands use the `reboot` and `poweroff` binaries, which are present on most Linux systems. 241 | However, since the recommended way of using ReGreet is to avoid running it as root, the `reboot`/`poweroff` commands might not work on systems where superuser access is needed to run these commands. 242 | In this case, if there is another command to reboot or shut down the system without superuser access, these commands can be set in the config file under the `[commands]` section. 243 | 244 | For example, to use `loginctl reboot` as the reboot command, use the following config: 245 | ```toml 246 | [commands] 247 | reboot = [ "loginctl", "reboot" ] 248 | ``` 249 | Here, each command needs to be separated into a list containing the main command, followed by individual arguments. 250 | 251 | These commands can also be specified during compilation using the `REBOOT_CMD` and `POWEROFF_CMD` environment variables. 252 | 253 | ### Logging and Caching 254 | The state is are stored in `/var/lib/regreet/state.toml` (configurable during installation). 255 | It contains the last authenticated user and the last used session per user, which are automatically selected on next login. 256 | If the greeter is unable to write to this file, then it reverts to the default behaviour. 257 | 258 | By default, the logs are stored in `/var/log/regreet/log` (configurable during installation). 259 | You can use a log file in a different location with the `--logs` argument as follows: 260 | ```sh 261 | regreet --logs /path/to/custom/regreet/logs 262 | ``` 263 | 264 | Once the log file reaches a limit, it is compressed and rotated to `log.X.gz` in the same directory, where `X` is the index of the log file. 265 | The higher the index, the older the log file. 266 | After reaching a limit, the oldest log file is removed. 267 | 268 | If the greeter is unable to write to this file or create files in the log directory, then it logs to stdout. 269 | You can also print the logs to stdout in addition to the log file, with the `--verbose` argument as follows: 270 | ```sh 271 | regreet --verbose 272 | ``` 273 | 274 | The recommended configuration is to run greetd greeters as a separate user (`greeter` in the above examples). 275 | This can lead to insufficient permissions for either creating the state/log directories, or writing to them. 276 | To make use of the caching and logging features, please create the directories manually with the correct permissions, if not done during installation with systemd-tmpfiles. 277 | 278 | ## Contributing 279 | [pre-commit](https://pre-commit.com/) is used for managing hooks that run before each commit (such as clippy), to ensure code quality. 280 | Thus, this needs to be set up only when one intends to commit changes to git. 281 | 282 | Firstly, [install pre-commit](https://pre-commit.com/#installation) itself. 283 | Next, install pre-commit hooks: 284 | ```sh 285 | pre-commit install 286 | ``` 287 | 288 | Now, pre-commit should ensure that the code passes all linters locally before committing. 289 | This will save time when creating PRs, since these linters also run in CI, and thus fail code that hasn't been linted well. 290 | 291 | ### Demo mode 292 | To aid development, a "demo" mode is included within ReGreet that runs ReGreet independent of greetd. 293 | Simply run ReGreet as follows: 294 | ```sh 295 | regreet --demo 296 | ``` 297 | 298 | Since the demo mode doesn't use greetd, authentication is done using hardcoded credentials within the codebase. 299 | These credentials are logged with the warning log-level, so that you don't have to read the source code. 300 | 301 | ## Licenses 302 | This repository uses [REUSE](https://reuse.software/) to document licenses. 303 | Each file either has a header containing copyright and license information, or has an entry in the [TOML file](https://reuse.software/spec-3.3/#reusetoml) at [REUSE.toml](./REUSE.toml). 304 | The license files that are used in this project can be found in the [LICENSES](./LICENSES) directory. 305 | 306 | A copy of the GPL-3.0-or-later license is placed in [LICENSE](./LICENSE), to signify that it constitutes the majority of the codebase, and for compatibility with GitHub. 307 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[annotations]] 4 | path = "Cargo.lock" 5 | SPDX-FileCopyrightText = "2022 Harish Rajagopal " 6 | SPDX-License-Identifier = "CC0-1.0" 7 | -------------------------------------------------------------------------------- /regreet.sample.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | [background] 6 | # Path to the background image 7 | path = "/usr/share/backgrounds/greeter.jpg" 8 | 9 | # How the background image covers the screen if the aspect ratio doesn't match 10 | # Available values: "Fill", "Contain", "Cover", "ScaleDown" 11 | # Refer to: https://docs.gtk.org/gtk4/enum.ContentFit.html 12 | # NOTE: This is ignored if ReGreet isn't compiled with GTK v4.8 support. 13 | fit = "Contain" 14 | 15 | # The entries defined in this section will be passed to the session as environment variables when it is started 16 | [env] 17 | ENV_VARIABLE = "value" 18 | 19 | [GTK] 20 | # Whether to use the dark theme 21 | application_prefer_dark_theme = true 22 | 23 | # Cursor theme name 24 | cursor_theme_name = "Adwaita" 25 | 26 | # Font name and size 27 | font_name = "Cantarell 16" 28 | 29 | # Icon theme name 30 | icon_theme_name = "Adwaita" 31 | 32 | # GTK theme name 33 | theme_name = "Adwaita" 34 | 35 | [commands] 36 | # The command used to reboot the system 37 | reboot = ["systemctl", "reboot"] 38 | 39 | # The command used to shut down the system 40 | poweroff = ["systemctl", "poweroff"] 41 | 42 | # The command prefix for X11 sessions to start the X server 43 | x11_prefix = [ "startx", "/usr/bin/env" ] 44 | 45 | [appearance] 46 | # The message that initially displays on startup 47 | greeting_msg = "Welcome back!" 48 | 49 | 50 | [widget.clock] 51 | # strftime format argument 52 | # See https://docs.rs/jiff/0.1.14/jiff/fmt/strtime/index.html#conversion-specifications 53 | format = "%a %H:%M" 54 | 55 | # How often to update the text 56 | resolution = "500ms" 57 | 58 | # Override system timezone (IANA Time Zone Database name, aka /etc/zoneinfo path) 59 | # Remove to use the system time zone. 60 | timezone = "America/Chicago" 61 | 62 | # Ask GTK to make the label at least this wide. This helps keeps the parent element layout and width consistent. 63 | # Experiment with different widths, the interpretation of this value is entirely up to GTK. 64 | label_width = 150 65 | -------------------------------------------------------------------------------- /src/cache/lru.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Wrapper for `lru::LruCache` 6 | //! 7 | //! This is needed because `lru::LruCache` doesn't implement (de)serialization. 8 | 9 | use std::cmp::Eq; 10 | use std::fmt::{Formatter, Result as FmtResult}; 11 | use std::hash::{BuildHasher, Hash}; 12 | use std::marker::PhantomData; 13 | use std::num::NonZeroUsize; 14 | use std::ops::{Deref, DerefMut}; 15 | 16 | use lru::{DefaultHasher, LruCache as OrigLruCache}; 17 | use serde::{ 18 | de::{MapAccess, Visitor}, 19 | ser::SerializeMap, 20 | Deserialize, Deserializer, Serialize, Serializer, 21 | }; 22 | 23 | /// Wrapper to enable (de)serialization 24 | pub(super) struct LruCache(OrigLruCache); 25 | 26 | impl LruCache { 27 | pub(super) fn new(capacity: usize) -> Self { 28 | if let Some(capacity) = NonZeroUsize::new(capacity) { 29 | Self(OrigLruCache::new(capacity)) 30 | } else { 31 | // In case of an erroneous capacity, revert to the safe behaviour of an unbounded 32 | // cache. 33 | warn!("Zero capacity cache requested; instead using an unbounded cache"); 34 | Self(OrigLruCache::unbounded()) 35 | } 36 | } 37 | 38 | pub(super) fn unbounded() -> Self { 39 | Self(OrigLruCache::unbounded()) 40 | } 41 | } 42 | 43 | /// Avoid usage of self.0 with self. 44 | /// 45 | /// This makes life easier when using the wrapper struct. 46 | impl Deref for LruCache { 47 | type Target = OrigLruCache; 48 | 49 | fn deref(&self) -> &Self::Target { 50 | &self.0 51 | } 52 | } 53 | impl DerefMut for LruCache { 54 | fn deref_mut(&mut self) -> &mut Self::Target { 55 | &mut self.0 56 | } 57 | } 58 | 59 | // Deserialization code heavily "inspired" by: https://serde.rs/deserialize-map.html 60 | 61 | /// Helper struct to deserialize a map into an LRU cache 62 | struct LruVisitor { 63 | // Use phantoms to "use" the generic params without actually using them. 64 | // They're needed to correspond to an LRUCache. 65 | phantom_key: PhantomData, 66 | phantom_value: PhantomData, 67 | } 68 | 69 | impl LruVisitor { 70 | fn new() -> Self { 71 | Self { 72 | phantom_key: PhantomData, 73 | phantom_value: PhantomData, 74 | } 75 | } 76 | } 77 | 78 | /// Allow the LRU visitor to talk to the deserializer and deserialize a map into an LRU cache. 79 | impl<'de, K, V> Visitor<'de> for LruVisitor 80 | where 81 | K: Deserialize<'de> + Hash + Eq, 82 | V: Deserialize<'de>, 83 | { 84 | type Value = LruCache; 85 | 86 | fn expecting(&self, formatter: &mut Formatter) -> FmtResult { 87 | write!(formatter, "a map of String keys and String values") 88 | } 89 | 90 | fn visit_map>(self, mut access: A) -> Result { 91 | // If the size is unknown, use an unbounded LRU to be on the safe side. 92 | let mut lru = match access.size_hint() { 93 | Some(size) => LruCache::new(size), 94 | None => LruCache::unbounded(), 95 | }; 96 | 97 | // Add all map entries one-by-one. 98 | while let Some((key, value)) = access.next_entry()? { 99 | lru.push(key, value); 100 | } 101 | Ok(lru) 102 | } 103 | } 104 | 105 | /// Make the LRU cache deserializable as a map. 106 | impl<'de, K, V> Deserialize<'de> for LruCache 107 | where 108 | K: Deserialize<'de> + Hash + Eq, 109 | V: Deserialize<'de>, 110 | { 111 | fn deserialize>(deserializer: D) -> Result { 112 | deserializer.deserialize_map(LruVisitor::new()) 113 | } 114 | } 115 | 116 | // Serialization code heavily "inspired" by: 117 | // https://serde.rs/impl-serialize.html#serializing-a-sequence-or-map 118 | 119 | /// Make the LRU cache serializable as a map. 120 | impl Serialize for LruCache 121 | where 122 | K: Serialize + Hash + Eq, 123 | V: Serialize, 124 | H: BuildHasher, 125 | { 126 | fn serialize(&self, serializer: S) -> Result { 127 | let mut map = serializer.serialize_map(Some(self.len()))?; 128 | // Serialize all LRU entries one-by-one. 129 | for (k, v) in self.into_iter() { 130 | map.serialize_entry(&k, &v)?; 131 | } 132 | map.end() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Utility for caching info between logins 6 | 7 | mod lru; 8 | 9 | use std::fs::{create_dir_all, write}; 10 | use std::num::NonZeroUsize; 11 | use std::path::Path; 12 | 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use self::lru::LruCache; 16 | use crate::constants::CACHE_PATH; 17 | use crate::tomlutils::{load_toml, TomlFileResult}; 18 | 19 | /// Limit to the size of the user to last-used session mapping. 20 | const CACHE_LIMIT: usize = 100; 21 | 22 | /// Holds info needed to persist between logins 23 | #[derive(Deserialize, Serialize)] 24 | pub struct Cache { 25 | /// The last user who logged in 26 | last_user: Option, 27 | /// The last-used session for each user 28 | user_to_last_sess: LruCache, 29 | } 30 | 31 | impl Default for Cache { 32 | fn default() -> Self { 33 | Self { 34 | last_user: None, 35 | user_to_last_sess: LruCache::new(CACHE_LIMIT), 36 | } 37 | } 38 | } 39 | 40 | impl Cache { 41 | /// Load the cache file from disk. 42 | pub fn new() -> Self { 43 | let mut cache: Self = load_toml(CACHE_PATH); 44 | // Make sure that the LRU can contain the needed amount of mappings. 45 | cache 46 | .user_to_last_sess 47 | .resize(NonZeroUsize::new(CACHE_LIMIT).expect("Cache limit cannot be zero")); 48 | cache 49 | } 50 | 51 | /// Save the cache file to disk. 52 | pub fn save(&self) -> TomlFileResult<()> { 53 | let cache_path = Path::new(CACHE_PATH); 54 | if !cache_path.exists() { 55 | // Create the cache directory. 56 | if let Some(cache_dir) = cache_path.parent() { 57 | info!("Creating missing cache directory: {}", cache_dir.display()); 58 | create_dir_all(cache_dir)?; 59 | }; 60 | } 61 | 62 | info!("Saving cache to disk"); 63 | write(cache_path, toml::to_string_pretty(self)?)?; 64 | Ok(()) 65 | } 66 | 67 | /// Get the last user to login. 68 | pub fn get_last_user(&self) -> Option<&str> { 69 | self.last_user.as_deref() 70 | } 71 | 72 | /// Get the last used session by the given user. 73 | pub fn get_last_session(&mut self, user: &str) -> Option<&str> { 74 | self.user_to_last_sess.get(user).map(String::as_str) 75 | } 76 | 77 | /// Set the last user to login. 78 | pub fn set_last_user(&mut self, user: &str) { 79 | self.last_user = Some(String::from(user)); 80 | } 81 | 82 | /// Set the last used session by the given user. 83 | pub fn set_last_session(&mut self, user: &str, session: &str) { 84 | self.user_to_last_sess 85 | .push(String::from(user), String::from(session)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Client that communicates with greetd 6 | 7 | use std::env; 8 | use std::io::Result as IOResult; 9 | 10 | use greetd_ipc::{ 11 | codec::{Error as GreetdError, TokioCodec}, 12 | AuthMessageType, ErrorType, Request, Response, 13 | }; 14 | use tokio::net::UnixStream; 15 | 16 | /// Environment variable containing the path to the greetd socket 17 | const GREETD_SOCK_ENV_VAR: &str = "GREETD_SOCK"; 18 | 19 | /// Demo mode credentials 20 | const DEMO_AUTH_MSG_OPT: &str = "One-Time Password:"; 21 | const DEMO_AUTH_MSG_PASSWD: &str = "Password:"; 22 | const DEMO_AUTH_MSG_ERROR: &str = "pam_authenticate: AUTH_ERR"; 23 | const DEMO_OTP: &str = "0248"; 24 | const DEMO_PASSWD: &str = "pass"; 25 | 26 | pub type GreetdResult = Result; 27 | 28 | /// The authentication status of the current greetd session 29 | #[derive(Clone)] 30 | pub enum AuthStatus { 31 | NotStarted, 32 | InProgress, 33 | Done, 34 | } 35 | 36 | /// Client that uses UNIX sockets to communicate with greetd 37 | pub struct GreetdClient { 38 | /// Socket to communicate with greetd 39 | socket: Option, 40 | /// Current authentication status 41 | auth_status: AuthStatus, 42 | } 43 | 44 | impl GreetdClient { 45 | /// Initialize the socket to communicate with greetd. 46 | pub async fn new(demo: bool) -> IOResult { 47 | let socket: Option = if demo { 48 | warn!( 49 | "Run as demo: [otp: {}, password: {}]", 50 | DEMO_OTP, DEMO_PASSWD 51 | ); 52 | None 53 | } else { 54 | let sock_path = env::var(GREETD_SOCK_ENV_VAR).unwrap_or_else(|_| { 55 | panic!("Missing environment variable '{GREETD_SOCK_ENV_VAR}'. Is greetd running?",) 56 | }); 57 | Some(UnixStream::connect(sock_path).await?) 58 | }; 59 | 60 | Ok(Self { 61 | socket, 62 | auth_status: AuthStatus::NotStarted, 63 | }) 64 | } 65 | 66 | /// Initialize a greetd session. 67 | pub async fn create_session(&mut self, username: &str) -> GreetdResult { 68 | info!("Creating session for username: {username}"); 69 | 70 | let resp: Response = if let Some(socket) = &mut self.socket { 71 | let msg = Request::CreateSession { 72 | username: username.to_string(), 73 | }; 74 | msg.write_to(socket).await?; 75 | Response::read_from(socket).await? 76 | } else { 77 | Response::AuthMessage { 78 | auth_message_type: AuthMessageType::Secret, 79 | auth_message: DEMO_AUTH_MSG_OPT.to_string(), 80 | } 81 | }; 82 | 83 | match resp { 84 | Response::Success => { 85 | self.auth_status = AuthStatus::Done; 86 | } 87 | Response::AuthMessage { .. } => { 88 | self.auth_status = AuthStatus::InProgress; 89 | } 90 | Response::Error { .. } => { 91 | self.auth_status = AuthStatus::NotStarted; 92 | } 93 | }; 94 | Ok(resp) 95 | } 96 | 97 | /// Send an auth message response to a greetd session. 98 | pub async fn send_auth_response(&mut self, input: Option) -> GreetdResult { 99 | info!("Sending password to greetd"); 100 | 101 | let resp: Response = if let Some(socket) = &mut self.socket { 102 | let msg = Request::PostAuthMessageResponse { response: input }; 103 | msg.write_to(socket).await?; 104 | Response::read_from(socket).await? 105 | } else { 106 | match input.as_deref() { 107 | Some(DEMO_OTP) => Response::AuthMessage { 108 | auth_message_type: AuthMessageType::Secret, 109 | auth_message: DEMO_AUTH_MSG_PASSWD.to_string(), 110 | }, 111 | Some(DEMO_PASSWD) => Response::Success, 112 | _ => Response::Error { 113 | error_type: ErrorType::AuthError, 114 | description: DEMO_AUTH_MSG_ERROR.to_string(), 115 | }, 116 | } 117 | }; 118 | 119 | match resp { 120 | Response::Success => { 121 | self.auth_status = AuthStatus::Done; 122 | } 123 | Response::AuthMessage { .. } => { 124 | self.auth_status = AuthStatus::InProgress; 125 | } 126 | Response::Error { .. } => { 127 | self.auth_status = AuthStatus::InProgress; 128 | } 129 | }; 130 | Ok(resp) 131 | } 132 | 133 | /// Schedule starting a greetd session. 134 | /// 135 | /// On success, the session will start when this greeter terminates. 136 | pub async fn start_session( 137 | &mut self, 138 | command: Vec, 139 | environment: Vec, 140 | ) -> GreetdResult { 141 | info!("Starting greetd session with command: {command:?}"); 142 | 143 | if self.socket.is_none() { 144 | return Ok(Response::Success); 145 | } 146 | 147 | let socket = self.socket.as_mut().unwrap(); 148 | let msg = Request::StartSession { 149 | cmd: command, 150 | env: environment, 151 | }; 152 | msg.write_to(socket).await?; 153 | 154 | let resp = Response::read_from(socket).await?; 155 | if let Response::AuthMessage { .. } = resp { 156 | unimplemented!("greetd responded with auth request after requesting session start."); 157 | } 158 | Ok(resp) 159 | } 160 | 161 | /// Cancel an initialized greetd session. 162 | pub async fn cancel_session(&mut self) -> GreetdResult { 163 | info!("Cancelling greetd session"); 164 | self.auth_status = AuthStatus::NotStarted; 165 | 166 | if self.socket.is_none() { 167 | return Ok(Response::Success); 168 | } 169 | 170 | let socket = self.socket.as_mut().unwrap(); 171 | let msg = Request::CancelSession; 172 | msg.write_to(socket).await?; 173 | 174 | let resp = Response::read_from(socket).await?; 175 | if let Response::AuthMessage { .. } = resp { 176 | unimplemented!( 177 | "greetd responded with auth request after requesting session cancellation." 178 | ); 179 | } 180 | Ok(resp) 181 | } 182 | 183 | pub fn get_auth_status(&self) -> &AuthStatus { 184 | &self.auth_status 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Configuration for the greeter 6 | 7 | use std::collections::HashMap; 8 | use std::path::Path; 9 | 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::constants::{GREETING_MSG, POWEROFF_CMD, REBOOT_CMD, X11_CMD_PREFIX}; 13 | use crate::gui::widget::clock::ClockConfig; 14 | use crate::tomlutils::load_toml; 15 | 16 | #[derive(Deserialize, Serialize)] 17 | pub struct AppearanceSettings { 18 | #[serde(default = "default_greeting_msg")] 19 | pub greeting_msg: String, 20 | } 21 | 22 | impl Default for AppearanceSettings { 23 | fn default() -> Self { 24 | AppearanceSettings { 25 | greeting_msg: default_greeting_msg(), 26 | } 27 | } 28 | } 29 | 30 | /// Struct holding all supported GTK settings 31 | #[derive(Default, Deserialize, Serialize)] 32 | pub struct GtkSettings { 33 | #[serde(default)] 34 | pub application_prefer_dark_theme: bool, 35 | #[serde(default)] 36 | pub cursor_theme_name: Option, 37 | #[serde(default)] 38 | pub font_name: Option, 39 | #[serde(default)] 40 | pub icon_theme_name: Option, 41 | #[serde(default)] 42 | pub theme_name: Option, 43 | } 44 | 45 | /// Analogue to `gtk4::ContentFit` 46 | #[derive(Default, Deserialize, Serialize)] 47 | pub enum BgFit { 48 | Fill, 49 | #[default] 50 | Contain, 51 | Cover, 52 | ScaleDown, 53 | } 54 | 55 | /// Struct for info about the background image 56 | #[derive(Default, Deserialize, Serialize)] 57 | struct Background { 58 | #[serde(default)] 59 | path: Option, 60 | #[serde(default)] 61 | fit: BgFit, 62 | } 63 | 64 | /// Struct for various system commands 65 | #[derive(Deserialize, Serialize)] 66 | pub struct SystemCommands { 67 | #[serde(default = "default_reboot_command")] 68 | pub reboot: Vec, 69 | #[serde(default = "default_poweroff_command")] 70 | pub poweroff: Vec, 71 | #[serde(default = "default_x11_command_prefix")] 72 | pub x11_prefix: Vec, 73 | } 74 | 75 | impl Default for SystemCommands { 76 | fn default() -> Self { 77 | SystemCommands { 78 | reboot: default_reboot_command(), 79 | poweroff: default_poweroff_command(), 80 | x11_prefix: default_x11_command_prefix(), 81 | } 82 | } 83 | } 84 | 85 | fn default_reboot_command() -> Vec { 86 | shlex::split(REBOOT_CMD).expect("Unable to lex reboot command") 87 | } 88 | 89 | fn default_poweroff_command() -> Vec { 90 | shlex::split(POWEROFF_CMD).expect("Unable to lex poweroff command") 91 | } 92 | 93 | fn default_x11_command_prefix() -> Vec { 94 | shlex::split(X11_CMD_PREFIX).expect("Unable to lex X11 command prefix") 95 | } 96 | 97 | fn default_greeting_msg() -> String { 98 | GREETING_MSG.to_string() 99 | } 100 | 101 | /// The configuration struct 102 | #[derive(Default, Deserialize)] 103 | pub struct Config { 104 | #[serde(default)] 105 | appearance: AppearanceSettings, 106 | 107 | #[serde(default)] 108 | env: HashMap, 109 | 110 | #[serde(default)] 111 | background: Background, 112 | 113 | #[serde(default, rename = "GTK")] 114 | gtk: Option, 115 | 116 | #[serde(default)] 117 | commands: SystemCommands, 118 | 119 | #[serde(default)] 120 | pub(crate) widget: WidgetConfig, 121 | } 122 | 123 | #[derive(Deserialize, Default)] 124 | pub struct WidgetConfig { 125 | #[serde(default)] 126 | pub(crate) clock: ClockConfig, 127 | } 128 | 129 | impl Config { 130 | pub fn new(path: &Path) -> Self { 131 | load_toml(path) 132 | } 133 | 134 | pub fn get_env(&self) -> &HashMap { 135 | &self.env 136 | } 137 | 138 | pub fn get_background(&self) -> Option<&str> { 139 | self.background.path.as_deref() 140 | } 141 | 142 | #[cfg(feature = "gtk4_8")] 143 | pub fn get_background_fit(&self) -> &BgFit { 144 | &self.background.fit 145 | } 146 | 147 | pub fn get_gtk_settings(&self) -> &Option { 148 | &self.gtk 149 | } 150 | 151 | pub fn get_sys_commands(&self) -> &SystemCommands { 152 | &self.commands 153 | } 154 | 155 | pub fn get_default_message(&self) -> String { 156 | self.appearance.greeting_msg.clone() 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Stores constants that can be configured at compile time 6 | 7 | /// Get an environment variable during compile time, else return a default. 8 | macro_rules! env_or { 9 | ($name:expr, $default:expr) => { 10 | // This is needed because `Option.unwrap_or` is not a const fn: 11 | // https://github.com/rust-lang/rust/issues/91930 12 | if let Some(value) = option_env!($name) { 13 | value 14 | } else { 15 | $default 16 | } 17 | }; 18 | } 19 | 20 | /// The name for this greeter 21 | const GREETER_NAME: &str = "regreet"; 22 | /// The app ID for this GTK app 23 | pub const APP_ID: &str = concatcp!("apps.", GREETER_NAME); 24 | 25 | /// The greetd config directory 26 | const GREETD_CONFIG_DIR: &str = env_or!("GREETD_CONFIG_DIR", "/etc/greetd"); 27 | /// Path to the config file 28 | pub const CONFIG_PATH: &str = concatcp!(GREETD_CONFIG_DIR, "/", GREETER_NAME, ".toml"); 29 | /// Path to the config file 30 | pub const CSS_PATH: &str = concatcp!(GREETD_CONFIG_DIR, "/", GREETER_NAME, ".css"); 31 | 32 | /// The directory for system cache files 33 | const CACHE_DIR: &str = env_or!("STATE_DIR", concatcp!("/var/lib/", GREETER_NAME)); 34 | /// Path to the cache file 35 | pub const CACHE_PATH: &str = concatcp!(CACHE_DIR, "/state.toml"); 36 | 37 | /// The directory for system log files 38 | const LOG_DIR: &str = env_or!("LOG_DIR", concatcp!("/var/log/", GREETER_NAME)); 39 | /// Path to the log file 40 | pub const LOG_PATH: &str = concatcp!(LOG_DIR, "/log"); 41 | 42 | /// Default command for rebooting 43 | pub const REBOOT_CMD: &str = env_or!("REBOOT_CMD", "reboot"); 44 | /// Default command for shutting down 45 | pub const POWEROFF_CMD: &str = env_or!("POWEROFF_CMD", "poweroff"); 46 | 47 | /// Default greeting message 48 | pub const GREETING_MSG: &str = "Welcome back!"; 49 | 50 | /// `:`-separated search path for `login.defs` file. 51 | /// 52 | /// By default this file is at `/etc/login.defs`, however some distros (e.g. Tumbleweed) move it to other locations. 53 | /// 54 | /// See: 55 | pub const LOGIN_DEFS_PATHS: &[&str] = { 56 | const ENV: &str = env_or!("LOGIN_DEFS_PATHS", "/etc/login.defs:/usr/etc/login.defs"); 57 | &str_split!(ENV, ':') 58 | }; 59 | 60 | lazy_static! { 61 | /// Override the default `UID_MIN` in `login.defs`. If the string cannot be parsed at runtime, the value is `1_000`. 62 | /// 63 | /// This is not meant as a configuration facility. Only override this value if it's a different default in the 64 | /// `passwd` suite. 65 | pub static ref LOGIN_DEFS_UID_MIN: u64 = { 66 | const DEFAULT: u64 = 1_000; 67 | const ENV: &str = env_or!("LOGIN_DEFS_UID_MIN", formatcp!("{DEFAULT}")); 68 | 69 | ENV.parse() 70 | .map_err(|e| error!("Failed to parse LOGIN_DEFS_UID_MIN='{ENV}': {e}. This is a compile time mistake!")) 71 | .unwrap_or(DEFAULT) 72 | }; 73 | 74 | /// Override the default `UID_MAX` in `login.defs`. If the string cannot be parsed at runtime, the value is 75 | /// `60_000`. 76 | /// 77 | /// This is not meant as a configuration facility. Only override this value if it's a different default in the 78 | /// `passwd` suite. 79 | pub static ref LOGIN_DEFS_UID_MAX: u64 = { 80 | const DEFAULT: u64 = 60_000; 81 | const ENV: &str = env_or!("LOGIN_DEFS_UID_MAX", formatcp!("{DEFAULT}")); 82 | 83 | ENV.parse() 84 | .map_err(|e| error!("Failed to parse LOGIN_DEFS_UID_MAX='{ENV}': {e}. This is a compile time mistake!")) 85 | .unwrap_or(DEFAULT) 86 | }; 87 | } 88 | 89 | /// Directories separated by `:`, containing desktop files for X11/Wayland sessions 90 | pub const SESSION_DIRS: &str = env_or!( 91 | "SESSION_DIRS", 92 | "/usr/share/xsessions:/usr/share/wayland-sessions" 93 | ); 94 | 95 | /// Command prefix for X11 sessions to start the X server 96 | pub const X11_CMD_PREFIX: &str = env_or!("X11_CMD_PREFIX", "startx /usr/bin/env"); 97 | -------------------------------------------------------------------------------- /src/gui/component.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Setup for using the greeter as a Relm4 component 6 | 7 | use std::path::PathBuf; 8 | 9 | use relm4::{ 10 | component::{AsyncComponent, AsyncComponentParts}, 11 | gtk::prelude::*, 12 | prelude::*, 13 | AsyncComponentSender, 14 | }; 15 | use tracing::{debug, info, warn}; 16 | 17 | #[cfg(feature = "gtk4_8")] 18 | use crate::config::BgFit; 19 | 20 | use super::messages::{CommandMsg, InputMsg, UserSessInfo}; 21 | use super::model::{Greeter, InputMode, Updates}; 22 | use super::templates::Ui; 23 | 24 | /// Load GTK settings from the greeter config. 25 | fn setup_settings(model: &Greeter, root: >k::ApplicationWindow) { 26 | let settings = root.settings(); 27 | let config = if let Some(config) = model.config.get_gtk_settings() { 28 | config 29 | } else { 30 | return; 31 | }; 32 | 33 | debug!( 34 | "Setting dark theme: {}", 35 | config.application_prefer_dark_theme 36 | ); 37 | settings.set_gtk_application_prefer_dark_theme(config.application_prefer_dark_theme); 38 | 39 | if let Some(cursor_theme) = &config.cursor_theme_name { 40 | debug!("Setting cursor theme: {cursor_theme}"); 41 | settings.set_gtk_cursor_theme_name(config.cursor_theme_name.as_deref()); 42 | }; 43 | 44 | if let Some(font) = &config.font_name { 45 | debug!("Setting font: {font}"); 46 | settings.set_gtk_font_name(config.font_name.as_deref()); 47 | }; 48 | 49 | if let Some(icon_theme) = &config.icon_theme_name { 50 | debug!("Setting icon theme: {icon_theme}"); 51 | settings.set_gtk_icon_theme_name(config.icon_theme_name.as_deref()); 52 | }; 53 | 54 | if let Some(theme) = &config.theme_name { 55 | debug!("Setting theme: {theme}"); 56 | settings.set_gtk_theme_name(config.theme_name.as_deref()); 57 | }; 58 | } 59 | 60 | /// Populate the user and session combo boxes with entries. 61 | fn setup_users_sessions(model: &Greeter, widgets: &GreeterWidgets) { 62 | // The user that is shown during initial login 63 | let mut initial_username = None; 64 | 65 | // Populate the usernames combo box. 66 | for (user, username) in model.sys_util.get_users().iter() { 67 | debug!("Found user: {user}"); 68 | if initial_username.is_none() { 69 | initial_username = Some(username.clone()); 70 | } 71 | widgets.ui.usernames_box.append(Some(username), user); 72 | } 73 | 74 | // Populate the sessions combo box. 75 | for session in model.sys_util.get_sessions().keys() { 76 | debug!("Found session: {session}"); 77 | widgets.ui.sessions_box.append(Some(session), session); 78 | } 79 | 80 | // If the last user is known, show their login initially. 81 | if let Some(last_user) = model.cache.get_last_user() { 82 | initial_username = Some(last_user.to_string()); 83 | } else if let Some(user) = &initial_username { 84 | info!("Using first found user '{user}' as initial user"); 85 | } 86 | 87 | // Set the user shown initially at login. 88 | if !widgets 89 | .ui 90 | .usernames_box 91 | .set_active_id(initial_username.as_deref()) 92 | { 93 | if let Some(user) = initial_username { 94 | warn!("Couldn't find user '{user}' to set as the initial user"); 95 | } 96 | } 97 | } 98 | 99 | /// The info required to initialize the greeter 100 | pub struct GreeterInit { 101 | pub config_path: PathBuf, 102 | pub css_path: PathBuf, 103 | pub demo: bool, 104 | } 105 | 106 | #[relm4::component(pub, async)] 107 | impl AsyncComponent for Greeter { 108 | type Input = InputMsg; 109 | type Output = (); 110 | type Init = GreeterInit; 111 | type CommandOutput = CommandMsg; 112 | 113 | view! { 114 | // The `view!` macro needs a proper widget, not a template, as the root. 115 | #[name = "window"] 116 | gtk::ApplicationWindow { 117 | set_visible: true, 118 | 119 | // Name the UI widget, otherwise the inner children cannot be accessed by name. 120 | #[name = "ui"] 121 | #[template] 122 | Ui { 123 | #[template_child] 124 | background { set_filename: model.config.get_background() }, 125 | 126 | #[template_child] 127 | clock_frame { 128 | model.clock.widget(), 129 | }, 130 | 131 | #[template_child] 132 | message_label { 133 | #[track(model.updates.changed(Updates::message()))] 134 | set_label: &model.updates.message, 135 | }, 136 | #[template_child] 137 | session_label { 138 | #[track(model.updates.changed(Updates::input_mode()))] 139 | set_visible: !model.updates.is_input(), 140 | }, 141 | #[template_child] 142 | usernames_box { 143 | #[track( 144 | model.updates.changed(Updates::manual_user_mode()) 145 | || model.updates.changed(Updates::input_mode()) 146 | )] 147 | set_sensitive: !model.updates.manual_user_mode && !model.updates.is_input(), 148 | #[track(model.updates.changed(Updates::manual_user_mode()))] 149 | set_visible: !model.updates.manual_user_mode, 150 | connect_changed[ 151 | sender, 152 | username_entry = ui.username_entry.clone(), 153 | sessions_box = ui.sessions_box.clone(), 154 | session_entry = ui.session_entry.clone(), 155 | ] => move |this| sender.input( 156 | Self::Input::UserChanged( 157 | UserSessInfo::extract(this, &username_entry, &sessions_box, &session_entry) 158 | ) 159 | ), 160 | }, 161 | #[template_child] 162 | username_entry { 163 | #[track( 164 | model.updates.changed(Updates::manual_user_mode()) 165 | || model.updates.changed(Updates::input_mode()) 166 | )] 167 | set_sensitive: model.updates.manual_user_mode && !model.updates.is_input(), 168 | #[track(model.updates.changed(Updates::manual_user_mode()))] 169 | set_visible: model.updates.manual_user_mode, 170 | }, 171 | #[template_child] 172 | sessions_box { 173 | #[track( 174 | model.updates.changed(Updates::manual_sess_mode()) 175 | || model.updates.changed(Updates::input_mode()) 176 | )] 177 | set_visible: !model.updates.manual_sess_mode && !model.updates.is_input(), 178 | #[track(model.updates.changed(Updates::active_session_id()))] 179 | set_active_id: model.updates.active_session_id.as_deref(), 180 | }, 181 | #[template_child] 182 | session_entry { 183 | #[track( 184 | model.updates.changed(Updates::manual_sess_mode()) 185 | || model.updates.changed(Updates::input_mode()) 186 | )] 187 | set_visible: model.updates.manual_sess_mode && !model.updates.is_input(), 188 | }, 189 | #[template_child] 190 | input_label { 191 | #[track(model.updates.changed(Updates::input_mode()))] 192 | set_visible: model.updates.is_input(), 193 | #[track(model.updates.changed(Updates::input_prompt()))] 194 | set_label: &model.updates.input_prompt, 195 | }, 196 | #[template_child] 197 | secret_entry { 198 | #[track(model.updates.changed(Updates::input_mode()))] 199 | set_visible: model.updates.input_mode == InputMode::Secret, 200 | #[track( 201 | model.updates.changed(Updates::input_mode()) 202 | && model.updates.input_mode == InputMode::Secret 203 | )] 204 | grab_focus: (), 205 | #[track(model.updates.changed(Updates::input()))] 206 | set_text: &model.updates.input, 207 | connect_activate[ 208 | sender, 209 | usernames_box = ui.usernames_box.clone(), 210 | username_entry = ui.username_entry.clone(), 211 | sessions_box = ui.sessions_box.clone(), 212 | session_entry = ui.session_entry.clone(), 213 | ] => move |this| { 214 | sender.input(Self::Input::Login { 215 | input: this.text().to_string(), 216 | info: UserSessInfo::extract( 217 | &usernames_box, &username_entry, &sessions_box, &session_entry 218 | ), 219 | }) 220 | } 221 | }, 222 | #[template_child] 223 | visible_entry { 224 | #[track(model.updates.changed(Updates::input_mode()))] 225 | set_visible: model.updates.input_mode == InputMode::Visible, 226 | #[track( 227 | model.updates.changed(Updates::input_mode()) 228 | && model.updates.input_mode == InputMode::Visible 229 | )] 230 | grab_focus: (), 231 | #[track(model.updates.changed(Updates::input()))] 232 | set_text: &model.updates.input, 233 | connect_activate[ 234 | sender, 235 | usernames_box = ui.usernames_box.clone(), 236 | username_entry = ui.username_entry.clone(), 237 | sessions_box = ui.sessions_box.clone(), 238 | session_entry = ui.session_entry.clone(), 239 | ] => move |this| { 240 | sender.input(Self::Input::Login { 241 | input: this.text().to_string(), 242 | info: UserSessInfo::extract( 243 | &usernames_box, &username_entry, &sessions_box, &session_entry 244 | ), 245 | }) 246 | } 247 | }, 248 | #[template_child] 249 | user_toggle { 250 | #[track(model.updates.changed(Updates::input_mode()))] 251 | set_sensitive: !model.updates.is_input(), 252 | connect_clicked => Self::Input::ToggleManualUser, 253 | }, 254 | #[template_child] 255 | sess_toggle { 256 | #[track(model.updates.changed(Updates::input_mode()))] 257 | set_visible: !model.updates.is_input(), 258 | connect_clicked => Self::Input::ToggleManualSess, 259 | }, 260 | #[template_child] 261 | cancel_button { 262 | #[track(model.updates.changed(Updates::input_mode()))] 263 | set_visible: model.updates.is_input(), 264 | connect_clicked => Self::Input::Cancel, 265 | }, 266 | #[template_child] 267 | login_button { 268 | #[track( 269 | model.updates.changed(Updates::input_mode()) 270 | && !model.updates.is_input() 271 | )] 272 | grab_focus: (), 273 | connect_clicked[ 274 | sender, 275 | secret_entry = ui.secret_entry.clone(), 276 | visible_entry = ui.visible_entry.clone(), 277 | usernames_box = ui.usernames_box.clone(), 278 | username_entry = ui.username_entry.clone(), 279 | sessions_box = ui.sessions_box.clone(), 280 | session_entry = ui.session_entry.clone(), 281 | ] => move |_| { 282 | sender.input(Self::Input::Login { 283 | input: if secret_entry.is_visible() { 284 | // This should correspond to `InputMode::Secret`. 285 | secret_entry.text().to_string() 286 | } else if EntryExt::is_visible(&visible_entry) { 287 | // This should correspond to `InputMode::Visible`. 288 | visible_entry.text().to_string() 289 | } else { 290 | // This should correspond to `InputMode::None`. 291 | String::new() 292 | }, 293 | info: UserSessInfo::extract( 294 | &usernames_box, &username_entry, &sessions_box, &session_entry 295 | ), 296 | }) 297 | } 298 | }, 299 | #[template_child] 300 | error_info { 301 | #[track(model.updates.changed(Updates::error()))] 302 | set_revealed: model.updates.error.is_some(), 303 | }, 304 | #[template_child] 305 | error_label { 306 | #[track(model.updates.changed(Updates::error()))] 307 | set_label: model.updates.error.as_ref().unwrap_or(&"".to_string()), 308 | }, 309 | #[template_child] 310 | reboot_button { connect_clicked => Self::Input::Reboot }, 311 | #[template_child] 312 | poweroff_button { connect_clicked => Self::Input::PowerOff }, 313 | } 314 | } 315 | } 316 | 317 | fn post_view() { 318 | if model.updates.changed(Updates::monitor()) { 319 | if let Some(monitor) = &model.updates.monitor { 320 | widgets.window.fullscreen_on_monitor(monitor); 321 | // For some reason, the GTK settings are reset when changing monitors, so re-apply them. 322 | setup_settings(self, &widgets.window); 323 | } 324 | } 325 | } 326 | 327 | /// Initialize the greeter. 328 | async fn init( 329 | input: Self::Init, 330 | root: Self::Root, 331 | sender: AsyncComponentSender, 332 | ) -> AsyncComponentParts { 333 | let mut model = Self::new(&input.config_path, input.demo).await; 334 | let widgets = view_output!(); 335 | 336 | // Make the info bar permanently visible, since it was made invisible during init. The 337 | // actual visuals are controlled by `InfoBar::set_revealed`. 338 | widgets.ui.error_info.set_visible(true); 339 | 340 | // cfg directives don't work inside Relm4 view! macro. 341 | #[cfg(feature = "gtk4_8")] 342 | widgets 343 | .ui 344 | .background 345 | .set_content_fit(match model.config.get_background_fit() { 346 | BgFit::Fill => gtk4::ContentFit::Fill, 347 | BgFit::Contain => gtk4::ContentFit::Contain, 348 | BgFit::Cover => gtk4::ContentFit::Cover, 349 | BgFit::ScaleDown => gtk4::ContentFit::ScaleDown, 350 | }); 351 | 352 | // Cancel any previous session, just in case someone started one. 353 | if let Err(err) = model.greetd_client.lock().await.cancel_session().await { 354 | warn!("Couldn't cancel greetd session: {err}"); 355 | }; 356 | 357 | model.choose_monitor(widgets.ui.display().name().as_str(), &sender); 358 | if let Some(monitor) = &model.updates.monitor { 359 | // The window needs to be manually fullscreened, since the monitor is `None` at widget 360 | // init. 361 | root.fullscreen_on_monitor(monitor); 362 | } else { 363 | // Couldn't choose a monitor, so let the compositor choose it for us. 364 | root.fullscreen(); 365 | } 366 | 367 | // For some reason, the GTK settings are reset when changing monitors, so apply them after 368 | // full-screening. 369 | setup_settings(&model, &root); 370 | setup_users_sessions(&model, &widgets); 371 | 372 | if input.css_path.exists() { 373 | debug!("Loading custom CSS from file: {}", input.css_path.display()); 374 | let provider = gtk::CssProvider::new(); 375 | provider.load_from_path(input.css_path); 376 | gtk::style_context_add_provider_for_display( 377 | &widgets.ui.display(), 378 | &provider, 379 | gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, 380 | ); 381 | }; 382 | 383 | // Set the default behaviour of pressing the Return key to act like the login button. 384 | root.set_default_widget(Some(&widgets.ui.login_button)); 385 | 386 | AsyncComponentParts { model, widgets } 387 | } 388 | 389 | async fn update( 390 | &mut self, 391 | msg: Self::Input, 392 | sender: AsyncComponentSender, 393 | _root: &Self::Root, 394 | ) { 395 | debug!("Got input message: {msg:?}"); 396 | 397 | // Reset the tracker for update changes. 398 | self.updates.reset(); 399 | 400 | match msg { 401 | Self::Input::Login { input, info } => { 402 | self.sess_info = Some(info); 403 | self.login_click_handler(&sender, input).await 404 | } 405 | Self::Input::Cancel => self.cancel_click_handler().await, 406 | Self::Input::UserChanged(info) => { 407 | self.sess_info = Some(info); 408 | self.user_change_handler(); 409 | } 410 | Self::Input::ToggleManualUser => self 411 | .updates 412 | .set_manual_user_mode(!self.updates.manual_user_mode), 413 | Self::Input::ToggleManualSess => self 414 | .updates 415 | .set_manual_sess_mode(!self.updates.manual_sess_mode), 416 | Self::Input::Reboot => self.reboot_click_handler(&sender), 417 | Self::Input::PowerOff => self.poweroff_click_handler(&sender), 418 | } 419 | } 420 | 421 | /// Perform the requested changes when a background task sends a message. 422 | async fn update_cmd( 423 | &mut self, 424 | msg: Self::CommandOutput, 425 | sender: AsyncComponentSender, 426 | _root: &Self::Root, 427 | ) { 428 | debug!("Got command message: {msg:?}"); 429 | 430 | // Reset the tracker for update changes. 431 | self.updates.reset(); 432 | 433 | match msg { 434 | Self::CommandOutput::ClearErr => self.updates.set_error(None), 435 | Self::CommandOutput::HandleGreetdResponse(response) => { 436 | self.handle_greetd_response(&sender, response).await 437 | } 438 | Self::CommandOutput::MonitorRemoved(display_name) => { 439 | self.choose_monitor(display_name.as_str(), &sender) 440 | } 441 | }; 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/gui/messages.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Message definitions for communication between the view and the model 6 | 7 | use educe::Educe; 8 | use greetd_ipc::Response; 9 | use relm4::gtk::{glib::GString, prelude::*, ComboBoxText, Entry}; 10 | 11 | #[derive(Debug)] 12 | /// Info about the current user and chosen session 13 | pub struct UserSessInfo { 14 | /// The ID for the currently chosen user 15 | pub(super) user_id: Option, 16 | /// The entry text for the currently chosen user 17 | pub(super) user_text: GString, 18 | /// The ID for the currently chosen session 19 | pub(super) sess_id: Option, 20 | /// The entry text for the currently chosen session 21 | pub(super) sess_text: GString, 22 | } 23 | 24 | impl UserSessInfo { 25 | /// Extract session and user info from the relevant widgets. 26 | pub(super) fn extract( 27 | usernames_box: &ComboBoxText, 28 | username_entry: &Entry, 29 | sessions_box: &ComboBoxText, 30 | session_entry: &Entry, 31 | ) -> Self { 32 | Self { 33 | user_id: usernames_box.active_id(), 34 | user_text: username_entry.text(), 35 | sess_id: sessions_box.active_id(), 36 | sess_text: session_entry.text(), 37 | } 38 | } 39 | } 40 | 41 | /// The messages sent by the view to the model 42 | #[derive(Educe)] 43 | #[educe(Debug)] 44 | pub enum InputMsg { 45 | /// Login request 46 | Login { 47 | #[educe(Debug(ignore))] 48 | input: String, 49 | info: UserSessInfo, 50 | }, 51 | /// Cancel the login request 52 | Cancel, 53 | /// The current user was changed in the GUI. 54 | UserChanged(UserSessInfo), 55 | /// Toggle manual entry of user. 56 | ToggleManualUser, 57 | /// Toggle manual entry of session. 58 | ToggleManualSess, 59 | Reboot, 60 | PowerOff, 61 | } 62 | 63 | #[derive(Debug)] 64 | /// The messages sent to the sender to run tasks in the background 65 | pub enum CommandMsg { 66 | /// Clear the error message. 67 | ClearErr, 68 | /// Handle a response received from greetd 69 | HandleGreetdResponse(Response), 70 | /// Notify the greeter that a monitor was removed. 71 | // The Gstring is the name of the display. 72 | MonitorRemoved(GString), 73 | } 74 | -------------------------------------------------------------------------------- /src/gui/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! The main GUI for the greeter 6 | 7 | mod component; 8 | mod messages; 9 | mod model; 10 | mod templates; 11 | pub(crate) mod widget { 12 | pub mod clock; 13 | } 14 | 15 | pub use component::GreeterInit; 16 | pub use model::Greeter; 17 | -------------------------------------------------------------------------------- /src/gui/model.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | // SPDX-FileCopyrightText: 2021 Maximilian Moser 6 | // 7 | // SPDX-License-Identifier: MIT 8 | 9 | //! The main logic for the greeter 10 | 11 | use std::path::Path; 12 | use std::process::Command; 13 | use std::sync::Arc; 14 | use std::time::Duration; 15 | 16 | use greetd_ipc::{AuthMessageType, ErrorType, Response}; 17 | use relm4::{ 18 | gtk::{ 19 | gdk::{Display, Monitor}, 20 | prelude::*, 21 | }, 22 | AsyncComponentSender, Component, Controller, 23 | }; 24 | use tokio::{sync::Mutex, time::sleep}; 25 | 26 | use crate::cache::Cache; 27 | use crate::client::{AuthStatus, GreetdClient}; 28 | use crate::config::Config; 29 | use crate::sysutil::{SessionInfo, SessionType, SysUtil}; 30 | 31 | use super::{ 32 | messages::{CommandMsg, UserSessInfo}, 33 | widget::clock::Clock, 34 | }; 35 | 36 | const ERROR_MSG_CLEAR_DELAY: u64 = 5; 37 | 38 | #[derive(PartialEq)] 39 | pub(super) enum InputMode { 40 | None, 41 | Secret, 42 | Visible, 43 | } 44 | 45 | // Fields only set by the model, that are meant to be read only by the widgets 46 | #[tracker::track] 47 | pub(super) struct Updates { 48 | /// Message to be shown to the user 49 | pub(super) message: String, 50 | /// Error message to be shown to the user below the prompt 51 | pub(super) error: Option, 52 | /// Text in the password field 53 | pub(super) input: String, 54 | /// Whether the username is being entered manually 55 | pub(super) manual_user_mode: bool, 56 | /// Whether the session is being entered manually 57 | pub(super) manual_sess_mode: bool, 58 | /// Input prompt sent by greetd for text input 59 | pub(super) input_prompt: String, 60 | /// Whether the user is currently entering a secret, something visible or nothing 61 | pub(super) input_mode: InputMode, 62 | /// ID of the active session 63 | pub(super) active_session_id: Option, 64 | /// Time that is displayed 65 | pub(super) time: String, 66 | /// Monitor where the window is displayed 67 | pub(super) monitor: Option, 68 | } 69 | 70 | impl Updates { 71 | pub(super) fn is_input(&self) -> bool { 72 | self.input_mode != InputMode::None 73 | } 74 | } 75 | 76 | /// Capitalize the first letter of the string. 77 | fn capitalize(string: &str) -> String { 78 | string[0..1].to_uppercase() + &string[1..] 79 | } 80 | 81 | /// Greeter model that holds its state 82 | pub struct Greeter { 83 | /// Client to communicate with greetd 84 | pub(super) greetd_client: Arc>, 85 | /// System utility to get available users and sessions 86 | pub(super) sys_util: SysUtil, 87 | /// The cache that persists between logins 88 | pub(super) cache: Cache, 89 | /// The config for this greeter 90 | pub(super) config: Config, 91 | /// Session info set after pressing login 92 | pub(super) sess_info: Option, 93 | /// The updates from the model that are read by the view 94 | pub(super) updates: Updates, 95 | /// Is it run as demo 96 | pub(super) demo: bool, 97 | 98 | pub(super) clock: Controller, 99 | } 100 | 101 | impl Greeter { 102 | pub(super) async fn new(config_path: &Path, demo: bool) -> Self { 103 | let config = Config::new(config_path); 104 | 105 | let updates = Updates { 106 | message: config.get_default_message(), 107 | error: None, 108 | input: String::new(), 109 | manual_user_mode: false, 110 | manual_sess_mode: false, 111 | input_mode: InputMode::None, 112 | input_prompt: String::new(), 113 | active_session_id: None, 114 | tracker: 0, 115 | time: "".to_string(), 116 | monitor: None, 117 | }; 118 | let greetd_client = Arc::new(Mutex::new( 119 | GreetdClient::new(demo) 120 | .await 121 | .expect("Couldn't initialize greetd client"), 122 | )); 123 | 124 | let clock = Clock::builder() 125 | .launch(config.widget.clock.clone()) 126 | .detach(); 127 | 128 | Self { 129 | greetd_client, 130 | sys_util: SysUtil::new(&config).expect("Couldn't read available users and sessions"), 131 | cache: Cache::new(), 132 | sess_info: None, 133 | config, 134 | updates, 135 | demo, 136 | clock, 137 | } 138 | } 139 | 140 | /// Make the greeter full screen over the first monitor. 141 | #[instrument(skip(self, sender))] 142 | pub(super) fn choose_monitor( 143 | &mut self, 144 | display_name: &str, 145 | sender: &AsyncComponentSender, 146 | ) { 147 | let display = match Display::open(Some(display_name)) { 148 | Some(display) => display, 149 | None => { 150 | error!("Couldn't get display with name: {display_name}"); 151 | return; 152 | } 153 | }; 154 | 155 | let mut chosen_monitor = None; 156 | for monitor in display 157 | .monitors() 158 | .into_iter() 159 | .filter_map(|item| { 160 | item.ok() 161 | .and_then(|object| object.downcast::().ok()) 162 | }) 163 | .filter(Monitor::is_valid) 164 | { 165 | let sender = sender.clone(); 166 | monitor.connect_invalidate(move |monitor| { 167 | let display_name = monitor.display().name(); 168 | sender.oneshot_command(async move { CommandMsg::MonitorRemoved(display_name) }) 169 | }); 170 | if chosen_monitor.is_none() { 171 | // Choose the first monitor. 172 | chosen_monitor = Some(monitor); 173 | } 174 | } 175 | 176 | self.updates.set_monitor(chosen_monitor); 177 | } 178 | 179 | /// Run a command and log any errors in a background thread. 180 | fn run_cmd(command: &[String], sender: &AsyncComponentSender) { 181 | let mut process = Command::new(&command[0]); 182 | process.args(command[1..].iter()); 183 | // Run the command and check its output in a separate thread, so as to not block the GUI. 184 | sender.spawn_command(move |_| match process.output() { 185 | Ok(output) => { 186 | if !output.status.success() { 187 | if let Ok(err) = std::str::from_utf8(&output.stderr) { 188 | error!("Failed to launch command: {err}") 189 | } else { 190 | error!("Failed to launch command: {:?}", output.stderr) 191 | } 192 | } 193 | } 194 | Err(err) => error!("Failed to launch command: {err}"), 195 | }); 196 | } 197 | 198 | /// Event handler for clicking the "Reboot" button 199 | /// 200 | /// This reboots the PC. 201 | #[instrument(skip_all)] 202 | pub(super) fn reboot_click_handler(&self, sender: &AsyncComponentSender) { 203 | if self.demo { 204 | info!("demo: skip reboot"); 205 | return; 206 | } 207 | info!("Rebooting"); 208 | Self::run_cmd(&self.config.get_sys_commands().reboot, sender); 209 | } 210 | 211 | /// Event handler for clicking the "Power-Off" button 212 | /// 213 | /// This shuts down the PC. 214 | #[instrument(skip_all)] 215 | pub(super) fn poweroff_click_handler(&self, sender: &AsyncComponentSender) { 216 | if self.demo { 217 | info!("demo: skip shutdown"); 218 | return; 219 | } 220 | info!("Shutting down"); 221 | Self::run_cmd(&self.config.get_sys_commands().poweroff, sender); 222 | } 223 | 224 | /// Event handler for clicking the "Cancel" button 225 | /// 226 | /// This cancels the created session and goes back to the user/session chooser. 227 | #[instrument(skip_all)] 228 | pub(super) async fn cancel_click_handler(&mut self) { 229 | if let Err(err) = self.greetd_client.lock().await.cancel_session().await { 230 | warn!("Couldn't cancel greetd session: {err}"); 231 | }; 232 | self.updates.set_input(String::new()); 233 | self.updates.set_input_mode(InputMode::None); 234 | self.updates.set_message(self.config.get_default_message()) 235 | } 236 | 237 | /// Create a greetd session, i.e. start a login attempt for the current user. 238 | async fn create_session(&mut self, sender: &AsyncComponentSender) { 239 | let username = if let Some(username) = self.get_current_username() { 240 | username 241 | } else { 242 | // No username found (which shouldn't happen), so we can't create the session. 243 | return; 244 | }; 245 | 246 | // Before trying to create a session, check if the session command (if manually entered) is 247 | // valid. 248 | if self.updates.manual_sess_mode { 249 | let info = self.sess_info.as_ref().expect("No session info set yet"); 250 | if shlex::split(info.sess_text.as_str()).is_none() { 251 | // This must be an invalid command. 252 | self.display_error( 253 | sender, 254 | "Invalid session command", 255 | &format!("Invalid session command: {}", info.sess_text), 256 | ); 257 | return; 258 | }; 259 | debug!("Manually entered session command is parsable"); 260 | }; 261 | 262 | info!("Creating session for user: {username}"); 263 | 264 | // Create a session for the current user. 265 | let response = self 266 | .greetd_client 267 | .lock() 268 | .await 269 | .create_session(&username) 270 | .await 271 | .unwrap_or_else(|err| { 272 | panic!("Failed to create session for username '{username}': {err}",) 273 | }); 274 | 275 | self.handle_greetd_response(sender, response).await; 276 | } 277 | 278 | /// This function handles a greetd response as follows: 279 | /// - if the response indicates authentication success, start the session 280 | /// - if the response is an authentication message: 281 | /// - for info and error messages (no input request), display/log the text and send an empty authentication response to greetd. 282 | /// This allows for immediate greetd updates when using authentication procedures that don't use text input. 283 | /// Also reset input mode to `None` 284 | /// - for input requests (visible/secret), set the input mode accordingly and return 285 | /// - if the response is an error, display it and return 286 | /// 287 | /// This way of handling responses allows for composite authentication procedures, e.g.: 288 | /// 1. Fingerprint 289 | /// 2. Password 290 | pub(super) async fn handle_greetd_response( 291 | &mut self, 292 | sender: &AsyncComponentSender, 293 | response: Response, 294 | ) { 295 | match response { 296 | Response::Success => { 297 | // Authentication was successful and the session may be started. 298 | // This may happen on the first request, in which case logging in 299 | // as the given user requires no authentication. 300 | info!("Successfully logged in; starting session"); 301 | self.start_session(sender).await; 302 | return; 303 | } 304 | Response::AuthMessage { 305 | auth_message, 306 | auth_message_type, 307 | } => { 308 | match auth_message_type { 309 | AuthMessageType::Secret => { 310 | // Greetd has requested input that should be hidden 311 | // e.g.: a password 312 | info!("greetd asks for a secret auth input: {auth_message}"); 313 | self.updates.set_input_mode(InputMode::Secret); 314 | self.updates.set_input(String::new()); 315 | self.updates 316 | .set_input_prompt(auth_message.trim_end().to_string()); 317 | return; 318 | } 319 | AuthMessageType::Visible => { 320 | // Greetd has requested input that need not be hidden 321 | info!("greetd asks for a visible auth input: {auth_message}"); 322 | self.updates.set_input_mode(InputMode::Visible); 323 | self.updates.set_input(String::new()); 324 | self.updates 325 | .set_input_prompt(auth_message.trim_end().to_string()); 326 | return; 327 | } 328 | AuthMessageType::Info => { 329 | // Greetd has sent an info message that should be displayed 330 | // e.g.: asking for a fingerprint 331 | info!("greetd sent an info: {auth_message}"); 332 | self.updates.set_input_mode(InputMode::None); 333 | self.updates.set_message(auth_message); 334 | } 335 | AuthMessageType::Error => { 336 | // Greetd has sent an error message that should be displayed and logged 337 | self.updates.set_input_mode(InputMode::None); 338 | // Reset outdated info message, if any 339 | self.updates.set_message(self.config.get_default_message()); 340 | self.display_error( 341 | sender, 342 | &capitalize(&auth_message), 343 | &format!("Authentication message error from greetd: {auth_message}"), 344 | ); 345 | } 346 | } 347 | } 348 | Response::Error { 349 | description, 350 | error_type, 351 | } => { 352 | // some general response error. This can be an authentication failure or a general error 353 | self.display_error( 354 | sender, 355 | &format!("Login failed: {}", capitalize(&description)), 356 | &format!("Error from greetd: {description}"), 357 | ); 358 | 359 | // In case this is an authentication error (e.g. wrong password), the session should be cancelled. 360 | if let ErrorType::AuthError = error_type { 361 | self.cancel_click_handler().await 362 | } 363 | return; 364 | } 365 | } 366 | 367 | debug!("Sending empty auth response to greetd"); 368 | let client = Arc::clone(&self.greetd_client); 369 | sender.oneshot_command(async move { 370 | debug!("Sending empty auth response to greetd"); 371 | let response = client 372 | .lock() 373 | .await 374 | .send_auth_response(None) 375 | .await 376 | .unwrap_or_else(|err| panic!("Failed to respond to greetd: {err}")); 377 | CommandMsg::HandleGreetdResponse(response) 378 | }); 379 | } 380 | 381 | /// Event handler for selecting a different username in the `ComboBoxText` 382 | /// 383 | /// This changes the session in the combo box according to the last used session of the current user. 384 | #[instrument(skip_all)] 385 | pub(super) fn user_change_handler(&mut self) { 386 | let username = if let Some(username) = self.get_current_username() { 387 | username 388 | } else { 389 | // No username found (which shouldn't happen), so we can't change the session. 390 | return; 391 | }; 392 | 393 | if let Some(last_session) = self.cache.get_last_session(&username) { 394 | // Set the last session used by this user in the session combo box. 395 | self.updates 396 | .set_active_session_id(Some(last_session.to_string())); 397 | } else { 398 | // Last session not found, so skip changing the session. 399 | info!("Last session for user '{username}' missing"); 400 | }; 401 | } 402 | 403 | /// Event handler for clicking the "Login" button 404 | /// 405 | /// This does one of the following, depending of the state of authentication: 406 | /// - Begins a login attempt for the given user 407 | /// - Submits the entered password for logging in and starts the session 408 | #[instrument(skip_all)] 409 | pub(super) async fn login_click_handler( 410 | &mut self, 411 | sender: &AsyncComponentSender, 412 | input: String, 413 | ) { 414 | // Check if a password is needed. If not, then directly start the session. 415 | let auth_status = self.greetd_client.lock().await.get_auth_status().clone(); 416 | match auth_status { 417 | AuthStatus::Done => { 418 | // No password is needed, but the session should've been already started by 419 | // `create_session`. 420 | warn!("No password needed for current user, but session not already started"); 421 | self.start_session(sender).await; 422 | } 423 | AuthStatus::InProgress => { 424 | self.send_input(sender, input).await; 425 | } 426 | AuthStatus::NotStarted => { 427 | self.create_session(sender).await; 428 | } 429 | }; 430 | } 431 | 432 | /// Send the entered input for logging in. 433 | async fn send_input(&mut self, sender: &AsyncComponentSender, input: String) { 434 | // Reset the password field, for convenience when the user has to re-enter a password. 435 | self.updates.set_input(String::new()); 436 | 437 | // Send the password, as authentication for the current user. 438 | let resp = self 439 | .greetd_client 440 | .lock() 441 | .await 442 | .send_auth_response(Some(input)) 443 | .await 444 | .unwrap_or_else(|err| panic!("Failed to send input: {err}")); 445 | 446 | self.handle_greetd_response(sender, resp).await; 447 | } 448 | 449 | /// Get the currently selected username. 450 | fn get_current_username(&self) -> Option { 451 | let info = self.sess_info.as_ref().expect("No session info set yet"); 452 | if self.updates.manual_user_mode { 453 | debug!( 454 | "Retrieved username '{}' through manual entry", 455 | info.user_text 456 | ); 457 | Some(info.user_text.to_string()) 458 | } else if let Some(username) = &info.user_id { 459 | // Get the currently selected user's ID, which should be their username. 460 | debug!("Retrieved username '{username}' from options"); 461 | Some(username.to_string()) 462 | } else { 463 | error!("No username entered"); 464 | None 465 | } 466 | } 467 | 468 | /// Get the currently selected session name (if available) and command. 469 | fn get_current_session_info( 470 | &mut self, 471 | sender: &AsyncComponentSender, 472 | ) -> (Option, Option) { 473 | let info = self.sess_info.as_ref().expect("No session info set yet"); 474 | if self.updates.manual_sess_mode { 475 | debug!( 476 | "Retrieved session command '{}' through manual entry", 477 | info.sess_text 478 | ); 479 | if let Some(cmd) = shlex::split(info.sess_text.as_str()) { 480 | ( 481 | None, 482 | Some(SessionInfo { 483 | command: cmd, 484 | sess_type: SessionType::Unknown, 485 | }), 486 | ) 487 | } else { 488 | // This must be an invalid command. 489 | self.display_error( 490 | sender, 491 | "Invalid session command", 492 | &format!("Invalid session command: {}", info.sess_text), 493 | ); 494 | (None, None) 495 | } 496 | } else if let Some(session) = &info.sess_id { 497 | // Get the currently selected session. 498 | debug!("Retrieved current session: {session}"); 499 | if let Some(sess_info) = self.sys_util.get_sessions().get(session.as_str()) { 500 | (Some(session.to_string()), Some(sess_info.clone())) 501 | } else { 502 | // Shouldn't happen, unless there are no sessions available. 503 | let error_msg = format!("Session '{session}' not found"); 504 | self.display_error(sender, &error_msg, &error_msg); 505 | (None, None) 506 | } 507 | } else { 508 | let username = if let Some(username) = self.get_current_username() { 509 | username 510 | } else { 511 | // This shouldn't happen, because a session should've been created with a username. 512 | unimplemented!("Trying to create session without a username"); 513 | }; 514 | warn!("No entry found; using default login shell of user: {username}",); 515 | if let Some(cmd) = self.sys_util.get_shells().get(username.as_str()) { 516 | ( 517 | None, 518 | Some(SessionInfo { 519 | command: cmd.clone(), 520 | sess_type: SessionType::Unknown, 521 | }), 522 | ) 523 | } else { 524 | // No login shell exists. 525 | let error_msg = "No session or login shell found"; 526 | self.display_error(sender, error_msg, error_msg); 527 | (None, None) 528 | } 529 | } 530 | } 531 | 532 | /// Start the session for the selected user. 533 | async fn start_session(&mut self, sender: &AsyncComponentSender) { 534 | // Get the session command. 535 | let (session, info) = if let (session, Some(info)) = self.get_current_session_info(sender) { 536 | (session, info) 537 | } else { 538 | // Error handling should be inside `get_current_session_info`, so simply return. 539 | return; 540 | }; 541 | 542 | // Generate env string that will be passed to greetd when starting the session 543 | let env = self.config.get_env(); 544 | let mut environment = Vec::with_capacity(env.len() + 1); 545 | match info.sess_type { 546 | SessionType::X11 => { 547 | environment.push("XDG_SESSION_TYPE=x11".to_string()); 548 | } 549 | SessionType::Wayland => { 550 | environment.push("XDG_SESSION_TYPE=wayland".to_string()); 551 | } 552 | SessionType::Unknown => {} 553 | }; 554 | for (k, v) in env { 555 | environment.push(format!("{}={}", k, v)); 556 | } 557 | 558 | if let Some(username) = self.get_current_username() { 559 | self.cache.set_last_user(&username); 560 | if let Some(session) = session { 561 | self.cache.set_last_session(&username, &session); 562 | } 563 | debug!("Updated cache with current user: {username}"); 564 | } 565 | 566 | if !self.demo { 567 | info!("Saving cache to disk"); 568 | if let Err(err) = self.cache.save() { 569 | error!("Error saving cache to disk: {err}"); 570 | } 571 | } 572 | 573 | // Start the session. 574 | let response = self 575 | .greetd_client 576 | .lock() 577 | .await 578 | .start_session(info.command, environment) 579 | .await 580 | .unwrap_or_else(|err| panic!("Failed to start session: {err}")); 581 | 582 | match response { 583 | Response::Success => { 584 | info!("Session successfully started"); 585 | std::process::exit(0); 586 | } 587 | 588 | Response::AuthMessage { .. } => unimplemented!(), 589 | 590 | Response::Error { description, .. } => { 591 | self.cancel_click_handler().await; 592 | self.display_error( 593 | sender, 594 | "Failed to start session", 595 | &format!("Failed to start session; error: {description}"), 596 | ); 597 | } 598 | } 599 | } 600 | 601 | /// Show an error message to the user. 602 | fn display_error( 603 | &mut self, 604 | sender: &AsyncComponentSender, 605 | display_text: &str, 606 | log_text: &str, 607 | ) { 608 | self.updates.set_error(Some(display_text.to_string())); 609 | error!("{log_text}"); 610 | 611 | sender.oneshot_command(async move { 612 | sleep(Duration::from_secs(ERROR_MSG_CLEAR_DELAY)).await; 613 | CommandMsg::ClearErr 614 | }); 615 | } 616 | } 617 | 618 | impl Drop for Greeter { 619 | fn drop(&mut self) { 620 | // Cancel any created session, just to be safe. 621 | let client = Arc::clone(&self.greetd_client); 622 | tokio::spawn(async move { 623 | client 624 | .lock() 625 | .await 626 | .cancel_session() 627 | .await 628 | .expect("Couldn't cancel session on exit.") 629 | }); 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /src/gui/templates.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Templates for various GUI components 6 | #![allow(dead_code)] // Silence dead code warnings for UI code that isn't dead 7 | 8 | use gtk::prelude::*; 9 | use relm4::{gtk, RelmWidgetExt, WidgetTemplate}; 10 | 11 | /// Button that ends the greeter (eg. Reboot) 12 | #[relm4::widget_template(pub)] 13 | impl WidgetTemplate for EndButton { 14 | view! { 15 | gtk::Button { 16 | set_focusable: true, 17 | add_css_class: "destructive-action", 18 | } 19 | } 20 | } 21 | 22 | /// Label for an entry/combo box 23 | #[relm4::widget_template(pub)] 24 | impl WidgetTemplate for EntryLabel { 25 | view! { 26 | gtk::Label { 27 | set_width_request: 100, 28 | set_xalign: 1.0, 29 | } 30 | } 31 | } 32 | 33 | /// Main UI of the greeter 34 | #[relm4::widget_template(pub)] 35 | impl WidgetTemplate for Ui { 36 | view! { 37 | gtk::Overlay { 38 | /// Background image 39 | #[name = "background"] 40 | gtk::Picture, 41 | 42 | /// Main login box 43 | add_overlay = >k::Frame { 44 | set_halign: gtk::Align::Center, 45 | set_valign: gtk::Align::Center, 46 | add_css_class: "background", 47 | 48 | gtk::Grid { 49 | set_column_spacing: 15, 50 | set_margin_bottom: 15, 51 | set_margin_end: 15, 52 | set_margin_start: 15, 53 | set_margin_top: 15, 54 | set_row_spacing: 15, 55 | set_width_request: 500, 56 | 57 | /// Widget to display messages to the user 58 | #[name = "message_label"] 59 | attach[0, 0, 3, 1] = >k::Label { 60 | set_margin_bottom: 15, 61 | 62 | // Format all messages in boldface. 63 | #[wrap(Some)] 64 | set_attributes = >k::pango::AttrList { 65 | insert: { 66 | let mut font_desc = gtk::pango::FontDescription::new(); 67 | font_desc.set_weight(gtk::pango::Weight::Bold); 68 | gtk::pango::AttrFontDesc::new(&font_desc) 69 | }, 70 | }, 71 | }, 72 | 73 | #[template] 74 | attach[0, 1, 1, 1] = &EntryLabel { 75 | set_label: "User:", 76 | set_height_request: 45, 77 | }, 78 | 79 | /// Label for the sessions widget 80 | #[name = "session_label"] 81 | #[template] 82 | attach[0, 2, 1, 1] = &EntryLabel { 83 | set_label: "Session:", 84 | set_height_request: 45, 85 | }, 86 | 87 | /// Widget containing the usernames 88 | #[name = "usernames_box"] 89 | attach[1, 1, 1, 1] = >k::ComboBoxText { set_hexpand: true }, 90 | 91 | /// Widget where the user enters the username 92 | #[name = "username_entry"] 93 | attach[1, 1, 1, 1] = >k::Entry { set_hexpand: true }, 94 | 95 | /// Widget containing the sessions 96 | #[name = "sessions_box"] 97 | attach[1, 2, 1, 1] = >k::ComboBoxText, 98 | 99 | /// Widget where the user enters the session 100 | #[name = "session_entry"] 101 | attach[1, 2, 1, 1] = >k::Entry, 102 | 103 | /// Label for the password widget 104 | #[name = "input_label"] 105 | #[template] 106 | attach[0, 2, 1, 1] = &EntryLabel { 107 | set_height_request: 45, 108 | }, 109 | 110 | /// Widget where the user enters a secret 111 | #[name = "secret_entry"] 112 | attach[1, 2, 1, 1] = >k::PasswordEntry { set_show_peek_icon: true }, 113 | 114 | /// Widget where the user enters something visible 115 | #[name = "visible_entry"] 116 | attach[1, 2, 1, 1] = >k::Entry, 117 | 118 | /// Button to toggle manual user entry 119 | #[name = "user_toggle"] 120 | attach[2, 1, 1, 1] = >k::ToggleButton { 121 | set_icon_name: "document-edit-symbolic", 122 | set_tooltip_text: Some("Manually enter username"), 123 | }, 124 | 125 | /// Button to toggle manual session entry 126 | #[name = "sess_toggle"] 127 | attach[2, 2, 1, 1] = >k::ToggleButton { 128 | set_icon_name: "document-edit-symbolic", 129 | set_tooltip_text: Some("Manually enter session command"), 130 | }, 131 | 132 | /// Collection of action buttons (eg. Login) 133 | attach[1, 3, 2, 1] = >k::Box { 134 | set_halign: gtk::Align::End, 135 | set_spacing: 15, 136 | 137 | /// Button to cancel password entry 138 | #[name = "cancel_button"] 139 | gtk::Button { 140 | set_focusable: true, 141 | set_label: "Cancel", 142 | }, 143 | 144 | /// Button to enter the password and login 145 | #[name = "login_button"] 146 | gtk::Button { 147 | set_focusable: true, 148 | set_label: "Login", 149 | set_receives_default: true, 150 | add_css_class: "suggested-action", 151 | }, 152 | }, 153 | }, 154 | }, 155 | 156 | /// Clock widget 157 | #[name = "clock_frame"] 158 | add_overlay = >k::Frame { 159 | set_halign: gtk::Align::Center, 160 | set_valign: gtk::Align::Start, 161 | 162 | add_css_class: "background", 163 | 164 | // Make it fit cleanly onto the top edge of the screen. 165 | inline_css: " 166 | border-top-right-radius: 0px; 167 | border-top-left-radius: 0px; 168 | border-top-width: 0px; 169 | ", 170 | }, 171 | 172 | /// Collection of widgets appearing at the bottom 173 | add_overlay = >k::Box { 174 | set_orientation: gtk::Orientation::Vertical, 175 | set_halign: gtk::Align::Center, 176 | set_valign: gtk::Align::End, 177 | set_margin_bottom: 15, 178 | set_spacing: 15, 179 | 180 | gtk::Frame { 181 | /// Notification bar for error messages 182 | #[name = "error_info"] 183 | gtk::InfoBar { 184 | // During init, the info bar closing animation is shown. To hide that, make 185 | // it invisible. Later, the code will permanently make it visible, so that 186 | // `InfoBar::set_revealed` will work properly with animations. 187 | set_visible: false, 188 | set_message_type: gtk::MessageType::Error, 189 | 190 | /// The actual error message 191 | #[name = "error_label"] 192 | gtk::Label { 193 | set_halign: gtk::Align::Center, 194 | set_margin_top: 10, 195 | set_margin_bottom: 10, 196 | set_margin_start: 10, 197 | set_margin_end: 10, 198 | }, 199 | } 200 | }, 201 | 202 | /// Collection of buttons that close the greeter (eg. Reboot) 203 | gtk::Box { 204 | set_halign: gtk::Align::Center, 205 | set_homogeneous: true, 206 | set_spacing: 15, 207 | 208 | /// Button to reboot 209 | #[name = "reboot_button"] 210 | #[template] 211 | EndButton { set_label: "Reboot" }, 212 | 213 | /// Button to power-off 214 | #[name = "poweroff_button"] 215 | #[template] 216 | EndButton { set_label: "Power Off" }, 217 | }, 218 | }, 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/gui/widget/clock.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 max-ishere <47008271+max-ishere@users.noreply.github.com> 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! A [serde-configurable][`ClockConfig`] clock label widget. 6 | 7 | use std::time::Duration; 8 | 9 | use jiff::{fmt::strtime::format, tz::TimeZone, Timestamp, Zoned}; 10 | use relm4::{gtk::prelude::*, prelude::*}; 11 | use serde::{ 12 | de::{self, Visitor}, 13 | Deserialize, Deserializer, 14 | }; 15 | use tokio::time::sleep; 16 | 17 | #[derive(Deserialize, Clone)] 18 | pub struct ClockConfig { 19 | /// A [strftime][fmt] argument 20 | /// 21 | /// [fmt]: jiff::fmt::strtime 22 | #[serde(alias = "fmt", default = "weekday_and_24h_time")] 23 | pub format: String, 24 | 25 | /// Amount of time between the clock's text updates 26 | #[serde( 27 | alias = "interval", 28 | alias = "frequency", 29 | with = "humantime_serde", 30 | default = "half_second" 31 | )] 32 | pub resolution: Duration, 33 | 34 | /// A timezone from the [IANA Time Zone Database](https://en.wikipedia.org/wiki/Tz_database). If the ID is invalid 35 | /// or [`None`], uses the system timezone. 36 | #[serde(alias = "tz", deserialize_with = "parse_tz", default = "system_tz")] 37 | pub timezone: TimeZone, 38 | 39 | /// Ask GTK to make the label this wide. This way as the text changes, the label's size can stay static. 40 | #[serde(default)] 41 | pub label_width: u32, 42 | } 43 | 44 | fn weekday_and_24h_time() -> String { 45 | "%a %H:%M".into() 46 | } 47 | 48 | const fn half_second() -> Duration { 49 | Duration::from_millis(500) 50 | } 51 | 52 | fn system_tz() -> TimeZone { 53 | TimeZone::system() 54 | } 55 | 56 | const fn label_width() -> u32 { 57 | 150 58 | } 59 | 60 | impl Default for ClockConfig { 61 | fn default() -> Self { 62 | Self { 63 | format: weekday_and_24h_time(), 64 | resolution: half_second(), 65 | timezone: system_tz(), 66 | label_width: label_width(), 67 | } 68 | } 69 | } 70 | 71 | fn parse_tz<'de, D>(data: D) -> Result 72 | where 73 | D: Deserializer<'de>, 74 | { 75 | struct TimeZoneVisitor; 76 | impl Visitor<'_> for TimeZoneVisitor { 77 | type Value = TimeZone; 78 | 79 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 80 | formatter.write_str("a string containing an IANA Time Zone name") 81 | } 82 | 83 | fn visit_str(self, time_zone_name: &str) -> Result 84 | where 85 | E: de::Error, 86 | { 87 | Ok(TimeZone::get(time_zone_name).unwrap_or_else(|e| { 88 | error!("Invalid timezone '{time_zone_name}' in the config: {e}"); 89 | TimeZone::system() 90 | })) 91 | } 92 | } 93 | 94 | data.deserialize_any(TimeZoneVisitor) 95 | } 96 | 97 | #[derive(Debug)] 98 | pub struct Clock { 99 | format: String, 100 | timezone: TimeZone, 101 | 102 | current_time: String, 103 | } 104 | 105 | /// A fixed-interval command output. 106 | /// 107 | /// The duration between the ticks may be skewed by various factors such as the command future not being polled, so the 108 | /// current time should be measured and formatted when the tick is recieved. 109 | #[derive(Debug)] 110 | pub struct Tick; 111 | 112 | #[relm4::component(pub)] 113 | impl Component for Clock { 114 | type Init = ClockConfig; 115 | type Input = (); 116 | type Output = (); 117 | type CommandOutput = Tick; 118 | 119 | view! { 120 | gtk::Label { 121 | set_width_request: label_width.min(i32::MAX as u32) as i32, 122 | 123 | #[watch] 124 | set_text: &model.current_time 125 | } 126 | } 127 | 128 | fn init( 129 | ClockConfig { 130 | format, 131 | resolution, 132 | timezone, 133 | label_width, 134 | }: Self::Init, 135 | root: Self::Root, 136 | sender: ComponentSender, 137 | ) -> ComponentParts { 138 | sender.command(move |sender, shutdown| { 139 | shutdown 140 | .register(async move { 141 | loop { 142 | if sender.send(Tick).is_err() { 143 | error!("No longer updating the clock widget because `send` failed"); 144 | break; 145 | } 146 | sleep(resolution).await; 147 | } 148 | }) 149 | .drop_on_shutdown() 150 | }); 151 | 152 | let model = Self { 153 | current_time: String::new(), 154 | format, 155 | timezone, 156 | }; 157 | 158 | let widgets = view_output!(); 159 | 160 | ComponentParts { model, widgets } 161 | } 162 | 163 | fn update_cmd(&mut self, Tick: Self::CommandOutput, _: ComponentSender, _: &Self::Root) { 164 | let now = Zoned::new(Timestamp::now(), self.timezone.clone()); 165 | 166 | let text = match jiff::fmt::strtime::format(&self.format, &now) { 167 | Ok(str) => str, 168 | Err(_) => format(weekday_and_24h_time(), &now) 169 | .unwrap_or_else(|_| "Time formatting error.".into()), 170 | }; 171 | 172 | self.current_time = text; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | mod cache; 6 | mod client; 7 | mod config; 8 | mod constants; 9 | mod gui; 10 | mod sysutil; 11 | mod tomlutils; 12 | 13 | use std::fs::{create_dir_all, OpenOptions}; 14 | use std::io::{Result as IoResult, Write}; 15 | use std::path::{Path, PathBuf}; 16 | 17 | use clap::{Parser, ValueEnum}; 18 | use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate}; 19 | use tracing::subscriber::set_global_default; 20 | use tracing_appender::{non_blocking, non_blocking::WorkerGuard}; 21 | use tracing_subscriber::{ 22 | filter::LevelFilter, fmt::layer, fmt::time::OffsetTime, layer::SubscriberExt, 23 | }; 24 | 25 | use crate::constants::{APP_ID, CONFIG_PATH, CSS_PATH, LOG_PATH}; 26 | use crate::gui::{Greeter, GreeterInit}; 27 | 28 | #[macro_use] 29 | extern crate tracing; 30 | #[macro_use] 31 | extern crate lazy_static; 32 | #[macro_use] 33 | extern crate const_format; 34 | 35 | #[cfg(test)] 36 | #[macro_use] 37 | extern crate test_case; 38 | 39 | const MAX_LOG_FILES: usize = 3; 40 | const MAX_LOG_SIZE: usize = 1024 * 1024; 41 | 42 | #[derive(Clone, Debug, ValueEnum)] 43 | enum LogLevel { 44 | Off, 45 | Error, 46 | Warn, 47 | Info, 48 | Debug, 49 | Trace, 50 | } 51 | 52 | #[derive(Parser, Debug)] 53 | #[command(author, version, about)] 54 | struct Args { 55 | /// The path to the log file 56 | #[arg(short = 'l', long, value_name = "PATH", default_value = LOG_PATH)] 57 | logs: PathBuf, 58 | 59 | /// The verbosity level of the logs 60 | #[arg(short = 'L', long, value_name = "LEVEL", default_value = "info")] 61 | log_level: LogLevel, 62 | 63 | /// Output all logs to stdout 64 | #[arg(short, long)] 65 | verbose: bool, 66 | 67 | /// The path to the config file 68 | #[arg(short, long, value_name = "PATH", default_value = CONFIG_PATH)] 69 | config: PathBuf, 70 | 71 | /// The path to the custom CSS stylesheet 72 | #[arg(short, long, value_name = "PATH", default_value = CSS_PATH)] 73 | style: PathBuf, 74 | 75 | /// Run in demo mode 76 | #[arg(long)] 77 | demo: bool, 78 | } 79 | 80 | fn main() { 81 | let args = Args::parse(); 82 | // Keep the guard alive till the end of the function, since logging depends on this. 83 | let _guard = init_logging(&args.logs, &args.log_level, args.verbose); 84 | 85 | let app = relm4::RelmApp::new(APP_ID); 86 | app.with_args(vec![]).run_async::(GreeterInit { 87 | config_path: args.config, 88 | css_path: args.style, 89 | demo: args.demo, 90 | }); 91 | } 92 | 93 | /// Initialize the log file with file rotation. 94 | fn setup_log_file(log_path: &Path) -> IoResult> { 95 | if !log_path.exists() { 96 | if let Some(log_dir) = log_path.parent() { 97 | create_dir_all(log_dir)?; 98 | }; 99 | }; 100 | 101 | // Manually write to the log file, since `FileRotate` will silently fail if the log file can't 102 | // be written to. 103 | let mut file = OpenOptions::new() 104 | .create(true) 105 | .append(true) 106 | .open(log_path)?; 107 | file.write_all(&[])?; 108 | 109 | Ok(FileRotate::new( 110 | log_path, 111 | AppendCount::new(MAX_LOG_FILES), 112 | ContentLimit::Bytes(MAX_LOG_SIZE), 113 | Compression::OnRotate(0), 114 | None, 115 | )) 116 | } 117 | 118 | /// Initialize logging with file rotation. 119 | fn init_logging(log_path: &Path, log_level: &LogLevel, stdout: bool) -> Vec { 120 | // Parse the log level string. 121 | let filter = match log_level { 122 | LogLevel::Off => LevelFilter::OFF, 123 | LogLevel::Error => LevelFilter::ERROR, 124 | LogLevel::Warn => LevelFilter::WARN, 125 | LogLevel::Info => LevelFilter::INFO, 126 | LogLevel::Debug => LevelFilter::DEBUG, 127 | LogLevel::Trace => LevelFilter::TRACE, 128 | }; 129 | 130 | // Load the timer before spawning threads, otherwise getting the local time offset will fail. 131 | let timer = OffsetTime::local_rfc_3339().expect("Couldn't get local time offset"); 132 | 133 | // Set up the logger. 134 | let builder = tracing_subscriber::fmt() 135 | .with_max_level(filter) 136 | // The timer could be reused later. 137 | .with_timer(timer.clone()); 138 | 139 | // Log in a separate non-blocking thread, then return the guard (otherise the non-blocking 140 | // writer will immediately stop). 141 | let mut guards = Vec::new(); 142 | match setup_log_file(log_path) { 143 | Ok(file) => { 144 | let (file, guard) = non_blocking(file); 145 | guards.push(guard); 146 | let builder = builder 147 | .with_writer(file) 148 | // Disable colouring through ANSI escape sequences in log files. 149 | .with_ansi(false); 150 | 151 | if stdout { 152 | let (stdout, guard) = non_blocking(std::io::stdout()); 153 | guards.push(guard); 154 | set_global_default( 155 | builder 156 | .finish() 157 | .with(layer().with_writer(stdout).with_timer(timer)), 158 | ) 159 | .unwrap(); 160 | } else { 161 | builder.init(); 162 | }; 163 | } 164 | Err(file_err) => { 165 | let (file, guard) = non_blocking(std::io::stdout()); 166 | guards.push(guard); 167 | builder.with_writer(file).init(); 168 | tracing::error!("Couldn't create log file '{LOG_PATH}': {file_err}"); 169 | } 170 | }; 171 | 172 | // Log all panics in the log file as well as stderr. 173 | std::panic::set_hook(Box::new(|panic| { 174 | tracing::error!("{panic}"); 175 | eprintln!("{panic}"); 176 | })); 177 | 178 | guards 179 | } 180 | -------------------------------------------------------------------------------- /src/sysutil.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Helper for system utilities like users and sessions 6 | 7 | use std::collections::{HashMap, HashSet}; 8 | use std::env; 9 | use std::fs::{read, read_to_string}; 10 | use std::io; 11 | use std::ops::ControlFlow; 12 | use std::path::Path; 13 | use std::str::from_utf8; 14 | 15 | use glob::glob; 16 | use pwd::Passwd; 17 | use regex::Regex; 18 | use shlex::Shlex; 19 | 20 | use crate::config::Config; 21 | use crate::constants::{LOGIN_DEFS_PATHS, LOGIN_DEFS_UID_MAX, LOGIN_DEFS_UID_MIN, SESSION_DIRS}; 22 | 23 | /// XDG data directory variable name (parent directory for X11/Wayland sessions) 24 | const XDG_DIR_ENV_VAR: &str = "XDG_DATA_DIRS"; 25 | 26 | #[derive(Clone, Copy)] 27 | pub enum SessionType { 28 | X11, 29 | Wayland, 30 | Unknown, 31 | } 32 | 33 | #[derive(Clone)] 34 | pub struct SessionInfo { 35 | pub command: Vec, 36 | pub sess_type: SessionType, 37 | } 38 | 39 | // Convenient aliases for used maps 40 | type UserMap = HashMap; 41 | type ShellMap = HashMap>; 42 | type SessionMap = HashMap; 43 | 44 | /// Stores info of all regular users and sessions 45 | pub struct SysUtil { 46 | /// Maps a user's full name to their system username 47 | users: UserMap, 48 | /// Maps a system username to their shell 49 | shells: ShellMap, 50 | /// Maps a session's full name to its command 51 | sessions: SessionMap, 52 | } 53 | 54 | impl SysUtil { 55 | pub fn new(config: &Config) -> io::Result { 56 | let path = (*LOGIN_DEFS_PATHS).iter().try_for_each(|path| { 57 | if let Ok(true) = AsRef::::as_ref(&path).try_exists() { 58 | ControlFlow::Break(path) 59 | } else { 60 | ControlFlow::Continue(()) 61 | } 62 | }); 63 | 64 | let normal_user = match path { 65 | ControlFlow::Break(path) => read_to_string(path) 66 | .map_err(|err| { 67 | warn!("Failed to read login.defs from '{path}', using default values: {err}") 68 | }) 69 | .map(|text| NormalUser::parse_login_defs(&text)) 70 | .unwrap_or_default(), 71 | ControlFlow::Continue(()) => { 72 | warn!("`login.defs` file not found in these paths: {LOGIN_DEFS_PATHS:?}",); 73 | 74 | NormalUser::default() 75 | } 76 | }; 77 | 78 | debug!("{normal_user:?}"); 79 | 80 | let (users, shells) = Self::init_users(normal_user)?; 81 | Ok(Self { 82 | users, 83 | shells, 84 | sessions: Self::init_sessions(config)?, 85 | }) 86 | } 87 | 88 | /// Get the list of regular users. 89 | /// 90 | /// These are defined as a list of users with UID between `UID_MIN` and `UID_MAX`. 91 | fn init_users(normal_user: NormalUser) -> io::Result<(UserMap, ShellMap)> { 92 | let mut users = HashMap::new(); 93 | let mut shells = HashMap::new(); 94 | 95 | for entry in Passwd::iter().filter(|entry| normal_user.is_normal_user(entry.uid)) { 96 | // Use the actual system username if the "full name" is not available. 97 | let full_name = if let Some(gecos) = entry.gecos { 98 | if gecos.is_empty() { 99 | debug!( 100 | "Found user '{}' with UID '{}' and empty full name", 101 | entry.name, entry.uid 102 | ); 103 | entry.name.clone() 104 | } else { 105 | // Only take first entry in gecos field. 106 | let gecos_name_part: &str = gecos.split(',').next().unwrap_or(&gecos); 107 | debug!( 108 | "Found user '{}' with UID '{}' and full name: {gecos_name_part}", 109 | entry.name, entry.uid 110 | ); 111 | gecos_name_part.into() 112 | } 113 | } else { 114 | debug!( 115 | "Found user '{}' with UID '{}' and missing full name", 116 | entry.name, entry.uid 117 | ); 118 | entry.name.clone() 119 | }; 120 | users.insert(full_name, entry.name.clone()); 121 | 122 | if let Some(cmd) = shlex::split(entry.shell.as_str()) { 123 | shells.insert(entry.name, cmd); 124 | } else { 125 | // Skip this user, since a missing command means that we can't use it. 126 | warn!( 127 | "Couldn't split shell of username '{}' into arguments: {}", 128 | entry.name, entry.shell 129 | ); 130 | }; 131 | } 132 | 133 | Ok((users, shells)) 134 | } 135 | 136 | /// Get available X11 and Wayland sessions. 137 | /// 138 | /// These are defined as either X11 or Wayland session desktop files stored in specific 139 | /// directories. 140 | fn init_sessions(config: &Config) -> io::Result { 141 | let mut found_session_names = HashSet::new(); 142 | let mut sessions = HashMap::new(); 143 | 144 | // Use the XDG spec if available, else use the one that's compiled. 145 | // The XDG env var can change after compilation in some distros like NixOS. 146 | let session_dirs = if let Ok(sess_parent_dirs) = env::var(XDG_DIR_ENV_VAR) { 147 | debug!("Found XDG env var {XDG_DIR_ENV_VAR}: {sess_parent_dirs}"); 148 | match sess_parent_dirs 149 | .split(':') 150 | .map(|parent_dir| format!("{parent_dir}/xsessions:{parent_dir}/wayland-sessions")) 151 | .reduce(|a, b| a + ":" + &b) 152 | { 153 | None => SESSION_DIRS.to_string(), 154 | Some(dirs) => dirs, 155 | } 156 | } else { 157 | SESSION_DIRS.to_string() 158 | }; 159 | 160 | for sess_dir in session_dirs.split(':') { 161 | let sess_dir_path = Path::new(sess_dir); 162 | let sess_parent_dir = if let Some(sess_parent_dir) = sess_dir_path.parent() { 163 | sess_parent_dir 164 | } else { 165 | warn!("Session directory does not have a parent: {sess_dir}"); 166 | continue; 167 | }; 168 | 169 | let is_x11 = if let Some(name) = sess_dir_path.file_name() { 170 | name == "xsessions" 171 | } else { 172 | false 173 | }; 174 | let cmd_prefix = if is_x11 { 175 | Some(&config.get_sys_commands().x11_prefix) 176 | } else { 177 | None 178 | }; 179 | 180 | debug!("Checking session directory: {sess_dir}"); 181 | // Iterate over all '.desktop' files. 182 | for glob_path in glob(&format!("{sess_dir}/*.desktop")) 183 | .expect("Invalid glob pattern for session desktop files") 184 | { 185 | let path = match glob_path { 186 | Ok(path) => path, 187 | Err(err) => { 188 | warn!("Error when globbing: {err}"); 189 | continue; 190 | } 191 | }; 192 | info!("Now scanning session file: {}", path.display()); 193 | 194 | let contents = read(&path)?; 195 | let text = from_utf8(contents.as_slice()).unwrap_or_else(|err| { 196 | panic!("Session file '{}' is not UTF-8: {}", path.display(), err) 197 | }); 198 | 199 | let fname_and_type = match path.strip_prefix(sess_parent_dir) { 200 | Ok(fname_and_type) => fname_and_type.to_owned(), 201 | Err(err) => { 202 | warn!("Error with file name: {err}"); 203 | continue; 204 | } 205 | }; 206 | 207 | if found_session_names.contains(&fname_and_type) { 208 | debug!( 209 | "{fname_and_type:?} was already found elsewhere, skipping {}", 210 | path.display() 211 | ); 212 | continue; 213 | }; 214 | 215 | // The session launch command is specified as: Exec=command arg1 arg2... 216 | let cmd_regex = 217 | Regex::new(r"Exec=(.*)").expect("Invalid regex for session command"); 218 | // The session name is specified as: Name=My Session 219 | let name_regex = Regex::new(r"Name=(.*)").expect("Invalid regex for session name"); 220 | 221 | // Hiding could be either as Hidden=true or NoDisplay=true 222 | let hidden_regex = Regex::new(r"Hidden=(.*)").expect("Invalid regex for hidden"); 223 | let no_display_regex = 224 | Regex::new(r"NoDisplay=(.*)").expect("Invalid regex for no display"); 225 | 226 | let hidden: bool = if let Some(hidden_str) = hidden_regex 227 | .captures(text) 228 | .and_then(|capture| capture.get(1)) 229 | { 230 | hidden_str.as_str().parse().unwrap_or(false) 231 | } else { 232 | false 233 | }; 234 | 235 | let no_display: bool = if let Some(no_display_str) = no_display_regex 236 | .captures(text) 237 | .and_then(|capture| capture.get(1)) 238 | { 239 | no_display_str.as_str().parse().unwrap_or(false) 240 | } else { 241 | false 242 | }; 243 | 244 | if hidden | no_display { 245 | found_session_names.insert(fname_and_type); 246 | continue; 247 | }; 248 | 249 | // Parse the desktop file to get the session command. 250 | let cmd = if let Some(cmd_str) = 251 | cmd_regex.captures(text).and_then(|capture| capture.get(1)) 252 | { 253 | let mut cmd = if let Some(prefix) = cmd_prefix { 254 | prefix.clone() 255 | } else { 256 | Vec::new() 257 | }; 258 | let prefix_len = cmd.len(); 259 | cmd.extend(Shlex::new(cmd_str.as_str())); 260 | if cmd.len() > prefix_len { 261 | cmd 262 | } else { 263 | warn!( 264 | "Couldn't split command of '{}' into arguments: {}", 265 | path.display(), 266 | cmd_str.as_str() 267 | ); 268 | // Skip the desktop file, since a missing command means that we can't 269 | // use it. 270 | continue; 271 | } 272 | } else { 273 | warn!("No command found for session: {}", path.display()); 274 | // Skip the desktop file, since a missing command means that we can't use it. 275 | continue; 276 | }; 277 | 278 | // Get the full name of this session. 279 | let name = if let Some(name) = 280 | name_regex.captures(text).and_then(|capture| capture.get(1)) 281 | { 282 | debug!( 283 | "Found name '{}' for session '{}' with command '{:?}'", 284 | name.as_str(), 285 | path.display(), 286 | cmd 287 | ); 288 | name.as_str() 289 | } else if let Some(stem) = path.file_stem() { 290 | // Get the stem of the filename of this desktop file. 291 | // This is used as backup, in case the file name doesn't exist. 292 | if let Some(stem) = stem.to_str() { 293 | debug!( 294 | "Using file stem '{stem}', since no name was found for session: {}", 295 | path.display() 296 | ); 297 | stem 298 | } else { 299 | warn!("Non-UTF-8 file stem in session file: {}", path.display()); 300 | // No way to display this session name, so just skip it. 301 | continue; 302 | } 303 | } else { 304 | warn!("No file stem found for session: {}", path.display()); 305 | // No file stem implies no file name, which shouldn't happen. 306 | // Since there's no full name nor file stem, just skip this anomalous 307 | // session. 308 | continue; 309 | }; 310 | found_session_names.insert(fname_and_type); 311 | sessions.insert( 312 | name.to_string(), 313 | SessionInfo { 314 | command: cmd, 315 | sess_type: if is_x11 { 316 | SessionType::X11 317 | } else { 318 | SessionType::Wayland 319 | }, 320 | }, 321 | ); 322 | } 323 | } 324 | 325 | Ok(sessions) 326 | } 327 | 328 | /// Get the mapping of a user's full name to their system username. 329 | /// 330 | /// If the full name is not available, their system username is used. 331 | pub fn get_users(&self) -> &UserMap { 332 | &self.users 333 | } 334 | 335 | /// Get the mapping of a system username to their shell. 336 | pub fn get_shells(&self) -> &ShellMap { 337 | &self.shells 338 | } 339 | 340 | /// Get the mapping of a session's full name to its command. 341 | /// 342 | /// If the full name is not available, the filename stem is used. 343 | pub fn get_sessions(&self) -> &SessionMap { 344 | &self.sessions 345 | } 346 | } 347 | 348 | /// A named tuple of min and max that stores UID limits for normal users. 349 | /// 350 | /// Use [`Self::parse_login_defs`] to obtain the system configuration. If the file is missing or there are 351 | /// parsing errors a fallback of [`Self::default`] should be used. 352 | #[derive(Debug, PartialEq, Eq)] 353 | struct NormalUser { 354 | uid_min: u64, 355 | uid_max: u64, 356 | } 357 | 358 | impl Default for NormalUser { 359 | fn default() -> Self { 360 | Self { 361 | uid_min: *LOGIN_DEFS_UID_MIN, 362 | uid_max: *LOGIN_DEFS_UID_MAX, 363 | } 364 | } 365 | } 366 | 367 | impl NormalUser { 368 | /// Parses the `login.defs` file content and looks for `UID_MIN` and `UID_MAX` definitions. If a definition is 369 | /// missing or causes parsing errors, the default values [`struct@LOGIN_DEFS_UID_MIN`] and 370 | /// [`struct@LOGIN_DEFS_UID_MAX`] are used. 371 | /// 372 | /// This parser is highly specific to parsing the 2 required values, thus it focuses on doing the least amout of 373 | /// compute required to extracting them. 374 | /// 375 | /// Errors are dropped because they are unlikely and their handling would result in the use of default values 376 | /// anyway. 377 | pub fn parse_login_defs(text: &str) -> Self { 378 | let mut min = None; 379 | let mut max = None; 380 | 381 | for line in text.lines().map(str::trim) { 382 | const KEY_LENGTH: usize = "UID_XXX".len(); 383 | 384 | // At MSRV 1.80 you could use `split_at_checked`, this is just a way to not raise it. 385 | // This checks if the string is of sufficient length too. 386 | if !line.is_char_boundary(KEY_LENGTH) { 387 | continue; 388 | } 389 | let (key, val) = line.split_at(KEY_LENGTH); 390 | 391 | if !val.starts_with(char::is_whitespace) { 392 | continue; 393 | } 394 | 395 | match (key, min, max) { 396 | ("UID_MIN", None, _) => min = Self::parse_number(val), 397 | ("UID_MAX", _, None) => max = Self::parse_number(val), 398 | _ => continue, 399 | } 400 | 401 | if min.is_some() && max.is_some() { 402 | break; 403 | } 404 | } 405 | 406 | Self { 407 | uid_min: min.unwrap_or(*LOGIN_DEFS_UID_MIN), 408 | uid_max: max.unwrap_or(*LOGIN_DEFS_UID_MAX), 409 | } 410 | } 411 | 412 | /// Parses a number value in a `/etc/login.defs` entry. As per the manpage: 413 | /// 414 | /// - `0x` prefix: hex number 415 | /// - `0` prefix: octal number 416 | /// - starts with `1..9`: decimal number 417 | /// 418 | /// In case the string value is not parsable as a number the entry value is considered invalid and `None` is 419 | /// returned. 420 | fn parse_number(num: &str) -> Option { 421 | let num = num.trim(); 422 | if num == "0" { 423 | return Some(0); 424 | } 425 | 426 | if let Some(octal) = num.strip_prefix('0') { 427 | if let Some(hex) = octal.strip_prefix('x') { 428 | return u64::from_str_radix(hex, 16).ok(); 429 | } 430 | 431 | return u64::from_str_radix(octal, 8).ok(); 432 | } 433 | 434 | num.parse().ok() 435 | } 436 | 437 | // Returns true for regular users, false for those outside the UID limit, eg. git or root. 438 | pub fn is_normal_user(&self, uid: T) -> bool 439 | where 440 | T: Into, 441 | { 442 | (self.uid_min..=self.uid_max).contains(&uid.into()) 443 | } 444 | } 445 | 446 | #[cfg(test)] 447 | mod tests { 448 | #[allow(non_snake_case)] 449 | mod UidLimit { 450 | use super::super::*; 451 | 452 | #[test_case( 453 | &["UID_MIN 1", "UID_MAX 10"].join("\n") 454 | => NormalUser { uid_min: 1, uid_max: 10 }; 455 | "both configured" 456 | )] 457 | #[test_case( 458 | &["UID_MAX 10", "UID_MIN 1"].join("\n") 459 | => NormalUser { uid_min: 1, uid_max: 10 }; 460 | "reverse order" 461 | )] 462 | #[test_case( 463 | &["OTHER 20", 464 | "# Comment", 465 | "", 466 | "UID_MAX 10", 467 | "UID_MIN 1", 468 | "MORE_TEXT 40"].join("\n") 469 | => NormalUser { uid_min: 1, uid_max: 10 }; 470 | "complex file" 471 | )] 472 | #[test_case( 473 | "UID_MAX10" 474 | => NormalUser::default(); 475 | "no space" 476 | )] 477 | #[test_case( 478 | "SUB_UID_MAX 10" 479 | => NormalUser::default(); 480 | "invalid field (with prefix)" 481 | )] 482 | #[test_case( 483 | "UID_MAX_BLAH 10" 484 | => NormalUser::default(); 485 | "invalid field (with suffix)" 486 | )] 487 | fn parse_login_defs(text: &str) -> NormalUser { 488 | NormalUser::parse_login_defs(text) 489 | } 490 | 491 | #[test_case("" => None; "empty")] 492 | #[test_case("no" => None; "string")] 493 | #[test_case("0" => Some(0); "zero")] 494 | #[test_case("0x" => None; "0x isn't a hex number")] 495 | #[test_case("10" => Some(10); "decimal")] 496 | #[test_case("0777" => Some(0o777); "octal")] 497 | #[test_case("0xDeadBeef" => Some(0xdead_beef); "hex")] 498 | fn parse_number(num: &str) -> Option { 499 | NormalUser::parse_number(num) 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/tomlutils.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Harish Rajagopal 2 | // 3 | // SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | //! Convenient TOML loading utilities 6 | 7 | use std::ffi::OsStr; 8 | use std::fs::read; 9 | use std::path::Path; 10 | 11 | use serde::de::DeserializeOwned; 12 | 13 | /// Contains possible errors when loading/saving TOML from/to disk 14 | #[derive(thiserror::Error, Debug)] 15 | pub enum TomlFileError { 16 | #[error("I/O error")] 17 | IO(#[from] std::io::Error), 18 | #[error("Error decoding UTF-8")] 19 | Utf8(#[from] std::str::Utf8Error), 20 | #[error("Error decoding TOML file contents")] 21 | TomlDecode(#[from] toml::de::Error), 22 | #[error("Error encoding into TOML")] 23 | TomlEncode(#[from] toml::ser::Error), 24 | } 25 | 26 | pub type TomlFileResult = Result; 27 | 28 | /// Load the TOML file from disk without any checks. 29 | fn load_raw_toml(path: &Path) -> TomlFileResult { 30 | Ok(toml::from_str(std::str::from_utf8( 31 | read(path)?.as_slice(), 32 | )?)?) 33 | } 34 | 35 | /// Load the TOML file from disk. 36 | /// 37 | /// If loading fails, then this returns the default value of the struct. 38 | pub fn load_toml(path: &P) -> R 39 | where 40 | P: AsRef + ?Sized, 41 | R: DeserializeOwned + Default, 42 | { 43 | let path = Path::new(path); 44 | if path.exists() { 45 | match load_raw_toml(path) { 46 | Ok(item) => { 47 | info!("Loaded TOML file: {}", path.display()); 48 | item 49 | } 50 | Err(err) => { 51 | warn!("Error loading TOML file '{}': {err}", path.display()); 52 | R::default() 53 | } 54 | } 55 | } else { 56 | warn!("Missing TOML file: {}", path.display()); 57 | R::default() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /systemd-tmpfiles.conf: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Harish Rajagopal 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # Create the log and state directories. 6 | d /var/log/regreet 0755 greeter greeter - - 7 | d /var/lib/regreet 0755 greeter greeter - - 8 | --------------------------------------------------------------------------------