diff --git a/README.md b/README.md index 271c0f1..13e5ffe 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,19 @@ The foundation for many embedded graphical display implementations. `ubuntu-fram The application you choose (or provide) gets a fullscreen window (or windows) and input from touch, keyboard and mouse without needing to deal with the specific hardware. +## Connections +```sh +snap connect ubuntu-frame:desktop-launch +``` + ## Configuration -There are three snap configuration options: +There are four snap configuration options: * `daemon=[true|false]` enables the daemon (defaults to false on classic systems) * `config=` * `display=` +* `launcher=[true|false]` The configuration options are described in detail in [the Ubuntu Frame reference](https://mir-server.io/docs/reference). diff --git a/launcher/.gitignore b/launcher/.gitignore new file mode 100644 index 0000000..008f408 --- /dev/null +++ b/launcher/.gitignore @@ -0,0 +1,29 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# dotenv environment variables file +.env* + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +.flutter-plugins +.flutter-plugins-dependencies + +linux/flutter/ephemeral \ No newline at end of file diff --git a/launcher/LICENSE b/launcher/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/launcher/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/launcher/README.md b/launcher/README.md new file mode 100644 index 0000000..a49c36f --- /dev/null +++ b/launcher/README.md @@ -0,0 +1,21 @@ +# ubuntu-frame-launcher +A flutter-based launcher for Ubuntu Frame. + +## Install +```sh +flutter pub get +``` + +## Development +1 . Run `ubuntu-frame`: + +```sh +WAYLAND_DISPLAY=wayland-98 ubuntu-frame --add-wayland-extensions zwlr_layer_shell_v1:zwlr_foreign_toplevel_manager_v1 +``` + +2. Run `launcher`: + +```sh +cd launcher +WAYLAND_DISPLAY=wayland-98 flutter run +``` \ No newline at end of file diff --git a/launcher/analysis_options.yaml b/launcher/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/launcher/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/launcher/lib/controllers/application_controller.dart b/launcher/lib/controllers/application_controller.dart new file mode 100644 index 0000000..6014563 --- /dev/null +++ b/launcher/lib/controllers/application_controller.dart @@ -0,0 +1,84 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:ubuntu_frame_launcher/models/desktop_file.dart'; +import 'package:ubuntu_frame_launcher/models/window_event.dart'; +import 'package:ubuntu_frame_launcher/services/window_service.dart'; + +import '../services/desktop_file_manager.dart'; + +class ApplicationController { + final WindowService windowService; + final DesktopFileManager desktopFileManager; + + final _controller = StreamController>.broadcast(); + late Future> _installedAppFuture; + final _logger = Logger("ApplicationController"); + + ApplicationController(this.windowService, this.desktopFileManager) { + _installedAppFuture = _loadInstalledApplications(); + + windowService.watcher.listen((event) async { + if (event.eventType == WindowEventType.created || + event.eventType == WindowEventType.removed) { + _controller.add(await getOpenApplications()); + } + }); + } + + Future> _loadInstalledApplications() async { + final applications = await desktopFileManager.getAllDesktopFiles(); + applications.sort((a, b) { + final aName = a.name?.toLowerCase() ?? ''; + final bName = b.name?.toLowerCase() ?? ''; + return aName.compareTo(bName); + }); + return applications; + } + + /// Returns a list of applications installed on the system. This list + /// is guaranteed to be sorted alphabetically by name. + Future> getInstalledApplications() async { + // TODO: Watch directories and dynamically reload if they change + return await _installedAppFuture; + } + + void focus(DesktopFile file) { + final handle = windowService.getWindowById(file.id); + if (handle != null) { + handle.activate(); + return; + } + + _logger.shout("Unable to focus desktop file with id = ${file.id}}"); + } + + Stream> getOpenedAppsStream() { + return _controller.stream; + } + + Future> getOpenApplications() async { + final List result = []; + for (final handle in windowService.getWindowList()) { + result.add(await desktopFileManager.resolveId(handle.getAppId())); + } + + return result; + } +} diff --git a/launcher/lib/controllers/window_controller.dart b/launcher/lib/controllers/window_controller.dart new file mode 100644 index 0000000..64a2b87 --- /dev/null +++ b/launcher/lib/controllers/window_controller.dart @@ -0,0 +1,38 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:ubuntu_frame_launcher/models/desktop_file.dart'; +import 'package:ubuntu_frame_launcher/services/window_handle.dart'; + +import '../services/window_service.dart'; + +class WindowController { + WindowService service; + + WindowController(this.service); + + WindowHandle? getWindowById(String appId) { + return service.getWindowById(appId); + } + + List getWindowList() { + return service.getWindowList(); + } + + bool isOpen(DesktopFile file) { + return service.getWindowById(file.id) != null; + } +} diff --git a/launcher/lib/main.dart b/launcher/lib/main.dart new file mode 100644 index 0000000..9399a1d --- /dev/null +++ b/launcher/lib/main.dart @@ -0,0 +1,87 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:ubuntu_frame_launcher/services/wayland_window_watcher.dart'; +import 'package:ubuntu_frame_launcher/controllers/window_controller.dart'; +import 'package:ubuntu_frame_launcher/services/window_service.dart'; +import 'services/desktop_file_manager.dart'; +import 'package:get_it/get_it.dart'; +import 'views/dock.dart'; +import 'controllers/application_controller.dart'; +import 'package:logging/logging.dart'; + +void main() async { + Logger.root.level = Level.ALL; // defaults to Level.INFO + Logger.root.onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }); + + final logger = Logger("main"); + logger.info("Ubuntu Frame launcher is starting"); + + final getIt = GetIt.instance; + + // Services + getIt.registerLazySingleton( + () => WindowService(WaylandWindowWatcherService())); + getIt.registerLazySingleton(() => DesktopFileManager()); + + // Controllers + getIt.registerLazySingleton(() => + ApplicationController( + getIt.get(), getIt.get())); + getIt.registerLazySingleton( + () => WindowController(getIt.get())); + + runApp(const LauncherApp()); +} + +class LauncherApp extends StatelessWidget { + const LauncherApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.black), + primaryColor: Colors.black, + useMaterial3: true, + fontFamily: 'Ubuntu', + iconTheme: const IconThemeData(color: Colors.white, size: 20), + textTheme: const TextTheme( + bodyMedium: TextStyle( + color: Colors.white, + ), + )), + home: const Launcher(), + ); + } +} + +class Launcher extends StatelessWidget { + const Launcher({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Dock()], + )); + } +} diff --git a/launcher/lib/models/application_desktop_file.dart b/launcher/lib/models/application_desktop_file.dart new file mode 100644 index 0000000..59a0319 --- /dev/null +++ b/launcher/lib/models/application_desktop_file.dart @@ -0,0 +1,122 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:ini/ini.dart'; +import 'package:logging/logging.dart'; +import 'dart:io'; + +import 'desktop_file.dart'; + +/// Represents an application that can be loaded from the fileystem +class ApplicationFileDesktopFile extends DesktopFile { + final String applicationPath; + final String identifier; + late Logger _logger; + Config config = Config(); + final launcherChannel = const MethodChannel('launcher'); + String? _displayName; + + ApplicationFileDesktopFile(this.applicationPath, this.identifier) { + _logger = Logger("DesktopFile $id"); + } + + Future load() async { + try { + final file = File(applicationPath); + config = Config.fromStrings(await file.readAsLines()); + return true; + } catch (e) { + _logger + .shout("Failed to load application from path $applicationPath: $e"); + return false; + } + } + + @override + String get id { + return identifier; + } + + @override + String get path { + return applicationPath; + } + + @override + String? get type { + return _getDesktopEntry('Type'); + } + + String? get domain { + return _getDesktopEntry('X-Ubuntu-Gettext-Domain'); + } + + @override + String? get name { + return _getDesktopEntry('Name'); + } + + @override + String get icon { + return _getDesktopEntry('Icon') ?? "application-x-executable"; + } + + @override + bool get noDisplay { + return _parseBool(_getDesktopEntry('NoDisplay')); + } + + @override + String? get exec { + return _getDesktopEntry('Exec'); + } + + String? _getDesktopEntry(String key) { + return config.get('Desktop Entry', key); + } + + @override + bool get singleMainWindow { + return _parseBool(_getDesktopEntry("SingleMainWindow")) || + _parseBool(_getDesktopEntry("X-GNOME-SingleWindow")); + } + + List? get onlyShowIn { + return _parseStringList(_getDesktopEntry("OnlyShowIn")); + } + + static bool _parseBool(String? value) { + return (value ?? "false") == "true"; + } + + static List? _parseStringList(String? value) { + if (value == null) { + return null; + } + + return value.split(";"); + } + + @override + void launch() { + launcherChannel.invokeMethod('launch', path); + } + + @override + String toString() => '$runtimeType($path)'; +} diff --git a/launcher/lib/models/desktop_file.dart b/launcher/lib/models/desktop_file.dart new file mode 100644 index 0000000..b6ad8a0 --- /dev/null +++ b/launcher/lib/models/desktop_file.dart @@ -0,0 +1,27 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +abstract class DesktopFile { + String get id; + String? get path; + String? get type; + String? get name; + String get icon; + bool get noDisplay; + String? get exec; + bool get singleMainWindow; + void launch(); +} diff --git a/launcher/lib/models/null_desktop_file.dart b/launcher/lib/models/null_desktop_file.dart new file mode 100644 index 0000000..5777a31 --- /dev/null +++ b/launcher/lib/models/null_desktop_file.dart @@ -0,0 +1,49 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:ubuntu_frame_launcher/models/desktop_file.dart'; + +class NullDesktopFile extends DesktopFile { + final String appId; + NullDesktopFile(this.appId); + + @override + String? get exec => null; + + @override + String get icon => "application-x-executable"; + + @override + String get id => appId; + + @override + void launch() {} + + @override + String? get name => appId; + + @override + bool get noDisplay => false; + + @override + String? get path => null; + + @override + bool get singleMainWindow => false; + + @override + String? get type => null; +} diff --git a/launcher/lib/models/window_event.dart b/launcher/lib/models/window_event.dart new file mode 100644 index 0000000..26e1073 --- /dev/null +++ b/launcher/lib/models/window_event.dart @@ -0,0 +1,27 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:ubuntu_frame_launcher/services/window_handle.dart'; + +enum WindowEventType { created, removed, focused, appId } + +class WindowEvent { + WindowEventType eventType; + WindowHandle? handle; + String? appId; + + WindowEvent(this.eventType, [this.handle, this.appId]); +} diff --git a/launcher/lib/services/desktop_file_manager.dart b/launcher/lib/services/desktop_file_manager.dart new file mode 100644 index 0000000..ae0cc10 --- /dev/null +++ b/launcher/lib/services/desktop_file_manager.dart @@ -0,0 +1,139 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:ubuntu_frame_launcher/models/application_desktop_file.dart'; +import 'package:ubuntu_frame_launcher/models/desktop_file.dart'; +import 'package:ubuntu_frame_launcher/models/null_desktop_file.dart'; +import 'package:path/path.dart' as p; +import 'package:xdg_directories/xdg_directories.dart'; + +class DesktopFileManager { + late Future> _filePromise; + final Map _idToApp = {}; + final List _desktopEnvironmentList = []; + final _logger = Logger("DesktopFileManager"); + + DesktopFileManager() { + final environment = Platform.environment["XDG_CURRENT_DESKTOP"]; + if (environment != null) { + for (final env in environment.split(':')) { + _desktopEnvironmentList.add(env); + } + } + _filePromise = _loadInternal(); + } + + Future> getAllDesktopFiles() async { + return await _filePromise; + } + + Future> resolveIdList(List idToList) async { + List result = []; + for (final id in idToList) { + result.add(await resolveId(id)); + } + return result; + } + + Future resolveId(String id) async { + _logger.info("Resolving desktop file for id: $id"); + if (_idToApp.containsKey(id)) { + return _idToApp[id]!; + } + + for (final dataDir in dataDirs) { + final f = File(p.join(dataDir.path, 'applications', id)); + final app = await _loadFromPath(f, false); + if (app != null) { + _idToApp[id] = app; + _logger.info("Found desktop file for id: $id"); + return app; + } + } + + _logger.warning("Creating null desktop file for id: $id"); + final nullFile = NullDesktopFile(id); + _idToApp[id] = nullFile; + return nullFile; + } + + Future> _loadInternal() async { + final desktopFiles = []; + for (final dataDir in dataDirs) { + final dir = Directory(p.join(dataDir.path, 'applications')); + if (!await dir.exists()) { + continue; + } + + _logger.info("Loading desktop file directory: ${dir.path}"); + await for (final f in dir.list()) { + _logger.info("Loading desktop file: ${f.path}"); + final app = await _loadFromPath(f, true); + if (app != null) { + _logger.info("Loaded desktop file: ${app.id}"); + desktopFiles.add(app); + _idToApp[app.id] = app; + } + } + } + + return desktopFiles; + } + + Future _loadFromPath( + FileSystemEntity f, bool ensureEnvironmentValidity) async { + // TODO: Handle file overrides, i.e. the same desktop file in different locations + if (!await f.exists()) { + return null; + } + + if (!f.path.endsWith('.desktop')) { + return null; + } + + final desktopFile = ApplicationFileDesktopFile( + f.path, Uri.parse(f.path).path.split("/").last); + if (!await desktopFile.load()) { + _logger.shout("Unable to load desktop file: ${f.path}"); + return null; + } + + if (ensureEnvironmentValidity) { + if (desktopFile.noDisplay) { + return null; + } + + final desktopEnvironmentList = desktopFile.onlyShowIn; + if (desktopEnvironmentList != null) { + for (final env in desktopEnvironmentList) { + if (_desktopEnvironmentList.contains(env)) { + _logger.info( + "Desktop file is valid in the provided environment: $env"); + return desktopFile; + } + } + + return null; + } + } + + _logger.info("Desktop file is valued: ${desktopFile.id}"); + return desktopFile; + } +} diff --git a/launcher/lib/services/wayland_window_watcher.dart b/launcher/lib/services/wayland_window_watcher.dart new file mode 100644 index 0000000..a924997 --- /dev/null +++ b/launcher/lib/services/wayland_window_watcher.dart @@ -0,0 +1,284 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:ubuntu_frame_launcher/models/window_event.dart'; +import 'package:ubuntu_frame_launcher/services/window_handle.dart'; + +import 'window_watcher_service.dart'; +import 'package:wayland/wayland.dart'; +import 'package:logging/logging.dart'; + +class WlrForeignTopLevelManager extends WaylandObject { + @override + String get interfaceName => "zwlr_foreign_toplevel_manager_v1"; + + Function(int)? onTopLevel; + Function()? onFinished; + + static const _closeRequest = 0; + + WlrForeignTopLevelManager(WaylandClient client, int id, + {this.onTopLevel, this.onFinished}) + : super(client, id); + + void stop() { + client.sendRequest(id, _closeRequest); + } + + @override + bool processEvent(int code, Uint8List payload) { + const topLevelEvent = 0; + const finishedEvent = 1; + + switch (code) { + case topLevelEvent: + var buffer = WaylandReadBuffer(payload); + var id = buffer.readUint(); + onTopLevel?.call(id); + return true; + case finishedEvent: + onFinished?.call(); + return true; + default: + return false; + } + } +} + +class WlrForeignToplevelHandleV1 extends WaylandObject implements WindowHandle { + final _logger = Logger("WlrForeignToplevelHandleV1"); + + @override + String get interfaceName => "zwlr_foreign_toplevel_handle_v1"; + + WaylandSeat? seat; + Function(WlrForeignToplevelHandleV1, String)? onName; + Function(WlrForeignToplevelHandleV1, String)? onAppId; + Function(WlrForeignToplevelHandleV1, WaylandOutput)? onOutputEnter; + Function(WlrForeignToplevelHandleV1, WaylandOutput)? onOutputLeave; + Function(WlrForeignToplevelHandleV1, List)? onState; + Function(WlrForeignToplevelHandleV1)? onDone; + Function(WlrForeignToplevelHandleV1)? onClosed; + + String name = ""; + String appId = ""; + String desktopAppId = ""; + + static const _maximizedRequest = 0; + static const _unsetMaximizedRequest = 1; + static const _minimizedRequest = 2; + static const _unsetMinimizedRequest = 3; + static const _activateRequest = 4; + static const _closeRequest = 5; + static const _setRectangleRequest = 6; + static const _destroyRequest = 7; + static const _setFullscreenRequest = 8; + static const _unsetFullscreenRequest = 9; + + WlrForeignToplevelHandleV1(WaylandClient client, int id, this.seat, + {this.onName, + this.onAppId, + this.onOutputEnter, + this.onOutputLeave, + this.onState, + this.onDone, + this.onClosed}) + : super(client, id); + + @override + void setMaximized() { + client.sendRequest(id, _maximizedRequest); + } + + @override + void unsetMaximized() { + client.sendRequest(id, _unsetMaximizedRequest); + } + + @override + void setMinimized() { + client.sendRequest(id, _minimizedRequest); + } + + @override + void unsetMinimized() { + client.sendRequest(id, _unsetMinimizedRequest); + } + + @override + void activate() { + if (seat == null) { + _logger.shout("Unable to activate the window because the seat is null"); + return; + } + + _logger.info("Activating window, handle_id=$id, id=$appId"); + final payload = WaylandWriteBuffer(); + payload.writeUint(seat!.id); + client.sendRequest(id, _activateRequest, payload.data); + } + + @override + void close() { + client.sendRequest(id, _closeRequest); + } + + void setRectangle( + WaylandSurface surface, int x, int y, int width, int height) { + var payload = WaylandWriteBuffer(); + payload.writeUint(surface.id); + payload.writeInt(x); + payload.writeInt(y); + payload.writeInt(width); + payload.writeInt(height); + client.sendRequest(id, _setRectangleRequest); + } + + void destroy() { + client.sendRequest(id, _destroyRequest); + } + + @override + void setFullscreen() { + client.sendRequest(id, _setFullscreenRequest); + } + + @override + void unsetFullscreen() { + client.sendRequest(id, _unsetFullscreenRequest); + } + + @override + String getAppId() { + return appId; + } + + @override + bool processEvent(int code, Uint8List payload) { + const nameEvent = 0; + const appIdEvent = 1; + const outputEnterEvent = 2; + const outputLeaveEvent = 3; + const statesEvent = 4; + const doneEvent = 5; + const closedEvent = 6; + + switch (code) { + case nameEvent: + var buffer = WaylandReadBuffer(payload); + name = buffer.readString(); + onName?.call(this, name); + return true; + case appIdEvent: + var buffer = WaylandReadBuffer(payload); + appId = buffer.readString(); + onAppId?.call(this, appId); + return true; + case outputEnterEvent: + var buffer = WaylandReadBuffer(payload); + var id = buffer.readUint(); + onOutputEnter?.call(this, WaylandOutput(client, id)); + return true; + case outputLeaveEvent: + var buffer = WaylandReadBuffer(payload); + var id = buffer.readUint(); + onOutputLeave?.call(this, WaylandOutput(client, id)); + return true; + case statesEvent: + var buffer = WaylandReadBuffer(payload); + var states = buffer.readUintArray(); + onState?.call(this, states); + return true; + case doneEvent: + onDone?.call(this); + return true; + case closedEvent: + onClosed?.call(this); + return true; + default: + return false; + } + } +} + +class WaylandWindowWatcherService extends WindowWatcherService { + final client = WaylandClient(); + late WaylandRegistry registry; + WlrForeignTopLevelManager? foreignTopLevelManager; + WaylandSeat? seat; + final _logger = Logger("WaylandWindowWatcherService"); + + WaylandWindowWatcherService() { + connect(); + } + + Future connect() async { + await client.connect(); + registry = client.getRegistry(onGlobal: _onGlobal); + } + + Future close() async { + await client.close(); + } + + void _onGlobal(int name, String interface, int version) { + if (interface == "zwlr_foreign_toplevel_manager_v1") { + foreignTopLevelManager = WlrForeignTopLevelManager( + client, registry.bind(name, interface, min(version, 2)), + onTopLevel: _onTopLevel); + } else if (interface == "wl_seat") { + seat = + WaylandSeat(client, registry.bind(name, interface, min(version, 4))); + } + } + + void _onTopLevel(int topLevel) { + final handle = WlrForeignToplevelHandleV1(client, topLevel, seat, + onClosed: _onWindowRemoved, + onState: _onWindowState, + onAppId: _onWindowAppId); + + _onWindowCreated(handle); + } + + void _onWindowRemoved(WlrForeignToplevelHandleV1 handle) { + _logger.info("Window closed: ${handle.id}"); + controller.add(WindowEvent(WindowEventType.removed, handle)); + } + + void _onWindowCreated(WlrForeignToplevelHandleV1 handle) { + _logger.info("Window opened: ${handle.id}"); + controller.add(WindowEvent(WindowEventType.created, handle)); + } + + void _onWindowState(WlrForeignToplevelHandleV1 handle, List state) { + const applicationFocused = 2; + if (state.contains(applicationFocused)) { + _logger + .info("Focused window, handle_id=${handle.id}, id=${handle.appId}"); + controller.add(WindowEvent(WindowEventType.focused, handle)); + } + } + + void _onWindowAppId(WlrForeignToplevelHandleV1 handle, String appId) { + _logger + .info("Received app id for window, handle_id=${handle.id}, id=$appId"); + controller.add(WindowEvent(WindowEventType.appId, handle, appId)); + } +} diff --git a/launcher/lib/services/window_handle.dart b/launcher/lib/services/window_handle.dart new file mode 100644 index 0000000..736e197 --- /dev/null +++ b/launcher/lib/services/window_handle.dart @@ -0,0 +1,27 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +abstract class WindowHandle { + void setMaximized(); + void unsetMaximized(); + void setMinimized(); + void unsetMinimized(); + void activate(); + void close(); + String getAppId(); + void setFullscreen(); + void unsetFullscreen(); +} diff --git a/launcher/lib/services/window_service.dart b/launcher/lib/services/window_service.dart new file mode 100644 index 0000000..25bff84 --- /dev/null +++ b/launcher/lib/services/window_service.dart @@ -0,0 +1,97 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import '../models/window_event.dart'; +import 'window_handle.dart'; +import 'window_watcher_service.dart'; + +class WindowService { + final WindowWatcherService watcher; + final List _windowList = []; + + WindowService(this.watcher) { + watcher.listen((event) { + switch (event.eventType) { + case WindowEventType.focused: + _onWindowFocused(event.handle!); + break; + case WindowEventType.removed: + _onWindowRemoved(event.handle!); + break; + case WindowEventType.created: + _onWindowCreated(event.handle!); + break; + default: + break; + } + }); + } + + void _onWindowCreated(WindowHandle handle) { + _windowList.add(handle); + } + + void _onWindowRemoved(WindowHandle handle) { + _windowList.remove(handle); + } + + void _onWindowFocused(WindowHandle handle) { + // Algoritm: Swap this handle to the position of the first instance + // of this DesktopFile in the list, such that it is first + // in the focus order AND the window creation order is maintained. + int firtInstanceIndex = -1; + int handleIndex = -1; + for (int i = 0; i < _windowList.length; i++) { + if (firtInstanceIndex < 0 && + _windowList[i].getAppId() == handle.getAppId()) { + firtInstanceIndex = i; + } + if (handleIndex < 0 && _windowList[i] == handle) { + handleIndex = i; + } + + if (handleIndex > -1 && firtInstanceIndex > -1) { + break; + } + } + + if (handleIndex < 0 || firtInstanceIndex < 0) { + return; + } + + final temp = _windowList[handleIndex]; + _windowList[handleIndex] = _windowList[firtInstanceIndex]; + _windowList[firtInstanceIndex] = temp; + } + + List getWindowList() { + // This function returns a copy of the list to combat the + // possibility that the caller will use async functions + // while the _windowList gets modified in the background. + return [..._windowList]; + } + + WindowHandle? getWindowById(String appId) { + final windowList = getWindowList(); + for (int i = 0; i < windowList.length; i++) { + final handle = windowList[i]; + if (handle.getAppId() == appId) { + return handle; + } + } + return null; + } +} diff --git a/launcher/lib/services/window_watcher_service.dart b/launcher/lib/services/window_watcher_service.dart new file mode 100644 index 0000000..2039601 --- /dev/null +++ b/launcher/lib/services/window_watcher_service.dart @@ -0,0 +1,31 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:ubuntu_frame_launcher/models/window_event.dart'; + +class WindowWatcherService extends Stream { + final controller = StreamController.broadcast(); + + @override + StreamSubscription listen(Function(WindowEvent event)? onData, + {Function? onError, VoidCallback? onDone, bool? cancelOnError}) { + return controller.stream.listen(onData, + onDone: onDone, onError: onError, cancelOnError: cancelOnError); + } +} diff --git a/launcher/lib/views/app_icon.dart b/launcher/lib/views/app_icon.dart new file mode 100644 index 0000000..38b5eef --- /dev/null +++ b/launcher/lib/views/app_icon.dart @@ -0,0 +1,52 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:xdg_icons/xdg_icons.dart'; +import 'package:path/path.dart' as path; +import 'package:flutter_svg/flutter_svg.dart'; + +class AppIcon extends StatelessWidget { + final String iconName; + final int size; + + const AppIcon({super.key, required this.iconName, required this.size}); + + @override + Widget build(BuildContext context) { + final extension = path.extension(iconName); + if (extension == ".png" || extension == ".jpeg" || extension == ".jpg") { + return Image( + image: AssetImage(iconName), + width: size.toDouble(), + height: size.toDouble()); + } + + if (extension == ".svg") { + return SvgPicture.asset(iconName, + semanticsLabel: iconName, + width: size.toDouble(), + height: size.toDouble()); + } + + return XdgIcon( + name: iconName, + size: size, + iconNotFoundBuilder: () { + return XdgIcon(name: "application-x-executable", size: size); + }); + } +} diff --git a/launcher/lib/views/dock.dart b/launcher/lib/views/dock.dart new file mode 100644 index 0000000..90ac191 --- /dev/null +++ b/launcher/lib/views/dock.dart @@ -0,0 +1,55 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:ubuntu_frame_launcher/controllers/application_controller.dart'; +import 'package:ubuntu_frame_launcher/views/dock_button.dart'; +import 'package:ubuntu_frame_launcher/views/stream_builder_with_future_initial_value.dart'; + +const double _dockWidthPx = 70; +const _dockPadding = EdgeInsets.fromLTRB(3, 6, 3, 6); + +class Dock extends StatelessWidget { + final applicationControler = GetIt.instance.get(); + + Dock({super.key}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + color: Colors.black, + width: _dockWidthPx, + padding: _dockPadding, + child: StreamBuilderWithFutureInitialValue( + future: applicationControler.getOpenApplications(), + stream: applicationControler.getOpenedAppsStream(), + loader: const Row(), + builder: (context, openedApps) { + List dockButtons = []; + for (final opened in openedApps) { + if (opened.id.isEmpty) { + continue; + } + dockButtons.add(DockButton(desktopFile: opened)); + } + + return SingleChildScrollView( + child: Column(children: dockButtons)); + }))); + } +} diff --git a/launcher/lib/views/dock_button.dart b/launcher/lib/views/dock_button.dart new file mode 100644 index 0000000..f895375 --- /dev/null +++ b/launcher/lib/views/dock_button.dart @@ -0,0 +1,50 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:ubuntu_frame_launcher/models/desktop_file.dart'; +import 'package:ubuntu_frame_launcher/views/app_icon.dart'; +import 'package:ubuntu_frame_launcher/controllers/application_controller.dart'; +import 'package:ubuntu_frame_launcher/controllers/window_controller.dart'; +import 'launcher_button.dart'; + +class DockButton extends StatefulWidget { + final DesktopFile desktopFile; + + const DockButton({super.key, required this.desktopFile}); + + @override + State createState() => _DockButtonState(); +} + +class _DockButtonState extends State { + @override + Widget build(BuildContext context) { + return LauncherButton( + onPressed: _onLeftClick, + child: AppIcon(iconName: widget.desktopFile.icon ?? '', size: 48), + ); + } + + void _onLeftClick() { + final windowController = GetIt.instance.get(); + final applicationController = GetIt.instance.get(); + if (windowController.isOpen(widget.desktopFile)) { + applicationController.focus(widget.desktopFile); + } + } +} diff --git a/launcher/lib/views/launcher_button.dart b/launcher/lib/views/launcher_button.dart new file mode 100644 index 0000000..f5edc23 --- /dev/null +++ b/launcher/lib/views/launcher_button.dart @@ -0,0 +1,72 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; + +class LauncherButton extends StatefulWidget { + final Widget child; + final bool active; + final VoidCallback onPressed; + final VoidCallback? onMiddleClick; + + const LauncherButton( + {super.key, + required this.child, + required this.onPressed, + this.onMiddleClick, + this.active = false}); + + @override + State createState() => _LauncherButtonState(); +} + +class _LauncherButtonState extends State { + bool mouseOver = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) => setMouseOver(true), + onExit: (event) => setMouseOver(false), + child: GestureDetector( + onTap: widget.onPressed, + onTertiaryTapDown: (details) => widget.onMiddleClick?.call(), + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Color.fromARGB( + widget.active + ? 127 + : mouseOver + ? 63 + : 0, + 255, + 255, + 255), + ), + alignment: Alignment.center, + child: widget.child, + ), + ), + ); + } + + void setMouseOver(bool mouseOver_) { + setState(() => mouseOver = mouseOver_); + } +} diff --git a/launcher/lib/views/stream_builder_with_future_initial_value.dart b/launcher/lib/views/stream_builder_with_future_initial_value.dart new file mode 100644 index 0000000..9fd227a --- /dev/null +++ b/launcher/lib/views/stream_builder_with_future_initial_value.dart @@ -0,0 +1,58 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; + +typedef Builder = Widget Function(BuildContext context, T futureValue); + +/// This widget loads a future as an initial value and then streams +/// subsequent updates to the data from the StreamBuilder. This is useful +/// if you want to load some initial value asynchronously and update the +/// value from a stream afterwards. +class StreamBuilderWithFutureInitialValue extends StatelessWidget { + final Future? future; + final Stream? stream; + final Widget loader; + final Builder builder; + + const StreamBuilderWithFutureInitialValue( + {super.key, + required this.future, + required this.stream, + required this.loader, + required this.builder}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot futureSnapshot) { + if (futureSnapshot.data == null) { + return loader; + } + + return StreamBuilder( + stream: stream, + builder: (BuildContext context, AsyncSnapshot streamSnapshot) { + if (streamSnapshot.hasData) { + return builder(context, streamSnapshot.data!); + } else { + return builder(context, futureSnapshot.data!); + } + }); + }); + } +} diff --git a/launcher/linux/CMakeLists.txt b/launcher/linux/CMakeLists.txt new file mode 100644 index 0000000..8bc6640 --- /dev/null +++ b/launcher/linux/CMakeLists.txt @@ -0,0 +1,143 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "ubuntu_frame_launcher") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.canonical.ubuntu_frame_launcher") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +set(CMAKE_CXX_STANDARD 23) + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GTKLayerShell REQUIRED IMPORTED_TARGET gtk-layer-shell-0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTKLayerShell) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/launcher/linux/flutter/CMakeLists.txt b/launcher/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/launcher/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/launcher/linux/flutter/generated_plugin_registrant.cc b/launcher/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..3a86145 --- /dev/null +++ b/launcher/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) xdg_icons_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "XdgIconsPlugin"); + xdg_icons_plugin_register_with_registrar(xdg_icons_registrar); +} diff --git a/launcher/linux/flutter/generated_plugin_registrant.h b/launcher/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/launcher/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/launcher/linux/flutter/generated_plugins.cmake b/launcher/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..8195fd1 --- /dev/null +++ b/launcher/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + xdg_icons +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/launcher/linux/main.cc b/launcher/linux/main.cc new file mode 100644 index 0000000..8e86925 --- /dev/null +++ b/launcher/linux/main.cc @@ -0,0 +1,22 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "my_application.h" + +int main(int argc, char **argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/launcher/linux/my_application.cc b/launcher/linux/my_application.cc new file mode 100644 index 0000000..9673942 --- /dev/null +++ b/launcher/linux/my_application.cc @@ -0,0 +1,105 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "my_application.h" + +#include +#include + +#include "flutter/generated_plugin_registrant.h" + +/// TODO: This is hardcoded for now +const gint LAUNCHER_SIZE_PX = 70; + +struct _MyApplication { + GtkApplication parent_instance; + GtkWindow *window; + char **dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +static void my_application_activate(GApplication *application) { + MyApplication *self = MY_APPLICATION(application); + GtkWindow *window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + gtk_layer_init_for_window(window); + gtk_layer_set_anchor(window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE); + gtk_layer_set_anchor(window, GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); + gtk_layer_set_anchor(window, GTK_LAYER_SHELL_EDGE_TOP, TRUE); + gtk_layer_auto_exclusive_zone_enable(window); + gtk_layer_set_layer(window, GTK_LAYER_SHELL_LAYER_TOP); + gtk_layer_set_exclusive_zone(window, LAUNCHER_SIZE_PX); + + // This is required as of gtk-layer-shell v0.8.1, but future releases + // shouldn't need it See https://github.com/wmww/gtk-layer-shell/pull/166 for + // details + gtk_widget_set_size_request(GTK_WIDGET(window), LAUNCHER_SIZE_PX, -1); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView *view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + self->window = window; + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static gboolean my_application_local_command_line(GApplication *application, + gchar ***arguments, + int *exit_status) { + MyApplication *self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +static void my_application_dispose(GObject *object) { + MyApplication *self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass *klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication *self) {} + +MyApplication *my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/launcher/linux/my_application.h b/launcher/linux/my_application.h new file mode 100644 index 0000000..2b28468 --- /dev/null +++ b/launcher/linux/my_application.h @@ -0,0 +1,34 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication *my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/launcher/pubspec.yaml b/launcher/pubspec.yaml new file mode 100644 index 0000000..135928a --- /dev/null +++ b/launcher/pubspec.yaml @@ -0,0 +1,32 @@ +name: ubuntu_frame_launcher +description: A launcher for Ubuntu Frame in Flutter +publish_to: 'none' + +version: 0.0.1 + +environment: + sdk: '>=3.0.5 <4.0.0' + +dependencies: + flutter: + sdk: flutter + ini: ^2.1.0 + path: ^1.8.3 + xdg_directories: ^1.0.4 + xdg_icons: ^0.0.5 + wayland: + git: + url: https://github.com/robert-ancell/wayland.dart + get_it: ^7.6.4 + logging: ^1.2.0 + flutter_svg: ^2.0.10+1 + +dev_dependencies: + mockito: ^5.4.3 + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + test: ^1.24.9 + +flutter: + uses-material-design: true diff --git a/launcher/test/window_service_test.dart b/launcher/test/window_service_test.dart new file mode 100644 index 0000000..b78ee4b --- /dev/null +++ b/launcher/test/window_service_test.dart @@ -0,0 +1,68 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:ubuntu_frame_launcher/models/window_event.dart'; +import 'package:ubuntu_frame_launcher/services/window_handle.dart'; +import 'package:ubuntu_frame_launcher/services/window_service.dart'; +import 'package:ubuntu_frame_launcher/services/window_watcher_service.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +class MockWindowWatcherService extends WindowWatcherService {} + +class MockWindowHandle extends Mock implements WindowHandle { + String appId; + + MockWindowHandle(this.appId); + + @override + String getAppId() { + return appId; + } +} + +void main() { + late WindowWatcherService watcher; + late WindowService windowService; + + setUp(() { + watcher = MockWindowWatcherService(); + windowService = WindowService(watcher); + }); + + test( + "When a window is focused, then it is swapped with the first occurence of the window with the same app id in the list", + () async { + const firstAppId = "first"; + const secondAppId = "second"; + MockWindowHandle firstHandle = MockWindowHandle(firstAppId); + MockWindowHandle secondHandle = MockWindowHandle(secondAppId); + MockWindowHandle thirdHandle = MockWindowHandle(firstAppId); + + watcher.controller.add(WindowEvent(WindowEventType.created, firstHandle)); + watcher.controller.add(WindowEvent(WindowEventType.created, secondHandle)); + watcher.controller.add(WindowEvent(WindowEventType.created, thirdHandle)); + + watcher.controller.add(WindowEvent(WindowEventType.focused, thirdHandle)); + + // We need to give the asynchronous "listen" time to run its events + await Future.delayed(const Duration(milliseconds: 0)); + + expect(windowService.getWindowList()[0], thirdHandle); + expect(windowService.getWindowList()[1], secondHandle); + expect(windowService.getWindowList()[2], firstHandle); + }); +} diff --git a/launcher/ubuntu_frame_launcher.fake b/launcher/ubuntu_frame_launcher.fake new file mode 100755 index 0000000..554fb3c --- /dev/null +++ b/launcher/ubuntu_frame_launcher.fake @@ -0,0 +1,3 @@ +#!/bin/sh +echo "This app is only available on amd64 and arm64" +exit 1 \ No newline at end of file diff --git a/scripts/bin/wayland-launch b/scripts/bin/wayland-launch new file mode 100755 index 0000000..8a50b38 --- /dev/null +++ b/scripts/bin/wayland-launch @@ -0,0 +1,46 @@ +#!/bin/sh +set -e + +for PLUG in desktop-launch opengl wayplug; do + if ! snapctl is-connected ${PLUG} + then + echo "WARNING: ${PLUG} interface not connected" + fi +done + +if ! command -v inotifywait > /dev/null +then + echo "ERROR: inotifywait could not be found, ubuntu-frame expects:" + echo " . . : stage-packages:" + echo " . . : - inotify-tools" + exit 1 +fi + +wait_for() +{ + until + until + inotifywait --event create "$(dirname "$1")"& + inotify_pid=$! + [ -e "$1" ] || sleep 2 && [ -e "$1" ] + do + wait "${inotify_pid}" + done + kill "${inotify_pid}" + [ -O "$1" ] + do + sleep 1 + done +} + +real_xdg_runtime_dir=$(dirname "${XDG_RUNTIME_DIR}") +export WAYLAND_DISPLAY="${real_xdg_runtime_dir}/${WAYLAND_DISPLAY:-wayland-0}" + +# On core systems may need to wait for real XDG_RUNTIME_DIR +wait_for "${real_xdg_runtime_dir}" +wait_for "${WAYLAND_DISPLAY}" + +mkdir -p "$XDG_RUNTIME_DIR" -m 700 +unset DISPLAY + +exec "$@" \ No newline at end of file diff --git a/snap/hooks/configure b/snap/hooks/configure index 7a613c6..662e413 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -110,3 +110,24 @@ if [ "$(snapctl get daemon)" = "true" ]; then else snapctl stop --disable "$SNAP_INSTANCE_NAME.daemon" 2>&1 || true fi + +# Start or stop the launcher +if [ "$(snapctl get launcher)" = "true" ]; then + ARCH=$(uname -m) + if [[ "$ARCH" != @(x86_64*|i*86|aarch64) ]]; then + echo "Cannot enable the launcher on $ARCH system" + exit 1 + fi + + if ! snapctl is-connected desktop-launch; then + echo "Please connect the "desktop-launch" interface:" + echo " sudo snap connect $SNAP_INTERFACE_NAME:desktop-launch" + exit 2 + fi + + if snapctl services "$SNAP_INSTANCE_NAME.launcher" | grep -q inactive; then + snapctl start --enable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true + fi +else + snapctl stop --disable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true +fi diff --git a/snap/hooks/disconnect-plug-desktop-launch b/snap/hooks/disconnect-plug-desktop-launch new file mode 100755 index 0000000..959e216 --- /dev/null +++ b/snap/hooks/disconnect-plug-desktop-launch @@ -0,0 +1,6 @@ +#!/bin/sh +set -eux + +# Disable the launcher to avoid subsequent configure issues +snapctl stop --disable $SNAP_INSTANCE_NAME.launcher +snapctl set launcher=false diff --git a/snap/local/plugs.json b/snap/local/plugs.json new file mode 100644 index 0000000..ecc5f70 --- /dev/null +++ b/snap/local/plugs.json @@ -0,0 +1,5 @@ +{ + "desktop-launch": { + "allow-installation": "true" + } +} \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 874a003..1bcc0ff 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -46,8 +46,27 @@ apps: - x11 # to run on a host X11 server - network-bind # to run via X-forwarding (e.g. development in a container/VM) - hardware-observe # libinput likes to scan udev + - desktop-launch slots: - wayland + environment: + XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop + + launcher: + command-chain: + - bin/wayland-launch + - bin/gpu-2404-wrapper + command: ubuntu_frame_launcher + daemon: simple + restart-delay: 3s + restart-condition: always + install-mode: disable + plugs: + - opengl + - desktop-launch + - wayplug + environment: + XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop daemon: command-chain: @@ -61,9 +80,11 @@ apps: # XDG config XDG_CONFIG_HOME: $SNAP_DATA HOME: $SNAP_DATA + XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop plugs: - hardware-observe # libinput likes to scan udev - opengl + - desktop-launch slots: - wayland @@ -75,15 +96,15 @@ apps: command: bin/screenshot plugs: - opengl - slots: - # Should be a plug but one snap can't have both the wayland slot and plug. This works. - - wayland + - wayplug plugs: gpu-2404: interface: content target: $SNAP/gpu-2404 default-provider: mesa-2404 + wayplug: + interface: wayland slots: ubuntu-frame-diagnostic: @@ -168,6 +189,7 @@ parts: - icons - scripts - grim + - launcher source: https://github.com/canonical/gpu-snap.git plugin: dump override-prime: | @@ -177,3 +199,30 @@ parts: prime: - bin/gpu-2404-wrapper + + launcher: + source: launcher + plugin: nil + build-packages: + - clang + - curl + - git + - cmake + - ninja-build + - unzip + - libgtk-layer-shell-dev + - libgtk-3-dev + stage-packages: + - libgtk-3-0t64 + - libgtk-layer-shell0 + override-build: | + if [[ "${CRAFT_ARCH_BUILD_FOR}" != @(amd64|arm64) ]]; then + install --mode 755 --no-target-directory ubuntu_frame_launcher.fake ${CRAFT_PART_INSTALL}/ubuntu_frame_launcher + else + git clone -b 3.19.6 https://github.com/flutter/flutter.git ${CRAFT_PART_BUILD}/flutter-distro + export PATH=${CRAFT_PART_BUILD}/flutter-distro/bin:$PATH + flutter config --no-enable-{web,ios,android,fuchsia,windows-desktop,macos-desktop} + flutter pub get + flutter build linux --release --verbose --target lib/main.dart + cp -r build/linux/*/release/bundle/* ${CRAFT_PART_INSTALL} + fi diff --git a/src/frame_authorization.cpp b/src/frame_authorization.cpp index 953c290..ada59df 100644 --- a/src/frame_authorization.cpp +++ b/src/frame_authorization.cpp @@ -35,6 +35,8 @@ AuthModel const auth_model{{ }}, {"ubuntu-frame", { WaylandExtensions::zwlr_screencopy_manager_v1, + WaylandExtensions::zwlr_layer_shell_v1, + WaylandExtensions::zwlr_foreign_toplevel_manager_v1 }}, }};