From 9417e1523a042f124bac082d33280e252fc98db7 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Jun 2024 08:46:11 -0400 Subject: [PATCH 01/19] feature: providing a minimal launcher written in flutter for ubuntu frame --- README.md | 8 +- launcher/.gitignore | 29 + launcher/LICENSE | 674 ++++++++++++++++++ launcher/README.md | 20 + launcher/analysis_options.yaml | 29 + .../controllers/application_controller.dart | 75 ++ .../lib/controllers/window_controller.dart | 22 + launcher/lib/main.dart | 71 ++ .../lib/models/application_desktop_file.dart | 107 +++ launcher/lib/models/desktop_file.dart | 11 + launcher/lib/models/null_desktop_file.dart | 33 + launcher/lib/models/window_event.dart | 11 + .../lib/services/desktop_file_manager.dart | 114 +++ .../lib/services/wayland_window_watcher.dart | 260 +++++++ launcher/lib/services/window_handle.dart | 11 + launcher/lib/services/window_service.dart | 81 +++ .../lib/services/window_watcher_service.dart | 15 + launcher/lib/views/app_icon.dart | 36 + launcher/lib/views/dock.dart | 39 + launcher/lib/views/dock_button.dart | 34 + launcher/lib/views/launcher_button.dart | 56 ++ ...eam_builder_with_future_initial_value.dart | 42 ++ launcher/linux/CMakeLists.txt | 143 ++++ launcher/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + launcher/linux/main.cc | 6 + launcher/linux/my_application.cc | 89 +++ launcher/linux/my_application.h | 18 + launcher/pubspec.yaml | 32 + launcher/test/window_service_test.dart | 52 ++ scripts/bin/initialise-display-config | 2 - snap/hooks/configure | 11 + snap/snapcraft.yaml | 38 +- src/frame_authorization.cpp | 2 + 36 files changed, 2307 insertions(+), 6 deletions(-) create mode 100644 launcher/.gitignore create mode 100644 launcher/LICENSE create mode 100644 launcher/README.md create mode 100644 launcher/analysis_options.yaml create mode 100644 launcher/lib/controllers/application_controller.dart create mode 100644 launcher/lib/controllers/window_controller.dart create mode 100644 launcher/lib/main.dart create mode 100644 launcher/lib/models/application_desktop_file.dart create mode 100644 launcher/lib/models/desktop_file.dart create mode 100644 launcher/lib/models/null_desktop_file.dart create mode 100644 launcher/lib/models/window_event.dart create mode 100644 launcher/lib/services/desktop_file_manager.dart create mode 100644 launcher/lib/services/wayland_window_watcher.dart create mode 100644 launcher/lib/services/window_handle.dart create mode 100644 launcher/lib/services/window_service.dart create mode 100644 launcher/lib/services/window_watcher_service.dart create mode 100644 launcher/lib/views/app_icon.dart create mode 100644 launcher/lib/views/dock.dart create mode 100644 launcher/lib/views/dock_button.dart create mode 100644 launcher/lib/views/launcher_button.dart create mode 100644 launcher/lib/views/stream_builder_with_future_initial_value.dart create mode 100644 launcher/linux/CMakeLists.txt create mode 100644 launcher/linux/flutter/CMakeLists.txt create mode 100644 launcher/linux/flutter/generated_plugin_registrant.cc create mode 100644 launcher/linux/flutter/generated_plugin_registrant.h create mode 100644 launcher/linux/flutter/generated_plugins.cmake create mode 100644 launcher/linux/main.cc create mode 100644 launcher/linux/my_application.cc create mode 100644 launcher/linux/my_application.h create mode 100644 launcher/pubspec.yaml create mode 100644 launcher/test/window_service_test.dart 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..fc8b4e3 --- /dev/null +++ b/launcher/README.md @@ -0,0 +1,20 @@ +# ubuntu-frame-launcher +A flutter-based launcher for Ubuntu Frame. + +## Install +```sh +flutter pub get +``` + +## Development +1 . Run `miral-shell`: + +```sh +WAYLAND_DISPLAY=wayland-98 miral-shell --add-wayland-extensions zwlr_layer_shell_v1:zwlr_foreign_toplevel_manager_v1 +``` + +2. Run `ubuntu-frame-launcher`: + +```sh +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..15e0ed6 --- /dev/null +++ b/launcher/lib/controllers/application_controller.dart @@ -0,0 +1,75 @@ +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; + } + + static DesktopFile? findDesktopFile( + List list, DesktopFile file) { + return list + .cast() + .firstWhere((other) => other!.id == file.id, orElse: () => null); + } +} diff --git a/launcher/lib/controllers/window_controller.dart b/launcher/lib/controllers/window_controller.dart new file mode 100644 index 0000000..2e42814 --- /dev/null +++ b/launcher/lib/controllers/window_controller.dart @@ -0,0 +1,22 @@ +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..2c0aeb5 --- /dev/null +++ b/launcher/lib/main.dart @@ -0,0 +1,71 @@ +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 log = Logger("main"); + log.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..f0fbda2 --- /dev/null +++ b/launcher/lib/models/application_desktop_file.dart @@ -0,0 +1,107 @@ +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..2e0a59d --- /dev/null +++ b/launcher/lib/models/desktop_file.dart @@ -0,0 +1,11 @@ +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..30879bf --- /dev/null +++ b/launcher/lib/models/null_desktop_file.dart @@ -0,0 +1,33 @@ +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..43656fc --- /dev/null +++ b/launcher/lib/models/window_event.dart @@ -0,0 +1,11 @@ +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..0b204ce --- /dev/null +++ b/launcher/lib/services/desktop_file_manager.dart @@ -0,0 +1,114 @@ +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 { + 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; + return app; + } + } + + 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; + } + + await for (final f in dir.list()) { + final app = await _loadFromPath(f, true); + if (app != null) { + 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)) { + return desktopFile; + } + } + + return null; + } + } + + 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..8503e2b --- /dev/null +++ b/launcher/lib/services/wayland_window_watcher.dart @@ -0,0 +1,260 @@ +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; + } + + var 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; + + 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) { + controller.add(WindowEvent(WindowEventType.removed, handle)); + } + + void _onWindowCreated(WlrForeignToplevelHandleV1 handle) { + controller.add(WindowEvent(WindowEventType.created, handle)); + } + + void _onWindowState(WlrForeignToplevelHandleV1 handle, List state) { + const applicationFocused = 2; + if (state.contains(applicationFocused)) { + controller.add(WindowEvent(WindowEventType.focused, handle)); + } + } + + void _onWindowAppId(WlrForeignToplevelHandleV1 handle, String 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..7b0a58f --- /dev/null +++ b/launcher/lib/services/window_handle.dart @@ -0,0 +1,11 @@ +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..40b75cc --- /dev/null +++ b/launcher/lib/services/window_service.dart @@ -0,0 +1,81 @@ +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..cb97dd6 --- /dev/null +++ b/launcher/lib/services/window_watcher_service.dart @@ -0,0 +1,15 @@ +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..7fa96ff --- /dev/null +++ b/launcher/lib/views/app_icon.dart @@ -0,0 +1,36 @@ +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..c82015a --- /dev/null +++ b/launcher/lib/views/dock.dart @@ -0,0 +1,39 @@ +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..97fb14c --- /dev/null +++ b/launcher/lib/views/dock_button.dart @@ -0,0 +1,34 @@ +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..f1ada29 --- /dev/null +++ b/launcher/lib/views/launcher_button.dart @@ -0,0 +1,56 @@ +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..f830601 --- /dev/null +++ b/launcher/lib/views/stream_builder_with_future_initial_value.dart @@ -0,0 +1,42 @@ +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..e7c5c54 --- /dev/null +++ b/launcher/linux/main.cc @@ -0,0 +1,6 @@ +#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..65cbc2e --- /dev/null +++ b/launcher/linux/my_application.cc @@ -0,0 +1,89 @@ +#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..3258a73 --- /dev/null +++ b/launcher/linux/my_application.h @@ -0,0 +1,18 @@ +#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..d8e21c0 --- /dev/null +++ b/launcher/test/window_service_test.dart @@ -0,0 +1,52 @@ +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/scripts/bin/initialise-display-config b/scripts/bin/initialise-display-config index 16521ba..505b652 100755 --- a/scripts/bin/initialise-display-config +++ b/scripts/bin/initialise-display-config @@ -25,6 +25,4 @@ wait_for "${display_file}" if [ -z "$(snapctl get display)" ] && ! grep "DO NOT EDIT THIS FILE BY HAND" "${display_file}" then snapctl set display="$(cat "${display_file}")" - # Need to run the configure script ourselves - $SNAP/meta/hooks/configure fi diff --git a/snap/hooks/configure b/snap/hooks/configure index 7a613c6..9d2aeb4 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -110,3 +110,14 @@ if [ "$(snapctl get daemon)" = "true" ]; then else snapctl stop --disable "$SNAP_INSTANCE_NAME.daemon" 2>&1 || true fi + +# (Re)start the launcher +if [ "$(snapctl get launcher)" = "true" ]; then + if snapctl services "$SNAP_INSTANCE_NAME.launcher" | grep -q inactive; then + snapctl start --enable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true + elif [ "$config_changes" -gt 0 ]; then + snapctl restart "$SNAP_INSTANCE_NAME.launcher" || true + fi +else + snapctl stop --disable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true +fi diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 874a003..ebd0e1a 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: + - wayslot + environment: + XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop + + launcher: + command-chain: + - bin/run-client + - bin/gpu-2404-wrapper + command: ubuntu_frame_launcher + daemon: simple + restart-delay: 3s + restart-condition: always + install-mode: disable + plugs: + - opengl + - desktop-launch - wayland + environment: + XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop daemon: command-chain: @@ -65,7 +84,7 @@ apps: - hardware-observe # libinput likes to scan udev - opengl slots: - - wayland + - wayslot screenshot: command-chain: @@ -75,8 +94,6 @@ 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 plugs: @@ -91,6 +108,8 @@ slots: content: diagnostic-text write: - $SNAP_COMMON/diagnostic + wayslot: + interface: wayland parts: ubuntu-frame: @@ -168,6 +187,7 @@ parts: - icons - scripts - grim + - launcher source: https://github.com/canonical/gpu-snap.git plugin: dump override-prime: | @@ -177,3 +197,15 @@ parts: prime: - bin/gpu-2404-wrapper + + launcher: + source: launcher + plugin: flutter + flutter-target: lib/main.dart + build-packages: + - curl + - libgtk-layer-shell-dev + - libgtk-3-dev + stage-packages: + - libgtk-3-0t64 + - libgtk-layer-shell0 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 }}, }}; From 1b431ec73257311b7213133171d7ac29f356e3a4 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Jun 2024 10:00:25 -0400 Subject: [PATCH 02/19] feedback: run-client-wait when running the launcher so that we have a display ready --- scripts/bin/run-client-wait | 39 +++++++++++++++++++++++++++++++++++++ snap/snapcraft.yaml | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100755 scripts/bin/run-client-wait diff --git a/scripts/bin/run-client-wait b/scripts/bin/run-client-wait new file mode 100755 index 0000000..68b243e --- /dev/null +++ b/scripts/bin/run-client-wait @@ -0,0 +1,39 @@ +#!/bin/sh +set -e + +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/snapcraft.yaml b/snap/snapcraft.yaml index ebd0e1a..7ac2aa8 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -54,7 +54,7 @@ apps: launcher: command-chain: - - bin/run-client + - bin/run-client-wait - bin/gpu-2404-wrapper command: ubuntu_frame_launcher daemon: simple From 44290747dbd9e6906727ff2154e7c309c4f22711 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Jun 2024 10:03:39 -0400 Subject: [PATCH 03/19] feedback: plugs.json + remove config_changes check in configure + README --- launcher/README.md | 7 ++++--- snap/hooks/configure | 2 -- snap/local/plugs.json | 5 +++++ 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 snap/local/plugs.json diff --git a/launcher/README.md b/launcher/README.md index fc8b4e3..a49c36f 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -7,14 +7,15 @@ flutter pub get ``` ## Development -1 . Run `miral-shell`: +1 . Run `ubuntu-frame`: ```sh -WAYLAND_DISPLAY=wayland-98 miral-shell --add-wayland-extensions zwlr_layer_shell_v1:zwlr_foreign_toplevel_manager_v1 +WAYLAND_DISPLAY=wayland-98 ubuntu-frame --add-wayland-extensions zwlr_layer_shell_v1:zwlr_foreign_toplevel_manager_v1 ``` -2. Run `ubuntu-frame-launcher`: +2. Run `launcher`: ```sh +cd launcher WAYLAND_DISPLAY=wayland-98 flutter run ``` \ No newline at end of file diff --git a/snap/hooks/configure b/snap/hooks/configure index 9d2aeb4..6fb8f66 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -115,8 +115,6 @@ fi if [ "$(snapctl get launcher)" = "true" ]; then if snapctl services "$SNAP_INSTANCE_NAME.launcher" | grep -q inactive; then snapctl start --enable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true - elif [ "$config_changes" -gt 0 ]; then - snapctl restart "$SNAP_INSTANCE_NAME.launcher" || true fi else snapctl stop --disable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true 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 From 0337ef18e3a11319e55ae893dfc697812b59c432 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Jun 2024 13:28:37 -0400 Subject: [PATCH 04/19] rename run-client-wait to wayland-launch + license --- .../controllers/application_controller.dart | 16 ++++++++++++++++ .../lib/controllers/window_controller.dart | 16 ++++++++++++++++ launcher/lib/main.dart | 16 ++++++++++++++++ .../lib/models/application_desktop_file.dart | 17 ++++++++++++++++- launcher/lib/models/desktop_file.dart | 16 ++++++++++++++++ launcher/lib/models/null_desktop_file.dart | 16 ++++++++++++++++ launcher/lib/models/window_event.dart | 16 ++++++++++++++++ .../lib/services/desktop_file_manager.dart | 16 ++++++++++++++++ .../lib/services/wayland_window_watcher.dart | 16 ++++++++++++++++ launcher/lib/services/window_handle.dart | 16 ++++++++++++++++ launcher/lib/services/window_service.dart | 16 ++++++++++++++++ .../lib/services/window_watcher_service.dart | 16 ++++++++++++++++ launcher/lib/views/app_icon.dart | 16 ++++++++++++++++ launcher/lib/views/dock.dart | 16 ++++++++++++++++ launcher/lib/views/dock_button.dart | 16 ++++++++++++++++ launcher/lib/views/launcher_button.dart | 16 ++++++++++++++++ ...ream_builder_with_future_initial_value.dart | 16 ++++++++++++++++ launcher/linux/main.cc | 18 +++++++++++++++++- launcher/linux/my_application.cc | 16 ++++++++++++++++ launcher/linux/my_application.h | 16 ++++++++++++++++ launcher/test/window_service_test.dart | 16 ++++++++++++++++ .../bin/{run-client-wait => wayland-launch} | 0 snap/snapcraft.yaml | 2 +- 23 files changed, 338 insertions(+), 3 deletions(-) rename scripts/bin/{run-client-wait => wayland-launch} (100%) diff --git a/launcher/lib/controllers/application_controller.dart b/launcher/lib/controllers/application_controller.dart index 15e0ed6..2eab7ca 100644 --- a/launcher/lib/controllers/application_controller.dart +++ b/launcher/lib/controllers/application_controller.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/controllers/window_controller.dart b/launcher/lib/controllers/window_controller.dart index 2e42814..64a2b87 100644 --- a/launcher/lib/controllers/window_controller.dart +++ b/launcher/lib/controllers/window_controller.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/main.dart b/launcher/lib/main.dart index 2c0aeb5..d87dae5 100644 --- a/launcher/lib/main.dart +++ b/launcher/lib/main.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/models/application_desktop_file.dart b/launcher/lib/models/application_desktop_file.dart index f0fbda2..59a0319 100644 --- a/launcher/lib/models/application_desktop_file.dart +++ b/launcher/lib/models/application_desktop_file.dart @@ -1,3 +1,19 @@ +/* + * 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'; @@ -70,7 +86,6 @@ class ApplicationFileDesktopFile extends DesktopFile { return _getDesktopEntry('Exec'); } - String? _getDesktopEntry(String key) { return config.get('Desktop Entry', key); } diff --git a/launcher/lib/models/desktop_file.dart b/launcher/lib/models/desktop_file.dart index 2e0a59d..b6ad8a0 100644 --- a/launcher/lib/models/desktop_file.dart +++ b/launcher/lib/models/desktop_file.dart @@ -1,3 +1,19 @@ +/* + * 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; diff --git a/launcher/lib/models/null_desktop_file.dart b/launcher/lib/models/null_desktop_file.dart index 30879bf..5777a31 100644 --- a/launcher/lib/models/null_desktop_file.dart +++ b/launcher/lib/models/null_desktop_file.dart @@ -1,3 +1,19 @@ +/* + * 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 { diff --git a/launcher/lib/models/window_event.dart b/launcher/lib/models/window_event.dart index 43656fc..26e1073 100644 --- a/launcher/lib/models/window_event.dart +++ b/launcher/lib/models/window_event.dart @@ -1,3 +1,19 @@ +/* + * 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 } diff --git a/launcher/lib/services/desktop_file_manager.dart b/launcher/lib/services/desktop_file_manager.dart index 0b204ce..fcd9c45 100644 --- a/launcher/lib/services/desktop_file_manager.dart +++ b/launcher/lib/services/desktop_file_manager.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/services/wayland_window_watcher.dart b/launcher/lib/services/wayland_window_watcher.dart index 8503e2b..5d96151 100644 --- a/launcher/lib/services/wayland_window_watcher.dart +++ b/launcher/lib/services/wayland_window_watcher.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/services/window_handle.dart b/launcher/lib/services/window_handle.dart index 7b0a58f..736e197 100644 --- a/launcher/lib/services/window_handle.dart +++ b/launcher/lib/services/window_handle.dart @@ -1,3 +1,19 @@ +/* + * 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(); diff --git a/launcher/lib/services/window_service.dart b/launcher/lib/services/window_service.dart index 40b75cc..25bff84 100644 --- a/launcher/lib/services/window_service.dart +++ b/launcher/lib/services/window_service.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/services/window_watcher_service.dart b/launcher/lib/services/window_watcher_service.dart index cb97dd6..2039601 100644 --- a/launcher/lib/services/window_watcher_service.dart +++ b/launcher/lib/services/window_watcher_service.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/views/app_icon.dart b/launcher/lib/views/app_icon.dart index 7fa96ff..38b5eef 100644 --- a/launcher/lib/views/app_icon.dart +++ b/launcher/lib/views/app_icon.dart @@ -1,3 +1,19 @@ +/* + * 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; diff --git a/launcher/lib/views/dock.dart b/launcher/lib/views/dock.dart index c82015a..90ac191 100644 --- a/launcher/lib/views/dock.dart +++ b/launcher/lib/views/dock.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/views/dock_button.dart b/launcher/lib/views/dock_button.dart index 97fb14c..f895375 100644 --- a/launcher/lib/views/dock_button.dart +++ b/launcher/lib/views/dock_button.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/launcher/lib/views/launcher_button.dart b/launcher/lib/views/launcher_button.dart index f1ada29..f5edc23 100644 --- a/launcher/lib/views/launcher_button.dart +++ b/launcher/lib/views/launcher_button.dart @@ -1,3 +1,19 @@ +/* + * 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 { diff --git a/launcher/lib/views/stream_builder_with_future_initial_value.dart b/launcher/lib/views/stream_builder_with_future_initial_value.dart index f830601..9fd227a 100644 --- a/launcher/lib/views/stream_builder_with_future_initial_value.dart +++ b/launcher/lib/views/stream_builder_with_future_initial_value.dart @@ -1,3 +1,19 @@ +/* + * 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); diff --git a/launcher/linux/main.cc b/launcher/linux/main.cc index e7c5c54..8e86925 100644 --- a/launcher/linux/main.cc +++ b/launcher/linux/main.cc @@ -1,6 +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) { +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 index 65cbc2e..9673942 100644 --- a/launcher/linux/my_application.cc +++ b/launcher/linux/my_application.cc @@ -1,3 +1,19 @@ +/* + * 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 diff --git a/launcher/linux/my_application.h b/launcher/linux/my_application.h index 3258a73..2b28468 100644 --- a/launcher/linux/my_application.h +++ b/launcher/linux/my_application.h @@ -1,3 +1,19 @@ +/* + * 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_ diff --git a/launcher/test/window_service_test.dart b/launcher/test/window_service_test.dart index d8e21c0..b78ee4b 100644 --- a/launcher/test/window_service_test.dart +++ b/launcher/test/window_service_test.dart @@ -1,3 +1,19 @@ +/* + * 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'; diff --git a/scripts/bin/run-client-wait b/scripts/bin/wayland-launch similarity index 100% rename from scripts/bin/run-client-wait rename to scripts/bin/wayland-launch diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 7ac2aa8..622c3fd 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -54,7 +54,7 @@ apps: launcher: command-chain: - - bin/run-client-wait + - bin/wayland-launch - bin/gpu-2404-wrapper command: ubuntu_frame_launcher daemon: simple From df4e362021f01c7474fe5da90958132ca50c15d7 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Jun 2024 13:30:11 -0400 Subject: [PATCH 05/19] Revert accidental change --- scripts/bin/initialise-display-config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/bin/initialise-display-config b/scripts/bin/initialise-display-config index 505b652..16521ba 100755 --- a/scripts/bin/initialise-display-config +++ b/scripts/bin/initialise-display-config @@ -25,4 +25,6 @@ wait_for "${display_file}" if [ -z "$(snapctl get display)" ] && ! grep "DO NOT EDIT THIS FILE BY HAND" "${display_file}" then snapctl set display="$(cat "${display_file}")" + # Need to run the configure script ourselves + $SNAP/meta/hooks/configure fi From 16dcc95650c854ba26f8822af40a4f79650301fb Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 25 Jun 2024 08:05:01 -0400 Subject: [PATCH 06/19] Checking if interfaces are connected --- scripts/bin/wayland-launch | 15 +++++++++++++++ snap/hooks/configure | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/bin/wayland-launch b/scripts/bin/wayland-launch index 68b243e..1cf82fb 100755 --- a/scripts/bin/wayland-launch +++ b/scripts/bin/wayland-launch @@ -1,6 +1,21 @@ #!/bin/sh set -e +if ! snapctl is-connected desktop-launch ; +then + echo "WARNING: desktop-launch interface is not connected" +fi + +if ! snapctl is-connected opengl ; +then + echo "WARNING: opengl interface is not connected" +fi + +if ! snapctl is-connected wayland ; +then + echo "WARNING: wayland interface is not connected" +fi + if ! command -v inotifywait > /dev/null then echo "ERROR: inotifywait could not be found, ubuntu-frame expects:" diff --git a/snap/hooks/configure b/snap/hooks/configure index 6fb8f66..1a99d98 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -111,7 +111,7 @@ else snapctl stop --disable "$SNAP_INSTANCE_NAME.daemon" 2>&1 || true fi -# (Re)start the launcher +# Start or stop the launcher if [ "$(snapctl get launcher)" = "true" ]; then if snapctl services "$SNAP_INSTANCE_NAME.launcher" | grep -q inactive; then snapctl start --enable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true From 0b4ed7d3ca1ba213244df3b5ac2f3f44b40adafd Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 25 Jun 2024 09:11:58 -0400 Subject: [PATCH 07/19] update: less verbose is-connected checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Sawicz --- scripts/bin/wayland-launch | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/scripts/bin/wayland-launch b/scripts/bin/wayland-launch index 1cf82fb..68c67dc 100755 --- a/scripts/bin/wayland-launch +++ b/scripts/bin/wayland-launch @@ -1,20 +1,12 @@ #!/bin/sh set -e -if ! snapctl is-connected desktop-launch ; -then - echo "WARNING: desktop-launch interface is not connected" -fi - -if ! snapctl is-connected opengl ; -then - echo "WARNING: opengl interface is not connected" -fi - -if ! snapctl is-connected wayland ; -then - echo "WARNING: wayland interface is not connected" -fi +for PLUG in desktop-launch opengl wayland; do + if ! snapctl is-connected ${PLUG} + then + echo "WARNING: ${PLUG} interface not connected" + fi +done if ! command -v inotifywait > /dev/null then From fdaf845302442cd4c671980c69e9a2dbf519f89e Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 16 Jul 2024 08:01:30 -0400 Subject: [PATCH 08/19] disable snap build for flutter launcher on armhf since it does not support it --- launcher/ubuntu_frame_launcher.fake | 3 +++ snap/snapcraft.yaml | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100755 launcher/ubuntu_frame_launcher.fake 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/snap/snapcraft.yaml b/snap/snapcraft.yaml index 622c3fd..ee5e33c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -209,3 +209,9 @@ parts: 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 + craftctl default + fi From e4cc0521f604c903876dcc7065ae4f20c6f850db Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 16 Jul 2024 08:32:45 -0400 Subject: [PATCH 09/19] if trying to enable the launcher on an armhf system. ten we throw an error --- snap/hooks/configure | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/snap/hooks/configure b/snap/hooks/configure index 1a99d98..35900d9 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -113,6 +113,11 @@ fi # Start or stop the launcher if [ "$(snapctl get launcher)" = "true" ]; then + if [ $(uname -m) = "arm" ]; then + echo "Cannot enable the launcher on an armhf system" + exit 1 + fi + if snapctl services "$SNAP_INSTANCE_NAME.launcher" | grep -q inactive; then snapctl start --enable "$SNAP_INSTANCE_NAME.launcher" 2>&1 || true fi From 5595548e5916e7a928125c619ab24e87a3bacc40 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 16 Jul 2024 09:29:23 -0400 Subject: [PATCH 10/19] Enabling launcher only on x86, i32, and amd64 systems --- snap/hooks/configure | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/snap/hooks/configure b/snap/hooks/configure index 35900d9..9e120f6 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -113,8 +113,9 @@ fi # Start or stop the launcher if [ "$(snapctl get launcher)" = "true" ]; then - if [ $(uname -m) = "arm" ]; then - echo "Cannot enable the launcher on an armhf system" + ARCH=$(uname -m) + if [[ "$ARCH" != x86_64* ]] && [[ "$ARCH" != i*86 ]] && [[ "$ARCH" != arm64 ]]; then + echo "Cannot enable the launcher on $ARCH system" exit 1 fi From ebdb4b19de0bc2cffeb1a1f3599fbcc9c0f3153a Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 16 Jul 2024 10:08:29 -0400 Subject: [PATCH 11/19] Cuter check on the architecture conditional for launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Sawicz --- snap/hooks/configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/hooks/configure b/snap/hooks/configure index 9e120f6..54df4e9 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -114,7 +114,7 @@ fi # Start or stop the launcher if [ "$(snapctl get launcher)" = "true" ]; then ARCH=$(uname -m) - if [[ "$ARCH" != x86_64* ]] && [[ "$ARCH" != i*86 ]] && [[ "$ARCH" != arm64 ]]; then + if [[ "$ARCH" != @(x86_64*|i*86|arm64) ]]; then echo "Cannot enable the launcher on $ARCH system" exit 1 fi From e9069fb5de9687de3261e765649c361a5f116a32 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Wed, 17 Jul 2024 09:38:42 -0400 Subject: [PATCH 12/19] daemon connects to desktop-launch and increased logging --- launcher/lib/controllers/application_controller.dart | 7 ------- launcher/lib/main.dart | 4 ++-- launcher/lib/services/desktop_file_manager.dart | 9 +++++++++ launcher/lib/services/wayland_window_watcher.dart | 10 +++++++++- snap/snapcraft.yaml | 1 + 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/launcher/lib/controllers/application_controller.dart b/launcher/lib/controllers/application_controller.dart index 2eab7ca..6014563 100644 --- a/launcher/lib/controllers/application_controller.dart +++ b/launcher/lib/controllers/application_controller.dart @@ -81,11 +81,4 @@ class ApplicationController { return result; } - - static DesktopFile? findDesktopFile( - List list, DesktopFile file) { - return list - .cast() - .firstWhere((other) => other!.id == file.id, orElse: () => null); - } } diff --git a/launcher/lib/main.dart b/launcher/lib/main.dart index d87dae5..9399a1d 100644 --- a/launcher/lib/main.dart +++ b/launcher/lib/main.dart @@ -30,8 +30,8 @@ void main() async { print('${record.level.name}: ${record.time}: ${record.message}'); }); - final log = Logger("main"); - log.info("Ubuntu Frame launcher is starting"); + final logger = Logger("main"); + logger.info("Ubuntu Frame launcher is starting"); final getIt = GetIt.instance; diff --git a/launcher/lib/services/desktop_file_manager.dart b/launcher/lib/services/desktop_file_manager.dart index fcd9c45..ae0cc10 100644 --- a/launcher/lib/services/desktop_file_manager.dart +++ b/launcher/lib/services/desktop_file_manager.dart @@ -52,6 +52,7 @@ class DesktopFileManager { } Future resolveId(String id) async { + _logger.info("Resolving desktop file for id: $id"); if (_idToApp.containsKey(id)) { return _idToApp[id]!; } @@ -61,10 +62,12 @@ class DesktopFileManager { 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; @@ -78,9 +81,12 @@ class DesktopFileManager { 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; } @@ -117,6 +123,8 @@ class DesktopFileManager { 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; } } @@ -125,6 +133,7 @@ class DesktopFileManager { } } + _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 index 5d96151..a924997 100644 --- a/launcher/lib/services/wayland_window_watcher.dart +++ b/launcher/lib/services/wayland_window_watcher.dart @@ -128,7 +128,8 @@ class WlrForeignToplevelHandleV1 extends WaylandObject implements WindowHandle { return; } - var payload = WaylandWriteBuffer(); + _logger.info("Activating window, handle_id=$id, id=$appId"); + final payload = WaylandWriteBuffer(); payload.writeUint(seat!.id); client.sendRequest(id, _activateRequest, payload.data); } @@ -221,6 +222,7 @@ class WaylandWindowWatcherService extends WindowWatcherService { late WaylandRegistry registry; WlrForeignTopLevelManager? foreignTopLevelManager; WaylandSeat? seat; + final _logger = Logger("WaylandWindowWatcherService"); WaylandWindowWatcherService() { connect(); @@ -256,21 +258,27 @@ class WaylandWindowWatcherService extends WindowWatcherService { } 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/snap/snapcraft.yaml b/snap/snapcraft.yaml index ee5e33c..c3c5a90 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -83,6 +83,7 @@ apps: plugs: - hardware-observe # libinput likes to scan udev - opengl + - desktop-launch slots: - wayslot From 37a7375dac3aa8525548002bdfeb1924d6654664 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Wed, 17 Jul 2024 10:10:19 -0400 Subject: [PATCH 13/19] Set XDG_DATA_DIRS for the daemon --- snap/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c3c5a90..dc9324d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -80,6 +80,7 @@ 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 From 9dea9dc7f7f5e305f5b1cef4d3af8bc73fa65597 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 18 Jul 2024 09:39:12 -0400 Subject: [PATCH 14/19] Update scripts/bin/wayland-launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Sawicz --- scripts/bin/wayland-launch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bin/wayland-launch b/scripts/bin/wayland-launch index 68c67dc..8a50b38 100755 --- a/scripts/bin/wayland-launch +++ b/scripts/bin/wayland-launch @@ -1,7 +1,7 @@ #!/bin/sh set -e -for PLUG in desktop-launch opengl wayland; do +for PLUG in desktop-launch opengl wayplug; do if ! snapctl is-connected ${PLUG} then echo "WARNING: ${PLUG} interface not connected" From b1575960a9eb36ecc1b8a1d895f85eb637dd18d5 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 18 Jul 2024 09:57:00 -0400 Subject: [PATCH 15/19] snap: using wayplug instead of wayslot --- snap/snapcraft.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index dc9324d..098df45 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -48,7 +48,7 @@ apps: - hardware-observe # libinput likes to scan udev - desktop-launch slots: - - wayslot + - wayland environment: XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop @@ -64,7 +64,7 @@ apps: plugs: - opengl - desktop-launch - - wayland + - wayplug environment: XDG_DATA_DIRS: $SNAP/usr/share:/var/lib/snapd/desktop @@ -86,7 +86,7 @@ apps: - opengl - desktop-launch slots: - - wayslot + - wayland screenshot: command-chain: @@ -96,13 +96,15 @@ apps: command: bin/screenshot plugs: - opengl - - wayland + - wayplug plugs: gpu-2404: interface: content target: $SNAP/gpu-2404 default-provider: mesa-2404 + wayplug: + interface: wayland slots: ubuntu-frame-diagnostic: @@ -110,8 +112,6 @@ slots: content: diagnostic-text write: - $SNAP_COMMON/diagnostic - wayslot: - interface: wayland parts: ubuntu-frame: From 17bd048af55edeb96a3418d88c311ba7367ec291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sawicz?= Date: Thu, 25 Jul 2024 13:20:18 +0200 Subject: [PATCH 16/19] configure: fix arm64 -> aarch64 --- snap/hooks/configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/hooks/configure b/snap/hooks/configure index 54df4e9..a538888 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -114,7 +114,7 @@ fi # Start or stop the launcher if [ "$(snapctl get launcher)" = "true" ]; then ARCH=$(uname -m) - if [[ "$ARCH" != @(x86_64*|i*86|arm64) ]]; then + if [[ "$ARCH" != @(x86_64*|i*86|aarch64) ]]; then echo "Cannot enable the launcher on $ARCH system" exit 1 fi From e149fa75c8e1b96ef4a0d3df82908150e5c18937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sawicz?= Date: Thu, 25 Jul 2024 15:23:54 +0200 Subject: [PATCH 17/19] launcher: pin to Flutter 3.19.6 Working around shader issue on Pi: https://github.com/flutter/flutter/issues/152297 --- snap/snapcraft.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 098df45..1bcc0ff 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -202,10 +202,14 @@ parts: launcher: source: launcher - plugin: flutter - flutter-target: lib/main.dart + plugin: nil build-packages: + - clang - curl + - git + - cmake + - ninja-build + - unzip - libgtk-layer-shell-dev - libgtk-3-dev stage-packages: @@ -215,5 +219,10 @@ parts: 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 - craftctl default + 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 From f29e1a46c020ffa3bd002e538b63df07c5f9cb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sawicz?= Date: Thu, 25 Jul 2024 17:24:07 +0100 Subject: [PATCH 18/19] configure: require desktop-launch to enable the launcher --- snap/hooks/configure | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/snap/hooks/configure b/snap/hooks/configure index a538888..662e413 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -119,6 +119,12 @@ if [ "$(snapctl get launcher)" = "true" ]; then 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 From bc5d16f11bc4a0fc59ebc2c2f35b31a139036358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sawicz?= Date: Fri, 26 Jul 2024 09:53:46 +0100 Subject: [PATCH 19/19] snap: add disconnect plug on desktop-launch --- snap/hooks/disconnect-plug-desktop-launch | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 snap/hooks/disconnect-plug-desktop-launch 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