diff options
234 files changed, 25787 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/README.md b/README.md new file mode 100644 index 0000000..71594f7 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# libastal + +> [!WARNING] +> WIP: everything is subject to change + +The main goal of this project is to further abstract gtk bindings in higher level +languages with custom state management mechanisms, namely in javascript (gjs, node), +lua (lua-lgi). + +`libastal`, which is the library written in Vala, +comes with a few widgets built on top of gtk3 and +tools to execute external binaries and store their output. +It also comes with a builtin cli client to send messages to the running +processes through a socket. + +## Developing + +first install libastal or enter nix shell + +```bash +# non nix +meson setup build +meson insall -C build +``` + +```bash +# nix +nix develop .#astal +``` + +lua should be stright forward, just run the interpreter + +for javascript do + +```bash +cd gjs +npm i +npm run types +npm run build +``` + +## Gtk abstractions + +`Variable` and `Binding` objects and a function that turns widget constructors +into ones that can take `Binding` objects as parameters are added on top +of gtk bindings. This mechanism takes care of all state management one would need. + +This works the same in lua too, but demonstrated in js + +```javascript +// this example will work with Variable<string> +// but it can take any type of value +const v = Variable("value") + .poll(1000, "some-executable on $PATH") + .poll(1000, ["some-executable", "with", "args"]) + .poll(1000, () => "some-function") + .watch("some-executable") + .watch(["some-executable", "with", "args"]) + .observe(someGObject, "signal", (...args) => "some output") + .observe([[gobj1, "signal"], [gobj2, "signal"]], (...args) => "some output") + .onError(console.error) // when the script fails + .onDropped(() => "clean-up") // cleanup resources if needed on drop() or GC + +Button({ + label: bind(v), + label: bind(v).as(v => "transformed"), + label: v(t => "transformed"), // shorthand for the above + + // in ags we have Service.bind("prop") + // here we will do this, since gobject implementations + // will come from Vala code and not js + label: bind(anyGObject, "one-of-its-prop").as(prop => "transformed"), + + // event handlers + on_signalname(self, ...args) { print(self, args) }, + + // setup prop is still here, but should be rarely needed + setup(self) { + self.hook(v, (self) => print(self)) + self.hook(gobject, "signal", (self) => print(self)) + } +}) + +// some additional Variable and Binding methods +v.stop_poll() +v.start_poll() + +v.stop_watch() +v.start_watch() + +v.get() +v.set("new-value") + +const unsub = v.subscribe(value => console.log(value)) +unsub() // to unsubscribe + +const b = bind(v) +b.get() +// note that its value cannot be set through a Binding +// if you want to, you are doing something wrong + +// same subscribe mechanism +const unsub = b.subscribe(value => console.log(value)) +unsub() + +const derived = Variable.derive([v, b], (vval, bval) => { + return "can take a list of Variable | Binding" +}) + +v.drop() // dispose when no longer needed + +// handle cli client +App.start({ + instanceName: "my-instance", + responseHandler(msg, response) { + console.log("message from cli", msg) + response("hi") + } +}) +``` + +after `App.start` is called, it will open a socket, which can be used +with the cli client that comes with libastal + +```bash +astal --instance-name my-instance "message was sent from cli" +``` + +## Lower level languages + +As said before, the main goal is to make js/lua DX better, but libastal +can be used in **any** language that has bindings for glib/gtk. +`Binding` is not implemented in Vala, but in each language, because +they are language specific, and it doesn't make much sense for lower +level languages as they usually don't have a way to declaratively build +layouts. Subclassed widgets and `Variable` can still be used, but they will +need to be hooked **imperatively**. For languages like rust/go/c++ +you will mostly benefit from the other libraries (called `Service` in ags). +I can also recommend using [blueprint](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) +which lets you define layouts declaratively and hook functionality in your +preferred language. + +I am open to add support for any other language if it makes sense, +but if using blueprint makes more sense, I would rather maintain +templates and examples instead to get started with development. + +## Goals + +- libastal + - Variables + - [x] poll (interval, string) + - [x] pollv (interval, string[]) + - [x] pollfn (interval, closure) + - [x] watch (string) + - [x] watchv (string[]) + - ~~[ ] observe (object, signal, closure)~~ + - Time + - [x] interval + - [x] timeout + - [x] idle + - [x] now signal + - Process + - [x] exec: string, error as Error + - [x] execAsync: proc, stdout, stderr signal + - [x] subprocess: proc, stdout, stderr signal + - app instance with a socket: Application + - [x] gtk settings as props + - [x] window getters + - [x] include cli client + - few additional widgets + - [x] window widget with gtk-layer-shell + - [x] box with children prop + - [x] button with abstract signals for button-event + - [ ] ?custom calendar like gtk4 + - [x] centerbox + - [ ] circularprogress + - [x] eventbox + - [x] icon + - [x] overlay + - [ ] scrollable/viewport + - [x] slider + - [ ] stack, shown, children setter + - widgets with no additional behaviour only for the sake of it + - [ ] ?drawingarea + - [ ] ?entry + - [ ] ?fixed + - [ ] ?flowbox + - [ ] ?label + - [ ] ?levelbar + - [ ] ?revealer + - [ ] ?switch + - widget prop setters + - [x] css + - [x] class-names + - [x] cursor + - [x] click-through + +- language bindings + - Binding for Variable and any GObject `bind(gobject, property).as(transform)` + - .hook() for widgets + - setup prop for widgets + - constructor overrides to take bindings + - override default `visible` for widgets to true + - wrap Variable in native object to make sure no GValue crashes + - Variable.observe for signals + - Variable.derive that takes either Variables or Bindings diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ diff --git a/apps/LICENSE b/apps/LICENSE new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/apps/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..04e2629 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,8 @@ +# astal-apps + +Library and cli tool for querying applications + +## TODO + +- docs +- query PATH executables diff --git a/apps/flake.lock b/apps/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/apps/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/apps/flake.nix b/apps/flake.nix new file mode 100644 index 0000000..419f962 --- /dev/null +++ b/apps/flake.nix @@ -0,0 +1,49 @@ +{ + description = "Library and cli tool for querying applications"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + json-glib + ]; + in { + packages.${system} = rec { + default = apps; + apps = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-apps"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + apps = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = buildInputs ++ [ + pkgs.gjs + self.packages.${system}.default + ]; + }; + }; + }; +} diff --git a/apps/meson-install.sh b/apps/meson-install.sh new file mode 100644 index 0000000..0e6b258 --- /dev/null +++ b/apps/meson-install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +meson setup \ + --prefix /usr \ + --libexecdir lib \ + --sbindir bin \ + --buildtype plain \ + --auto-features enabled \ + --wrap-mode nodownload \ + -D b_lto=false \ + -D b_pie=true \ + -D python.bytecompile=1 \ + --wipe \ + build + +meson install -C build diff --git a/apps/meson.build b/apps/meson.build new file mode 100644 index 0000000..27f8e7a --- /dev/null +++ b/apps/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-apps', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/apps/meson_options.txt b/apps/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/apps/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/apps/src/application.vala b/apps/src/application.vala new file mode 100644 index 0000000..5748fc6 --- /dev/null +++ b/apps/src/application.vala @@ -0,0 +1,118 @@ +namespace AstalApps { +public class Application : Object { + public DesktopAppInfo app { get; construct set; } + public int frequency { get; set; default = 0; } + public string name { get { return app.get_name(); } } + public string entry { get { return app.get_id(); } } + public string description { get { return app.get_description(); } } + public string wm_class { get { return app.get_startup_wm_class(); } } + public string executable { owned get { return app.get_string("Exec"); } } + public string icon_name { owned get { return app.get_string("Icon"); } } + + internal Application(string id, int? frequency = 0) { + Object(app: new DesktopAppInfo(id)); + this.frequency = frequency; + } + + public string get_key(string key) { + return app.get_string(key); + } + + public bool launch() { + try { + var s = app.launch(null, null); + ++frequency; + return s; + } catch (Error err) { + critical(err.message); + return false; + } + } + + public Score fuzzy_match(string term) { + var score = Score(); + if (name != null) + score.name = levenshtein(term, name); + if (entry != null) + score.entry = levenshtein(term, entry); + if (executable != null) + score.executable = levenshtein(term, executable); + if (description != null) + score.description = levenshtein(term, description); + + return score; + } + + public Score exact_match(string term) { + var score = Score(); + if (name != null) + score.name = name.down().contains(term.down()) ? 1 : 0; + if (entry != null) + score.entry = entry.down().contains(term.down()) ? 1 : 0; + if (executable != null) + score.executable = executable.down().contains(term.down()) ? 1 : 0; + if (description != null) + score.description = description.down().contains(term.down()) ? 1 : 0; + + return score; + } + + internal Json.Node to_json() { + return new Json.Builder() + .begin_object() + .set_member_name("name").add_string_value(name) + .set_member_name("entry").add_string_value(entry) + .set_member_name("executable").add_string_value(executable) + .set_member_name("description").add_string_value(description) + .set_member_name("icon_name").add_string_value(icon_name) + .set_member_name("frequency").add_int_value(frequency) + .end_object() + .get_root(); + } +} + +int min3(int a, int b, int c) { + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); +} + +double levenshtein(string s1, string s2) { + int len1 = s1.length; + int len2 = s2.length; + + int[, ] d = new int[len1 + 1, len2 + 1]; + + for (int i = 0; i <= len1; i++) { + d[i, 0] = i; + } + for (int j = 0; j <= len2; j++) { + d[0, j] = j; + } + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + d[i, j] = min3( + d[i - 1, j] + 1, // deletion + d[i, j - 1] + 1, // insertion + d[i - 1, j - 1] + cost // substitution + ); + } + } + + var distance = d[len1, len2]; + int max_len = len1 > len2 ? len1 : len2; + + if (max_len == 0) { + return 1.0; + } + + return 1.0 - ((double)distance / max_len); +} + +public struct Score { + double name; + double entry; + double executable; + double description; +} +} diff --git a/apps/src/apps.vala b/apps/src/apps.vala new file mode 100644 index 0000000..2a0d507 --- /dev/null +++ b/apps/src/apps.vala @@ -0,0 +1,172 @@ +namespace AstalApps { +public class Apps : Object { + private string cache_directory; + private string cache_file; + private List<Application> _list; + private HashTable<string, int> frequents { get; private set; } + + public bool show_hidden { get; set; } + public List<weak Application> list { owned get { return _list.copy(); } } + + public double min_score { get; set; default = 0.5; } + + public double name_multiplier { get; set; default = 2; } + public double entry_multiplier { get; set; default = 1; } + public double executable_multiplier { get; set; default = 1; } + public double description_multiplier { get; set; default = 0.5; } + + public bool include_name { get; set; default = true; } + public bool include_entry { get; set; default = false; } + public bool include_executable { get; set; default = false; } + public bool include_description { get; set; default = false; } + + construct { + cache_directory = Environment.get_user_cache_dir() + "/astal"; + cache_file = cache_directory + "/apps-frequents.json"; + frequents = new HashTable<string, int>(str_hash, str_equal); + + AppInfoMonitor.get().changed.connect(() => { + reload(); + }); + + if (FileUtils.test(cache_file, FileTest.EXISTS)) { + try { + uint8[] content; + File.new_for_path(cache_file).load_contents(null, out content, null); + + var parser = new Json.Parser(); + parser.load_from_data((string)content); + var obj = parser.get_root().get_object(); + foreach (var member in obj.get_members()) { + var v = obj.get_member(member).get_value().get_int64(); + frequents.set(member, (int)v); + } + } catch (Error err) { + critical("cannot read cache: %s\n", err.message); + } + } + + reload(); + } + + private double score (string search, Application a, bool exact) { + var am = exact ? a.exact_match(search) : a.fuzzy_match(search); + double r = 0; + + if (include_name) + r += am.name * name_multiplier; + if (include_entry) + r += am.entry * entry_multiplier; + if (include_executable) + r += am.executable * executable_multiplier; + if (include_description) + r += am.description * description_multiplier; + + return r; + } + + public List<weak Application> query(string? search = "", bool exact = false) { + if (search == null) + search = ""; + + var arr = list.copy(); + + // empty search, sort by frequency + if (search == "") { + arr.sort_with_data((a, b) => { + return (int)b.frequency - (int)a.frequency; + }); + + return arr; + } + + // single character, sort by frequency and exact match + if (search.length == 1) { + foreach (var app in list) { + if (score(search, app, true) == 0) + arr.remove(app); + } + + arr.sort_with_data((a, b) => { + return (int)b.frequency - (int)a.frequency; + }); + + return arr; + } + + // filter + foreach (var app in list) { + if (score(search, app, exact) < min_score) + arr.remove(app); + } + + // sort by score, frequency + arr.sort_with_data((a, b) => { + var s1 = score(search, a, exact); + var s2 = score(search, b, exact); + + if (s1 == s2) + return (int)b.frequency - (int)a.frequency; + + return s1 < s2 ? 1 : -1; + }); + + return arr; + } + + public List<weak Application> fuzzy_query(string? search = "") { + return query(search, false); + } + + public List<weak Application> exact_query(string? search = "") { + return query(search, true); + } + + public void reload() { + var arr = AppInfo.get_all(); + + _list = new List<Application>(); + foreach (var app in arr) { + if (!show_hidden && !app.should_show()) + continue; + + var a = new Application( + app.get_id(), + frequents.get(app.get_id()) + ); + a.notify.connect((pspec) => { + if (pspec.name != "frequency") + return; + + var f = frequents.get(app.get_id()); + frequents.set(app.get_id(), ++f); + + _list.sort((a, b) => { + return (int)a.frequency - (int)b.frequency; + }); + cache(); + }); + _list.append(a); + } + + cache(); + } + + private void cache() { + var json = new Json.Builder().begin_object(); + foreach (string key in frequents.get_keys()) + json.set_member_name(key).add_int_value(frequents.get(key)); + + try { + if (!FileUtils.test(cache_directory, FileTest.EXISTS)) + File.new_for_path(cache_directory).make_directory_with_parents(null); + + var generator = new Json.Generator(); + generator.set_root(json.end_object().get_root()); + FileUtils.set_contents_full(cache_file, generator.to_data(null)); + } catch (Error err) { + critical("cannot cache frequents: %s", err.message); + } + } +} +} diff --git a/apps/src/cli.vala b/apps/src/cli.vala new file mode 100644 index 0000000..d926c87 --- /dev/null +++ b/apps/src/cli.vala @@ -0,0 +1,66 @@ +static bool help; +static bool version; +static string search; +static string launch; +static bool json; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "search", 's', OptionFlags.NONE, OptionArg.STRING, ref search, null, null }, + { "launch", 'l', OptionFlags.NONE, OptionArg.STRING, ref launch, null, null }, + { "json", 'j', OptionFlags.NONE, OptionArg.NONE, ref json, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (help) { + print("Usage:\n"); + print(" %s [flags]\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -s, --search Sort by a search term\n"); + print(" -l, --launch Launch an application\n"); + print(" -j, --json Print list in json format\n"); + return 0; + } + + if (version) { + print(AstalApps.VERSION); + return 0; + } + + var apps = new AstalApps.Apps(); + + if (launch != null) { + apps.query(launch).first().data.launch(); + return 0; + } + + if (json) { + var b = new Json.Builder().begin_array(); + foreach (var app in apps.query(search)) + b.add_value(app.to_json()); + + var generator = new Json.Generator(); + generator.set_root(b.end_array().get_root()); + stdout.printf(generator.to_data(null)); + } else { + foreach (var app in apps.query(search)) + stdout.printf("%s\n", app.entry); + } + + return 0; +} diff --git a/apps/src/config.vala.in b/apps/src/config.vala.in new file mode 100644 index 0000000..b3a9f49 --- /dev/null +++ b/apps/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalApps { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/apps/src/meson.build b/apps/src/meson.build new file mode 100644 index 0000000..e4f0d59 --- /dev/null +++ b/apps/src/meson.build @@ -0,0 +1,77 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalApps-' + api_version + '.gir' +typelib = 'AstalApps-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-unix-2.0'), + dependency('json-glib-1.0'), +] + +sources = [ + config, + 'apps.vala', + 'application.vala', + 'cli.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/apps/version b/apps/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/apps/version @@ -0,0 +1 @@ +0.1.0 diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 0000000..6bf41b5 --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,3 @@ +build/ +result/ +.cache/ diff --git a/auth/LICENSE b/auth/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/auth/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/auth/README.md b/auth/README.md new file mode 100644 index 0000000..f5d52a3 --- /dev/null +++ b/auth/README.md @@ -0,0 +1,66 @@ +# libastal-auth +This library provides a way for authentication using pam for the libastal suite. + +## Build from source +### Dependencies + +- meson +- glib +- gobject-introspection +- pam +- vala (only required for the vapi option) + +### Meson options + +* `-Dintrospection` (default: `true`): build GObject Introspection data (needed for language bindings) +* `-Dvapi` (default: `true`): build VAPI data (required to make this lib usable in vala). Requires `-Dintrospection=true` +* `-Dexamples` (default: `false`): build examples + +```sh +# Clone the repository +git clone https://github.com/astal-sh/libastal-auth +cd libastal-auth + +# Setup and build +meson setup build +meson compile -C build + +# Install +meson install -C build +``` + +> [!NOTE] +> on NixOS you will have to add `security.pam.services.astal-auth = {}` in `configuration.nix` + +## Usage +This library can be used from any language supporting GObject Introspection. +Have a look at the [examples](examples) for how it can be used in C and gjs. + +The authentication is done asynchronously in its own thread, therefore the GLib mainloop is required to run. +This is already given in all gtk application, but has to be started manually in some cases like in the small examples in this repo. + +Until there are better docs, please refer to the [auth.h](include/auth.h) file for detailed usage. + +For simple authentication using only a password, using the `Pam.authenticate()` method is recommended. +Look at the simple examples for how to use it. + +There is also a way to get access to the pam conversation, to allow for a more complex authentication process, like using multiple factor authentication. +The full examples show how this can be achieved. +Generally it can be used like this: + +1. create the Pam object. +2. set username and service if so required. It has sane defaults, so in most cases you can skip this. +3. connect to the signals + - `auth-prompt-hidden`: is emitted when user input is required, and the input should be hidden (eg, passwords) + - `auth-prompt-visible`: is emitted when user input is required, and the input should be visible (eg, OTP) + - `auth-info`: an information message should be displayed (eg, tell the user to touch his security key) + - `auth-error`: an error message should be displayed + - `sucess`: emitted on successful authentication + - `fail`: emitted on failed authentication + + all signals except the `success` signal have a string containing the message as a parameter. + After an `auth-*` signal is emitted, it hs to be responded with exactly one `pam.supply_secret(secret)` call. The secret is a string containing the user input. For `auth-info` and `auth-error` it can be `NULL`. + Not connecting those signals, is equivalent to calling `pam.supply_secret(NULL)` immediately after the signal is emitted. +4. start authentication process using `Pam.start_authentication()`. This function will return whether the authentication was started or not. +5. it is possible to reuse the same Pam object for multiple sequential authentication attempts. Just call `pam.start_authentication()` again after the `success` or `fail` signal was emitted. + diff --git a/auth/examples/full_example.c b/auth/examples/full_example.c new file mode 100644 index 0000000..a20c02b --- /dev/null +++ b/auth/examples/full_example.c @@ -0,0 +1,66 @@ +#include <bsd/readpassphrase.h> + +#include "astal-auth.h" + +GMainLoop *loop; + +static void authenticate(AstalAuthPam *pam) { + if (!astal_auth_pam_start_authenticate(pam)) { + g_print("could not start authentication process\n"); + g_object_unref(pam); + g_main_loop_quit(loop); + } +} + +static void on_visible(AstalAuthPam *pam, const gchar *data) { + gchar passbuf[1024]; + readpassphrase(data, passbuf, sizeof(passbuf), RPP_ECHO_ON); + astal_auth_pam_supply_secret(pam, passbuf); +} + +static void on_hidden(AstalAuthPam *pam, const gchar *data) { + gchar passbuf[1024]; + readpassphrase(data, passbuf, sizeof(passbuf), RPP_ECHO_OFF); + astal_auth_pam_supply_secret(pam, passbuf); +} + +static void on_info(AstalAuthPam *pam, const gchar *data) { + g_print("info: %s\n", data); + astal_auth_pam_supply_secret(pam, NULL); +} + +static void on_error(AstalAuthPam *pam, const gchar *data) { + g_print("error: %s\n", data); + astal_auth_pam_supply_secret(pam, NULL); +} + +static void on_success(AstalAuthPam *pam) { + g_print("success\n"); + g_object_unref(pam); + g_main_loop_quit(loop); +} + +static void on_fail(AstalAuthPam *pam, const gchar *data) { + g_print("fail: %s\n", data); + authenticate(pam); +} + +int main(void) { + GMainContext *loopctx = NULL; + + loop = g_main_loop_new(loopctx, FALSE); + + AstalAuthPam *pam = g_object_new(ASTAL_AUTH_TYPE_PAM, NULL); + + g_signal_connect(pam, "auth-prompt-visible", G_CALLBACK(on_visible), NULL); + g_signal_connect(pam, "auth-prompt-hidden", G_CALLBACK(on_hidden), NULL); + g_signal_connect(pam, "auth-info", G_CALLBACK(on_info), NULL); + g_signal_connect(pam, "auth-error", G_CALLBACK(on_error), NULL); + + g_signal_connect(pam, "success", G_CALLBACK(on_success), NULL); + g_signal_connect(pam, "fail", G_CALLBACK(on_fail), NULL); + + authenticate(pam); + + g_main_loop_run(loop); +} diff --git a/auth/examples/full_example.js b/auth/examples/full_example.js new file mode 100644 index 0000000..7359784 --- /dev/null +++ b/auth/examples/full_example.js @@ -0,0 +1,38 @@ +#!/usr/bin/env -S gjs -m + +import Auth from "gi://AstalAuth"; +import GLib from "gi://GLib"; + +const loop = GLib.MainLoop.new(null, false); + +const pam = new Auth.Pam(); +pam.connect("auth-prompt-visible", (p, msg) => { + print(msg); + p.supply_secret(""); +}); +pam.connect("auth-prompt-hidden", (p, msg) => { + print(msg); + p.supply_secret("password"); +}); +pam.connect("auth-info", (p, msg) => { + print(msg); + p.supply_secret(""); +}); +pam.connect("auth-error", (p, msg) => { + print(msg); + p.supply_secret(""); +}); + +pam.connect("success", p => { + print("authentication sucessful"); + loop.quit(); +}); +pam.connect("fail", (p, msg) => { + print(msg); + loop.quit(); +}); + +pam.start_authenticate(); + +loop.runAsync() + diff --git a/auth/examples/meson.build b/auth/examples/meson.build new file mode 100644 index 0000000..cf23d3f --- /dev/null +++ b/auth/examples/meson.build @@ -0,0 +1,18 @@ + +deps_example = [ + dependency('gobject-2.0'), + dependency('libbsd'), + libastal_auth +] + +astal_auth_full_exmple = executable( + 'astal_auth_full_example', + files('full_example.c'), + dependencies : deps_example, + install : false) + +astal_auth_simple_example = executable( + 'astal_auth_simple_example', + files('simple_example.c'), + dependencies : deps_example, + install : false) diff --git a/auth/examples/simple_example.c b/auth/examples/simple_example.c new file mode 100644 index 0000000..d00bad2 --- /dev/null +++ b/auth/examples/simple_example.c @@ -0,0 +1,31 @@ +#include <bsd/readpassphrase.h> + +#include "astal-auth.h" + +GMainLoop *loop; + +void ready_callback(AstalAuthPam *pam, GAsyncResult *res, gpointer user_data) { + GError *error = NULL; + astal_auth_pam_authenticate_finish(res, &error); + if (error == NULL) { + g_print("success\n"); + } else { + g_print("failure: %s\n", error->message); + g_error_free(error); + } + + g_main_loop_quit(loop); +} + +int main(void) { + GMainContext *loopctx = NULL; + loop = g_main_loop_new(loopctx, FALSE); + + gchar *passbuf = calloc(1024, sizeof(gchar)); + readpassphrase("Password: ", passbuf, 1024, RPP_ECHO_OFF); + astal_auth_pam_authenticate(passbuf, (GAsyncReadyCallback)ready_callback, NULL); + g_free(passbuf); + + g_main_loop_run(loop); + exit(EXIT_SUCCESS); +} diff --git a/auth/examples/simple_example.js b/auth/examples/simple_example.js new file mode 100644 index 0000000..2bf38c1 --- /dev/null +++ b/auth/examples/simple_example.js @@ -0,0 +1,9 @@ +#!/usr/bin/env -S gjs -m +import Auth from "gi://AstalAuth"; +import Gio from "gi://Gio"; + +Gio._promisify(Auth.Pam, "authenticate"); + +await Auth.Pam.authenticate("password") + .then(_ => print("authentication sucessful")) + .catch(logError);
\ No newline at end of file diff --git a/auth/flake.lock b/auth/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/auth/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/auth/flake.nix b/auth/flake.nix new file mode 100644 index 0000000..39b0289 --- /dev/null +++ b/auth/flake.nix @@ -0,0 +1,42 @@ +{ + description = "Authentication library and cli tool"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + + outputs = { self, nixpkgs }: + let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + pam + ]; + in { + packages.${system} = rec { + default = auth; + auth = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-auth"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + }; + }; +} diff --git a/auth/include/astal-auth.h b/auth/include/astal-auth.h new file mode 100644 index 0000000..a3073ff --- /dev/null +++ b/auth/include/astal-auth.h @@ -0,0 +1,32 @@ +#ifndef ASTAL_AUTH_PAM_H +#define ASTAL_AUTH_PAM_H + +#include <gio/gio.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_AUTH_TYPE_PAM (astal_auth_pam_get_type()) + +G_DECLARE_FINAL_TYPE(AstalAuthPam, astal_auth_pam, ASTAL_AUTH, PAM, GObject) + +void astal_auth_pam_set_username(AstalAuthPam *self, const gchar *username); + +const gchar *astal_auth_pam_get_username(AstalAuthPam *self); + +void astal_auth_pam_set_service(AstalAuthPam *self, const gchar *service); + +const gchar *astal_auth_pam_get_service(AstalAuthPam *self); + +gboolean astal_auth_pam_start_authenticate(AstalAuthPam *self); + +void astal_auth_pam_supply_secret(AstalAuthPam *self, const gchar *secret); + +gboolean astal_auth_pam_authenticate(const gchar *password, GAsyncReadyCallback result_callback, + gpointer user_data); + +gssize astal_auth_pam_authenticate_finish(GAsyncResult *res, GError **error); + +G_END_DECLS + +#endif // !ASTAL_AUTH_PAM_H diff --git a/auth/include/meson.build b/auth/include/meson.build new file mode 100644 index 0000000..0575998 --- /dev/null +++ b/auth/include/meson.build @@ -0,0 +1,4 @@ +astal_auth_inc = include_directories('.') +astal_auth_headers = files('astal-auth.h') + +install_headers('astal-auth.h') diff --git a/auth/meson.build b/auth/meson.build new file mode 100644 index 0000000..e9facb1 --- /dev/null +++ b/auth/meson.build @@ -0,0 +1,33 @@ +project('astal_auth', + 'c', + version : run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + default_options : [ + 'c_std=gnu11', + 'warning_level=3', + 'prefix=/usr' + ] +) + +add_project_arguments( + ['-Wno-pedantic'], + language : 'c') + +version_split = meson.project_version().split('.') +lib_so_version = version_split[0] + '.' + version_split[1] + +pkg_config = import('pkgconfig') +gnome = import('gnome') + +subdir('include') +subdir('src') + + +if get_option('examples') + subdir('examples') +endif + + +install_data( + 'pam/astal-auth', + install_dir : get_option('sysconfdir') / 'pam.d' +) diff --git a/auth/meson_options.txt b/auth/meson_options.txt new file mode 100644 index 0000000..e28447e --- /dev/null +++ b/auth/meson_options.txt @@ -0,0 +1,3 @@ +option('examples', type : 'boolean', value : false, description : 'Build example applications') +option('introspection', type : 'boolean', value : true, description : 'Build gobject-introspection data') +option('vapi', type : 'boolean', value : true, description : 'Generate vapi data (needs vapigen & introspection option)') diff --git a/auth/pam/astal-auth b/auth/pam/astal-auth new file mode 100644 index 0000000..41f79d7 --- /dev/null +++ b/auth/pam/astal-auth @@ -0,0 +1,5 @@ +# PAM configuration file for the astal-auth library. +# By default, it only includes the 'login' +# configuration file (see /etc/pam.d/login) + +auth include login diff --git a/auth/src/astal-auth.c b/auth/src/astal-auth.c new file mode 100644 index 0000000..1ac2bd7 --- /dev/null +++ b/auth/src/astal-auth.c @@ -0,0 +1,153 @@ +#include "astal-auth.h" + +#include <getopt.h> +#include <stdio.h> +#include <termios.h> + +GMainLoop *loop; + +static void cleanup_and_quit(AstalAuthPam *pam, int status) { + g_object_unref(pam); + g_main_loop_quit(loop); + exit(status); +} + +static char *read_secret(const char *msg, gboolean echo) { + struct termios oldt, newt; + char *password = NULL; + size_t size = 0; + ssize_t len; + + if (tcgetattr(STDIN_FILENO, &oldt) != 0) { + return NULL; + } + newt = oldt; + if (echo) { + newt.c_lflag |= ECHO; + } else { + newt.c_lflag &= ~(ECHO); + } + if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) != 0) { + return NULL; + } + g_print("%s", msg); + if ((len = getline(&password, &size, stdin)) == -1) { + g_free(password); + return NULL; + } + + if (password[len - 1] == '\n') { + password[len - 1] = '\0'; + } + + printf("\n"); + + if (tcsetattr(STDIN_FILENO, TCSANOW, &oldt) != 0) { + return NULL; + } + + return password; +} + +static void authenticate(AstalAuthPam *pam) { + static int attempts = 0; + if (attempts >= 3) { + g_print("%d failed attempts.\n", attempts); + cleanup_and_quit(pam, EXIT_FAILURE); + } + if (!astal_auth_pam_start_authenticate(pam)) { + g_print("could not start authentication process\n"); + cleanup_and_quit(pam, EXIT_FAILURE); + } + attempts++; +} + +static void on_visible(AstalAuthPam *pam, const gchar *data) { + char *secret = read_secret(data, TRUE); + if (secret == NULL) cleanup_and_quit(pam, EXIT_FAILURE); + astal_auth_pam_supply_secret(pam, secret); + g_free(secret); +} + +static void on_hidden(AstalAuthPam *pam, const gchar *data, gchar *secret) { + if (!secret) secret = read_secret(data, FALSE); + if (secret == NULL) cleanup_and_quit(pam, EXIT_FAILURE); + astal_auth_pam_supply_secret(pam, secret); + g_free(secret); +} + +static void on_info(AstalAuthPam *pam, const gchar *data) { + g_print("info: %s\n", data); + astal_auth_pam_supply_secret(pam, NULL); +} + +static void on_error(AstalAuthPam *pam, const gchar *data) { + g_print("error: %s\n", data); + astal_auth_pam_supply_secret(pam, NULL); +} + +static void on_success(AstalAuthPam *pam) { + g_print("Authentication successful\n"); + cleanup_and_quit(pam, EXIT_SUCCESS); +} + +static void on_fail(AstalAuthPam *pam, const gchar *data, gboolean retry) { + g_print("%s\n", data); + if (retry) + authenticate(pam); + else + cleanup_and_quit(pam, EXIT_FAILURE); +} + +int main(int argc, char **argv) { + char *password = NULL; + char *username = NULL; + char *service = NULL; + + int opt; + const char *optstring = "p:u:s:"; + + static struct option long_options[] = {{"password", required_argument, NULL, 'p'}, + {"username", required_argument, NULL, 'u'}, + {"service", required_argument, NULL, 's'}, + {NULL, 0, NULL, 0}}; + + while ((opt = getopt_long(argc, argv, optstring, long_options, NULL)) != -1) { + switch (opt) { + case 'p': + password = optarg; + break; + case 'u': + username = optarg; + break; + case 's': + service = optarg; + break; + default: + g_print("Usage: %s [-p password] [-u username] [-s service]\n", argv[0]); + exit(EXIT_FAILURE); + } + } + + loop = g_main_loop_new(NULL, FALSE); + + AstalAuthPam *pam = g_object_new(ASTAL_AUTH_TYPE_PAM, NULL); + + if (username) astal_auth_pam_set_username(pam, username); + if (service) astal_auth_pam_set_service(pam, service); + if (password) { + g_signal_connect(pam, "fail", G_CALLBACK(on_fail), (void *)FALSE); + } else { + g_signal_connect(pam, "auth-prompt-visible", G_CALLBACK(on_visible), NULL); + g_signal_connect(pam, "auth-info", G_CALLBACK(on_info), NULL); + g_signal_connect(pam, "auth-error", G_CALLBACK(on_error), NULL); + g_signal_connect(pam, "fail", G_CALLBACK(on_fail), (void *)TRUE); + } + + g_signal_connect(pam, "auth-prompt-hidden", G_CALLBACK(on_hidden), g_strdup(password)); + g_signal_connect(pam, "success", G_CALLBACK(on_success), NULL); + + authenticate(pam); + + g_main_loop_run(loop); +} diff --git a/auth/src/meson.build b/auth/src/meson.build new file mode 100644 index 0000000..6a34ae0 --- /dev/null +++ b/auth/src/meson.build @@ -0,0 +1,65 @@ +srcs = files( + 'pam.c', +) + +deps = [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('pam') +] + +astal_auth_lib = library( + 'astal-auth', + sources : srcs, + include_directories : astal_auth_inc, + dependencies : deps, + version : meson.project_version(), + install : true +) + +libastal_auth = declare_dependency( + link_with : astal_auth_lib, + include_directories : astal_auth_inc) + +astal_auth_executable = executable( + 'astal-auth', + files('astal-auth.c'), + dependencies : [ + dependency('gobject-2.0'), + libastal_auth + ], + install : true) + +pkg_config_name = 'astal-auth-' + lib_so_version + +if get_option('introspection') + gir = gnome.generate_gir( + astal_auth_lib, + sources : srcs + astal_auth_headers, + nsversion : '0.1', + namespace : 'AstalAuth', + symbol_prefix : 'astal_auth', + identifier_prefix : 'AstalAuth', + includes : ['GObject-2.0', 'Gio-2.0'], + header : 'astal-auth.h', + export_packages : pkg_config_name, + install : true + ) + + if get_option('vapi') + gnome.generate_vapi( + pkg_config_name, + sources : [gir[0]], + packages : ['gobject-2.0', 'gio-2.0'], + install : true) + endif +endif + +pkg_config.generate( + name : 'astal-auth', + version : meson.project_version(), + libraries : [astal_auth_lib], + filebase : pkg_config_name, + subdirs : 'astal', + description : 'astal authentication module', + url : 'https://github.com/astal-sh/auth') diff --git a/auth/src/pam.c b/auth/src/pam.c new file mode 100644 index 0000000..d0afec4 --- /dev/null +++ b/auth/src/pam.c @@ -0,0 +1,524 @@ +#include <pwd.h> +#include <security/_pam_types.h> +#include <security/pam_appl.h> + +#include "astal-auth.h" + +struct _AstalAuthPam { + GObject parent_instance; + + gchar *username; + gchar *service; +}; + +typedef struct { + GTask *task; + GMainContext *context; + GMutex data_mutex; + GCond data_cond; + + gchar *secret; + gboolean secret_set; +} AstalAuthPamPrivate; + +typedef struct { + AstalAuthPam *pam; + guint signal_id; + gchar *msg; +} AstalAuthPamSignalEmitData; + +static void astal_auth_pam_signal_emit_data_free(AstalAuthPamSignalEmitData *data) { + g_free(data->msg); + g_free(data); +} + +typedef enum { + ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE, + ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN, + ASTAL_AUTH_PAM_SIGNAL_INFO, + ASTAL_AUTH_PAM_SIGNAL_ERROR, + ASTAL_AUTH_PAM_SIGNAL_SUCCESS, + ASTAL_AUTH_PAM_SIGNAL_FAIL, + ASTAL_AUTH_PAM_N_SIGNALS +} AstalAuthPamSignals; + +typedef enum { + ASTAL_AUTH_PAM_PROP_USERNAME = 1, + ASTAL_AUTH_PAM_PROP_SERVICE, + ASTAL_AUTH_PAM_N_PROPERTIES +} AstalAuthPamProperties; + +static guint astal_auth_pam_signals[ASTAL_AUTH_PAM_N_SIGNALS] = { + 0, +}; +static GParamSpec *astal_auth_pam_properties[ASTAL_AUTH_PAM_N_PROPERTIES] = { + NULL, +}; + +G_DEFINE_TYPE_WITH_PRIVATE(AstalAuthPam, astal_auth_pam, G_TYPE_OBJECT); + +/** + * + * AstalAuthPam + * + * For simple authentication using only a password, using the [[email protected]] + * method is recommended. Look at the simple examples for how to use it. + * + * There is also a way to get access to the pam conversation, to allow for a more complex + * authentication process, like using multiple factor authentication. Generally it can be used like + * this: + * + * 1. create the Pam object. + * 2. set username and service if so required. It has sane defaults, so in most cases you can skip + * this. + * 3. connect to the signals. + * After an `auth-*` signal is emitted, it has to be responded with exactly one + * [[email protected]_secret] call. The secret is a string containing the user input. For + * [auth-info][[email protected]::auth-info:] and [auth-error][[email protected]::auth-error:] + * it should be `NULL`. Not connecting those signals, is equivalent to calling + * [[email protected]_secret] with `NULL` immediately after the signal is emitted. + * 4. start authentication process using [[email protected]_authenticate]. + * 5. it is possible to reuse the same Pam object for multiple sequential authentication attempts. + * Just call [[email protected]_authenticate] again after the `success` or `fail` signal + * was emitted. + * + */ + +/** + * astal_auth_pam_set_username + * @self: a AstalAuthPam object + * @username: the new username + * + * Sets the username to be used for authentication. This must be set to + * before calling start_authenticate. + * Changing it afterwards has no effect on the authentication process. + * + * Defaults to the owner of the process. + * + */ +void astal_auth_pam_set_username(AstalAuthPam *self, const gchar *username) { + g_return_if_fail(ASTAL_AUTH_IS_PAM(self)); + g_return_if_fail(username != NULL); + + g_free(self->username); + self->username = g_strdup(username); + g_object_notify(G_OBJECT(self), "username"); +} + +/** + * astal_auth_pam_supply_secret + * @self: a AstalAuthPam Object + * @secret: (nullable): the secret to be provided to pam. Can be NULL. + * + * provides pam with a secret. This method must be called exactly once after a + * auth-* signal is emitted. + */ +void astal_auth_pam_supply_secret(AstalAuthPam *self, const gchar *secret) { + g_return_if_fail(ASTAL_AUTH_IS_PAM(self)); + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + + g_mutex_lock(&priv->data_mutex); + g_free(priv->secret); + priv->secret = g_strdup(secret); + priv->secret_set = TRUE; + g_cond_signal(&priv->data_cond); + g_mutex_unlock(&priv->data_mutex); +} + +/** + * astal_auth_pam_set_service + * @self: a AstalAuthPam object + * @service: the pam service used for authentication + * + * Sets the service to be used for authentication. This must be set to + * before calling start_authenticate. + * Changing it afterwards has no effect on the authentication process. + * + * Defaults to `astal-auth`. + * + */ +void astal_auth_pam_set_service(AstalAuthPam *self, const gchar *service) { + g_return_if_fail(ASTAL_AUTH_IS_PAM(self)); + g_return_if_fail(service != NULL); + + g_free(self->service); + self->service = g_strdup(service); + g_object_notify(G_OBJECT(self), "service"); +} + +/** + * astal_auth_pam_get_username + * @self: a AstalAuthPam object + * + * Fetches the username from AsalAuthPam object. + * + * Returns: the username of the AsalAuthPam object. This string is + * owned by the object and must not be modified or freed. + */ + +const gchar *astal_auth_pam_get_username(AstalAuthPam *self) { + g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), NULL); + return self->username; +} + +/** + * astal_auth_pam_get_service + * @self: a AstalAuthPam + * + * Fetches the service from AsalAuthPam object. + * + * Returns: the service of the AsalAuthPam object. This string is + * owned by the object and must not be modified or freed. + */ +const gchar *astal_auth_pam_get_service(AstalAuthPam *self) { + g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), NULL); + return self->service; +} + +static void astal_auth_pam_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalAuthPam *self = ASTAL_AUTH_PAM(object); + + switch (property_id) { + case ASTAL_AUTH_PAM_PROP_USERNAME: + astal_auth_pam_set_username(self, g_value_get_string(value)); + break; + case ASTAL_AUTH_PAM_PROP_SERVICE: + astal_auth_pam_set_service(self, g_value_get_string(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_auth_pam_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalAuthPam *self = ASTAL_AUTH_PAM(object); + + switch (property_id) { + case ASTAL_AUTH_PAM_PROP_USERNAME: + g_value_set_string(value, self->username); + break; + case ASTAL_AUTH_PAM_PROP_SERVICE: + g_value_set_string(value, self->service); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_auth_pam_callback(GObject *object, GAsyncResult *res, gpointer user_data) { + AstalAuthPam *self = ASTAL_AUTH_PAM(object); + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + + GTask *task = g_steal_pointer(&priv->task); + + GError *error = NULL; + g_task_propagate_int(task, &error); + + if (error == NULL) { + g_signal_emit(self, astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_SUCCESS], 0); + } else { + g_signal_emit(self, astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_FAIL], 0, error->message); + g_error_free(error); + } + g_object_unref(task); +} + +static gboolean astal_auth_pam_emit_signal_in_context(gpointer user_data) { + AstalAuthPamSignalEmitData *data = user_data; + g_signal_emit(data->pam, data->signal_id, 0, data->msg); + return G_SOURCE_REMOVE; +} + +static void astal_auth_pam_emit_signal(AstalAuthPam *pam, guint signal, const gchar *msg) { + GSource *emit_source; + AstalAuthPamSignalEmitData *data; + + data = g_new0(AstalAuthPamSignalEmitData, 1); + data->pam = pam; + data->signal_id = astal_auth_pam_signals[signal]; + data->msg = g_strdup(msg); + + emit_source = g_idle_source_new(); + g_source_set_callback(emit_source, astal_auth_pam_emit_signal_in_context, data, + (GDestroyNotify)astal_auth_pam_signal_emit_data_free); + g_source_set_priority(emit_source, G_PRIORITY_DEFAULT); + g_source_attach(emit_source, + ((AstalAuthPamPrivate *)astal_auth_pam_get_instance_private(pam))->context); + g_source_unref(emit_source); +} + +int astal_auth_pam_handle_conversation(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) { + AstalAuthPam *self = appdata_ptr; + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + + struct pam_response *replies = NULL; + if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) { + return PAM_CONV_ERR; + } + replies = (struct pam_response *)calloc(num_msg, sizeof(struct pam_response)); + if (replies == NULL) { + return PAM_BUF_ERR; + } + for (int i = 0; i < num_msg; ++i) { + guint signal; + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + signal = ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN; + break; + case PAM_PROMPT_ECHO_ON: + signal = ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE; + break; + case PAM_ERROR_MSG: + signal = ASTAL_AUTH_PAM_SIGNAL_ERROR; + ; + break; + case PAM_TEXT_INFO: + signal = ASTAL_AUTH_PAM_SIGNAL_INFO; + break; + default: + g_free(replies); + return PAM_CONV_ERR; + break; + } + guint signal_id = astal_auth_pam_signals[signal]; + if (g_signal_has_handler_pending(self, signal_id, 0, FALSE)) { + astal_auth_pam_emit_signal(self, signal, msg[i]->msg); + g_mutex_lock(&priv->data_mutex); + while (!priv->secret_set) { + g_cond_wait(&priv->data_cond, &priv->data_mutex); + } + replies[i].resp_retcode = 0; + replies[i].resp = g_strdup(priv->secret); + g_free(priv->secret); + priv->secret = NULL; + priv->secret_set = FALSE; + g_mutex_unlock(&priv->data_mutex); + } + } + *resp = replies; + return PAM_SUCCESS; +} + +static void astal_auth_pam_thread(GTask *task, gpointer object, gpointer task_data, + GCancellable *cancellable) { + AstalAuthPam *self = g_task_get_source_object(task); + + pam_handle_t *pamh = NULL; + const struct pam_conv conv = { + .conv = astal_auth_pam_handle_conversation, + .appdata_ptr = self, + }; + + int retval; + retval = pam_start(self->service, self->username, &conv, &pamh); + if (retval == PAM_SUCCESS) { + retval = pam_authenticate(pamh, 0); + pam_end(pamh, retval); + } + if (retval != PAM_SUCCESS) { + g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", + pam_strerror(pamh, retval)); + } else { + g_task_return_int(task, retval); + } +} + +gboolean astal_auth_pam_start_authenticate_with_callback(AstalAuthPam *self, + GAsyncReadyCallback result_callback, + gpointer user_data) { + g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), FALSE); + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + g_return_val_if_fail(priv->task == NULL, FALSE); + + priv->task = g_task_new(self, NULL, result_callback, user_data); + g_task_set_priority(priv->task, 0); + g_task_set_name(priv->task, "[AstalAuth] authenticate"); + g_task_run_in_thread(priv->task, astal_auth_pam_thread); + + return TRUE; +} + +/** + * astal_auth_pam_start_authenticate: + * @self: a AstalAuthPam Object + * + * starts a new authentication process using the PAM (Pluggable Authentication Modules) system. + * Note that this will cancel an already running authentication process + * associated with this AstalAuthPam object. + */ +gboolean astal_auth_pam_start_authenticate(AstalAuthPam *self) { + return astal_auth_pam_start_authenticate_with_callback( + self, (GAsyncReadyCallback)astal_auth_pam_callback, NULL); +} + +static void astal_auth_pam_on_hidden(AstalAuthPam *pam, const gchar *msg, gchar *password) { + astal_auth_pam_supply_secret(pam, password); + g_free(password); +} + +/** + * astal_auth_pam_authenticate: + * @password: the password to be authenticated + * @result_callback: (scope async) (closure user_data): a GAsyncReadyCallback + * to call when the request is satisfied + * @user_data: the data to pass to callback function + * + * Requests authentication of the provided password using the PAM (Pluggable Authentication Modules) + * system. + */ +gboolean astal_auth_pam_authenticate(const gchar *password, GAsyncReadyCallback result_callback, + gpointer user_data) { + AstalAuthPam *pam = g_object_new(ASTAL_AUTH_TYPE_PAM, NULL); + g_signal_connect(pam, "auth-prompt-hidden", G_CALLBACK(astal_auth_pam_on_hidden), + (void *)g_strdup(password)); + + gboolean started = + astal_auth_pam_start_authenticate_with_callback(pam, result_callback, user_data); + g_object_unref(pam); + return started; +} + +gssize astal_auth_pam_authenticate_finish(GAsyncResult *res, GError **error) { + return g_task_propagate_int(G_TASK(res), error); +} + +static void astal_auth_pam_init(AstalAuthPam *self) { + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + + priv->secret = NULL; + + g_cond_init(&priv->data_cond); + g_mutex_init(&priv->data_mutex); + + priv->context = g_main_context_get_thread_default(); +} + +static void astal_auth_pam_finalize(GObject *gobject) { + AstalAuthPam *self = ASTAL_AUTH_PAM(gobject); + AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self); + + g_free(self->username); + g_free(self->service); + + g_free(priv->secret); + + g_cond_clear(&priv->data_cond); + g_mutex_clear(&priv->data_mutex); + + G_OBJECT_CLASS(astal_auth_pam_parent_class)->finalize(gobject); +} + +static void astal_auth_pam_class_init(AstalAuthPamClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + + object_class->get_property = astal_auth_pam_get_property; + object_class->set_property = astal_auth_pam_set_property; + + object_class->finalize = astal_auth_pam_finalize; + + struct passwd *passwd = getpwuid(getuid()); + + /** + * AstalAuthPam:username: + * + * The username used for authentication. + * Changing the value of this property has no affect on an already started authentication + * process. + * + * Defaults to the user that owns this process. + */ + astal_auth_pam_properties[ASTAL_AUTH_PAM_PROP_USERNAME] = + g_param_spec_string("username", "username", "username used for authentication", + passwd->pw_name, G_PARAM_CONSTRUCT | G_PARAM_READWRITE); + /** + * AstalAuthPam:service: + * + * The pam service used for authentication. + * Changing the value of this property has no affect on an already started authentication + * process. + * + * Defaults to the astal-auth pam service. + */ + astal_auth_pam_properties[ASTAL_AUTH_PAM_PROP_SERVICE] = + g_param_spec_string("service", "service", "the pam service to use", "astal-auth", + G_PARAM_CONSTRUCT | G_PARAM_READWRITE); + + g_object_class_install_properties(object_class, ASTAL_AUTH_PAM_N_PROPERTIES, + astal_auth_pam_properties); + /** + * AstalAuthPam::auth-prompt-visible: + * @pam: the object which received the signal. + * @msg: the prompt to be shown to the user + * + * This signal is emitted when user input is required. The input should be visible + * when entered (e.g., for One-Time Passwords (OTP)). + * + * This signal has to be matched with exaclty one supply_secret call. + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE] = + g_signal_new("auth-prompt-visible", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * AstalAuthPam::auth-prompt-hidden: + * @pam: the object which received the signal. + * @msg: the prompt to be shown to the user + * + * This signal is emitted when user input is required. The input should be hidden + * when entered (e.g., for passwords). + * + * This signal has to be matched with exaclty one supply_secret call. + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN] = + g_signal_new("auth-prompt-hidden", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * AstalAuthPam::auth-info: + * @pam: the object which received the signal. + * @msg: the info mssage to be shown to the user + * + * This signal is emitted when the user should receive an information (e.g., tell the user to + * touch a security key, or the remaining time pam has been locked after multiple failed + * attempts) + * + * This signal has to be matched with exaclty one supply_secret call. + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_INFO] = + g_signal_new("auth-info", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * AstalAuthPam::auth-error: + * @pam: the object which received the signal. + * @msg: the error message + * + * This signal is emitted when an authentication error has occured. + * + * This signal has to be matched with exaclty one supply_secret call. + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_ERROR] = + g_signal_new("auth-error", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * AstalAuthPam::success: + * @pam: the object which received the signal. + * + * This signal is emitted after successful authentication + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_SUCCESS] = + g_signal_new("success", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * AstalAuthPam::fail: + * @pam: the object which received the signal. + * @msg: the authentication failure message + * + * This signal is emitted when authentication failed. + */ + astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_FAIL] = + g_signal_new("fail", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); +} diff --git a/auth/version b/auth/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/auth/version @@ -0,0 +1 @@ +0.1.0 diff --git a/bluetooth/.gitignore b/bluetooth/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/bluetooth/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ diff --git a/bluetooth/LICENSE b/bluetooth/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/bluetooth/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/bluetooth/README.md b/bluetooth/README.md new file mode 100644 index 0000000..b8e3f13 --- /dev/null +++ b/bluetooth/README.md @@ -0,0 +1,7 @@ +# astal-bluetooth + +Library to control bluez over dbus + +## TODO + +- docs diff --git a/bluetooth/flake.lock b/bluetooth/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/bluetooth/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/bluetooth/flake.nix b/bluetooth/flake.nix new file mode 100644 index 0000000..04effee --- /dev/null +++ b/bluetooth/flake.nix @@ -0,0 +1,52 @@ +{ + description = "Library to control bluez over dbus"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + ]; + in { + packages.${system} = rec { + default = bluetooth; + bluetooth = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-bluetooth"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + bluetooth = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/bluetooth/meson.build b/bluetooth/meson.build new file mode 100644 index 0000000..7877365 --- /dev/null +++ b/bluetooth/meson.build @@ -0,0 +1,14 @@ +project( + 'astal-bluetooth', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +subdir('src') diff --git a/bluetooth/src/adapter.vala b/bluetooth/src/adapter.vala new file mode 100644 index 0000000..0c9d00e --- /dev/null +++ b/bluetooth/src/adapter.vala @@ -0,0 +1,89 @@ +namespace AstalBluetooth { +[DBus (name = "org.bluez.Adapter1")] +internal interface IAdapter : DBusProxy { + public abstract void remove_device(ObjectPath device) throws Error; + public abstract void start_discovery() throws Error; + public abstract void stop_discovery() throws Error; + + public abstract string[] uuids { owned get; } + public abstract bool discoverable { get; set; } + public abstract bool discovering { get; } + public abstract bool pairable { get; set; } + public abstract bool powered { get; set; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint class { get; } + public abstract uint discoverable_timeout { get; set; } + public abstract uint pairable_timeout { get; set; } +} + +public class Adapter : Object { + private IAdapter proxy; + public string object_path { owned get; construct set; } + + internal Adapter(IAdapter proxy) { + this.proxy = proxy; + this.object_path = proxy.g_object_path; + proxy.g_properties_changed.connect((props) => { + var map = (HashTable<string, Variant>)props; + foreach (var key in map.get_keys()) { + var prop = kebab_case(key); + if (get_class().find_property(prop) != null) { + notify_property(prop); + } + } + }); + } + + public string[] uuids { owned get { return proxy.uuids; } } + public bool discovering { get { return proxy.discovering; } } + public string modalias { owned get { return proxy.modalias; } } + public string name { owned get { return proxy.name; } } + public uint class { get { return proxy.class; } } + public string address { owned get { return proxy.address; } } + + public bool discoverable { + get { return proxy.discoverable; } + set { proxy.discoverable = value; } + } + + public bool pairable { + get { return proxy.pairable; } + set { proxy.pairable = value; } + } + + public bool powered { + get { return proxy.powered; } + set { proxy.powered = value; } + } + + public string alias { + owned get { return proxy.alias; } + set { proxy.alias = value; } + } + + public uint discoverable_timeout { + get { return proxy.discoverable_timeout; } + set { proxy.discoverable_timeout = value; } + } + + public uint pairable_timeout { + get { return proxy.pairable_timeout; } + set { proxy.pairable_timeout = value; } + } + + public void remove_device(Device device) { + try { proxy.remove_device((ObjectPath)device.object_path); } catch (Error err) { critical(err.message); } + } + + public void start_discovery() { + try { proxy.start_discovery(); } catch (Error err) { critical(err.message); } + } + + public void stop_discovery() { + try { proxy.stop_discovery(); } catch (Error err) { critical(err.message); } + } +} +} diff --git a/bluetooth/src/bluetooth.vala b/bluetooth/src/bluetooth.vala new file mode 100644 index 0000000..ce086ba --- /dev/null +++ b/bluetooth/src/bluetooth.vala @@ -0,0 +1,181 @@ +namespace AstalBluetooth { +public Bluetooth get_default() { + return Bluetooth.get_default(); +} + +public class Bluetooth : Object { + private static Bluetooth _instance; + + public static Bluetooth get_default() { + if (_instance == null) + _instance = new Bluetooth(); + + return _instance; + } + + private DBusObjectManagerClient manager; + + private HashTable<string, Adapter> _adapters = + new HashTable<string, Adapter>(str_hash, str_equal); + + private HashTable<string, Device> _devices = + new HashTable<string, Device>(str_hash, str_equal); + + public signal void device_added (Device device) { + notify_property("devices"); + } + + public signal void device_removed (Device device) { + notify_property("devices"); + } + + public signal void adapter_added (Adapter adapter) { + notify_property("adapters"); + } + + public signal void adapter_removed (Adapter adapter) { + notify_property("adapters"); + } + + public bool is_powered { get; private set; default = false; } + public bool is_connected { get; private set; default = false; } + public Adapter? adapter { get { return adapters.nth_data(0); } } + + public List<weak Adapter> adapters { + owned get { return _adapters.get_values(); } + } + + public List<weak Device> devices { + owned get { return _devices.get_values(); } + } + + construct { + try { + manager = new DBusObjectManagerClient.for_bus_sync( + BusType.SYSTEM, + DBusObjectManagerClientFlags.NONE, + "org.bluez", + "/", + manager_proxy_get_type, + null + ); + + foreach (var object in manager.get_objects()) { + foreach (var iface in object.get_interfaces()) { + on_interface_added(object, iface); + } + } + + manager.interface_added.connect(on_interface_added); + manager.interface_removed.connect(on_interface_removed); + + manager.object_added.connect((object) => { + foreach (var iface in object.get_interfaces()) { + on_interface_added(object, iface); + } + }); + + manager.object_removed.connect((object) => { + foreach (var iface in object.get_interfaces()) { + on_interface_removed(object, iface); + } + }); + } catch (Error err) { + critical(err.message); + } + } + + public void toggle() { + adapter.powered = !adapter.powered; + } + + [CCode (cname="astal_bluetooth_idevice_proxy_get_type")] + extern static GLib.Type get_idevice_proxy_type(); + + [CCode (cname="astal_bluetooth_iadapter_proxy_get_type")] + extern static GLib.Type get_iadapter_proxy_type(); + + private Type manager_proxy_get_type(DBusObjectManagerClient _, string object_path, string? interface_name) { + if (interface_name == null) + return typeof(DBusObjectProxy); + + switch (interface_name) { + case "org.bluez.Device1": + return get_idevice_proxy_type(); + case "org.bluez.Adapter1": + return get_iadapter_proxy_type(); + default: + return typeof(DBusProxy); + } + } + + private void on_interface_added(DBusObject object, DBusInterface iface) { + if (iface is IDevice) { + var device = new Device((IDevice)iface); + _devices.set(device.object_path, device); + device_added(device); + device.notify.connect(sync); + sync(); + } + + if (iface is IAdapter) { + var adapter = new Adapter((IAdapter)iface); + _adapters.set(adapter.object_path, adapter); + adapter_added(adapter); + adapter.notify.connect(sync); + sync(); + } + } + + private void on_interface_removed (DBusObject object, DBusInterface iface) { + if (iface is IDevice) { + unowned var device = (IDevice)iface; + device_removed(_devices.get(device.g_object_path)); + _devices.remove(device.g_object_path); + } + + if (iface is IAdapter) { + unowned var adapter = (IAdapter)iface; + adapter_removed(_adapters.get(adapter.g_object_path)); + _adapters.remove(adapter.g_object_path); + } + + sync(); + } + + private void sync() { + var powered = get_powered(); + var connected = get_connected(); + + if (powered != is_powered || connected != is_connected) { + if (powered != is_powered) { + is_powered = powered; + } + + if (connected != is_connected) { + is_connected = connected; + } + } + } + + private bool get_powered() { + foreach (var adapter in adapters) { + if (adapter.powered) { + return true; + } + } + + return false; + } + + private bool get_connected() { + foreach (var device in devices) { + if (device.connected) { + return true; + } + } + + return false; + } +} +} diff --git a/bluetooth/src/config.vala.in b/bluetooth/src/config.vala.in new file mode 100644 index 0000000..9fce720 --- /dev/null +++ b/bluetooth/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalBluetooth { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/bluetooth/src/device.vala b/bluetooth/src/device.vala new file mode 100644 index 0000000..8fe086f --- /dev/null +++ b/bluetooth/src/device.vala @@ -0,0 +1,106 @@ +namespace AstalBluetooth { +[DBus (name = "org.bluez.Device1")] +internal interface IDevice : DBusProxy { + public abstract void cancel_pairing() throws Error; + public abstract async void connect() throws Error; + public abstract void connect_profile(string uuid) throws Error; + public abstract async void disconnect() throws Error; + public abstract void disconnect_profile(string uuid) throws Error; + public abstract void pair() throws Error; + + public abstract string[] uuids { owned get; } + public abstract bool blocked { get; set; } + public abstract bool connected { get; } + public abstract bool legacy_pairing { get; } + public abstract bool paired { get; } + public abstract bool trusted { get; set; } + public abstract int16 rssi { get; } + public abstract ObjectPath adapter { owned get; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string icon { owned get; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint16 appearance { get; } + public abstract uint32 class { get; } +} + +public class Device : Object { + private IDevice proxy; + public string object_path { owned get; construct set; } + + internal Device(IDevice proxy) { + this.proxy = proxy; + this.object_path = proxy.g_object_path; + proxy.g_properties_changed.connect((props) => { + var map = (HashTable<string, Variant>)props; + foreach (var key in map.get_keys()) { + var prop = kebab_case(key); + if (get_class().find_property(prop) != null) { + notify_property(prop); + } + } + }); + } + + public string[] uuids { owned get { return proxy.uuids; } } + public bool connected { get { return proxy.connected; } } + public bool legacy_pairing { get { return proxy.legacy_pairing; } } + public bool paired { get { return proxy.paired; } } + public int16 rssi { get { return proxy.rssi; } } + public ObjectPath adapter { owned get { return proxy.adapter; } } + public string address { owned get { return proxy.address; } } + public string icon { owned get { return proxy.icon; } } + public string modalias { owned get { return proxy.modalias; } } + public string name { owned get { return proxy.name; } } + public uint16 appearance { get { return proxy.appearance; } } + public uint32 class { get { return proxy.class; } } + public bool connecting { get; private set; } + + public bool blocked { + get { return proxy.blocked; } + set { proxy.blocked = value; } + } + + public bool trusted { + get { return proxy.trusted; } + set { proxy.trusted = value; } + } + + public string alias { + owned get { return proxy.alias; } + set { proxy.alias = value; } + } + + public void cancel_pairing() { + try { proxy.cancel_pairing(); } catch (Error err) { critical(err.message); } + } + + public async void connect_device() { + try { + connecting = true; + yield proxy.connect(); + } catch (Error err) { + critical(err.message); + } finally { + connecting = false; + } + } + + public async void disconnect_device() { + try { yield proxy.disconnect(); } catch (Error err) { critical(err.message); } + } + + public void connect_profile(string uuid) { + try { proxy.connect_profile(uuid); } catch (Error err) { critical(err.message); } + } + + public void disconnect_profile(string uuid) { + try { proxy.disconnect_profile(uuid); } catch (Error err) { critical(err.message); } + } + + public void pair() { + try { proxy.pair(); } catch (Error err) { critical(err.message); } + } +} +} diff --git a/bluetooth/src/meson.build b/bluetooth/src/meson.build new file mode 100644 index 0000000..cbc38ce --- /dev/null +++ b/bluetooth/src/meson.build @@ -0,0 +1,66 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalBluetooth-' + api_version + '.gir' +typelib = 'AstalBluetooth-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), +] + +sources = [ + config, + 'utils.vala', + 'device.vala', + 'adapter.vala', + 'bluetooth.vala', +] + +lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], +) + +import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', +) + +custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', +) diff --git a/bluetooth/src/utils.vala b/bluetooth/src/utils.vala new file mode 100644 index 0000000..5dcaff6 --- /dev/null +++ b/bluetooth/src/utils.vala @@ -0,0 +1,21 @@ +namespace AstalBluetooth { +internal string kebab_case(string pascal_case) { + StringBuilder kebab_case = new StringBuilder(); + + for (int i = 0; i < pascal_case.length; i++) { + char c = pascal_case[i]; + + if (c >= 'A' && c <= 'Z') { + if (i != 0) { + kebab_case.append_c('-'); + } + + kebab_case.append_c((char)(c + 32)); + } else { + kebab_case.append_c(c); + } + } + + return kebab_case.str; +} +} diff --git a/bluetooth/version b/bluetooth/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/bluetooth/version @@ -0,0 +1 @@ +0.1.0 diff --git a/core/default.nix b/core/default.nix new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/core/default.nix diff --git a/core/gjs/.gitignore b/core/gjs/.gitignore new file mode 100644 index 0000000..8c2cc59 --- /dev/null +++ b/core/gjs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +result/ +dist/ diff --git a/core/gjs/eslint.config.mjs b/core/gjs/eslint.config.mjs new file mode 100644 index 0000000..99dad7d --- /dev/null +++ b/core/gjs/eslint.config.mjs @@ -0,0 +1,18 @@ +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import stylistic from "@stylistic/eslint-plugin" + +export default tseslint.config({ + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + stylistic.configs.customize({ + semi: false, + indent: 4, + quotes: "double", + }), + ], + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, +}) diff --git a/core/gjs/index.ts b/core/gjs/index.ts new file mode 100644 index 0000000..901b264 --- /dev/null +++ b/core/gjs/index.ts @@ -0,0 +1,13 @@ +import { Gtk } from "./src/imports.js" + +export * from "./src/imports.js" +export * from "./src/process.js" +export * from "./src/time.js" +export * from "./src/file.js" +export { bind, default as Binding } from "./src/binding.js" +export { Variable } from "./src/variable.js" +export * as Widget from "./src/widgets.js" +export { default as App } from "./src/application.js" + +// gjs crashes if a widget is constructed before Gtk.init +Gtk.init(null) diff --git a/core/gjs/package-lock.json b/core/gjs/package-lock.json new file mode 100644 index 0000000..aa679c8 --- /dev/null +++ b/core/gjs/package-lock.json @@ -0,0 +1,3209 @@ +{ + "name": "astal", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "astal", + "version": "0.1.0", + "license": "GPL", + "os": [ + "linux" + ], + "devDependencies": { + "@eslint/js": "^9.7.0", + "@stylistic/eslint-plugin": "latest", + "@ts-for-gir/cli": "latest", + "@types/eslint__js": "^8.42.3", + "eslint": "^8.57.0", + "typescript": "^5.5.3", + "typescript-eslint": "^7.16.1" + }, + "engines": { + "gjs": ">=1.79.0" + }, + "funding": { + "type": "kofi", + "url": "https://ko-fi.com/aylur" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.7.0.tgz", + "integrity": "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gi.ts/parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@gi.ts/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-Tz5T+3Ep+qY7rfBnYMGdVraCCUf1CKkDfxNd2fggfHLzjI7u5Th8a/piPgj0001jDs5czI+Ec3peh+6gkKPmHw==", + "dev": true, + "dependencies": { + "fast-xml-parser": "^4.3.5" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@inquirer/figures": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.4.tgz", + "integrity": "sha512-R7Gsg6elpuqdn55fBH2y9oYzrU/yKrSmIsDX4ROT51vohrECFzTf2zw9BfUbOW8xjfmM2QbVoVYdTwhrtEKWSQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.3.0.tgz", + "integrity": "sha512-rtiz6u5gRyyEZp36FcF1/gHJbsbT3qAgXZ1qkad6Nr/xJ9wrSJkiSFFQhpYVTIZ7FJNRJurEcumZDCwN9dEI4g==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.3.0", + "@stylistic/eslint-plugin-jsx": "2.3.0", + "@stylistic/eslint-plugin-plus": "2.3.0", + "@stylistic/eslint-plugin-ts": "2.3.0", + "@types/eslint": "^8.56.10" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz", + "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.3.0.tgz", + "integrity": "sha512-tsQ0IEKB195H6X9A4iUSgLLLKBc8gUBWkBIU8tp1/3g2l8stu+PtMQVV/VmK1+3bem5FJCyvfcZIQ/WF1fsizA==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.3.0", + "@types/eslint": "^8.56.10", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.3.0.tgz", + "integrity": "sha512-xboPWGUU5yaPlR+WR57GwXEuY4PSlPqA0C3IdNA/+1o2MuBi95XgDJcZiJ9N+aXsqBXAPIpFFb+WQ7QEHo4f7g==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.12.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.3.0.tgz", + "integrity": "sha512-wqOR38/uz/0XPnHX68ftp8sNMSAqnYGjovOTN7w00xnjS6Lxr3Sk7q6AaxWWqbMvOj7V2fQiMC5HWAbTruJsCg==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.3.0", + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.12.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@ts-for-gir/cli": { + "version": "4.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ts-for-gir/cli/-/cli-4.0.0-beta.7.tgz", + "integrity": "sha512-s345hGCB2su0NIFA7A10GIY9mLiNtrbSXp9ZscKsasx6Fm77QZybO09dLcIPMuePq8gbRlOpNp8MedAqO7zWRw==", + "dev": true, + "dependencies": { + "@gi.ts/parser": "^2.0.0", + "@ts-for-gir/generator-base": "^4.0.0-beta.7", + "@ts-for-gir/generator-html-doc": "^4.0.0-beta.7", + "@ts-for-gir/generator-typescript": "^4.0.0-beta.7", + "@ts-for-gir/lib": "^4.0.0-beta.7", + "colorette": "^2.0.20", + "cosmiconfig": "^9.0.0", + "glob": "^11.0.0", + "inquirer": "^9.3.5", + "prettier": "^3.3.3", + "yargs": "^17.7.2" + }, + "bin": { + "ts-for-gir": "lib/start.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-for-gir/generator-base": { + "version": "4.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ts-for-gir/generator-base/-/generator-base-4.0.0-beta.7.tgz", + "integrity": "sha512-Z3dlwea0LvbGwcb51xjSqp33n5wvqmsX7r1EwbA4vEPuN7AgPm2LDbL68G5HO6QOmFDt05Fe0gjumfzjGE8Xlw==", + "dev": true, + "dependencies": { + "@ts-for-gir/lib": "^4.0.0-beta.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-for-gir/generator-html-doc": { + "version": "4.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ts-for-gir/generator-html-doc/-/generator-html-doc-4.0.0-beta.7.tgz", + "integrity": "sha512-srz4nSSfcqCQUQcV4Ia5booXEy7gC54iv7Q1E80cdnM/cg0XWq4XaTfCbH27CqcY2zS+KsVUJm7PWI7LHeUeiw==", + "dev": true, + "dependencies": { + "@ts-for-gir/generator-base": "^4.0.0-beta.7", + "@ts-for-gir/lib": "^4.0.0-beta.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-for-gir/generator-typescript": { + "version": "4.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ts-for-gir/generator-typescript/-/generator-typescript-4.0.0-beta.7.tgz", + "integrity": "sha512-6WVPHVod6YINT5ZX1TSaPy8b1+A1t3TAi+1Byx/nH5aF4o1jPmn1KKorvxOIDcvlqnjux053r+rjweIR+1w9Zw==", + "dev": true, + "dependencies": { + "@ts-for-gir/generator-base": "^4.0.0-beta.7", + "@ts-for-gir/lib": "^4.0.0-beta.7", + "ejs": "^3.1.10", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-for-gir/lib": { + "version": "4.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@ts-for-gir/lib/-/lib-4.0.0-beta.7.tgz", + "integrity": "sha512-9hhCk3OBA0diIG5KuqSCJAy6D91hVUK2JiuLbxNfdmvAaMS+4BvYSGKg+DdXvmgoTip4LqCo4EsFTHfuo7QQHg==", + "dev": true, + "dependencies": { + "@gi.ts/parser": "^2.0.0", + "colorette": "^2.0.20", + "ejs": "^3.1.10", + "glob": "^11.0.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", + "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", + "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.6.tgz", + "integrity": "sha512-riK/iQB2ctwkpWYgjjWIRv3MBLt2gzb2Sj0JNQNbyTXgyXsLWcDPJ5WS5ZDTCx7BRFnJsARtYh+58fjP5M2Y0Q==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.16.1.tgz", + "integrity": "sha512-889oE5qELj65q/tGeOSvlreNKhimitFwZqQ0o7PcWC7/lgRkAMknznsCsV8J8mZGTP/Z+cIbX8accf2DE33hrA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.16.1", + "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/utils": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/core/gjs/package.json b/core/gjs/package.json new file mode 100644 index 0000000..e3c86d0 --- /dev/null +++ b/core/gjs/package.json @@ -0,0 +1,46 @@ +{ + "name": "astal", + "version": "0.1.0", + "description": "Building blocks for buildin linux desktop shell", + "type": "module", + "author": "Aylur", + "license": "GPL", + "repository": { + "type": "git", + "url": "https://github.com/astal-sh/libastal.git", + "directory": "gjs" + }, + "funding": { + "type": "kofi", + "url": "https://ko-fi.com/aylur" + }, + "exports": { + ".": "./index.ts", + "./app": "./src/application.ts", + "./file": "./src/file.ts", + "./process": "./src/process.ts", + "./time": "./src/time.ts", + "./variable": "./src/variable.ts", + "./widgets": "./src/widgets.ts" + }, + "engines": { + "gjs": ">=1.79.0" + }, + "os": [ + "linux" + ], + "publishConfig": {}, + "devDependencies": { + "@eslint/js": "^9.7.0", + "@stylistic/eslint-plugin": "latest", + "@ts-for-gir/cli": "latest", + "@types/eslint__js": "^8.42.3", + "eslint": "^8.57.0", + "typescript": "^5.5.3", + "typescript-eslint": "^7.16.1" + }, + "scripts": { + "lint": "eslint . --fix", + "types": "ts-for-gir generate -o node_modules/@girs --package" + } +} diff --git a/core/gjs/src/application.ts b/core/gjs/src/application.ts new file mode 100644 index 0000000..0ba247e --- /dev/null +++ b/core/gjs/src/application.ts @@ -0,0 +1,105 @@ +import { Astal, GObject, Gio, GLib } from "./imports.js" + +type RequestHandler = { + (request: string, res: (response: any) => void): void +} + +type Config = Partial<{ + icons: string + instanceName: string + gtkTheme: string + iconTheme: string + cursorTheme: string + css: string + requestHandler: RequestHandler + main(...args: string[]): void + client(message: (msg: string) => string, ...args: string[]): void + hold: boolean +}> + +// @ts-expect-error missing types +// https://github.com/gjsify/ts-for-gir/issues/164 +import { setConsoleLogDomain } from "console" +import { exit, programArgs } from "system" + +class AstalJS extends Astal.Application { + static { GObject.registerClass(this) } + + eval(body: string): Promise<any> { + return new Promise((res, rej) => { + try { + const fn = Function(`return (async function() { + ${body.includes(";") ? body : `return ${body};`} + })`) + fn()() + .then(res) + .catch(rej) + } + catch (error) { + rej(error) + } + }) + } + + requestHandler?: RequestHandler + + vfunc_request(msg: string, conn: Gio.SocketConnection): void { + if (typeof this.requestHandler === "function") { + this.requestHandler(msg, (response) => { + Astal.write_sock(conn, String(response), (_, res) => + Astal.write_sock_finish(res), + ) + }) + } + else { + super.vfunc_request(msg, conn) + } + } + + apply_css(style: string, reset = false) { + super.apply_css(style, reset) + } + + quit(code?: number): void { + super.quit() + exit(code ?? 0) + } + + start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) { + client ??= () => { + print(`Astal instance "${this.instanceName}" already running`) + exit(1) + } + + Object.assign(this, cfg) + setConsoleLogDomain(this.instanceName) + + this.requestHandler = requestHandler + this.connect("activate", () => { + const path: string[] = import.meta.url.split("/").slice(3) + const file = path.at(-1)!.replace(".js", ".css") + const css = `/${path.slice(0, -1).join("/")}/${file}` + if (file.endsWith(".css") && GLib.file_test(css, GLib.FileTest.EXISTS)) + this.apply_css(css, false) + + main?.(...programArgs) + }) + + if (!this.acquire_socket()) + return client(msg => Astal.Application.send_message(this.instanceName, msg)!, ...programArgs) + + if (css) + this.apply_css(css, false) + + if (icons) + this.add_icons(icons) + + hold ??= true + if (hold) + this.hold() + + this.runAsync([]) + } +} + +export default new AstalJS() diff --git a/core/gjs/src/astalify.ts b/core/gjs/src/astalify.ts new file mode 100644 index 0000000..be395ee --- /dev/null +++ b/core/gjs/src/astalify.ts @@ -0,0 +1,331 @@ +import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "./binding.js" +import { Astal, Gtk, Gdk } from "./imports.js" +import { execAsync } from "./process.js" +import Variable from "./variable.js" + +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +function setChildren(parent: Gtk.Widget, children: Gtk.Widget[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + // remove + if (parent instanceof Gtk.Bin) { + const ch = parent.get_child() + if (ch) + parent.remove(ch) + } + + // FIXME: add rest of the edge cases like Stack + if (parent instanceof Astal.Box) { + parent.set_children(children) + } + + else if (parent instanceof Astal.CenterBox) { + parent.startWidget = children[0] + parent.centerWidget = children[1] + parent.endWidget = children[2] + } + + else if (parent instanceof Astal.Overlay) { + const [child, ...overlays] = children + parent.set_child(child) + parent.set_overlays(overlays) + } + + else if (parent instanceof Gtk.Container) { + for (const ch of children) + parent.add(ch) + } +} + +function mergeBindings(array: any[]) { + function getValues(...args: any[]) { + let i = 0 + return array.map(value => value instanceof Binding + ? args[i++] + : value, + ) + } + + const bindings = array.filter(i => i instanceof Binding) + + if (bindings.length === 0) + return array + + if (bindings.length === 1) + return bindings[0].as(getValues) + + return Variable.derive(bindings, getValues)() +} + +function setProp(obj: any, prop: string, value: any) { + try { + const setter = `set_${snakeify(prop)}` + if (typeof obj[setter] === "function") + return obj[setter](value) + + if (Object.hasOwn(obj, prop)) + return (obj[prop] = value) + } + catch (error) { + console.error(`could not set property "${prop}" on ${obj}:`, error) + } + + console.error(`could not set property "${prop}" on ${obj}`) +} + +export type Widget<C extends InstanceType<typeof Gtk.Widget>> = C & { + className: string + css: string + cursor: Cursor + clickThrough: boolean + toggleClassName(name: string, on?: boolean): void + hook( + object: Connectable, + signal: string, + callback: (self: Widget<C>, ...args: any[]) => void, + ): Widget<C> + hook( + object: Subscribable, + callback: (self: Widget<C>, ...args: any[]) => void, + ): Widget<C> +} + +function hook( + self: Gtk.Widget, + object: Connectable | Subscribable, + signalOrCallback: string | ((self: Gtk.Widget, ...args: any[]) => void), + callback?: (self: Gtk.Widget, ...args: any[]) => void, +) { + if (typeof object.connect === "function" && callback) { + const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { + callback(self, ...args) + }) + self.connect("destroy", () => { + (object.disconnect as Connectable["disconnect"])(id) + }) + } + + else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { + const unsub = object.subscribe((...args: unknown[]) => { + signalOrCallback(self, ...args) + }) + self.connect("destroy", unsub) + } + + return self +} + +function ctor(self: any, config: any = {}, children: any = []) { + const { setup, ...props } = config + props.visible ??= true + + const bindings = Object.keys(props).reduce((acc: any, prop) => { + if (props[prop] instanceof Binding) { + const binding = props[prop] + setProp(self, prop, binding.get()) + delete props[prop] + return [...acc, [prop, binding]] + } + return acc + }, []) + + const onHandlers = Object.keys(props).reduce((acc: any, key) => { + if (key.startsWith("on")) { + const sig = kebabify(key).split("-").slice(1).join("-") + const handler = props[key] + delete props[key] + return [...acc, [sig, handler]] + } + return acc + }, []) + + Object.assign(self, props) + + for (const [signal, callback] of onHandlers) { + if (typeof callback === "function") { + self.connect(signal, callback) + } + else { + self.connect(signal, () => execAsync(callback) + .then(print).catch(console.error)) + } + } + + for (const [prop, bind] of bindings) { + if (prop === "child" || prop === "children") { + self.connect("destroy", bind.subscribe((v: any) => { + setChildren(self, v) + })) + } + self.connect("destroy", bind.subscribe((v: any) => { + setProp(self, prop, v) + })) + } + + children = mergeBindings(children.flat(Infinity)) + if (children instanceof Binding) { + setChildren(self, children.get()) + self.connect("destroy", children.subscribe((v) => { + setChildren(self, v) + })) + } + else { + if (children.length > 0) + setChildren(self, children) + } + + setup?.(self) + return self +} + +function proxify< + C extends typeof Gtk.Widget, +>(klass: C) { + Object.defineProperty(klass.prototype, "className", { + get() { return Astal.widget_get_class_names(this).join(" ") }, + set(v) { Astal.widget_set_class_names(this, v.split(/\s+/)) }, + }) + + Object.defineProperty(klass.prototype, "css", { + get() { return Astal.widget_get_css(this) }, + set(v) { Astal.widget_set_css(this, v) }, + }) + + Object.defineProperty(klass.prototype, "cursor", { + get() { return Astal.widget_get_cursor(this) }, + set(v) { Astal.widget_set_cursor(this, v) }, + }) + + Object.defineProperty(klass.prototype, "clickThrough", { + get() { return Astal.widget_get_click_through(this) }, + set(v) { Astal.widget_set_click_through(this, v) }, + }) + + Object.assign(klass.prototype, { + hook: function (obj: any, sig: any, callback: any) { + return hook(this as InstanceType<C>, obj, sig, callback) + }, + toggleClassName: function name(cn: string, cond = true) { + Astal.widget_toggle_class_name(this as InstanceType<C>, cn, cond) + }, + set_class_name: function (name: string) { + // @ts-expect-error unknown key + this.className = name + }, + set_css: function (css: string) { + // @ts-expect-error unknown key + this.css = css + }, + set_cursor: function (cursor: string) { + // @ts-expect-error unknown key + this.cursor = cursor + }, + set_click_through: function (clickThrough: boolean) { + // @ts-expect-error unknown key + this.clickThrough = clickThrough + }, + }) + + const proxy = new Proxy(klass, { + construct(_, [conf, ...children]) { + // @ts-expect-error abstract class + return ctor(new klass(), conf, children) + }, + apply(_t, _a, [conf, ...children]) { + // @ts-expect-error abstract class + return ctor(new klass(), conf, children) + }, + }) + + return proxy +} + +export default function astalify< + C extends typeof Gtk.Widget, + P extends Record<string, any>, + N extends string = "Widget", +>(klass: C) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Astal<N> = Omit<C, "new"> & { + new(props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>> + (props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>> + } + + return proxify(klass) as unknown as Astal<N> +} + +type BindableProps<T> = { + [K in keyof T]: Binding<T[K]> | T[K]; +} + +type SigHandler< + W extends InstanceType<typeof Gtk.Widget>, + Args extends Array<unknown>, +> = ((self: Widget<W>, ...args: Args) => unknown) | string | string[] + +export type ConstructProps< + Self extends InstanceType<typeof Gtk.Widget>, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler<Self, Signals[S]> +}> & Partial<{ + [Key in `on${string}`]: SigHandler<Self, any[]> +}> & BindableProps<Partial<Props> & { + className?: string + css?: string + cursor?: string + clickThrough?: boolean +}> & { + onDestroy?: (self: Widget<Self>) => unknown + onDraw?: (self: Widget<Self>) => unknown + onKeyPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onKeyReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onButtonPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onButtonReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onRealize?: (self: Widget<Self>) => unknown + setup?: (self: Widget<Self>) => void +} + +type Cursor = + | "default" + | "help" + | "pointer" + | "context-menu" + | "progress" + | "wait" + | "cell" + | "crosshair" + | "text" + | "vertical-text" + | "alias" + | "copy" + | "no-drop" + | "move" + | "not-allowed" + | "grab" + | "grabbing" + | "all-scroll" + | "col-resize" + | "row-resize" + | "n-resize" + | "e-resize" + | "s-resize" + | "w-resize" + | "ne-resize" + | "nw-resize" + | "sw-resize" + | "se-resize" + | "ew-resize" + | "ns-resize" + | "nesw-resize" + | "nwse-resize" + | "zoom-in" + | "zoom-out" diff --git a/core/gjs/src/binding.ts b/core/gjs/src/binding.ts new file mode 100644 index 0000000..feec6fc --- /dev/null +++ b/core/gjs/src/binding.ts @@ -0,0 +1,88 @@ +export const snakeify = (str: string) => str + .replace(/([a-z])([A-Z])/g, "$1_$2") + .replaceAll("-", "_") + .toLowerCase() + +export const kebabify = (str: string) => str + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replaceAll("_", "-") + .toLowerCase() + +export interface Subscribable<T = unknown> { + subscribe(callback: (value: T) => void): () => void + get(): T + [key: string]: any +} + +export interface Connectable { + connect(signal: string, callback: (...args: any[]) => unknown): number + disconnect(id: number): void + [key: string]: any +} + +export default class Binding<Value> { + private emitter: Subscribable<Value> | Connectable + private prop?: string + private transformFn = (v: any) => v + + static bind< + T extends Connectable, + P extends keyof T, + >(object: T, property: P): Binding<T[P]> + + static bind<T>(object: Subscribable<T>): Binding<T> + + static bind(emitter: Connectable | Subscribable, prop?: string) { + return new Binding(emitter, prop) + } + + private constructor(emitter: Connectable | Subscribable<Value>, prop?: string) { + this.emitter = emitter + this.prop = prop && kebabify(prop) + } + + toString() { + return `Binding<${this.emitter}${this.prop ? `, "${this.prop}"` : ""}>` + } + + as<T>(fn: (v: Value) => T): Binding<T> { + const bind = new Binding(this.emitter, this.prop) + bind.transformFn = (v: Value) => fn(this.transformFn(v)) + return bind as unknown as Binding<T> + } + + get(): Value { + if (typeof this.emitter.get === "function") + return this.transformFn(this.emitter.get()) + + if (typeof this.prop === "string") { + const getter = `get_${snakeify(this.prop)}` + if (typeof this.emitter[getter] === "function") + return this.transformFn(this.emitter[getter]()) + + return this.transformFn(this.emitter[this.prop]) + } + + throw Error("can not get value of binding") + } + + subscribe(callback: (value: Value) => void): () => void { + if (typeof this.emitter.subscribe === "function") { + return this.emitter.subscribe(() => { + callback(this.get()) + }) + } + else if (typeof this.emitter.connect === "function") { + const signal = `notify::${this.prop}` + const id = this.emitter.connect(signal, () => { + callback(this.get()) + }) + return () => { + (this.emitter.disconnect as Connectable["disconnect"])(id) + } + } + throw Error(`${this.emitter} is not bindable`) + } +} + +export const { bind } = Binding diff --git a/core/gjs/src/file.ts b/core/gjs/src/file.ts new file mode 100644 index 0000000..90b33a1 --- /dev/null +++ b/core/gjs/src/file.ts @@ -0,0 +1,44 @@ +import { Astal, Gio } from "./imports.js" + +export function readFile(path: string): string { + return Astal.read_file(path) || "" +} + +export function readFileAsync(path: string): Promise<string> { + return new Promise((resolve, reject) => { + Astal.read_file_async(path, (_, res) => { + try { + resolve(Astal.read_file_finish(res) || "") + } + catch (error) { + reject(error) + } + }) + }) +} + +export function writeFile(path: string, content: string): void { + Astal.write_file(path, content) +} + +export function writeFileAsync(path: string, content: string): Promise<void> { + return new Promise((resolve, reject) => { + Astal.write_file_async(path, content, (_, res) => { + try { + resolve(Astal.write_file_finish(res)) + } + catch (error) { + reject(error) + } + }) + }) +} + +export function monitorFile( + path: string, + callback: (file: string, event: Gio.FileMonitorEvent) => void, +): Gio.FileMonitor { + return Astal.monitor_file(path, (file: string, event: Gio.FileMonitorEvent) => { + callback(file, event) + })! +} diff --git a/core/gjs/src/imports.ts b/core/gjs/src/imports.ts new file mode 100644 index 0000000..cbed004 --- /dev/null +++ b/core/gjs/src/imports.ts @@ -0,0 +1,10 @@ +// this file's purpose is to have glib versions in one place +// this is only really needed for Gtk/Astal because +// ts-gir might generate gtk4 versions too + +export { default as Astal } from "gi://Astal?version=0.1" +export { default as GObject } from "gi://GObject?version=2.0" +export { default as Gio } from "gi://Gio?version=2.0" +export { default as Gtk } from "gi://Gtk?version=3.0" +export { default as Gdk } from "gi://Gdk?version=3.0" +export { default as GLib } from "gi://GLib?version=2.0" diff --git a/core/gjs/src/jsx/jsx-runtime.ts b/core/gjs/src/jsx/jsx-runtime.ts new file mode 100644 index 0000000..70f098f --- /dev/null +++ b/core/gjs/src/jsx/jsx-runtime.ts @@ -0,0 +1,87 @@ +import { Gtk } from "../imports.js" +import * as Widget from "../widgets.js" + +function isArrowFunction(func: any): func is (args: any) => any { + return !Object.hasOwn(func, "prototype") +} + +export function jsx( + ctor: keyof typeof ctors | typeof Gtk.Widget, + { children, ...props }: any, +) { + children ??= [] + + if (!Array.isArray(children)) + children = [children] + + children = children.filter(Boolean) + + if (typeof ctor === "string") + return (ctors as any)[ctor](props, children) + + if (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children + + if (isArrowFunction(ctor)) + return ctor(props) + + // @ts-expect-error can be class or function + return new ctor(props) +} + +const ctors = { + box: Widget.Box, + button: Widget.Button, + centerbox: Widget.CenterBox, + // TODO: circularprogress + drawingarea: Widget.DrawingArea, + entry: Widget.Entry, + eventbox: Widget.EventBox, + // TODO: fixed + // TODO: flowbox + icon: Widget.Icon, + label: Widget.Label, + levelbar: Widget.LevelBar, + // TODO: listbox + overlay: Widget.Overlay, + revealer: Widget.Revealer, + scrollable: Widget.Scrollable, + slider: Widget.Slider, + // TODO: stack + switch: Widget.Switch, + window: Widget.Window, +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + type Element = Gtk.Widget + type ElementClass = Gtk.Widget + interface IntrinsicElements { + box: Widget.BoxProps + button: Widget.ButtonProps + centerbox: Widget.CenterBoxProps + // TODO: circularprogress + drawingarea: Widget.DrawingAreaProps + entry: Widget.EntryProps + eventbox: Widget.EventBoxProps + // TODO: fixed + // TODO: flowbox + icon: Widget.IconProps + label: Widget.LabelProps + levelbar: Widget.LevelBarProps + // TODO: listbox + overlay: Widget.OverlayProps + revealer: Widget.RevealerProps + scrollable: Widget.ScrollableProps + slider: Widget.SliderProps + // TODO: stack + switch: Widget.SwitchProps + window: Widget.WindowProps + } + } +} + +export const jsxs = jsx diff --git a/core/gjs/src/process.ts b/core/gjs/src/process.ts new file mode 100644 index 0000000..c5329e2 --- /dev/null +++ b/core/gjs/src/process.ts @@ -0,0 +1,69 @@ +import { Astal } from "./imports.js" + +type Args<Out = void, Err = void> = { + cmd: string | string[] + out?: (stdout: string) => Out + err?: (stderr: string) => Err +} + +function args<O, E>(argsOrCmd: Args | string | string[], onOut: O, onErr: E) { + const params = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string" + return { + cmd: params ? argsOrCmd : argsOrCmd.cmd, + err: params ? onErr : argsOrCmd.err || onErr, + out: params ? onOut : argsOrCmd.out || onOut, + } +} + +export function subprocess(args: Args): Astal.Process +export function subprocess( + cmd: string | string[], + onOut?: (stdout: string) => void, + onErr?: (stderr: string) => void, +): Astal.Process +export function subprocess( + argsOrCmd: Args | string | string[], + onOut: (stdout: string) => void = print, + onErr: (stderr: string) => void = printerr, +) { + const { cmd, err, out } = args(argsOrCmd, onOut, onErr) + const proc = Array.isArray(cmd) + ? Astal.Process.subprocessv(cmd) + : Astal.Process.subprocess(cmd) + + proc.connect("stdout", (_, stdout: string) => out(stdout)) + proc.connect("stderr", (_, stderr: string) => err(stderr)) + return proc +} + +/** @throws {GLib.Error} Throws stderr */ +export function exec(cmd: string | string[]) { + return Array.isArray(cmd) + ? Astal.Process.execv(cmd) + : Astal.Process.exec(cmd) +} + +export function execAsync(cmd: string | string[]): Promise<string> { + return new Promise((resolve, reject) => { + if (Array.isArray(cmd)) { + Astal.Process.exec_asyncv(cmd, (_, res) => { + try { + resolve(Astal.Process.exec_asyncv_finish(res)) + } + catch (error) { + reject(error) + } + }) + } + else { + Astal.Process.exec_async(cmd, (_, res) => { + try { + resolve(Astal.Process.exec_finish(res)) + } + catch (error) { + reject(error) + } + }) + } + }) +} diff --git a/core/gjs/src/time.ts b/core/gjs/src/time.ts new file mode 100644 index 0000000..4e28ad0 --- /dev/null +++ b/core/gjs/src/time.ts @@ -0,0 +1,13 @@ +import { Astal } from "./imports.js" + +export function interval(interval: number, callback?: () => void) { + return Astal.Time.interval(interval, () => void callback?.()) +} + +export function timeout(timeout: number, callback?: () => void) { + return Astal.Time.timeout(timeout, () => void callback?.()) +} + +export function idle(callback?: () => void) { + return Astal.Time.idle(() => void callback?.()) +} diff --git a/core/gjs/src/variable.ts b/core/gjs/src/variable.ts new file mode 100644 index 0000000..d583ab1 --- /dev/null +++ b/core/gjs/src/variable.ts @@ -0,0 +1,227 @@ +import Binding, { type Connectable } from "./binding.js" +import { Astal } from "./imports.js" +import { interval } from "./time.js" +import { execAsync, subprocess } from "./process.js" + +class VariableWrapper<T> extends Function { + private variable!: Astal.VariableBase + private errHandler? = console.error + + private _value: T + private _poll?: Astal.Time + private _watch?: Astal.Process + + private pollInterval = 1000 + private pollExec?: string[] | string + private pollTransform?: (stdout: string, prev: T) => T + private pollFn?: (prev: T) => T | Promise<T> + + private watchTransform?: (stdout: string, prev: T) => T + private watchExec?: string[] | string + + constructor(init: T) { + super() + this._value = init + this.variable = new Astal.VariableBase() + this.variable.connect("dropped", () => { + this.stopWatch() + this.stopPoll() + }) + this.variable.connect("error", (_, err) => this.errHandler?.(err)) + return new Proxy(this, { + apply: (target, _, args) => target._call(args[0]), + }) + } + + private _call<R = T>(transform?: (value: T) => R): Binding<R> { + const b = Binding.bind(this) + return transform ? b.as(transform) : b as unknown as Binding<R> + } + + toString() { + return String(`Variable<${this.get()}>`) + } + + get(): T { return this._value } + set(value: T) { + if (value !== this._value) { + this._value = value + this.variable.emit("changed") + } + } + + startPoll() { + if (this._poll) + return + + if (this.pollFn) { + this._poll = interval(this.pollInterval, () => { + const v = this.pollFn!(this.get()) + if (v instanceof Promise) { + v.then(v => this.set(v)) + .catch(err => this.variable.emit("error", err)) + } + else { + this.set(v) + } + }) + } + else if (this.pollExec) { + this._poll = interval(this.pollInterval, () => { + execAsync(this.pollExec!) + .then(v => this.set(this.pollTransform!(v, this.get()))) + .catch(err => this.variable.emit("error", err)) + }) + } + } + + startWatch() { + if (this._watch) + return + + this._watch = subprocess({ + cmd: this.watchExec!, + out: out => this.set(this.watchTransform!(out, this.get())), + err: err => this.variable.emit("error", err), + }) + } + + stopPoll() { + this._poll?.cancel() + delete this._poll + } + + stopWatch() { + this._watch?.kill() + delete this._watch + } + + isPolling() { return !!this._poll } + isWatching() { return !!this._watch } + + drop() { + this.variable.emit("dropped") + this.variable.run_dispose() + } + + onDropped(callback: () => void) { + this.variable.connect("dropped", callback) + return this as unknown as Variable<T> + } + + onError(callback: (err: string) => void) { + delete this.errHandler + this.variable.connect("error", (_, err) => callback(err)) + return this as unknown as Variable<T> + } + + subscribe(callback: (value: T) => void) { + const id = this.variable.connect("changed", () => { + callback(this.get()) + }) + return () => this.variable.disconnect(id) + } + + poll( + interval: number, + exec: string | string[], + transform?: (stdout: string, prev: T) => T + ): Variable<T> + + poll( + interval: number, + callback: (prev: T) => T | Promise<T> + ): Variable<T> + + poll( + interval: number, + exec: string | string[] | ((prev: T) => T | Promise<T>), + transform: (stdout: string, prev: T) => T = out => out as T, + ) { + this.stopPoll() + this.pollInterval = interval + this.pollTransform = transform + if (typeof exec === "function") { + this.pollFn = exec + delete this.pollExec + } + else { + this.pollExec = exec + delete this.pollFn + } + this.startPoll() + return this as unknown as Variable<T> + } + + watch( + exec: string | string[], + transform: (stdout: string, prev: T) => T = out => out as T, + ) { + this.stopWatch() + this.watchExec = exec + this.watchTransform = transform + this.startWatch() + return this as unknown as Variable<T> + } + + observe( + objs: Array<[obj: Connectable, signal: string]>, + callback: (...args: any[]) => T): Variable<T> + + observe( + obj: Connectable, + signal: string, + callback: (...args: any[]) => T): Variable<T> + + observe( + objs: Connectable | Array<[obj: Connectable, signal: string]>, + sigOrFn: string | ((obj: Connectable, ...args: any[]) => T), + callback?: (obj: Connectable, ...args: any[]) => T, + ) { + const f = typeof sigOrFn === "function" ? sigOrFn : callback ?? (() => this.get()) + const set = (obj: Connectable, ...args: any[]) => this.set(f(obj, ...args)) + + if (Array.isArray(objs)) { + for (const obj of objs) { + const [o, s] = obj + o.connect(s, set) + } + } + else { + if (typeof sigOrFn === "string") + objs.connect(sigOrFn, set) + } + + return this as unknown as Variable<T> + } + + static derive< + const Deps extends Array<Variable<any> | Binding<any>>, + Args extends { + [K in keyof Deps]: Deps[K] extends Variable<infer T> + ? T : Deps[K] extends Binding<infer T> ? T : never + }, + V = Args, + >(deps: Deps, fn: (...args: Args) => V = (...args) => args as unknown as V) { + const update = () => fn(...deps.map(d => d.get()) as Args) + const derived = new Variable(update()) + const unsubs = deps.map(dep => dep.subscribe(() => derived.set(update()))) + derived.onDropped(() => unsubs.map(unsub => unsub())) + return derived + } +} + +export interface Variable<T> extends Omit<VariableWrapper<T>, "bind"> { + <R>(transform: (value: T) => R): Binding<R> + (): Binding<T> +} + +export const Variable = new Proxy(VariableWrapper as any, { + apply: (_t, _a, args) => new VariableWrapper(args[0]), +}) as { + derive: typeof VariableWrapper["derive"] + <T>(init: T): Variable<T> + new<T>(init: T): Variable<T> +} + +export default Variable diff --git a/core/gjs/src/widgets.ts b/core/gjs/src/widgets.ts new file mode 100644 index 0000000..82d4708 --- /dev/null +++ b/core/gjs/src/widgets.ts @@ -0,0 +1,109 @@ +/* eslint-disable max-len */ +import { Astal, Gtk } from "./imports.js" +import astalify, { type ConstructProps, type Widget } from "./astalify.js" + +export { astalify, ConstructProps } + +// Box +export type Box = Widget<Astal.Box> +export const Box = astalify<typeof Astal.Box, BoxProps, "Box">(Astal.Box) +export type BoxProps = ConstructProps<Astal.Box, Astal.Box.ConstructorProps> + +// Button +export type Button = Widget<Astal.Button> +export const Button = astalify<typeof Astal.Button, ButtonProps, "Button">(Astal.Button) +export type ButtonProps = ConstructProps<Astal.Button, Astal.Button.ConstructorProps, { + onClicked: [] + onClick: [event: Astal.ClickEvent] + onClickRelease: [event: Astal.ClickEvent] + onHover: [event: Astal.HoverEvent] + onHoverLost: [event: Astal.HoverEvent] + onScroll: [event: Astal.ScrollEvent] +}> + +// CenterBox +export type CenterBox = Widget<Astal.CenterBox> +export const CenterBox = astalify<typeof Astal.CenterBox, CenterBoxProps, "CenterBox">(Astal.CenterBox) +export type CenterBoxProps = ConstructProps<Astal.CenterBox, Astal.CenterBox.ConstructorProps> + +// TODO: CircularProgress + +// DrawingArea +export type DrawingArea = Widget<Gtk.DrawingArea> +export const DrawingArea = astalify<typeof Gtk.DrawingArea, DrawingAreaProps, "DrawingArea">(Gtk.DrawingArea) +export type DrawingAreaProps = ConstructProps<Gtk.DrawingArea, Gtk.DrawingArea.ConstructorProps, { + onDraw: [cr: any] // TODO: cairo types +}> + +// Entry +export type Entry = Widget<Gtk.Entry> +export const Entry = astalify<typeof Gtk.Entry, EntryProps, "Entry">(Gtk.Entry) +export type EntryProps = ConstructProps<Gtk.Entry, Gtk.Entry.ConstructorProps, { + onChanged: [] + onActivate: [] +}> + +// EventBox +export type EventBox = Widget<Astal.EventBox> +export const EventBox = astalify<typeof Astal.EventBox, EventBoxProps, "EventBox">(Astal.EventBox) +export type EventBoxProps = ConstructProps<Astal.EventBox, Astal.EventBox.ConstructorProps, { + onClick: [event: Astal.ClickEvent] + onClickRelease: [event: Astal.ClickEvent] + onHover: [event: Astal.HoverEvent] + onHoverLost: [event: Astal.HoverEvent] + onScroll: [event: Astal.ScrollEvent] +}> + +// TODO: Fixed +// TODO: FlowBox + +// Icon +export type Icon = Widget<Astal.Icon> +export const Icon = astalify<typeof Astal.Icon, IconProps, "Icon">(Astal.Icon) +export type IconProps = ConstructProps<Astal.Icon, Astal.Icon.ConstructorProps> + +// Label +export type Label = Widget<Astal.Label> +export const Label = astalify<typeof Astal.Label, LabelProps, "Label">(Astal.Label) +export type LabelProps = ConstructProps<Astal.Label, Astal.Label.ConstructorProps> + +// LevelBar +export type LevelBar = Widget<Astal.LevelBar> +export const LevelBar = astalify<typeof Astal.LevelBar, LevelBarProps, "LevelBar">(Astal.LevelBar) +export type LevelBarProps = ConstructProps<Astal.LevelBar, Astal.LevelBar.ConstructorProps> + +// TODO: ListBox + +// Overlay +export type Overlay = Widget<Astal.Overlay> +export const Overlay = astalify<typeof Astal.Overlay, OverlayProps, "Overlay">(Astal.Overlay) +export type OverlayProps = ConstructProps<Astal.Overlay, Astal.Overlay.ConstructorProps> + +// Revealer +export type Revealer = Widget<Gtk.Revealer> +export const Revealer = astalify<typeof Gtk.Revealer, RevealerProps, "Revealer">(Gtk.Revealer) +export type RevealerProps = ConstructProps<Gtk.Revealer, Gtk.Revealer.ConstructorProps> + +// Scrollable +export type Scrollable = Widget<Astal.Scrollable> +export const Scrollable = astalify<typeof Astal.Scrollable, ScrollableProps, "Scrollable">(Astal.Scrollable) +export type ScrollableProps = ConstructProps<Astal.Scrollable, Astal.Scrollable.ConstructorProps> + +// Slider +export type Slider = Widget<Astal.Slider> +export const Slider = astalify<typeof Astal.Slider, SliderProps, "Slider">(Astal.Slider) +export type SliderProps = ConstructProps<Astal.Slider, Astal.Slider.ConstructorProps, { + onDragged: [] +}> + +// TODO: Stack + +// Switch +export type Switch = Widget<Gtk.Switch> +export const Switch = astalify<typeof Gtk.Switch, SwitchProps, "Switch">(Gtk.Switch) +export type SwitchProps = ConstructProps<Gtk.Switch, Gtk.Switch.ConstructorProps> + +// Window +export type Window = Widget<Astal.Window> +export const Window = astalify<typeof Astal.Window, WindowProps, "Window">(Astal.Window) +export type WindowProps = ConstructProps<Astal.Window, Astal.Window.ConstructorProps> diff --git a/core/gjs/tsconfig.json b/core/gjs/tsconfig.json new file mode 100644 index 0000000..b93779f --- /dev/null +++ b/core/gjs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": [ + "ESNext" + ], + "outDir": "dist", + "declaration": true, + "strict": true, + "moduleResolution": "Bundler", + "skipLibCheck": true, + "checkJs": true, + "allowJs": true, + "jsx": "react-jsx", + "jsxImportSource": "./src/jsx", + }, + "include": [ + "./node_modules/@girs", + "./src/**/*", + "./index.ts", + ] +} diff --git a/core/lua/astal-dev-1.rockspec b/core/lua/astal-dev-1.rockspec new file mode 100644 index 0000000..7f5ae95 --- /dev/null +++ b/core/lua/astal-dev-1.rockspec @@ -0,0 +1,31 @@ +package = "astal" +version = "dev-1" + +source = { + url = "git+https://github.com/astal-sh/libastal", +} + +description = { + summary = "lua bindings for libastal.", + homepage = "https://github.com/astal-sh/libastal", + license = "GPL-3", +} + +dependencies = { + "lua >= 5.1, < 5.4", + "lgi >= 0.9.2", +} + +build = { + type = "builtin", + modules = { + ["astal.application"] = "astal/application.lua", + ["astal.binding"] = "astal/binding.lua", + ["astal.init"] = "astal/init.lua", + ["astal.process"] = "astal/process.lua", + ["astal.time"] = "astal/time.lua", + ["astal.variable"] = "astal/variable.lua", + ["astal.widget"] = "astal/widget.lua", + ["astal.file"] = "astal/file.lua", + }, +} diff --git a/core/lua/astal/application.lua b/core/lua/astal/application.lua new file mode 100644 index 0000000..663a457 --- /dev/null +++ b/core/lua/astal/application.lua @@ -0,0 +1,94 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") + +local AstalLua = Astal.Application:derive("AstalLua") +local request_handler + +function AstalLua:do_request(msg, conn) + if type(request_handler) == "function" then + request_handler(msg, function(response) + Astal.write_sock(conn, tostring(response), function(_, res) + Astal.write_sock_finish(res) + end) + end) + else + Astal.Application.do_request(self, msg, conn) + end +end + +function AstalLua:quit(code) + Astal.Application.quit(self) + os.exit(code) +end + +local app = AstalLua() + +---@class StartConfig +---@field icons? string +---@field instance_name? string +---@field gtk_theme? string +---@field icon_theme? string +---@field cursor_theme? string +---@field css? string +---@field hold? boolean +---@field request_handler? fun(msg: string, response: fun(res: any)) +---@field main? fun(...): unknown +---@field client? fun(message: fun(msg: string): string, ...): unknown + +---@param config StartConfig | nil +function Astal.Application:start(config) + if config == nil then + config = {} + end + + if config.client == nil then + config.client = function() + print('Astal instance "' .. app.instance_name .. '" is already running') + os.exit(1) + end + end + + if config.hold == nil then + config.hold = true + end + + request_handler = config.request_handler + + if config.css then + self:apply_css(config.css) + end + if config.icons then + self:add_icons(config.icons) + end + if config.instance_name then + self.instance_name = config.instance_name + end + if config.gtk_theme then + self.gtk_theme = config.gtk_theme + end + if config.icon_theme then + self.icon_theme = config.icon_theme + end + if config.cursor_theme then + self.cursor_theme = config.cursor_theme + end + + app.on_activate = function() + if type(config.main) == "function" then + config.main(table.unpack(arg)) + end + if config.hold then + self:hold() + end + end + + if not app:acquire_socket() then + return config.client(function(msg) + return Astal.Application.send_message(self.instance_name, msg) + end, table.unpack(arg)) + end + + self:run(nil) +end + +return app diff --git a/core/lua/astal/binding.lua b/core/lua/astal/binding.lua new file mode 100644 index 0000000..50509d1 --- /dev/null +++ b/core/lua/astal/binding.lua @@ -0,0 +1,65 @@ +local lgi = require("lgi") +local GObject = lgi.require("GObject", "2.0") + +---@class Binding +---@field emitter table|Variable +---@field property? string +---@field transformFn function +local Binding = {} + +---@param emitter table +---@param property? string +---@return Binding +function Binding.new(emitter, property) + return setmetatable({ + emitter = emitter, + property = property, + transformFn = function(v) + return v + end, + }, Binding) +end + +function Binding:__tostring() + local str = "Binding<" .. tostring(self.emitter) + if self.property ~= nil then + str = str .. ", " .. self.property + end + return str .. ">" +end + +function Binding:get() + if type(self.emitter.get) == "function" then + return self.transformFn(self.emitter:get()) + end + return self.transformFn(self.emitter[self.property]) +end + +---@param transform fun(value: any): any +---@return Binding +function Binding:as(transform) + local b = Binding.new(self.emitter, self.property) + b.transformFn = function(v) + return transform(self.transformFn(v)) + end + return b +end + +---@param callback fun(value: any) +---@return function +function Binding:subscribe(callback) + if type(self.emitter.subscribe) == "function" then + return self.emitter:subscribe(function() + callback(self:get()) + end) + end + local id = self.emitter.on_notify:connect(function() + callback(self:get()) + end, self.property, false) + return function() + GObject.signal_handler_disconnect(self.emitter, id) + end +end + +Binding.__index = Binding +return Binding diff --git a/core/lua/astal/file.lua b/core/lua/astal/file.lua new file mode 100644 index 0000000..ca5a592 --- /dev/null +++ b/core/lua/astal/file.lua @@ -0,0 +1,45 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local GObject = lgi.require("GObject", "2.0") + +local M = {} + +---@param path string +---@return string +function M.read_file(path) + return Astal.read_file(path) +end + +---@param path string +---@param callback fun(content: string, err: string): nil +function M.read_file_async(path, callback) + Astal.read_file_async(path, function(_, res) + local content, err = Astal.read_file_finish(res) + callback(content, err) + end) +end + +---@param path string +---@param content string +function M.write_file(path, content) + Astal.write_file(path, content) +end + +---@param path string +---@param content string +---@param callback? fun(err: string): nil +function M.write_file_async(path, content, callback) + Astal.write_file_async(path, content, function(_, res) + if type(callback) == "function" then + callback(Astal.write_file_finish(res)) + end + end) +end + +---@param path string +---@param callback fun(file: string, event: integer): nil +function M.monitor_file(path, callback) + return Astal.monitor_file(path, GObject.Closure(callback)) +end + +return M diff --git a/core/lua/astal/init.lua b/core/lua/astal/init.lua new file mode 100644 index 0000000..f56c3f5 --- /dev/null +++ b/core/lua/astal/init.lua @@ -0,0 +1,41 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local Gtk = lgi.require("Gtk", "3.0") +local Gdk = lgi.require("Gdk", "3.0") +local GObject = lgi.require("GObject", "2.0") +local Widget = require("astal.widget") +local Variable = require("astal.variable") +local Binding = require("astal.binding") +local App = require("astal.application") +local Process = require("astal.process") +local Time = require("astal.time") +local File = require("astal.file") + +return { + App = App, + Variable = Variable, + Widget = Widget, + bind = Binding.new, + + interval = Time.interval, + timeout = Time.timeout, + idle = Time.idle, + + subprocess = Process.subprocess, + exec = Process.exec, + exec_async = Process.exec_async, + + read_file = File.read_file, + read_file_async = File.read_file_async, + write_file = File.write_file, + write_file_async = File.write_file_async, + monitor_file = File.monitor_file, + + Astal = Astal, + Gtk = Gtk, + Gdk = Gdk, + GObject = GObject, + GLib = lgi.require("GLib", "2.0"), + Gio = lgi.require("Gio", "2.0"), + require = lgi.require, +} diff --git a/core/lua/astal/process.lua b/core/lua/astal/process.lua new file mode 100644 index 0000000..3d10f8b --- /dev/null +++ b/core/lua/astal/process.lua @@ -0,0 +1,94 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") + +local M = {} + +local defualt_proc_args = function(on_stdout, on_stderr) + if on_stdout == nil then + on_stdout = function(out) + io.stdout:write(tostring(out) .. "\n") + return tostring(out) + end + end + + if on_stderr == nil then + on_stderr = function(err) + io.stderr:write(tostring(err) .. "\n") + return tostring(err) + end + end + + return on_stdout, on_stderr +end + +---@param commandline string | string[] +---@param on_stdout? fun(out: string): nil +---@param on_stderr? fun(err: string): nil +---@return { kill: function } | nil proc +function M.subprocess(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + local proc, fail + if type(commandline) == "table" then + proc, fail = Astal.Process.subprocessv(commandline) + else + proc, fail = Astal.Process.subprocess(commandline) + end + if fail ~= nil then + err(fail) + return nil + end + proc.on_stdout = function(_, str) + out(str) + end + proc.on_stderr = function(_, str) + err(str) + end + return proc +end + +---@generic T +---@param commandline string | string[] +---@param on_stdout? fun(out: string): T +---@param on_stderr? fun(err: string): T +---@return T +function M.exec(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + local stdout, stderr + if type(commandline) == "table" then + stdout, stderr = Astal.Process.execv(commandline) + else + stdout, stderr = Astal.Process.exec(commandline) + end + if stderr then + return err(stderr) + end + return out(stdout) +end + +---@param commandline string | string[] +---@param on_stdout? fun(out: string): nil +---@param on_stderr? fun(err: string): nil +function M.exec_async(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + if type(commandline) == "table" then + Astal.Process.exec_asyncv(commandline, function(_, res) + local stdout, fail = Astal.exec_asyncv_finish(res) + if fail ~= nil then + err(fail) + else + out(stdout) + end + end) + else + Astal.Process.exec_async(commandline, function(_, res) + local stdout, fail = Astal.exec_finish(res) + if fail ~= nil then + err(fail) + else + out(stdout) + end + end) + end +end + +return M diff --git a/core/lua/astal/time.lua b/core/lua/astal/time.lua new file mode 100644 index 0000000..f4e2b81 --- /dev/null +++ b/core/lua/astal/time.lua @@ -0,0 +1,27 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local GObject = lgi.require("GObject", "2.0") + +local M = {} + +---@param interval number +---@param fn function +---@return { cancel: function, on_now: function } +function M.interval(interval, fn) + return Astal.Time.interval(interval, GObject.Closure(fn)) +end + +---@param timeout number +---@param fn function +---@return { cancel: function, on_now: function } +function M.timeout(timeout, fn) + return Astal.Time.timeout(timeout, GObject.Closure(fn)) +end + +---@param fn function +---@return { cancel: function, on_now: function } +function M.idle(fn) + return Astal.Time.idle(GObject.Closure(fn)) +end + +return M diff --git a/core/lua/astal/variable.lua b/core/lua/astal/variable.lua new file mode 100644 index 0000000..1e894b5 --- /dev/null +++ b/core/lua/astal/variable.lua @@ -0,0 +1,276 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.binding") +local Time = require("astal.time") +local Process = require("astal.process") + +---@class Variable +---@field private variable table +---@field private err_handler? function +---@field private _value any +---@field private _poll? table +---@field private _watch? table +---@field private poll_interval number +---@field private poll_exec? string[] | string +---@field private poll_transform? fun(next: any, prev: any): any +---@field private poll_fn? function +---@field private watch_transform? fun(next: any, prev: any): any +---@field private watch_exec? string[] | string +local Variable = {} +Variable.__index = Variable + +---@param value any +---@return Variable +function Variable.new(value) + local v = Astal.VariableBase() + local variable = setmetatable({ + variable = v, + _value = value, + }, Variable) + v.on_dropped = function() + variable:stop_watch() + variable:stop_watch() + end + v.on_error = function(_, err) + if variable.err_handler then + variable.err_handler(err) + end + end + return variable +end + +---@param transform function +---@return Binding +function Variable:__call(transform) + if transform == nil then + transform = function(v) + return v + end + return Binding.new(self) + end + return Binding.new(self):as(transform) +end + +function Variable:__tostring() + return "Variable<" .. tostring(self:get()) .. ">" +end + +function Variable:get() + return self._value or nil +end + +function Variable:set(value) + if value ~= self:get() then + self._value = value + self.variable:emit_changed() + end +end + +function Variable:start_poll() + if self._poll ~= nil then + return + end + + if self.poll_fn then + self._poll = Time.interval(self.poll_interval, function() + self:set(self.poll_fn(self:get())) + end) + elseif self.poll_exec then + self._poll = Time.interval(self.poll_interval, function() + Process.exec_async(self.poll_exec, function(out) + self:set(self.poll_transform(out, self:get())) + end, function(err) + self.variable.emit_error(err) + end) + end) + end +end + +function Variable:start_watch() + if self._watch then + return + end + + self._watch = Process.subprocess(self.watch_exec, function(out) + self:set(self.watch_transform(out, self:get())) + end, function(err) + self.variable.emit_error(err) + end) +end + +function Variable:stop_poll() + if self._poll then + self._poll.cancel() + end + self._poll = nil +end + +function Variable:stop_watch() + if self._watch then + self._watch.kill() + end + self._watch = nil +end + +function Variable:is_polling() + return self._poll ~= nil +end + +function Variable:is_watching() + return self._watch ~= nil +end + +function Variable:drop() + self.variable.emit_dropped() + self.variable.run_dispose() +end + +---@param callback function +---@return Variable +function Variable:on_dropped(callback) + self.variable.on_dropped = callback + return self +end + +---@param callback function +---@return Variable +function Variable:on_error(callback) + self.err_handler = nil + self.variable.on_eror = function(_, err) + callback(err) + end + return self +end + +---@param callback fun(value: any) +---@return function +function Variable:subscribe(callback) + local id = self.variable.on_changed:connect(function() + callback(self:get()) + end) + return function() + GObject.signal_handler_disconnect(self.variable, id) + end +end + +---@param interval number +---@param exec string | string[] | function +---@param transform? fun(next: any, prev: any): any +function Variable:poll(interval, exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.poll_interval = interval + self.poll_transform = transform + + if type(exec) == "function" then + self.poll_fn = exec + self.poll_exec = nil + else + self.poll_exec = exec + self.poll_fn = nil + end + self:start_poll() + return self +end + +---@param exec string | string[] +---@param transform? fun(next: any, prev: any): any +function Variable:watch(exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.watch_exec = exec + self.watch_transform = transform + self:start_watch() + return self +end + +---@param object table | table[] +---@param sigOrFn string | fun(...): any +---@param callback fun(...): any +---@return Variable +function Variable:observe(object, sigOrFn, callback) + local f + if type(sigOrFn) == "function" then + f = sigOrFn + elseif type(callback) == "function" then + f = callback + else + f = function() + return self:get() + end + end + local set = function(...) + self:set(f(...)) + end + + if type(sigOrFn) == "string" then + object["on_" .. sigOrFn]:connect(set) + else + for _, obj in ipairs(object) do + obj[1]["on_" .. obj[2]]:connect(set) + end + end + return self +end + +---@param deps Variable | (Binding | Variable)[] +---@param transform? fun(...): any +---@return Variable +function Variable.derive(deps, transform) + if type(transform) == "nil" then + transform = function(...) + return { ... } + end + end + + if getmetatable(deps) == Variable then + local var = Variable.new(transform(deps:get())) + deps:subscribe(function(v) + var:set(transform(v)) + end) + return var + end + + for i, var in ipairs(deps) do + if getmetatable(var) == Variable then + deps[i] = Binding.new(var) + end + end + + local update = function() + local params = {} + for _, binding in ipairs(deps) do + table.insert(params, binding:get()) + end + return transform(table.unpack(params)) + end + + local var = Variable.new(update()) + + local unsubs = {} + for _, b in ipairs(deps) do + table.insert(unsubs, b:subscribe(update)) + end + + var.variable.on_dropped = function() + for _, unsub in ipairs(unsubs) do + var:set(unsub()) + end + end + return var +end + +return setmetatable(Variable, { + __call = function(_, v) + return Variable.new(v) + end, +}) diff --git a/core/lua/astal/widget.lua b/core/lua/astal/widget.lua new file mode 100644 index 0000000..d2dadc6 --- /dev/null +++ b/core/lua/astal/widget.lua @@ -0,0 +1,276 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local Gtk = lgi.require("Gtk", "3.0") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.binding") +local Variable = require("astal.variable") +local exec_async = require("astal.process").exec_async + +local function filter(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + if fn(value, key) then + if type(key) == "number" then + table.insert(copy, value) + else + copy[key] = value + end + end + end + return copy +end + +local function map(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + copy[key] = fn(value) + end + return copy +end + +local flatten +flatten = function(tbl) + local copy = {} + for _, value in pairs(tbl) do + if type(value) == "table" and getmetatable(value) == nil then + for _, inner in pairs(flatten(value)) do + table.insert(copy, inner) + end + else + table.insert(copy, value) + end + end + return copy +end + +local function set_children(parent, children) + children = map(flatten(children), function(item) + if Gtk.Widget:is_type_of(item) then + return item + end + return Gtk.Label({ + visible = true, + label = tostring(item), + }) + end) + + -- remove + if Gtk.Bin:is_type_of(parent) then + local rm = parent:get_child() + if rm ~= nil then + parent:remove(rm) + end + end + + -- FIXME: add rest of the edge cases like Stack + if Astal.Box:is_type_of(parent) then + parent:set_children(children) + elseif Astal.CenterBox:is_type_of(parent) then + parent.start_widget = children[1] + parent.center_widget = children[2] + parent.end_widget = children[3] + elseif Astal.Overlay:is_type_of(parent) then + parent:set_child(children[1]) + children[1] = nil + parent:set_overlays(children) + elseif Gtk.Container:is_type_of(parent) then + for _, child in pairs(children) do + if Gtk.Widget:is_type_of(child) then + parent:add(child) + end + end + end +end + +local function merge_bindings(array) + local function get_values(...) + local args = { ... } + local i = 0 + return map(array, function(value) + if getmetatable(value) == Binding then + i = i + 1 + return args[i] + else + return value + end + end) + end + + local bindings = filter(array, function(v) + return getmetatable(v) == Binding + end) + + if #bindings == 0 then + return array + end + + if #bindings == 1 then + return bindings[1]:as(get_values) + end + + return Variable.derive(bindings, get_values)() +end + +local function astalify(ctor) + function ctor:hook(object, signalOrCallback, callback) + if type(object.subscribe) == "function" then + local unsub = object.subscribe(function(...) + signalOrCallback(self, ...) + end) + self.on_destroy = unsub + return + end + local id = object["on_" .. signalOrCallback](function(_, ...) + callback(self, ...) + end) + self.on_destroy = function() + GObject.signal_handler_disconnect(object, id) + end + end + + function ctor:toggle_class_name(name, on) + Astal.toggle_class_name(self, name, on) + end + + return function(tbl) + if tbl == nil then + tbl = {} + end + + local bindings = {} + local setup = tbl.setup + + -- collect children + local children = merge_bindings(flatten(filter(tbl, function(_, key) + return type(key) == "number" + end))) + + -- default visible to true + if type(tbl.visible) ~= "boolean" then + tbl.visible = true + end + + -- filter props + local props = filter(tbl, function(_, key) + return type(key) == "string" and key ~= "setup" + end) + + -- handle on_ handlers that are strings + for prop, value in pairs(props) do + if string.sub(prop, 0, 2) == "on" and type(value) ~= "function" then + props[prop] = function() + exec_async(value, print, print) + end + end + end + + -- handle bindings + for prop, value in pairs(props) do + if getmetatable(value) == Binding then + bindings[prop] = value + props[prop] = value:get() + end + end + + -- construct, attach bindings, add children + local widget = ctor() + + for prop, value in pairs(props) do + widget[prop] = value + end + + for prop, binding in pairs(bindings) do + widget.on_destroy = binding:subscribe(function(v) + widget[prop] = v + end) + end + + if getmetatable(children) == Binding then + set_children(widget, children:get()) + widget.on_destroy = children:subscribe(function(v) + set_children(widget, v) + end) + else + if #children > 0 then + set_children(widget, children) + end + end + + if type(setup) == "function" then + setup(widget) + end + + return widget + end +end + +local Widget = { + astalify = astalify, + Box = astalify(Astal.Box), + Button = astalify(Astal.Button), + CenterBox = astalify(Astal.CenterBox), + -- TODO: CircularProgress + DrawingArea = astalify(Gtk.DrawingArea), + Entry = astalify(Gtk.Entry), + EventBox = astalify(Astal.EventBox), + -- TODO: Fixed + -- TODO: FlowBox + Icon = astalify(Astal.Icon), + Label = astalify(Gtk.Label), + LevelBar = astalify(Astal.LevelBar), + -- TODO: ListBox + Overlay = astalify(Astal.Overlay), + Revealer = astalify(Gtk.Revealer), + Scrollable = astalify(Astal.Scrollable), + Slider = astalify(Astal.Slider), + -- TODO: Stack + Switch = astalify(Gtk.Switch), + Window = astalify(Astal.Window), +} + +Gtk.Widget._attribute.css = { + get = Astal.widget_get_css, + set = Astal.widget_set_css, +} + +Gtk.Widget._attribute.class_name = { + get = function(self) + local result = "" + local strings = Astal.widget_set_class_names(self) + for i, str in ipairs(strings) do + result = result .. str + if i < #strings then + result = result .. " " + end + end + return result + end, + set = function(self, class_name) + local names = {} + for word in class_name:gmatch("%S+") do + table.insert(names, word) + end + Astal.widget_set_class_names(self, names) + end, +} + +Gtk.Widget._attribute.cursor = { + get = Astal.widget_get_cursor, + set = Astal.widget_set_cursor, +} + +Gtk.Widget._attribute.click_through = { + get = Astal.widget_get_click_through, + set = Astal.widget_set_click_through, +} + +Astal.Box._attribute.children = { + get = Astal.Box.get_children, + set = Astal.Box.set_children, +} + +return setmetatable(Widget, { + __call = function(_, ctor) + return astalify(ctor) + end, +}) diff --git a/core/lua/stylua.toml b/core/lua/stylua.toml new file mode 100644 index 0000000..d4a4951 --- /dev/null +++ b/core/lua/stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 4 +column_width = 100 diff --git a/core/lua/test.lua b/core/lua/test.lua new file mode 100644 index 0000000..f5123a3 --- /dev/null +++ b/core/lua/test.lua @@ -0,0 +1,8 @@ +local App = require("astal.application") + +App:start({ + instance_name = "test", + main = function() + App:quit(1) + end, +}) diff --git a/core/meson.build b/core/meson.build new file mode 100644 index 0000000..4020a81 --- /dev/null +++ b/core/meson.build @@ -0,0 +1,30 @@ +project( + 'astal', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +prefix = get_option('prefix') +libdir = get_option('prefix') / get_option('libdir') +pkgdatadir = prefix / get_option('datadir') / 'astal' + +# math +add_project_arguments(['-X', '-lm'], language: 'vala') + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +if get_option('gjs') + install_subdir('gjs', install_dir: pkgdatadir) +endif + +subdir('src') diff --git a/core/meson_options.txt b/core/meson_options.txt new file mode 100644 index 0000000..a60ff42 --- /dev/null +++ b/core/meson_options.txt @@ -0,0 +1,17 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) + +option( + 'gjs', + type: 'boolean', + value: true, +) diff --git a/core/src/astal.vala b/core/src/astal.vala new file mode 100644 index 0000000..316da6b --- /dev/null +++ b/core/src/astal.vala @@ -0,0 +1,342 @@ +namespace Astal { +[DBus (name="io.Astal.Application")] +public class Application : Gtk.Application { + private List<Gtk.CssProvider> css_providers = new List<Gtk.CssProvider>(); + private SocketService service; + private DBusConnection conn; + private string _instance_name; + + public string socket_path { get; private set; } + + [DBus (visible=false)] + public string instance_name { + get { return _instance_name; } + set { + application_id = "io.Astal." + value; + _instance_name = value; + } + } + + [DBus (visible=false)] + public List<Gtk.Window> windows { + get { return get_windows(); } + } + + [DBus (visible=false)] + public Gtk.Settings settings { + get { return Gtk.Settings.get_default(); } + } + + [DBus (visible=false)] + public Gdk.Screen screen { + get { return Gdk.Screen.get_default(); } + } + + [DBus (visible=false)] + public string gtk_theme { + owned get { return settings.gtk_theme_name; } + set { settings.gtk_theme_name = value; } + } + + [DBus (visible=false)] + public string icon_theme { + owned get { return settings.gtk_icon_theme_name; } + set { settings.gtk_icon_theme_name = value; } + } + + [DBus (visible=false)] + public string cursor_theme { + owned get { return settings.gtk_cursor_theme_name; } + set { settings.gtk_cursor_theme_name = value; } + } + + [DBus (visible=false)] + public void reset_css() { + foreach(var provider in css_providers) { + Gtk.StyleContext.remove_provider_for_screen(screen, provider); + } + css_providers = new List<Gtk.CssProvider>(); + } + + public void inspector() throws DBusError, IOError { + Gtk.Window.set_interactive_debugging(true); + } + + [DBus (visible=false)] + public Gtk.Window? get_window(string name) { + foreach(var win in windows) { + if (win.name == name) + return win; + } + + critical("no window with name \"%s\"".printf(name)); + return null; + } + + public void toggle_window(string window) throws DBusError, IOError { + var win = get_window(window); + if (win != null) { + win.visible = !win.visible; + } else { + throw new IOError.FAILED("window not found"); + } + } + + [DBus (visible=false)] + public void apply_css(string style, bool reset = false) { + var provider = new Gtk.CssProvider(); + + if (reset) + reset_css(); + + try { + if (FileUtils.test(style, FileTest.EXISTS)) + provider.load_from_path(style); + else + provider.load_from_data(style); + } catch (Error err) { + critical(err.message); + } + + Gtk.StyleContext.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); + + css_providers.append(provider); + } + + [DBus (visible=false)] + public void add_icons(string? path) { + if (path != null) + Gtk.IconTheme.get_default().prepend_search_path(path); + } + + private async void _socket_request(SocketConnection conn) { + string message = yield read_sock(conn); + request(message != null ? message.strip() : "", conn); + } + + [DBus (visible=false)] + public virtual void request(string msg, SocketConnection conn) { + write_sock.begin(conn, @"missing response implementation on $application_id"); + } + + /** + * should be called before `run()` + * the return value indicates if instance is already running + */ + [DBus (visible=false)] + public bool acquire_socket() { + foreach (var instance in get_instances()) { + if (instance == instance_name) { + return false; + } + } + + var rundir = GLib.Environment.get_user_runtime_dir(); + socket_path = @"$rundir/$instance_name.sock"; + + if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)) { + try { + File.new_for_path(socket_path).delete(null); + } catch (Error err) { + critical(err.message); + } + } + + try { + service = new SocketService(); + service.add_address( + new UnixSocketAddress(socket_path), + SocketType.STREAM, + SocketProtocol.DEFAULT, + null, + null); + + service.incoming.connect((conn) => { + _socket_request.begin(conn, (_, res) => _socket_request.end(res)); + return false; + }); + + Bus.own_name( + BusType.SESSION, + "io.Astal." + instance_name, + BusNameOwnerFlags.NONE, + (conn) => { + try { + this.conn = conn; + conn.register_object("/io/Astal/Application", this); + } catch (Error err) { + critical(err.message); + } + }, + () => {}, + () => {}); + + info("socket acquired: %s\n", socket_path); + return true; + } catch (Error err) { + critical("could not acquire socket %s\n", application_id); + critical(err.message); + return false; + } + } + + public string message(string? msg) throws DBusError, IOError { + var rundir = GLib.Environment.get_user_runtime_dir(); + var socket_path = @"$rundir/$instance_name.sock"; + var client = new SocketClient(); + + if (msg == null) + msg = ""; + + try { + var conn = client.connect(new UnixSocketAddress(socket_path), null); + conn.output_stream.write(msg.concat("\x04").data); + + var stream = new DataInputStream(conn.input_stream); + return stream.read_upto("\x04", -1, null, null); + } catch (Error err) { + printerr(err.message); + return ""; + } + } + + public new void quit() throws DBusError, IOError { + if (service != null) { + if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)){ + try { + File.new_for_path(socket_path).delete(null); + } catch (Error err) { + warning(err.message); + } + } + } + + base.quit(); + } + + construct { + if (instance_name == null) + instance_name = "astal"; + + shutdown.connect(() => { try { quit(); } catch(Error err) {} }); + Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + } + + public static List<string> get_instances() { + var list = new List<string>(); + var prefix = "io.Astal."; + + try { + DBusImpl dbus = Bus.get_proxy_sync( + BusType.SESSION, + "org.freedesktop.DBus", + "/org/freedesktop/DBus" + ); + + foreach (var busname in dbus.list_names()) { + if (busname.has_prefix(prefix)) + list.append(busname.replace(prefix, "")); + } + } catch (Error err) { + critical(err.message); + } + + return list; + } + + public static void quit_instance(string instance) { + try { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.quit(); + } catch (Error err) { + critical(err.message); + } + } + + public static void open_inspector(string instance) { + try { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.inspector(); + } catch (Error err) { + critical(err.message); + } + } + + public static void toggle_window_by_name(string instance, string window) { + try { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.toggle_window(window); + } catch (Error err) { + critical(err.message); + } + } + + public static string send_message(string instance_name, string msg) { + var rundir = GLib.Environment.get_user_runtime_dir(); + var socket_path = @"$rundir/$instance_name.sock"; + var client = new SocketClient(); + + try { + var conn = client.connect(new UnixSocketAddress(socket_path), null); + conn.output_stream.write(msg.concat("\x04").data); + + var stream = new DataInputStream(conn.input_stream); + return stream.read_upto("\x04", -1, null, null); + } catch (Error err) { + printerr(err.message); + return ""; + } + } +} + +[DBus (name="org.freedesktop.DBus")] +private interface DBusImpl : DBusProxy { + public abstract string[] list_names() throws GLib.Error; +} + +[DBus (name="io.Astal.Application")] +private interface IApplication : DBusProxy { + public abstract void quit() throws GLib.Error; + public abstract void inspector() throws GLib.Error; + public abstract void toggle_window(string window) throws GLib.Error; + public abstract string message(string window) throws GLib.Error; +} + +public async string read_sock(SocketConnection conn) { + try { + var stream = new DataInputStream(conn.input_stream); + return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); + } catch (Error err) { + critical(err.message); + return err.message; + } +} + +public async void write_sock(SocketConnection conn, string response) { + try { + yield conn.output_stream.write_async( + response.concat("\x04").data, + Priority.DEFAULT); + } catch (Error err) { + critical(err.message); + } +} +} diff --git a/core/src/cli.vala b/core/src/cli.vala new file mode 100644 index 0000000..0b60cd1 --- /dev/null +++ b/core/src/cli.vala @@ -0,0 +1,87 @@ +private static bool version; +private static bool help; +private static bool list; +private static bool quit; +private static bool inspector; +private static string? toggle_window; +private static string? instance_name; + +private const GLib.OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, + { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null }, + { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null }, + { "inspector", 'I', OptionFlags.NONE, OptionArg.NONE, ref inspector, null, null }, + { "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, ref toggle_window, null, null }, + { "instance", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (help) { + print("Client for Astal.Application instances\n\n"); + print("Usage:\n"); + print(" %s [flags] message\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -l, --list List running Astal instances and exit\n"); + print(" -q, --quit Quit an Astal.Application instance\n"); + print(" -i, --instance Instance name of the Astal instance\n"); + print(" -I, --inspector Open up Gtk debug tool\n"); + print(" -t, --toggle-window Show or hide a window\n"); + return 0; + } + + if (version) { + print(Astal.VERSION); + return 0; + } + + if (instance_name == null) + instance_name = "astal"; + + if (list) { + foreach (var name in Astal.Application.get_instances()) + stdout.printf("%s\n", name); + + return 0; + } + + if (quit) { + Astal.Application.quit_instance(instance_name); + return 0; + } + + if (inspector) { + Astal.Application.open_inspector(instance_name); + return 0; + } + + if (toggle_window != null) { + Astal.Application.toggle_window_by_name(instance_name, toggle_window); + return 0; + } + + var request = ""; + for (var i = 1; i < argv.length; ++i) { + request = request.concat(" ", argv[i]); + } + + var reply = Astal.Application.send_message(instance_name, request); + print("%s\n", reply); + + return 0; +} diff --git a/core/src/config.vala.in b/core/src/config.vala.in new file mode 100644 index 0000000..88bfe9c --- /dev/null +++ b/core/src/config.vala.in @@ -0,0 +1,6 @@ +namespace Astal { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/core/src/file.vala b/core/src/file.vala new file mode 100644 index 0000000..d8acccc --- /dev/null +++ b/core/src/file.vala @@ -0,0 +1,81 @@ +namespace Astal { +public string read_file(string path) { + var str = ""; + try { + FileUtils.get_contents(path, out str, null); + } catch (Error error) { + critical(error.message); + } + return str; +} + +public async string read_file_async(string path) throws Error { + uint8[] content; + yield File.new_for_path(path).load_contents_async(null, out content, null); + return (string)content; +} + +public void write_file(string path, string content) { + try { + FileUtils.set_contents(path, content); + } catch (Error error) { + critical(error.message); + } +} + +public async void write_file_async(string path, string content) throws Error { + yield File.new_for_path(path).replace_contents_async( + content.data, + null, + false, + GLib.FileCreateFlags.REPLACE_DESTINATION, + null, + null); +} + +public FileMonitor? monitor_file(string path, Closure callback) { + try { + var file = File.new_for_path(path); + var mon = file.monitor(GLib.FileMonitorFlags.NONE); + + mon.changed.connect((file, _file, event) => { + var f = Value(Type.STRING); + var e = Value(Type.INT); + var ret = Value(Type.POINTER); + + f.set_string(file.get_path()); + e.set_int(event); + + callback.invoke(ref ret, { f, e }); + }); + + if (FileUtils.test(path, FileTest.IS_DIR)) { + var enumerator = file.enumerate_children("standard::*", + FileQueryInfoFlags.NONE, null); + + var i = enumerator.next_file(null); + while (i != null) { + if (i.get_file_type() == FileType.DIRECTORY) { + var filepath = file.get_child(i.get_name()).get_path(); + if (filepath != null) { + var m = monitor_file(path, callback); + mon.notify["cancelled"].connect(() => { + m.cancel(); + }); + } + } + i = enumerator.next_file(null); + } + } + + mon.ref(); + mon.notify["cancelled"].connect(() => { + mon.unref(); + }); + return mon; + } catch (Error error) { + critical(error.message); + return null; + } +} +} diff --git a/core/src/meson.build b/core/src/meson.build new file mode 100644 index 0000000..3e28d16 --- /dev/null +++ b/core/src/meson.build @@ -0,0 +1,98 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'Astal-' + api_version + '.gir' +typelib = 'Astal-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gio-unix-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('gtk+-3.0'), + dependency('gdk-pixbuf-2.0'), + dependency('gtk-layer-shell-0'), +] + +sources = [ + config, + 'widget/box.vala', + 'widget/button.vala', + 'widget/centerbox.vala', + # 'widget/circularprogress.vala', # TODO: math lib -X -lm + 'widget/eventbox.vala', + 'widget/icon.vala', + 'widget/label.vala', + 'widget/levelbar.vala', + 'widget/overlay.vala', + 'widget/scrollable.vala', + 'widget/slider.vala', + 'widget/widget.vala', + 'widget/window.vala', + 'astal.vala', + 'file.vala', + 'process.vala', + 'time.vala', + 'variable.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: libdir / 'pkgconfig', + variables: { + 'gjs': pkgdatadir / 'gjs', + }, + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', libdir / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: libdir / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/core/src/process.vala b/core/src/process.vala new file mode 100644 index 0000000..073fe93 --- /dev/null +++ b/core/src/process.vala @@ -0,0 +1,119 @@ +public class Astal.Process : Object { + private void read_stream(DataInputStream stream, bool err) { + stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => { + try { + var output = stream.read_line_utf8_async.end(res); + if (output != null) { + if (err) + stdout(output.strip()); + else + stderr(output.strip()); + + read_stream(stream, err); + } + } catch (Error err) { + printerr("%s\n", err.message); + } + }); + } + + private DataInputStream out_stream; + private DataInputStream err_stream; + private DataOutputStream in_stream; + private Subprocess process; + public string[] argv { construct; get; } + + public signal void stdout (string out); + public signal void stderr (string err); + + public void kill() { + process.force_exit(); + } + + public void signal(int signal_num) { + process.send_signal(signal_num); + } + + public void write(string in) throws Error { + in_stream.put_string(in); + } + + public void write_async(string in) { + in_stream.write_all_async.begin( + in.data, + Priority.DEFAULT, null, (_, res) => { + try { + in_stream.write_all_async.end(res, null); + } catch (Error err) { + printerr("%s\n", err.message); + } + } + ); + } + + public Process.subprocessv(string[] cmd) throws Error { + Object(argv: cmd); + process = new Subprocess.newv(cmd, + SubprocessFlags.STDIN_PIPE | + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + out_stream = new DataInputStream(process.get_stdout_pipe()); + err_stream = new DataInputStream(process.get_stderr_pipe()); + in_stream = new DataOutputStream(process.get_stdin_pipe()); + read_stream(out_stream, true); + read_stream(err_stream, false); + } + + public static Process subprocess(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return new Process.subprocessv(argv); + } + + public static string execv(string[] cmd) throws Error { + var process = new Subprocess.newv( + cmd, + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + + string err_str, out_str; + process.communicate_utf8(null, null, out out_str, out err_str); + var success = process.get_successful(); + process.dispose(); + if (success) + return out_str.strip(); + else + throw new IOError.FAILED(err_str.strip()); + } + + public static string exec(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return Process.execv(argv); + } + + public static async string exec_asyncv(string[] cmd) throws Error { + var process = new Subprocess.newv( + cmd, + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + + string err_str, out_str; + yield process.communicate_utf8_async(null, null, out out_str, out err_str); + var success = process.get_successful(); + process.dispose(); + if (success) + return out_str.strip(); + else + throw new IOError.FAILED(err_str.strip()); + } + + public static async string exec_async(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return yield exec_asyncv(argv); + } +} diff --git a/core/src/time.vala b/core/src/time.vala new file mode 100644 index 0000000..4034c04 --- /dev/null +++ b/core/src/time.vala @@ -0,0 +1,73 @@ +namespace Astal { +public class Time : Object { + public signal void now (); + public signal void cancelled (); + private Cancellable cancellable; + private uint timeout_id; + private bool fulfilled = false; + + construct { + cancellable = new Cancellable(); + cancellable.cancelled.connect(() => { + if (!fulfilled) { + Source.remove(timeout_id); + cancelled(); + dispose(); + } + }); + } + + private void connect_closure(Closure? closure) { + if (closure == null) + return; + + now.connect(() => { + Value ret = Value(Type.POINTER); // void + closure.invoke(ref ret, {}); + }); + } + + public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) { + connect_closure(fn); + Idle.add_once(() => now()); + timeout_id = Timeout.add(interval, () => { + now(); + return Source.CONTINUE; + }, prio); + } + + public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) { + connect_closure(fn); + timeout_id = Timeout.add(timeout, () => { + now(); + fulfilled = true; + return Source.REMOVE; + }, prio); + } + + public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) { + connect_closure(fn); + timeout_id = Idle.add(() => { + now(); + fulfilled = true; + return Source.REMOVE; + }, prio); + } + + public static Time interval(uint interval, Closure? fn) { + return new Time.interval_prio(interval, Priority.DEFAULT, fn); + } + + public static Time timeout(uint timeout, Closure? fn) { + return new Time.timeout_prio(timeout, Priority.DEFAULT, fn); + } + + public static Time idle(Closure? fn) { + return new Time.idle_prio(Priority.DEFAULT_IDLE, fn); + } + + public void cancel() { + cancellable.cancel(); + } +} +} diff --git a/core/src/variable.vala b/core/src/variable.vala new file mode 100644 index 0000000..c7edb16 --- /dev/null +++ b/core/src/variable.vala @@ -0,0 +1,196 @@ +namespace Astal { +public class VariableBase : Object { + public signal void changed (); + public signal void dropped (); + public signal void error (string err); + + // lua-lgi crashes when using its emitting mechanism + public void emit_changed() { changed(); } + public void emit_dropped() { dropped(); } + public void emit_error(string err) { this.error(err); } + + ~VariableBase() { + dropped(); + } +} + +public class Variable : VariableBase { + public Value value { owned get; set; } + + private uint poll_id = 0; + private Process? watch_proc; + + private uint poll_interval { get; set; default = 1000; } + private string[] poll_exec { get; set; } + private Closure? poll_transform { get; set; } + private Closure? poll_fn { get; set; } + + private Closure? watch_transform { get; set; } + private string[] watch_exec { get; set; } + + public Variable(Value init) { + Object(value: init); + } + + public Variable poll( + uint interval, + string exec, + Closure? transform + ) throws Error { + string[] argv; + Shell.parse_argv(exec, out argv); + return pollv(interval, argv, transform); + } + + public Variable pollv( + uint interval, + string[] execv, + Closure? transform + ) throws Error { + if (is_polling()) + stop_poll(); + + poll_interval = interval; + poll_exec = execv; + poll_transform = transform; + poll_fn = null; + start_poll(); + return this; + } + + public Variable pollfn( + uint interval, + Closure fn + ) throws Error { + if (is_polling()) + stop_poll(); + + poll_interval = interval; + poll_fn = fn; + poll_exec = null; + start_poll(); + return this; + } + + public Variable watch( + string exec, + Closure? transform + ) throws Error { + string[] argv; + Shell.parse_argv(exec, out argv); + return watchv(argv, transform); + } + + public Variable watchv( + string[] execv, + Closure? transform + ) throws Error { + if (is_watching()) + stop_watch(); + + watch_exec = execv; + watch_transform = transform; + start_watch(); + return this; + } + + construct { + notify["value"].connect(() => changed()); + dropped.connect(() => { + if (is_polling()) + stop_poll(); + + if (is_watching()) + stop_watch(); + }); + } + + private void set_closure(string val, Closure? transform) { + if (transform != null) { + var str = Value(typeof(string)); + str.set_string(val); + + var ret_val = Value(this.value.type()); + transform.invoke(ref ret_val, { str, this.value }); + this.value = ret_val; + } + else { + if (this.value.type() == Type.STRING && this.value.get_string() == val) + return; + + var str = Value(typeof(string)); + str.set_string(val); + this.value = str; + } + } + + private void set_fn() { + var ret_val = Value(this.value.type()); + poll_fn.invoke(ref ret_val, { this.value }); + this.value = ret_val; + } + + public void start_poll() throws Error { + return_if_fail(poll_id == 0); + + if (poll_fn != null) { + set_fn(); + poll_id = Timeout.add(poll_interval, () => { + set_fn(); + return Source.CONTINUE; + }, Priority.DEFAULT); + } + if (poll_exec != null) { + Process.exec_asyncv.begin(poll_exec, (_, res) => { + try { + var str = Process.exec_asyncv.end(res); + set_closure(str, poll_transform); + } catch (Error err) { + this.error(err.message); + } + }); + poll_id = Timeout.add(poll_interval, () => { + Process.exec_asyncv.begin(poll_exec, (_, res) => { + try { + var str = Process.exec_asyncv.end(res); + set_closure(str, poll_transform); + } catch (Error err) { + this.error(err.message); + Source.remove(poll_id); + poll_id = 0; + } + }); + return Source.CONTINUE; + }, Priority.DEFAULT); + } + } + + public void start_watch() throws Error { + return_if_fail(watch_proc == null); + return_if_fail(watch_exec != null); + + watch_proc = new Process.subprocessv(watch_exec); + watch_proc.stdout.connect((str) => set_closure(str, watch_transform)); + watch_proc.stderr.connect((str) => this.error(str)); + } + + public void stop_poll() { + return_if_fail(poll_id != 0); + Source.remove(poll_id); + poll_id = 0; + } + + public void stop_watch() { + return_if_fail(watch_proc != null); + watch_proc.kill(); + watch_proc = null; + } + + public bool is_polling() { return poll_id > 0; } + public bool is_watching() { return watch_proc != null; } + + ~Variable() { + dropped(); + } +} +} diff --git a/core/src/widget/box.vala b/core/src/widget/box.vala new file mode 100644 index 0000000..39dee92 --- /dev/null +++ b/core/src/widget/box.vala @@ -0,0 +1,70 @@ +namespace Astal { +public class Box : Gtk.Box { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + /** + * wether to implicity destroy previous children when setting them + */ + public bool implicit_destroy { get; set; default = true; } + + public List<weak Gtk.Widget> children { + set { _set_children(value); } + owned get { return get_children(); } + } + + public new Gtk.Widget child { + owned get { return _get_child(); } + set { _set_child(value); } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } + + private void _set_child(Gtk.Widget child) { + var list = new List<weak Gtk.Widget>(); + list.append(child); + _set_children(list); + } + + private Gtk.Widget? _get_child() { + foreach(var child in get_children()) + return child; + + return null; + } + + private void _set_children(List<weak Gtk.Widget> arr) { + foreach(var child in get_children()) { + if (implicit_destroy && arr.find(child).length() == 0) + child.destroy(); + else + remove(child); + } + + foreach(var child in arr) + add(child); + } + + public Box(bool vertical, List<weak Gtk.Widget> children) { + this.vertical = vertical; + _set_children(children); + } + + public Box.newh(List<weak Gtk.Widget> children) { + this.vertical = false; + _set_children(children); + } + + public Box.newv(List<weak Gtk.Widget> children) { + this.vertical = true; + _set_children(children); + } +} +} diff --git a/core/src/widget/button.vala b/core/src/widget/button.vala new file mode 100644 index 0000000..036bc8e --- /dev/null +++ b/core/src/widget/button.vala @@ -0,0 +1,101 @@ +namespace Astal { +public class Button : Gtk.Button { + public signal void hover (HoverEvent event); + public signal void hover_lost (HoverEvent event); + public signal void click (ClickEvent event); + public signal void click_release (ClickEvent event); + public signal void scroll (ScrollEvent event); + + construct { + add_events(Gdk.EventMask.SCROLL_MASK); + add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); + + enter_notify_event.connect((self, event) => { + hover(HoverEvent(event) { lost = false }); + }); + + leave_notify_event.connect((self, event) => { + hover_lost(HoverEvent(event) { lost = true }); + }); + + button_press_event.connect((event) => { + click(ClickEvent(event) { release = false }); + }); + + button_release_event.connect((event) => { + click_release(ClickEvent(event) { release = true }); + }); + + scroll_event.connect((event) => { + scroll(ScrollEvent(event)); + }); + } +} + +public enum MouseButton { + PRIMARY = 0, + MIDDLE, + SECONDARY, + BACK, + FORWARD, +} + +// these structs are here because gjs converts every event +// into a union Gdk.Event, which cannot be destructured +// and are not as convinent to work with as a struct +public struct ClickEvent { + bool release; + uint time; + double x; + double y; + Gdk.ModifierType modifier; + MouseButton button; + + public ClickEvent(Gdk.EventButton event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.button = (MouseButton)event.button; + this.modifier = event.state; + } +} + +public struct HoverEvent { + bool lost; + uint time; + double x; + double y; + Gdk.ModifierType modifier; + Gdk.CrossingMode mode; + Gdk.NotifyType detail; + + public HoverEvent(Gdk.EventCrossing event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + this.mode = event.mode; + this.detail = event.detail; + } +} + +public struct ScrollEvent { + uint time; + double x; + double y; + Gdk.ModifierType modifier; + Gdk.ScrollDirection direction; + double delta_x; + double delta_y; + + public ScrollEvent(Gdk.EventScroll event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + this.direction = event.direction; + this.delta_x = event.delta_x; + this.delta_y = event.delta_y; + } +} +} diff --git a/core/src/widget/centerbox.vala b/core/src/widget/centerbox.vala new file mode 100644 index 0000000..0588828 --- /dev/null +++ b/core/src/widget/centerbox.vala @@ -0,0 +1,54 @@ +namespace Astal { +public class CenterBox : Gtk.Box { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } + + static construct { + set_css_name("centerbox"); + } + + private Gtk.Widget _start_widget; + public Gtk.Widget start_widget { + get { return _start_widget; } + set { + if (_start_widget != null) + remove(_start_widget); + + if (value != null) + pack_start(value, true, true, 0); + } + } + + private Gtk.Widget _end_widget; + public Gtk.Widget end_widget { + get { return _end_widget; } + set { + if (_end_widget != null) + remove(_end_widget); + + if (value != null) + pack_end(value, true, true, 0); + } + } + + public Gtk.Widget center_widget { + get { return get_center_widget(); } + set { + if (center_widget != null) + remove(center_widget); + + if (value != null) + set_center_widget(value); + } + } +} +} diff --git a/core/src/widget/circularprogress.vala b/core/src/widget/circularprogress.vala new file mode 100644 index 0000000..9cd3e26 --- /dev/null +++ b/core/src/widget/circularprogress.vala @@ -0,0 +1,173 @@ +namespace Astal { +public class CircularProgress : Gtk.Bin { + public new Gtk.Widget child { get; set; } + public double start_at { get; set; } + public double end_at { get; set; } + public double value { get; set; } + public bool inverted { get; set; } + public bool rounded { get; set; } + + construct { + notify["start-at"].connect(queue_draw); + notify["end-at"].connect(queue_draw); + notify["value"].connect(queue_draw); + notify["inverted"].connect(queue_draw); + notify["rounded"].connect(queue_draw); + notify["child"].connect(queue_draw); + } + + static construct { + set_css_name("circular-progress"); + } + + public new void get_preferred_height(out int minh, out int nath) { + var val = get_style_context().get_property("min-height", Gtk.StateFlags.NORMAL); + if (val.get_int() <= 0) { + minh = 40; + nath = 40; + } + + minh = val.get_int(); + nath = val.get_int(); + } + + public new void get_preferred_width(out int minw, out int natw) { + var val = get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL); + if (val.get_int() <= 0) { + minw = 40; + natw = 40; + } + + minw = val.get_int(); + natw = val.get_int(); + } + + private double _to_radian(double percentage) { + percentage = Math.floor(percentage * 100); + return (percentage / 100) * (2 * Math.PI); + } + + private bool _is_full_circle(double start, double end, double epsilon = 1e-10) { + // Ensure that start and end are between 0 and 1 + start = (start % 1 + 1) % 1; + end = (end % 1 + 1) % 1; + + // Check if the difference between start and end is close to 1 + return Math.fabs(start - end) <= epsilon; + } + + private double _map_arc_value_to_range(double start, double end, double value) { + // Ensure that start and end are between 0 and 1 + start = (start % 1 + 1) % 1; + end = (end % 1 + 1) % 1; + + // Calculate the length of the arc + var arcLength = end - start; + if (arcLength < 0) + arcLength += 1; // Adjust for circular representation + + // Calculate the position on the arc based on the percentage value + var position = start + (arcLength * value); + + // Ensure the position is between 0 and 1 + position = (position % 1 + 1) % 1; + + return position; + } + + private double _min(double[] arr) { + double min = arr[0]; + foreach(var i in arr) + if (min > i) min = i; + return min; + } + + private double _max(double[] arr) { + double max = arr[0]; + foreach(var i in arr) + if (max < i) max = i; + return max; + } + + public new bool draw(Cairo.Context cr) { + Gtk.Allocation allocation; + get_allocation(out allocation); + + var styles = get_style_context(); + var width = allocation.width; + var height = allocation.height; + var thickness = styles.get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + var margin = styles.get_margin(Gtk.StateFlags.NORMAL); + var fg = styles.get_color(Gtk.StateFlags.NORMAL); + var bg = styles.get_background_color(Gtk.StateFlags.NORMAL); + + var bg_stroke = thickness + _min({margin.bottom, margin.top, margin.left, margin.right}); + var fg_stroke = thickness; + var radius = _min({width, height}) / 2.0 - _max({bg_stroke, fg_stroke}) / 2.0; + var center_x = width / 2; + var center_y = height / 2; + + var start_background = _to_radian(this.start_at); + var end_background = _to_radian(this.end_at); + var ranged_value = this.value + this.start_at; + + var is_circle = _is_full_circle(this.start_at, this.end_at); + + if (is_circle) { + // Redefine endDraw in radius to create an accurate full circle + end_background = start_background + 2 * Math.PI; + } else { + // Range the value for the arc shape + ranged_value = _map_arc_value_to_range( + this.start_at, + this.end_at, + this.value + ); + } + + var to = _to_radian(ranged_value); + double start_progress, end_progress; + + if (this.inverted) { + start_progress = (2 * Math.PI - to) - start_background; + end_progress = (2 * Math.PI - start_background) - start_background; + } else { + start_progress = start_background; + end_progress = to; + } + + // Draw background + cr.set_source_rgba(bg.red, bg.green, bg.blue, bg.alpha); + cr.arc(center_x, center_y, radius, start_background, end_background); + + cr.set_line_width(bg_stroke); + cr.stroke(); + + // Draw progress + cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha); + cr.arc(center_x, center_y, radius, start_progress, end_progress); + cr.set_line_width(fg_stroke); + cr.stroke(); + + // Draw rounded ends + if (this.rounded) { + var start_x = center_x + Math.cos(start_background); + var start_y = center_y + Math.cos(start_background); + var end_x = center_x + Math.cos(to) * radius; + var end_y = center_y + Math.cos(to) * radius; + cr.set_line_width(0); + cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + } + + if (this.child != null) { + this.child.size_allocate(allocation); + this.propagate_draw(this.child, cr); + } + + return true; + } +} +} diff --git a/core/src/widget/eventbox.vala b/core/src/widget/eventbox.vala new file mode 100644 index 0000000..6b715cc --- /dev/null +++ b/core/src/widget/eventbox.vala @@ -0,0 +1,66 @@ +namespace Astal { +public class EventBox : Gtk.EventBox { + public signal void hover (HoverEvent event); + public signal void hover_lost (HoverEvent event); + public signal void click (ClickEvent event); + public signal void click_release (ClickEvent event); + public signal void scroll (ScrollEvent event); + public signal void motion (MotionEvent event); + + static construct { + set_css_name("eventbox"); + } + + construct { + add_events(Gdk.EventMask.SCROLL_MASK); + add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); + add_events(Gdk.EventMask.POINTER_MOTION_MASK); + + enter_notify_event.connect((self, event) => { + if (event.window == self.get_window() && + event.detail != Gdk.NotifyType.INFERIOR) { + this.set_state_flags(Gtk.StateFlags.PRELIGHT, false); + hover(HoverEvent(event) { lost = false }); + } + }); + + leave_notify_event.connect((self, event) => { + if (event.window == self.get_window() && + event.detail != Gdk.NotifyType.INFERIOR) { + this.unset_state_flags(Gtk.StateFlags.PRELIGHT); + hover_lost(HoverEvent(event) { lost = true }); + } + }); + + button_press_event.connect((event) => { + click(ClickEvent(event) { release = false }); + }); + + button_release_event.connect((event) => { + click_release(ClickEvent(event) { release = true }); + }); + + scroll_event.connect((event) => { + scroll(ScrollEvent(event)); + }); + + motion_notify_event.connect((event) => { + motion(MotionEvent(event)); + }); + } +} + +public struct MotionEvent { + uint time; + double x; + double y; + Gdk.ModifierType modifier; + + public MotionEvent(Gdk.EventMotion event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + } +} +} diff --git a/core/src/widget/icon.vala b/core/src/widget/icon.vala new file mode 100644 index 0000000..ef43baf --- /dev/null +++ b/core/src/widget/icon.vala @@ -0,0 +1,95 @@ +namespace Astal { +public Gtk.IconInfo? lookup_icon(string icon) { + var theme = Gtk.IconTheme.get_default(); + return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN); +} + +public class Icon : Gtk.Image { + private IconType type = IconType.NAMED; + private double size { get; set; default = 14; } + + public new Gdk.Pixbuf pixbuf { get; set; } + public string icon { get; set; default = ""; } + + private async void display_icon() { + switch(type) { + case IconType.NAMED: + icon_name = icon; + pixel_size = (int)size; + break; + case IconType.FILE: + try { + var file = File.new_for_path(icon); + var stream = yield file.read_async(); + var pb = yield new Gdk.Pixbuf.from_stream_at_scale_async( + stream, + (int)size * scale_factor, + (int)size * scale_factor, + true, + null + ); + var cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window()); + set_from_surface(cs); + } catch (Error err) { + printerr(err.message); + } + break; + case IconType.PIXBUF: + var pb_scaled = pixbuf.scale_simple( + (int)size * scale_factor, + (int)size * scale_factor, + Gdk.InterpType.BILINEAR + ); + if (pb_scaled != null) { + var cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window()); + set_from_surface(cs); + } + break; + } + } + + static construct { + set_css_name("icon"); + } + + construct { + notify["icon"].connect(() => { + if(FileUtils.test(icon, GLib.FileTest.EXISTS)) + type = IconType.FILE; + else if (lookup_icon(icon) != null) + type = IconType.NAMED; + else { + type = IconType.NAMED; + warning("cannot assign %s as icon, "+ + "it is not a file nor a named icon", icon); + } + display_icon.begin(); + }); + + notify["pixbuf"].connect(() => { + type = IconType.PIXBUF; + display_icon.begin(); + }); + + size_allocate.connect(() => { + size = get_style_context() + .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + + display_icon.begin(); + }); + + get_style_context().changed.connect(() => { + size = get_style_context() + .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + + display_icon.begin(); + }); + } +} + +private enum IconType { + NAMED, + FILE, + PIXBUF, +} +} diff --git a/core/src/widget/label.vala b/core/src/widget/label.vala new file mode 100644 index 0000000..4063b6f --- /dev/null +++ b/core/src/widget/label.vala @@ -0,0 +1,18 @@ +using Pango; + +public class Astal.Label : Gtk.Label { + public bool truncate { + set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; } + get { return ellipsize == EllipsizeMode.END; } + } + + public new bool justify_fill { + set { justify = value ? Gtk.Justification.FILL : Gtk.Justification.LEFT; } + get { return justify == Gtk.Justification.FILL; } + } + + construct { + notify["ellipsize"].connect(() => notify_property("truncate")); + notify["justify"].connect(() => notify_property("justify_fill")); + } +} diff --git a/core/src/widget/levelbar.vala b/core/src/widget/levelbar.vala new file mode 100644 index 0000000..1db2cc7 --- /dev/null +++ b/core/src/widget/levelbar.vala @@ -0,0 +1,15 @@ +namespace Astal { +public class LevelBar : Gtk.LevelBar { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } +} +} diff --git a/core/src/widget/overlay.vala b/core/src/widget/overlay.vala new file mode 100644 index 0000000..207aaa7 --- /dev/null +++ b/core/src/widget/overlay.vala @@ -0,0 +1,59 @@ +namespace Astal { +public class Overlay : Gtk.Overlay { + public bool pass_through { get; set; } + + public Gtk.Widget? overlay { + get { return overlays.nth_data(0); } + set { + foreach (var ch in get_children()) { + if (ch != child) + remove(ch); + } + + if (value != null) + add_overlay(value); + } + } + + public List<weak Gtk.Widget> overlays { + owned get { return get_children(); } + set { + foreach (var ch in get_children()) { + if (ch != child) + remove(ch); + } + + foreach (var ch in value) + add_overlay(ch); + } + } + + public new Gtk.Widget? child { + get { return get_child(); } + set { + var ch = get_child(); + if (ch != null) + remove(ch); + + if (value != null) + add(value); + } + } + + construct { + notify["pass-through"].connect(() => { + update_pass_through(); + }); + } + + private void update_pass_through() { + foreach (var child in get_children()) + set_overlay_pass_through(child, pass_through); + } + + public new void add_overlay(Gtk.Widget widget) { + base.add_overlay(widget); + set_overlay_pass_through(widget, pass_through); + } +} +} diff --git a/core/src/widget/scrollable.vala b/core/src/widget/scrollable.vala new file mode 100644 index 0000000..1a0e081 --- /dev/null +++ b/core/src/widget/scrollable.vala @@ -0,0 +1,42 @@ +namespace Astal { +public class Scrollable : Gtk.ScrolledWindow { + private Gtk.PolicyType _hscroll = Gtk.PolicyType.AUTOMATIC; + private Gtk.PolicyType _vscroll = Gtk.PolicyType.AUTOMATIC; + + public Gtk.PolicyType hscroll { + get { return _hscroll; } + set { + _hscroll = value; + set_policy(value, vscroll); + } + } + + public Gtk.PolicyType vscroll { + get { return _vscroll; } + set { + _vscroll = value; + set_policy(hscroll, value); + } + } + + static construct { + set_css_name("scrollable"); + } + + construct { + if (hadjustment != null) + hadjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (vadjustment != null) + vadjustment = new Gtk.Adjustment(0,0,0,0,0,0); + } + + public new Gtk.Widget get_child() { + var ch = base.get_child(); + if (ch is Gtk.Viewport) { + return ch.get_child(); + } + return ch; + } +} +} diff --git a/core/src/widget/slider.vala b/core/src/widget/slider.vala new file mode 100644 index 0000000..421b27a --- /dev/null +++ b/core/src/widget/slider.vala @@ -0,0 +1,71 @@ +namespace Astal { +public class Slider : Gtk.Scale { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + // emitted when the user drags the slider + public signal void dragged (); + + construct { + if (adjustment == null) + adjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (max == 0 && min == 0) { + max = 1; + } + + if (step == 0) { + step = 0.05; + } + + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + + button_press_event.connect(() => { dragging = true; }); + key_press_event.connect(() => { dragging = true; }); + button_release_event.connect(() => { dragging = false; }); + key_release_event.connect(() => { dragging = false; }); + scroll_event.connect((event) => { + dragging = true; + if (event.delta_y > 0) + value -= step; + else + value += step; + dragging = false; + }); + + value_changed.connect(() => { + if (dragging) + dragged(); + }); + } + + public bool dragging { get; private set; } + + public double value { + get { return adjustment.value; } + set { if (!dragging) adjustment.value = value; } + } + + public double min { + get { return adjustment.lower; } + set { adjustment.lower = value; } + } + + public double max { + get { return adjustment.upper; } + set { adjustment.upper = value; } + } + + public double step { + get { return adjustment.step_increment; } + set { adjustment.step_increment = value; } + } + + // TODO: marks +} +} diff --git a/core/src/widget/widget.vala b/core/src/widget/widget.vala new file mode 100644 index 0000000..2506bc8 --- /dev/null +++ b/core/src/widget/widget.vala @@ -0,0 +1,157 @@ +namespace Astal { +private class Css { + private static HashTable<Gtk.Widget, Gtk.CssProvider> _providers; + public static HashTable<Gtk.Widget, Gtk.CssProvider> providers { + get { + if (_providers == null) { + _providers = new HashTable<Gtk.Widget, Gtk.CssProvider>( + (w) => (uint)w, + (a, b) => a == b); + } + + return _providers; + } + } +} + +private void remove_provider(Gtk.Widget widget) { + var providers = Css.providers; + + if (providers.contains(widget)) { + var p = providers.get(widget); + widget.get_style_context().remove_provider(p); + providers.remove(widget); + p.dispose(); + } +} + +public void widget_set_css(Gtk.Widget widget, string css) { + var providers = Css.providers; + + if (providers.contains(widget)) { + remove_provider(widget); + } else { + widget.destroy.connect(() => { + remove_provider(widget); + }); + } + + var style = !css.contains("{") || !css.contains("}") + ? "* { ".concat(css, "}") : css; + + var p = new Gtk.CssProvider(); + widget.get_style_context() + .add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_USER); + + try { + p.load_from_data(style, style.length); + providers.set(widget, p); + } catch (Error err) { + warning(err.message); + } +} + +public string widget_get_css(Gtk.Widget widget) { + var providers = Css.providers; + + if (providers.contains(widget)) + return providers.get(widget).to_string(); + + return ""; +} + +public void widget_set_class_names(Gtk.Widget widget, string[] class_names) { + foreach (var name in widget_get_class_names(widget)) + widget_toggle_class_name(widget, name, false); + + foreach (var name in class_names) + widget_toggle_class_name(widget, name, true); +} + +public List<weak string> widget_get_class_names(Gtk.Widget widget) { + return widget.get_style_context().list_classes(); +} + +public void widget_toggle_class_name( + Gtk.Widget widget, + string class_name, + bool condition = true +) { + var c = widget.get_style_context(); + if (condition) + c.add_class(class_name); + else + c.remove_class(class_name); +} + +private class Cursor { + private static HashTable<Gtk.Widget, string> _cursors; + public static HashTable<Gtk.Widget, string> cursors { + get { + if (_cursors == null) { + _cursors = new HashTable<Gtk.Widget, string>( + (w) => (uint)w, + (a, b) => a == b); + } + return _cursors; + } + } +} + +private void widget_setup_cursor(Gtk.Widget widget) { + widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK); + widget.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK); + widget.enter_notify_event.connect(() => { + widget.get_window().set_cursor( + new Gdk.Cursor.from_name( + Gdk.Display.get_default(), + Cursor.cursors.get(widget))); + return false; + }); + widget.leave_notify_event.connect(() => { + widget.get_window().set_cursor( + new Gdk.Cursor.from_name( + Gdk.Display.get_default(), + "default")); + return false; + }); + widget.destroy.connect(() => { + if (Cursor.cursors.contains(widget)) + Cursor.cursors.remove(widget); + }); +} + +public void widget_set_cursor(Gtk.Widget widget, string cursor) { + if (!Cursor.cursors.contains(widget)) + widget_setup_cursor(widget); + + Cursor.cursors.set(widget, cursor); +} + +public string widget_get_cursor(Gtk.Widget widget) { + return Cursor.cursors.get(widget); +} + +private class ClickThrough { + private static HashTable<Gtk.Widget, bool> _click_through; + public static HashTable<Gtk.Widget, bool> click_through { + get { + if (_click_through == null) { + _click_through = new HashTable<Gtk.Widget, bool>( + (w) => (uint)w, + (a, b) => a == b); + } + return _click_through; + } + } +} + +public void widget_set_click_through(Gtk.Widget widget, bool click_through) { + ClickThrough.click_through.set(widget, click_through); + widget.input_shape_combine_region(click_through ? new Cairo.Region() : null); +} + +public bool widget_get_click_through(Gtk.Widget widget) { + return ClickThrough.click_through.get(widget); +} +} diff --git a/core/src/widget/window.vala b/core/src/widget/window.vala new file mode 100644 index 0000000..17dc76d --- /dev/null +++ b/core/src/widget/window.vala @@ -0,0 +1,227 @@ +using GtkLayerShell; + +namespace Astal { +public enum WindowAnchor { + NONE = 0, + TOP = 1, + RIGHT = 2, + LEFT = 4, + BOTTOM = 8, +} + +public enum Exclusivity { + NORMAL, + EXCLUSIVE, + IGNORE, +} + +public enum Layer { + BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND + BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM + TOP = 2, // GtkLayerShell.Layer.TOP + OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY +} + +public enum Keymode { + NONE = 0, // GtkLayerShell.KeyboardMode.NONE + ON_DEMAND = 1, // GtkLayerShell.KeyboardMode.ON_DEMAND + EXCLUSIVE = 2, // GtkLayerShell.KeyboardMode.EXCLUSIVE +} + +public class Window : Gtk.Window { + private static bool check(string action) { + if (!is_supported()) { + critical(@"can not $action on window: layer shell not supported"); + print("tip: running from an xwayland terminal can cause this, for example VsCode"); + return true; + } + return false; + } + + construct { + if (check("initialize layer shell")) + return; + + height_request = 1; + width_request = 1; + init_for_window(this); + } + + public string namespace { + get { return get_namespace(this); } + set { set_namespace(this, value); } + } + + public int anchor { + set { + if (check("set anchor")) + return; + + set_anchor(this, Edge.TOP, WindowAnchor.TOP in value); + set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value); + set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value); + set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value); + } + get { + var a = WindowAnchor.NONE; + if (get_anchor(this, Edge.TOP)) + a = a | WindowAnchor.TOP; + + if (get_anchor(this, Edge.RIGHT)) + a = a | WindowAnchor.RIGHT; + + if (get_anchor(this, Edge.LEFT)) + a = a | WindowAnchor.LEFT; + + if (get_anchor(this, Edge.BOTTOM)) + a = a | WindowAnchor.BOTTOM; + + return a; + } + } + + public Exclusivity exclusivity { + set { + if (check("set exclusivity")) + return; + + switch (value) { + case Exclusivity.NORMAL: + set_exclusive_zone(this, 0); + break; + case Exclusivity.EXCLUSIVE: + auto_exclusive_zone_enable(this); + break; + case Exclusivity.IGNORE: + set_exclusive_zone(this, -1); + break; + } + } + get { + if (auto_exclusive_zone_is_enabled(this)) + return Exclusivity.EXCLUSIVE; + + if (get_exclusive_zone(this) == -1) + return Exclusivity.IGNORE; + + return Exclusivity.NORMAL; + } + } + + public Layer layer { + get { return (Layer)get_layer(this); } + set { + if (check("set layer")) + return; + + set_layer(this, (GtkLayerShell.Layer)value); + } + } + + public Keymode keymode { + get { return (Keymode)get_keyboard_mode(this); } + set { + if (check("set keymode")) + return; + + set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); + } + } + + public Gdk.Monitor gdkmonitor { + get { return get_monitor(this); } + set { + if (check("set gdkmonitor")) + return; + + set_monitor (this, value); + } + } + + public new int margin_top { + get { return GtkLayerShell.get_margin(this, Edge.TOP); } + set { + if (check("set margin_top")) + return; + + GtkLayerShell.set_margin(this, Edge.TOP, value); + } + } + + public new int margin_bottom { + get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); } + set { + if (check("set margin_bottom")) + return; + + GtkLayerShell.set_margin(this, Edge.BOTTOM, value); + } + } + + public new int margin_left { + get { return GtkLayerShell.get_margin(this, Edge.LEFT); } + set { + if (check("set margin_left")) + return; + + GtkLayerShell.set_margin(this, Edge.LEFT, value); + } + } + + public new int margin_right { + get { return GtkLayerShell.get_margin(this, Edge.RIGHT); } + set { + if (check("set margin_right")) + return; + + GtkLayerShell.set_margin(this, Edge.RIGHT, value); + } + } + + public new int margin { + set { + if (check("set margin")) + return; + + margin_top = value; + margin_right = value; + margin_bottom = value; + margin_left = value; + } + } + + /** + * CAUTION: the id might not be the same mapped by the compositor + * to reset and let the compositor map it pass a negative number + */ + public int monitor { + set { + if (check("set monitor")) + return; + + if (value < 0) + set_monitor(this, (Gdk.Monitor)null); + + var m = Gdk.Display.get_default().get_monitor(value); + set_monitor(this, m); + } + get { + var m = get_monitor(this); + var d = Gdk.Display.get_default(); + for (var i = 0; i < d.get_n_monitors(); ++i) { + if (m == d.get_monitor(i)) + return i; + } + + return -1; + } + } +} + +/** + * CAUTION: the id might not be the same mapped by the compositor + */ +public uint get_num_monitors() { + return Gdk.Display.get_default().get_n_monitors(); +} +} diff --git a/core/version b/core/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/core/version @@ -0,0 +1 @@ +0.1.0 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e6e6355 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716293225, + "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d34c09d --- /dev/null +++ b/flake.nix @@ -0,0 +1,54 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + lib = name: src: inputs: + pkgs.stdenv.mkDerivation { + nativeBuildInputs = with pkgs; [ + wrapGAppsHook + gobject-introspection + meson + pkg-config + ninja + vala + ]; + buildInputs = [pkgs.glib] ++ inputs; + pname = name; + version = version; + src = src; + outputs = ["out" "dev"]; + }; + in { + packages.${system} = rec { + default = astal; + astal = with pkgs; lib "astal" ./core [gtk3 gtk-layer-shell]; + }; + + devShells.${system} = let + inputs = with pkgs; [ + wrapGAppsHook + gobject-introspection + meson + pkg-config + ninja + vala + (lua.withPackages (ps: [ps.lgi])) + gjs + ]; + in { + default = pkgs.mkShell { + inherit inputs; + }; + astal = pkgs.mkShell { + inputs = inputs ++ [self.packages.${system}.astal]; + }; + }; + }; +} diff --git a/hyprland/.gitignore b/hyprland/.gitignore new file mode 100644 index 0000000..f3ced36 --- /dev/null +++ b/hyprland/.gitignore @@ -0,0 +1,4 @@ +build/ +result +.cache/ +tmp/ diff --git a/hyprland/LICENSE b/hyprland/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/hyprland/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/hyprland/README.md b/hyprland/README.md new file mode 100644 index 0000000..c47cab0 --- /dev/null +++ b/hyprland/README.md @@ -0,0 +1,7 @@ +# astal-hyprland + +Library and cli tool for the Hyprland IPC sockets + +## TODO + +- docs diff --git a/hyprland/flake.lock b/hyprland/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/hyprland/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/hyprland/flake.nix b/hyprland/flake.nix new file mode 100644 index 0000000..c6b4efd --- /dev/null +++ b/hyprland/flake.nix @@ -0,0 +1,49 @@ +{ + description = "Library and cli tool for querying applications"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + json-glib + ]; + in { + packages.${system} = rec { + default = hyprland; + hyprland = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-hyprland"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + hyprland = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = buildInputs ++ [ + pkgs.gjs + self.packages.${system}.default + ]; + }; + }; + }; +} diff --git a/hyprland/meson.build b/hyprland/meson.build new file mode 100644 index 0000000..07cbb3f --- /dev/null +++ b/hyprland/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-hyprland', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/hyprland/meson_options.txt b/hyprland/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/hyprland/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/hyprland/src/cli.vala b/hyprland/src/cli.vala new file mode 100644 index 0000000..a68d63b --- /dev/null +++ b/hyprland/src/cli.vala @@ -0,0 +1,42 @@ +static bool help; +static bool version; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (help) { + print("Usage:\n"); + print(" %s [flags]\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + return 0; + } + + if (version) { + print(AstalHyprland.VERSION); + return 0; + } + + AstalHyprland.Hyprland.get_default().event.connect((event, args) => { + print("{ event: \"%s\", payload: \"%s\" }\n", event, args); + }); + + new MainLoop(null, false).run(); + return 0; +} diff --git a/hyprland/src/client.vala b/hyprland/src/client.vala new file mode 100644 index 0000000..6456667 --- /dev/null +++ b/hyprland/src/client.vala @@ -0,0 +1,75 @@ +namespace AstalHyprland { +public class Client : Object { + public signal void removed (); + public signal void moved_to (Workspace workspace); + + public string address { get; private set; } + public bool mapped { get; private set; } + public bool hidden { get; private set; } + public int x { get; private set; } + public int y { get; private set; } + public int width { get; private set; } + public int height { get; private set; } + public Workspace workspace { get; private set; } + public bool floating { get; private set; } + public Monitor monitor { get; private set; } + public string class { get; private set; } + public string title { get; private set; } + public string initial_class { get; private set; } + public string initial_title { get; private set; } + public uint pid { get; private set; } + public bool xwayland { get; private set; } + public bool pinned { get; private set; } + public bool fullscreen { get; private set; } + public int fullscreen_mode { get; private set; } + public bool fake_fullscreen { get; private set; } + // TODO: public Group[] grouped { get; private set; } + // TODO: public Tag[] tags { get; private set; } + public string swallowing { get; private set; } + public int focus_history_id { get; private set; } + + internal void sync(Json.Object obj) { + var hyprland = Hyprland.get_default(); + + address = obj.get_string_member("address").replace("0x", ""); + mapped = obj.get_boolean_member("mapped"); + hidden = obj.get_boolean_member("hidden"); + floating = obj.get_boolean_member("floating"); + class = obj.get_string_member("class"); + title = obj.get_string_member("title"); + initial_title = obj.get_string_member("initialTitle"); + initial_class = obj.get_string_member("initialClass"); + pid = (uint)obj.get_int_member("pid"); + xwayland = obj.get_boolean_member("xwayland"); + pinned = obj.get_boolean_member("pinned"); + fullscreen = obj.get_boolean_member("fullscreen"); + fullscreen_mode = (int)obj.get_int_member("fullscreenMode"); // is this used? + fake_fullscreen = obj.get_boolean_member("fakeFullscreen"); + swallowing = obj.get_string_member("swallowing"); + focus_history_id = (int)obj.get_int_member("focusHistoryID"); + x = (int)obj.get_array_member("at").get_int_element(0); + y = (int)obj.get_array_member("at").get_int_element(1); + width = (int)obj.get_array_member("size").get_int_element(0); + height = (int)obj.get_array_member("size").get_int_element(1); + + workspace = hyprland.get_workspace((int)obj.get_object_member("workspace").get_int_member("id")); + monitor = hyprland.get_monitor((int)obj.get_int_member("monitor")); + } + + public void kill() { + Hyprland.get_default().dispatch("closewindow", "address:" + "0x" + address); + } + + public void focus() { + Hyprland.get_default().dispatch("focuswindow", "address:" + "0x" + address); + } + + public void move_to(Workspace ws) { + Hyprland.get_default().dispatch("movetoworkspacesilent", ws.id.to_string() + ",address:" + "0x" + address); + } + + public void toggle_floating() { + Hyprland.get_default().dispatch("togglefloating", "address:" + "0x" + address); + } +} +} diff --git a/hyprland/src/config.vala.in b/hyprland/src/config.vala.in new file mode 100644 index 0000000..65993b2 --- /dev/null +++ b/hyprland/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalHyprland { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/hyprland/src/hyprland.vala b/hyprland/src/hyprland.vala new file mode 100644 index 0000000..5359d2e --- /dev/null +++ b/hyprland/src/hyprland.vala @@ -0,0 +1,451 @@ +namespace AstalHyprland { +public Hyprland get_default() { + return Hyprland.get_default(); +} + +public class Hyprland : Object { + private static string HIS = GLib.Environment.get_variable("HYPRLAND_INSTANCE_SIGNATURE"); + private static string RUN_DIR = GLib.Environment.get_user_runtime_dir(); + + private static Hyprland _instance; + public static Hyprland? get_default() { + if (_instance != null) + return _instance; + + var HIS = GLib.Environment.get_variable("HYPRLAND_INSTANCE_SIGNATURE"); + if (HIS == null) { + critical("Hyprland is not running"); + return null; + } + + var h = new Hyprland(); + _instance = h; + + h.socket2 = h.connection("socket2"); + h.watch_socket(new DataInputStream(h.socket2.input_stream)); + try { + h.init(); + } catch (Error err) { + critical("could not initialize: %s", err.message); + return null; + } + + return _instance; + } + + // monitors, workspaces, clients + private HashTable<int, Monitor> _monitors = + new HashTable<int, Monitor>((i) => i, (a, b) => a == b); + + private HashTable<int, Workspace> _workspaces = + new HashTable<int, Workspace>((i) => i, (a, b) => a == b); + + private HashTable<string, Client> _clients = + new HashTable<string, Client>(str_hash, str_equal); + + public List<weak Monitor> monitors { owned get { return _monitors.get_values(); } } + public List<weak Workspace> workspaces { owned get { return _workspaces.get_values(); } } + public List<weak Client> clients { owned get { return _clients.get_values(); } } + + public Monitor get_monitor(int id) { return _monitors.get(id); } + public Workspace get_workspace(int id) { return _workspaces.get(id); } + public Client? get_client(string address) { + if (address == "" || address == null) + return null; + + if (address.substring(0, 2) == "0x") + return _clients.get(address.substring(2, -1)); + + return _clients.get(address); + } + + public Monitor? get_monitor_by_name(string name) { + foreach (var mon in monitors) { + if (mon.name == name) + return mon; + } + return null; + } + + public Workspace? get_workspace_by_name(string name) { + foreach (var ws in workspaces) { + if (ws.name == name) + return ws; + } + return null; + } + + public Workspace focused_workspace { get; private set; } + public Monitor focused_monitor { get; private set; } + public Client focused_client { get; private set; } + + // other props + public List<Bind> binds { + owned get { + var list = new List<Bind>(); + try { + var arr = Json.from_string(message("j/binds")).get_array(); + foreach (var b in arr.get_elements()) + list.append(new Bind.from_json(b.get_object())); + } catch (Error err) { + critical(err.message); + } + return list; + } + } + + public Position cursor_position { + owned get { + return new Position.cursorpos(message("cursorpos")); + } + } + + // signals + public signal void event (string event, string args); + + // TODO: nag vaxry for fullscreenv2 + // public signal void fullscreen (bool fullscreen); + public signal void minimize (Client client, bool minimize); + public signal void floating (Client client, bool floating); + public signal void urgent (Client client); + public signal void client_moved (Client client, Workspace ws); + + public signal void submap (string name); + public signal void keyboard_layout (string keyboard, string layout); + public signal void config_reloaded (); + + // state + public signal void client_added (Client client); + public signal void client_removed (string address); + public signal void workspace_added (Workspace workspace); + public signal void workspace_removed (int id); + public signal void monitor_added (Monitor monitor); + public signal void monitor_removed (int id); + + private SocketConnection socket2; + + private SocketConnection? connection(string socket) { + var path = RUN_DIR + "/hypr/" + HIS + "/." + socket + ".sock"; + try { + return new SocketClient().connect(new UnixSocketAddress(path), null); + } catch (Error err) { + critical(err.message); + return null; + } + } + + private void watch_socket(DataInputStream stream) { + stream.read_line_async.begin(Priority.DEFAULT, null, (_, res) => { + try { + var line = stream.read_line_async.end(res); + handle_event.begin(line, (_, res) => { + try { + handle_event.end(res); + } catch (Error err) { + critical(err.message); + } + }); + watch_socket(stream); + } catch (Error err) { + critical(err.message); + } + }); + } + + private void write_socket( + string message, + out SocketConnection conn, + out DataInputStream stream + ) throws Error { + conn = connection("socket"); + conn.output_stream.write(message.data, null); + stream = new DataInputStream(conn.input_stream); + } + + public string message(string message) { + SocketConnection conn; + DataInputStream stream; + try { + write_socket(message, out conn, out stream); + return stream.read_upto("\x04", -1, null, null); + } catch (Error err) { + critical(err.message); + } finally { + try { + if (conn != null) + conn.close(null); + } catch (Error err) { + critical(err.message); + } + } + return ""; + } + + public async string message_async(string message) { + SocketConnection conn; + DataInputStream stream; + try { + write_socket(message, out conn, out stream); + return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); + } catch (Error err) { + critical(err.message); + } finally { + try { + conn.close(null); + } catch (Error err) { + critical(err.message); + } + } + return ""; + } + + public void dispatch(string dispatcher, string args) { + var msg = "dispatch " + dispatcher + " " + args; + message_async.begin(msg, (_, res) => { + var err = message_async.end(res); + if (err != "ok") + critical("dispatch error: %s", err); + }); + } + + public void move_cursor(int x, int y) { + dispatch("movecursor", x.to_string() + " " + y.to_string()); + + } + + // TODO: nag vaxry to make socket events and hyprctl more consistent + private void init() throws Error { + var mons = Json.from_string(message("j/monitors")).get_array(); + var wrkspcs = Json.from_string(message("j/workspaces")).get_array(); + var clnts = Json.from_string(message("j/clients")).get_array(); + + // create + foreach (var mon in mons.get_elements()) { + var id = (int)mon.get_object().get_member("id").get_int(); + var m = new Monitor(); + _monitors.insert(id, m); + + if (mon.get_object().get_member("focused").get_boolean()) + focused_monitor = m; + } + foreach (var wrkpsc in wrkspcs.get_elements()) { + var id = (int)wrkpsc.get_object().get_member("id").get_int(); + _workspaces.set(id, new Workspace()); + } + foreach (var clnt in clnts.get_elements()) { + var addr = clnt.get_object().get_member("address").get_string(); + _clients.set(addr.replace("0x", ""), new Client()); + } + + // init + foreach (var c in clnts.get_elements()) { + var addr = c.get_object().get_member("address").get_string(); + get_client(addr).sync(c.get_object()); + } + foreach (var ws in wrkspcs.get_elements()) { + var id = (int)ws.get_object().get_member("id").get_int(); + get_workspace(id).sync(ws.get_object()); + } + foreach (var mon in mons.get_elements()) { + var id = (int)mon.get_object().get_member("id").get_int(); + get_monitor(id).sync(mon.get_object()); + } + + // focused + focused_workspace = get_workspace((int)Json.from_string(message("j/activeworkspace")) + .get_object().get_member("id").get_int()); + + focused_client = get_client(Json.from_string(message("j/activewindow")) + .get_object().get_member("address").get_string()); + } + + ~Hyprland() { + if (socket2 != null) { + try { + socket2.close(null); + } catch (Error err) { + critical(err.message); + } + } + } + + public async void sync_monitors() throws Error { + var str = yield message_async("j/monitors"); + var arr = Json.from_string(str).get_array(); + foreach (var obj in arr.get_elements()) { + var id = (int)obj.get_object().get_int_member("id"); + var m = get_monitor(id); + if (m != null) + m.sync(obj.get_object()); + } + } + + public async void sync_workspaces() throws Error { + var str = yield message_async("j/workspaces"); + var arr = Json.from_string(str).get_array(); + foreach (var obj in arr.get_elements()) { + var id = (int)obj.get_object().get_int_member("id"); + var ws = get_workspace(id); + if (ws != null) + ws.sync(obj.get_object()); + + } + } + + public async void sync_clients() throws Error { + var str = yield message_async("j/clients"); + var arr = Json.from_string(str).get_array(); + foreach (var obj in arr.get_elements()) { + var addr = obj.get_object().get_string_member("address"); + var c = get_client(addr); + if (c != null) + c.sync(obj.get_object()); + } + } + + private async void handle_event(string line) throws Error { + var args = line.split(">>"); + + switch (args[0]) { + case "workspacev2": + focused_workspace = get_workspace(int.parse(args[1])); + break; + + case "focusedmon": + var argv = args[1].split(",", 2); + focused_monitor = get_monitor_by_name(argv[0]); + focused_workspace = get_workspace_by_name(argv[1]); + break; + + case "activewindowv2": + focused_client = get_client(args[1]); + break; + + // TODO: nag vaxry for fullscreenv2 that passes address + case "fullscreen": + yield sync_clients(); + break; + + case "monitorremoved": + var id = get_monitor_by_name(args[1]).id; + _monitors.get(id).removed(); + _monitors.remove(id); + monitor_removed(id); + notify_property("monitors"); + break; + + case "monitoraddedv2": + var id = int.parse(args[1].split(",", 2)[0]); + var mon = new Monitor(); + _monitors.insert(id, mon); + yield sync_monitors(); + monitor_added(mon); + notify_property("monitors"); + break; + + case "createworkspacev2": + var id = int.parse(args[1].split(",", 2)[0]); + var ws = new Workspace(); + _workspaces.insert(id, ws); + yield sync_workspaces(); + workspace_added(ws); + notify_property("workspaces"); + break; + + case "destroyworkspacev2": + var id = int.parse(args[1].split(",", 2)[0]); + _workspaces.get(id).removed(); + _workspaces.remove(id); + workspace_removed(id); + notify_property("workspaces"); + break; + + case "moveworkspacev2": + yield sync_workspaces(); + yield sync_monitors(); + break; + + case "renameworkspace": + yield sync_workspaces(); + break; + + case "activespecial": + yield sync_monitors(); + yield sync_workspaces(); + break; + + case "activelayout": + var argv = args[1].split(","); + keyboard_layout(argv[0], argv[1]); + break; + + case "openwindow": + var addr = args[1].split(",")[0]; + var client = new Client(); + _clients.insert(addr, client); + yield sync_clients(); + yield sync_workspaces(); + client_added(client); + notify_property("clients"); + break; + + case "closewindow": + _clients.get(args[1]).removed(); + _clients.remove(args[1]); + client_removed(args[1]); + yield sync_workspaces(); + notify_property("clients"); + break; + + case "movewindowv2": + yield sync_clients(); + yield sync_workspaces(); + var argv = args[1].split(","); + client_moved(get_client(argv[0]), get_workspace(int.parse(argv[1]))); + get_client(argv[0]).moved_to(get_workspace(int.parse(argv[1]))); + break; + + case "submap": + submap(args[1]); + break; + + case "changefloatingmode": + var argv = args[1].split(","); + yield sync_clients(); + floating(get_client(argv[0]), argv[1] == "0"); + break; + + case "urgent": + urgent(get_client(args[1])); + break; + + case "minimize": + var argv = args[1].split(","); + yield sync_clients(); + minimize(get_client(argv[0]), argv[1] == "0"); + break; + + case "windowtitle": + yield sync_clients(); + break; + + // TODO: + case "togglegroup": + case "moveintogroup": + case "moveoutofgroup": + case "ignoregrouplock": + case "lockgroups": + break; + + case "configreloaded": + config_reloaded(); + break; + + default: + break; + } + + event(args[0], args[1]); + } +} +} diff --git a/hyprland/src/meson.build b/hyprland/src/meson.build new file mode 100644 index 0000000..b6f7681 --- /dev/null +++ b/hyprland/src/meson.build @@ -0,0 +1,81 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalHyprland-' + api_version + '.gir' +typelib = 'AstalHyprland-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('gio-unix-2.0'), + dependency('json-glib-1.0'), +] + +sources = [ + config, + 'client.vala', + 'cli.vala', + 'hyprland.vala', + 'monitor.vala', + 'structs.vala', + 'workspace.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/hyprland/src/monitor.vala b/hyprland/src/monitor.vala new file mode 100644 index 0000000..d7b8028 --- /dev/null +++ b/hyprland/src/monitor.vala @@ -0,0 +1,71 @@ +namespace AstalHyprland { +public class Monitor : Object { + public signal void removed (); + + public int id { get; private set; } + public string name { get; private set; } + public string description { get; private set; } + public string make { get; private set; } + public string model { get; private set; } + public string serial { get; private set; } + public int width { get; private set; } + public int height { get; private set; } + public double refresh_rate { get; private set; } + public int x { get; private set; } + public int y { get; private set; } + public Workspace active_workspace { get; private set; } + public Workspace special_workspace { get; private set; } + public int reserved_top { get; private set; } + public int reserved_bottom { get; private set; } + public int reserved_left { get; private set; } + public int reserved_right { get; private set; } + public double scale { get; private set; } + public bool focused { get; private set; } + public bool dpms_status { get; private set; } + public bool vrr { get; private set; } + public bool actively_tearing { get; private set; } + public bool disabled { get; private set; } + public string current_format { get; private set; } + public Array<string> available_modes { get; private set; } + + internal void sync(Json.Object obj) { + var hyprland = Hyprland.get_default(); + + id = (int)obj.get_int_member("id"); + name = obj.get_string_member("name"); + description = obj.get_string_member("description"); + make = obj.get_string_member("make"); + model = obj.get_string_member("model"); + serial = obj.get_string_member("serial"); + width = (int)obj.get_int_member("width"); + height = (int)obj.get_int_member("height"); + refresh_rate = obj.get_double_member("refreshRate"); + x = (int)obj.get_int_member("x"); + y = (int)obj.get_int_member("y"); + scale = obj.get_double_member("scale"); + focused = obj.get_boolean_member("focused"); + dpms_status = obj.get_boolean_member("dpmsStatus"); + vrr = obj.get_boolean_member("vrr"); + actively_tearing = obj.get_boolean_member("activelyTearing"); + disabled = obj.get_boolean_member("disabled"); + current_format = obj.get_string_member("currentFormat"); + + var r = obj.get_array_member("reserved"); + reserved_top = (int)r.get_int_element(0); + reserved_bottom = (int)r.get_int_element(1); + reserved_left = (int)r.get_int_element(2); + reserved_right = (int)r.get_int_element(3); + + var modes = new Array<string>(); + foreach (var mode in obj.get_array_member("availableModes").get_elements()) + modes.append_val(mode.get_string()); + + active_workspace = hyprland.get_workspace((int)obj.get_object_member("activeWorkspace").get_int_member("id")); + special_workspace = hyprland.get_workspace((int)obj.get_object_member("specialWorkspace").get_int_member("id")); + } + + public void focus() { + Hyprland.get_default().dispatch("focusmonitor", id.to_string()); + } +} +} diff --git a/hyprland/src/structs.vala b/hyprland/src/structs.vala new file mode 100644 index 0000000..25f70c3 --- /dev/null +++ b/hyprland/src/structs.vala @@ -0,0 +1,42 @@ +namespace AstalHyprland { +public class Bind : Object { + public bool locked { get; construct set; } + public bool mouse { get; construct set; } + public bool release { get; construct set; } + public bool repeat { get; construct set; } + public bool non_consuming { get; construct set; } + public int64 modmask { get; construct set; } + public string submap { get; construct set; } + public string key { get; construct set; } + public int64 keycode { get; construct set; } + public bool catch_all { get; construct set; } + public string dispatcher { get; construct set; } + public string arg { get; construct set; } + + internal Bind.from_json(Json.Object obj) { + locked = obj.get_boolean_member("locked"); + mouse = obj.get_boolean_member("mouse"); + release = obj.get_boolean_member("release"); + repeat = obj.get_boolean_member("repeat"); + non_consuming = obj.get_boolean_member("non_consuming"); + modmask = obj.get_int_member("modmask"); + submap = obj.get_string_member("submap"); + key = obj.get_string_member("key"); + keycode = obj.get_int_member("keycode"); + catch_all = obj.get_boolean_member("catch_all"); + dispatcher = obj.get_string_member("dispatcher"); + arg = obj.get_string_member("arg"); + } +} + +public class Position : Object { + public int x { get; construct set; } + public int y { get; construct set; } + + internal Position.cursorpos(string pos) { + var xy = pos.split(","); + x = int.parse(xy[0].strip()); + y = int.parse(xy[1].strip()); + } +} +} diff --git a/hyprland/src/workspace.vala b/hyprland/src/workspace.vala new file mode 100644 index 0000000..075f86f --- /dev/null +++ b/hyprland/src/workspace.vala @@ -0,0 +1,57 @@ +namespace AstalHyprland { +public class Workspace : Object { + public signal void removed (); + + public List<weak Client> _clients = new List<weak Client>(); + + public int id { get; private set; } + public string name { get; private set; } + public Monitor monitor { get; private set; } + public List<weak Client> clients { owned get { return _clients.copy(); } } + public bool has_fullscreen { get; private set; } + public Client last_client { get; private set; } + + public Workspace.dummy(int id, Monitor? monitor) { + this.id = id; + this.name = id.to_string(); + this.monitor = monitor; + } + + internal List<weak Client> filter_clients() { + var hyprland = Hyprland.get_default(); + var list = new List<weak Client>(); + foreach (var client in hyprland.clients) { + if (client.workspace == this) { + list.append(client); + } + } + + return list; + } + + internal void sync(Json.Object obj) { + var hyprland = Hyprland.get_default(); + + id = (int)obj.get_int_member("id"); + name = obj.get_string_member("name"); + has_fullscreen = obj.get_boolean_member("hasfullscreen"); + + monitor = hyprland.get_monitor((int)obj.get_int_member("monitorID")); + last_client = hyprland.get_client(obj.get_string_member("lastwindow")); + + var list = filter_clients(); + if (_clients.length() != list.length()) { + _clients = list.copy(); + notify_property("clients"); + } + } + + public void focus() { + Hyprland.get_default().dispatch("workspace", id.to_string()); + } + + public void move_to(Monitor m) { + Hyprland.get_default().dispatch("moveworkspacetomonitor", id.to_string() + " " + m.id.to_string()); + } +} +} diff --git a/hyprland/version b/hyprland/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/hyprland/version @@ -0,0 +1 @@ +0.1.0 diff --git a/mpris/.gitignore b/mpris/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/mpris/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ diff --git a/mpris/LICENSE b/mpris/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/mpris/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/mpris/README.md b/mpris/README.md new file mode 100644 index 0000000..b05f3a5 --- /dev/null +++ b/mpris/README.md @@ -0,0 +1,7 @@ +# astal-mpris + +Library and cli tool for controlling media players + +## TODO + +- docs diff --git a/mpris/flake.lock b/mpris/flake.lock new file mode 100644 index 0000000..0821cb0 --- /dev/null +++ b/mpris/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1720768451, + "narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/mpris/flake.nix b/mpris/flake.nix new file mode 100644 index 0000000..fbacb73 --- /dev/null +++ b/mpris/flake.nix @@ -0,0 +1,49 @@ +{ + description = "Library and cli tool for controlling media players"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + gvfs + glib + json-glib + ]; + in { + packages.${system} = rec { + default = mpris; + mpris = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-mpris"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + mpris = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = buildInputs ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/mpris/meson.build b/mpris/meson.build new file mode 100644 index 0000000..d1ab98f --- /dev/null +++ b/mpris/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-mpris', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/mpris/meson_options.txt b/mpris/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/mpris/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/mpris/src/cli.vala b/mpris/src/cli.vala new file mode 100644 index 0000000..b71def9 --- /dev/null +++ b/mpris/src/cli.vala @@ -0,0 +1,331 @@ +namespace AstalMpris { +static bool help; +static bool version; +static bool list; +static bool raw; +[CCode (array_length = false, array_null_terminated = true)] +static string[] players; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "player", 'p', OptionFlags.NONE, OptionArg.STRING_ARRAY, ref players, null, null }, + { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, + { "raw", 'r', OptionFlags.NONE, OptionArg.NONE, ref raw, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr(err.message); + return 1; + } + + if (help) { + print("Usage:\n"); + print(" %s [flags] [command]\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -l, --list List available players\n"); + print(" -p, --player Operate on given player\n"); + print(" -r, --raw Print single line json info\n"); + print("\nCommands:\n"); + print(" info Print info about player\n"); + print(" monitor Monitor changes\n"); + print(" play Play track\n"); + print(" pause Pause track\n"); + print(" play-pause Play if paused, Pause if playing\n"); + print(" stop Stop player\n"); + print(" next Play next track\n"); + print(" previous Play previous track\n"); + print(" quit Quit player\n"); + print(" raise Ask compositor to raise the player\n"); + print(" position [OFFSET][+/-/%] Set position of player\n"); + print(" volume [LEVEL][+/-/%] Set volume of player\n"); + print(" loop [STATUS] One of: \"None\", \"Track\", \"Playlist\"\n"); + print(" shuffle [STATUS] One of: \"On\", \"Off\", \"Toggle\"\n"); + return 0; + } + + if (version) { + print(VERSION); + return 0; + } + + var mpris = new Mpris(); + var mpris_players = new List<Player>(); + + if (list) { + foreach (var p in mpris.players) + print("%s\n", p.bus_name.replace(Mpris.PREFIX, "")); + + return 0; + } + + if (players.length > 0) { + foreach (var name in players) + mpris_players.append(new Player(name)); + } else { + foreach (var p in mpris.players) + mpris_players.append(p); + } + + var cmd = argv[1]; + var arg = argv[2]; + + switch (cmd) { + case "monitor": + return do_monitor(mpris); + + case "info": + print_players(mpris_players.copy()); + break; + + case "play": + foreach (var player in mpris_players) + player.play(); + break; + + case "pause": + foreach (var player in mpris_players) + player.pause(); + break; + + case "play-pause": + foreach (var player in mpris_players) + player.play_pause(); + break; + + case "stop": + foreach (var player in mpris_players) + player.stop(); + break; + + case "next": + foreach (var player in mpris_players) + player.next(); + break; + + case "previous": + foreach (var player in mpris_players) + player.previous(); + break; + + case "raise": + foreach (var player in mpris_players) + player.raise(); + break; + + case "quit": + foreach (var player in mpris_players) + player.quit(); + break; + + case "position": + foreach (var player in mpris_players) { + if (do_position(player, arg) != 0) + return 1; + } + break; + + case "volume": + foreach (var player in mpris_players) { + if (do_volume(player, arg) != 0) + return 1; + } + break; + + case "loop": + foreach (var player in mpris_players) { + if (do_loop(player, arg) != 0) + return 1; + } + break; + + case "shuffle": + foreach (var player in mpris_players) { + if (do_shuffle(player, arg) != 0) + return 1; + } + break; + + case "open": + if (arg == null) { + stderr.printf("missing open arg"); + return 1; + } + + foreach (var player in mpris_players) + player.open_uri(arg); + break; + + default: + if (cmd == null) + stderr.printf("missing command\n"); + else + stderr.printf(@"unknown command \"$cmd\"\n"); + return 1; + } + + return 0; +} + +Json.Node to_json(Player p) { + var uris = new Json.Builder().begin_array(); + foreach (var uri in p.supported_uri_schemas) + uris.add_string_value(uri); + + uris.end_array(); + + return new Json.Builder().begin_object() + .set_member_name("bus_name").add_string_value(p.bus_name) + .set_member_name("available").add_boolean_value(p.available) + .set_member_name("identity").add_string_value(p.identity) + .set_member_name("entry").add_string_value(p.entry) + .set_member_name("supported_uri_schemas").add_value(uris.get_root()) + .set_member_name("loop_status").add_string_value(p.loop_status.to_string()) + .set_member_name("shuffle_status").add_string_value(p.shuffle_status.to_string()) + .set_member_name("rate").add_double_value(p.rate) + .set_member_name("volume").add_double_value(p.volume) + .set_member_name("position").add_double_value(p.position) + .set_member_name("cover_art").add_string_value(p.cover_art) + .set_member_name("metadata").add_value(Json.gvariant_serialize( + p.metadata != null ? p.metadata : new HashTable<string, Variant>(str_hash, str_equal))) + .end_object() + .get_root(); +} + +void print_players(List<weak Player> players) { + var json = new Json.Builder().begin_array(); + + foreach (var p in players) + json.add_value(to_json(p)); + + stdout.printf("%s\n", Json.to_string(json.end_array().get_root(), !raw)); + stdout.flush(); +} + +int do_monitor(Mpris mpris) { + print_players(mpris.players); + foreach (var player in mpris.players) { + player.notify.connect(() => print_players(mpris.players)); + } + + mpris.player_added.connect((player) => { + player.notify.connect(() => print_players(mpris.players)); + }); + + mpris.player_closed.connect(() => { + print_players(mpris.players); + }); + + new MainLoop(null, false).run(); + return 0; +} + +int do_position(Player player, string? arg) { + if (arg == null) { + stderr.printf("missing position argument\n"); + return 1; + } + + else if (arg.has_suffix("%")) { + var percent = double.parse(arg.slice(0, -1)) / 100; + player.position = player.length * percent; + } + + else if (arg.has_suffix("-")) { + player.position += double.parse(arg.slice(0, -1)) * -1; + } + + else if (arg.has_suffix("+")) { + player.position += double.parse(arg.slice(0, -1)); + } + + else { + player.position = double.parse(arg); + } + + return 0; +} + +int do_volume(Player player, string? arg) { + if (arg == null) { + stderr.printf("missing volume argument\n"); + return 1; + } + + else if (arg.has_suffix("%")) { + player.volume = double.parse(arg.slice(0, -1)) / 100; + } + + else if (arg.has_suffix("-")) { + player.volume += (double.parse(arg.slice(0, -1)) * -1) / 100; + } + + else if (arg.has_suffix("+")) { + player.volume += double.parse(arg.slice(0, -1)) / 100; + } + + else { + player.volume = double.parse(arg); + } + + return 0; +} + +int do_loop(Player player, string? arg) { + if (arg == null) { + player.loop(); + return 0; + } + + switch (arg) { + case "None": + player.loop_status = Loop.NONE; + break; + case "Track": + player.loop_status = Loop.TRACK; + break; + case "Playlist": + player.loop_status = Loop.PLAYLIST; + break; + default: + stderr.printf(@"unknown shuffle status \"$arg\""); + return 1; + } + + return 0; +} + +int do_shuffle(Player player, string? arg) { + if (arg == null) { + player.shuffle(); + return 1; + } + + switch (arg) { + case "On": + player.shuffle_status = Shuffle.ON; + break; + case "Off": + player.shuffle_status = Shuffle.OFF; + break; + case "Toggle": + player.shuffle(); + break; + default: + stderr.printf(@"unknown shuffle status \"$arg\""); + return 1; + } + + return 0; +} +} diff --git a/mpris/src/config.vala.in b/mpris/src/config.vala.in new file mode 100644 index 0000000..767c4bd --- /dev/null +++ b/mpris/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalMpris { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/mpris/src/ifaces.vala b/mpris/src/ifaces.vala new file mode 100644 index 0000000..4a9d715 --- /dev/null +++ b/mpris/src/ifaces.vala @@ -0,0 +1,60 @@ +namespace AstalMpris { +[DBus (name="org.freedesktop.DBus")] +internal interface DBusImpl : DBusProxy { + public abstract string[] list_names () throws GLib.Error; + public signal void name_owner_changed (string name, string old_owner, string new_owner); +} + +[DBus (name="org.freedesktop.DBus.Properties")] +internal interface PropsIface : DBusProxy { + public abstract HashTable<string, Variant> get_all (string iface); +} + +[DBus (name="org.mpris.MediaPlayer2")] +internal interface IMpris : PropsIface { + public abstract void raise () throws GLib.Error; + public abstract void quit () throws GLib.Error; + + public abstract bool can_quit { get; } + public abstract bool fullscreen { get; set; } + public abstract bool can_set_fullscreen { get; } + public abstract bool can_raise { get; } + public abstract bool has_track_list { get; } + public abstract string identity { owned get; } + public abstract string desktop_entry { owned get; } + public abstract string[] supported_uri_schemas { owned get; } + public abstract string[] supported_mime_types { owned get; } +} + +[DBus (name="org.mpris.MediaPlayer2.Player")] +internal interface IPlayer : IMpris { + public abstract void next () throws GLib.Error; + public abstract void previous () throws GLib.Error; + public abstract void pause () throws GLib.Error; + public abstract void play_pause () throws GLib.Error; + public abstract void stop () throws GLib.Error; + public abstract void play () throws GLib.Error; + public abstract void seek (int64 offset) throws GLib.Error; + public abstract void set_position (ObjectPath track_id, int64 position) throws GLib.Error; + public abstract void open_uri (string uri) throws GLib.Error; + + public signal void seeked (int64 position); + + public abstract string playback_status { owned get; } + public abstract string loop_status { owned get; set; } + public abstract double rate { get; set; } + public abstract bool shuffle { get; set; } + public abstract HashTable<string,Variant> metadata { owned get; } + public abstract double volume { get; set; } + public abstract int64 position { get; } + public abstract double minimum_rate { get; set; } + public abstract double maximum_rate { get; set; } + + public abstract bool can_go_next { get; } + public abstract bool can_go_previous { get; } + public abstract bool can_play { get; } + public abstract bool can_pause { get; } + public abstract bool can_seek { get; } + public abstract bool can_control { get; } +} +} diff --git a/mpris/src/meson.build b/mpris/src/meson.build new file mode 100644 index 0000000..9b3c51f --- /dev/null +++ b/mpris/src/meson.build @@ -0,0 +1,77 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalMpris-' + api_version + '.gir' +typelib = 'AstalMpris-' + api_version + '.typelib' +so = 'lib' + meson.project_name() + '.so.' + meson.project_version() + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('json-glib-1.0'), +] + +sources = [ + config, + 'ifaces.vala', + 'player.vala', + 'mpris.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/mpris/src/mpris.vala b/mpris/src/mpris.vala new file mode 100644 index 0000000..0e55a2e --- /dev/null +++ b/mpris/src/mpris.vala @@ -0,0 +1,66 @@ +namespace AstalMpris { +public Mpris get_default() { + return Mpris.get_default(); +} + +public class Mpris : Object { + internal static string PREFIX = "org.mpris.MediaPlayer2."; + + private static Mpris instance; + public static Mpris get_default() { + if (instance == null) + instance = new Mpris(); + + return instance; + } + + private DBusImpl proxy; + + private HashTable<string, Player> _players = + new HashTable<string, Player> (str_hash, str_equal); + + public List<weak Player> players { owned get { return _players.get_values(); } } + + public signal void player_added (Player player); + public signal void player_closed (Player player); + + construct { + try { + proxy = Bus.get_proxy_sync( + BusType.SESSION, + "org.freedesktop.DBus", + "/org/freedesktop/DBus" + ); + + foreach (var busname in proxy.list_names()) { + if (busname.has_prefix(Mpris.PREFIX)) + add_player(busname); + } + + proxy.name_owner_changed.connect((name, old_owner, new_owner) => { + if (!name.has_prefix(Mpris.PREFIX)) + return; + + if (new_owner != "" && old_owner == "") + add_player(name); + }); + } catch (Error error) { + critical(error.message); + } + } + + private void add_player(string busname) { + var p = new Player(busname); + _players.set(busname, p); + + p.closed.connect(() => { + player_closed(p); + _players.remove(busname); + notify_property("players"); + }); + + player_added(p); + notify_property("players"); + } +} +} diff --git a/mpris/src/player.vala b/mpris/src/player.vala new file mode 100644 index 0000000..ed146f6 --- /dev/null +++ b/mpris/src/player.vala @@ -0,0 +1,467 @@ +namespace AstalMpris { +public class Player : Object { + private static string COVER_CACHE = Environment.get_user_cache_dir() + "/astal/mpris"; + + private IPlayer proxy; + + public signal void appeared () { available = true; } + public signal void closed () { available = false; } + + // identifiers + public string bus_name { owned get; construct set; } + public bool available { get; private set; } + + // periodically notify position + private uint pollid; + + // mpris + public void raise() { + try { proxy.raise(); } catch (Error error) { critical(error.message); } + } + + public void quit() { + try { proxy.quit(); } catch (Error error) { critical(error.message); } + } + + public bool can_quit { get; private set; } + public bool fullscreen { get; private set; } + public bool can_set_fullscreen { get; private set; } + public bool can_raise { get; private set; } + public bool has_track_list { get; private set; } + public string identity { owned get; private set; } + public string entry { owned get; private set; } + public string[] supported_uri_schemas { owned get; private set; } + public string[] supported_mime_types { owned get; private set; } + + public void toggle_fullscreen() { + if (!can_set_fullscreen) + critical("can not set fullscreen on " + bus_name); + + proxy.fullscreen = !fullscreen; + } + + // player + public void next() { + try { proxy.next(); } catch (Error error) { critical(error.message); } + } + + public void previous() { + try { proxy.previous(); } catch (Error error) { critical(error.message); } + } + + public void pause() { + try { proxy.pause(); } catch (Error error) { critical(error.message); } + } + + public void play_pause() { + try { proxy.play_pause(); } catch (Error error) { critical(error.message); } + } + + public void stop() { + try { proxy.stop(); } catch (Error error) { critical(error.message); } + } + + public void play() { + try { proxy.play(); } catch (Error error) { critical(error.message); } + } + + public void open_uri(string uri) { + try { proxy.open_uri(uri); } catch (Error error) { critical(error.message); } + } + + public void loop() { + switch (loop_status) { + case Loop.NONE: + loop_status = Loop.TRACK; + break; + case Loop.TRACK: + loop_status = Loop.PLAYLIST; + break; + case Loop.PLAYLIST: + loop_status = Loop.NONE; + break; + default: + break; + } + } + + public void shuffle() { + shuffle_status = shuffle_status == Shuffle.ON + ? Shuffle.OFF + : Shuffle.ON; + } + + public signal void seeked (int64 position); + + public double _get_position() { + try { + var reply = proxy.call_sync( + "org.freedesktop.DBus.Properties.Get", + new Variant("(ss)", + "org.mpris.MediaPlayer2.Player", + "Position" + ), + DBusCallFlags.NONE, + -1, + null + ); + + var body = reply.get_child_value(0); + if (body.classify() == Variant.Class.STRING) { + return -1; // Position not supported + } + + return (double)body.get_variant().get_int64() / 1000000; + } catch (Error err) { + return -1; + } + } + + private void _set_position(double pos) { + try { + proxy.set_position((ObjectPath)trackid, (int64)(pos * 1000000)); + } catch (Error error) { + critical(error.message); + } + } + + private Loop _loop_status = Loop.UNSUPPORTED; + private double _rate; + private Shuffle _shuffle_status = Shuffle.UNSUPPORTED; + private double _volume = -1; + + public Loop loop_status { + get { return _loop_status; } + set { proxy.loop_status = value.to_string(); } + } + + public double rate { + get { return _rate; } + set { proxy.rate = value; } + } + + public Shuffle shuffle_status { + get { return _shuffle_status; } + set { proxy.shuffle = value == Shuffle.ON; } + } + + public double volume { + get { return _volume; } + set { proxy.volume = value; } + } + + public double position { + get { return _get_position(); } + set { _set_position(value); } + } + + public PlaybackStatus playback_status { get; private set; } + public double minimum_rate { get; private set; } + public double maximum_rate { get; private set; } + public bool can_go_next { get; private set; } + public bool can_go_previous { get; private set; } + public bool can_play { get; private set; } + public bool can_pause { get; private set; } + public bool can_seek { get; private set; } + public bool can_control { get; private set; } + + // metadata + [CCode (notify = false)] + public HashTable<string,Variant> metadata { owned get; private set; } + + public string trackid { owned get; private set; } + public double length { get; private set; } + public string art_url { owned get; private set; } + + public string album { owned get; private set; } + public string album_artist { owned get; private set; } + public string artist { owned get; private set; } + public string lyrics { owned get; private set; } + public string title { owned get; private set; } + public string composer { owned get; private set; } + public string comments { owned get; private set; } + + // cached cover art + public string cover_art { owned get; private set; } + + public Player(string name) { + Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.") + ? name : "org.mpris.MediaPlayer2." + name); + } + + private void sync() { + // mpris + can_quit = proxy.can_quit; + fullscreen = proxy.fullscreen; + can_set_fullscreen = proxy.can_set_fullscreen; + can_raise = proxy.can_raise; + has_track_list = proxy.has_track_list; + identity = proxy.identity; + entry = proxy.desktop_entry; + supported_uri_schemas = proxy.supported_uri_schemas; + supported_mime_types = proxy.supported_mime_types; + + if (position >= 0) + notify_property("position"); + + // LoopStatus and Shuffle are optional props + var props = proxy.get_all("org.mpris.MediaPlayer2.Player"); + + // player + if (props != null && props.get("LoopStatus") != null) { + if (loop_status != Loop.from_string(proxy.loop_status)) { + _loop_status = Loop.from_string(proxy.loop_status); + notify_property("loop-status"); + } + } + + if (rate != proxy.rate) { + _rate = proxy.rate; + notify_property("rate"); + } + + if (props != null && props.get("Shuffle") != null) { + if (shuffle_status != Shuffle.from_bool(proxy.shuffle)) { + _shuffle_status = Shuffle.from_bool(proxy.shuffle); + notify_property("shuffle-status"); + } + } + + if (volume != proxy.volume) { + _volume = proxy.volume; + notify_property("volume"); + } + + playback_status = PlaybackStatus.from_string(proxy.playback_status); + minimum_rate = proxy.minimum_rate; + maximum_rate = proxy.maximum_rate; + can_go_next = proxy.can_go_next; + can_go_previous = proxy.can_go_previous; + can_play = proxy.can_play; + can_pause = proxy.can_pause; + can_seek = proxy.can_seek; + can_control = proxy.can_control; + + // metadata + metadata = proxy.metadata; + if (metadata != null) { + if (metadata.get("mpris:length") != null) + length = (double)metadata.get("mpris:length").get_uint64() / 1000000; + else + length = -1; + + trackid = get_str("mpris:trackid"); + art_url = get_str("mpris:artUrl"); + album = get_str("xesam:album"); + lyrics = get_str("xesam:asText"); + title = get_str("xesam:title"); + album_artist = join_strv("xesam:albumArtist", ", "); + artist = join_strv("xesam:artist", ", "); + comments = join_strv("xesam:comments", "\n"); + composer = join_strv("xesam:composer", ", "); + cache_cover.begin((_, res) => cache_cover.end(res)); + notify_property("metadata"); + } + } + + private async void cache_cover() { + if (art_url == null || art_url == "") + return; + + var file = File.new_for_uri(art_url); + if (file.get_path() != null) { + cover_art = file.get_path(); + return; + } + + var path = COVER_CACHE + "/" + Checksum.compute_for_string(ChecksumType.SHA1, art_url, -1); + if (FileUtils.test(path, FileTest.EXISTS)) { + cover_art = path; + return; + } + + try { + if (!FileUtils.test(COVER_CACHE, FileTest.IS_DIR)) + File.new_for_path(COVER_CACHE).make_directory_with_parents(null); + + file.copy_async.begin( + File.new_for_path(path), + FileCopyFlags.OVERWRITE, + Priority.DEFAULT, + null, + null, + (_, res) => { + try { + file.copy_async.end(res); + cover_art = path; + } catch (Error err) { + critical("Failed to cache cover art with url \"%s\": %s", art_url, err.message); + } + } + ); + } catch (Error err) { + critical(err.message); + } + } + + public Variant? get_meta(string key) { + return metadata.lookup(key); + } + + private string get_str(string key) { + if (metadata.get(key) == null) + return ""; + + var str = metadata.get(key).get_string(null); + return str == null ? "" : str; + } + + private string? join_strv(string key, string sep) { + if (metadata.get(key) == null) + return null; + + var arr = metadata.get(key).get_strv(); + if (arr.length == 0) + return null; + + var builder = new StringBuilder(); + for (var i = 0; i < arr.length; ++i) { + builder.append(arr[i]); + if (i + 1 < arr.length) + builder.append(sep); + } + + return builder.str; + } + + construct { + try { + try_proxy(); + sync(); + } catch (Error error) { + critical(error.message); + } + } + + public void try_proxy() throws Error { + if (proxy != null) + return; + + proxy = Bus.get_proxy_sync( + BusType.SESSION, + bus_name, + "/org/mpris/MediaPlayer2" + ); + + if (proxy.g_name_owner != null) + appeared(); + + proxy.notify["g-name-owner"].connect(() => { + if (proxy.g_name_owner != null) + appeared(); + else + closed(); + }); + + proxy.g_properties_changed.connect(sync); + + pollid = Timeout.add_seconds(1, () => { + if (!available) + return Source.CONTINUE; + + if (position >= 0) { + notify_property("position"); + } + return Source.CONTINUE; + }, Priority.DEFAULT); + } + + ~Player() { + Source.remove(pollid); + } +} + +public enum PlaybackStatus { + PLAYING, + PAUSED, + STOPPED; + + public static PlaybackStatus from_string(string? str) { + switch (str) { + case "Playing": + return PLAYING; + case "Paused": + return PAUSED; + case "Stopped": + default: + return STOPPED; + } + } + + public string to_string() { + switch (this) { + case PLAYING: + return "Playing"; + case PAUSED: + return "Paused"; + case STOPPED: + default: + return "Stopped"; + } + } +} + +public enum Loop { + UNSUPPORTED, + NONE, + TRACK, + PLAYLIST; + + public static Loop from_string(string? str) { + switch (str) { + case "None": + return NONE; + case "Track": + return TRACK; + case "Playlist": + return PLAYLIST; + default: + return UNSUPPORTED; + } + } + + public string? to_string() { + switch (this) { + case NONE: + return "None"; + case TRACK: + return "Track"; + case PLAYLIST: + return "Playlist"; + default: + return "Unsupported"; + } + } +} + +public enum Shuffle { + UNSUPPORTED, + ON, + OFF; + + public static Shuffle from_bool(bool b) { + return b ? Shuffle.ON : Shuffle.OFF; + } + + public string? to_string() { + switch (this) { + case OFF: + return "Off"; + case ON: + return "On"; + default: + return "Unsupported"; + } + } +} +} diff --git a/mpris/version b/mpris/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/mpris/version @@ -0,0 +1 @@ +0.1.0 diff --git a/network/.gitignore b/network/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/network/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ diff --git a/network/LICENSE b/network/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/network/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/network/README.md b/network/README.md new file mode 100644 index 0000000..71151e5 --- /dev/null +++ b/network/README.md @@ -0,0 +1,7 @@ +# network + +NetworkManager wrapper library + +## TODO + +- docs diff --git a/network/flake.lock b/network/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/network/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/network/flake.nix b/network/flake.nix new file mode 100644 index 0000000..f8cf3dc --- /dev/null +++ b/network/flake.nix @@ -0,0 +1,53 @@ +{ + description = "NetworkManager wrapper library"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + networkmanager + ]; + in { + packages.${system} = rec { + default = network; + network = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-network"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + network = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/network/meson.build b/network/meson.build new file mode 100644 index 0000000..a9dc9c3 --- /dev/null +++ b/network/meson.build @@ -0,0 +1,14 @@ +project( + 'astal-network', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +subdir('src') diff --git a/network/src/accesspoint.vala b/network/src/accesspoint.vala new file mode 100644 index 0000000..3c51018 --- /dev/null +++ b/network/src/accesspoint.vala @@ -0,0 +1,49 @@ +public class AstalNetwork.AccessPoint : Object { + private Wifi wifi; + private NM.AccessPoint ap; + + public uint bandwidth { get { return ap.bandwidth; } } + public string bssid { owned get { return ap.bssid; } } + public uint frequency { get { return ap.frequency; } } + public int last_seen { get { return ap.last_seen; } } + public uint max_bitrate { get { return ap.max_bitrate; } } + public uint8 strength { get { return ap.strength; } } + public string icon_name { get; private set; } + public NM.80211Mode mode { get { return ap.mode; } } + public NM.80211ApFlags flags { get { return ap.flags; } } + public NM.80211ApSecurityFlags rsn_flags { get { return ap.rsn_flags; } } + public NM.80211ApSecurityFlags wpa_flags { get { return ap.wpa_flags; } } + + public string? ssid { + owned get { + if (ap.ssid == null) + return null; + + return (string)NM.Utils.ssid_to_utf8(ap.ssid.get_data()); + } + } + + internal AccessPoint(Wifi wifi, NM.AccessPoint ap) { + this.wifi = wifi; + this.ap = ap; + ap.notify.connect((pspec) => { + if (get_class().find_property(pspec.name) != null) + notify_property(pspec.name); + if (pspec.name == "strength") + icon_name = _icon(); + }); + icon_name = _icon(); + } + + private string _icon() { + if (strength >= 80) return Wifi.ICON_EXCELLENT; + if (strength >= 60) return Wifi.ICON_GOOD; + if (strength >= 40) return Wifi.ICON_OK; + if (strength >= 20) return Wifi.ICON_WEAK; + return Wifi.ICON_NONE; + } + + // TODO: connect to ap + // public signal void auth(); + // public void try_connect(string? password) { } +} diff --git a/network/src/config.vala.in b/network/src/config.vala.in new file mode 100644 index 0000000..dbec0f3 --- /dev/null +++ b/network/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalNetwork { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/network/src/meson.build b/network/src/meson.build new file mode 100644 index 0000000..3294ae3 --- /dev/null +++ b/network/src/meson.build @@ -0,0 +1,67 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalNetwork-' + api_version + '.gir' +typelib = 'AstalNetwork-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('libnm') +] + +sources = [ + config, + 'network.vala', + 'wifi.vala', + 'wired.vala', + 'wired.vala', + 'accesspoint.vala', +] + +lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], +) + +import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', +) + +custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', +) diff --git a/network/src/network.vala b/network/src/network.vala new file mode 100644 index 0000000..7c8e466 --- /dev/null +++ b/network/src/network.vala @@ -0,0 +1,206 @@ +namespace AstalNetwork { + public Network get_default() { + return Network.get_default(); + } +} + +public class AstalNetwork.Network : Object { + private static Network instance; + public static Network get_default() { + if (instance == null) + instance = new Network(); + + return instance; + } + + public NM.Client client { get; private set; } + + public Wifi? wifi { get; private set; } + public Wired? wired { get; private set; } + public Primary primary { get; private set; } + + public Connectivity connectivity { + get { return (Connectivity)client.connectivity; } + } + + public State state { + get { return (State)client.state; } + } + + construct { + try { + client = new NM.Client(); + var wifi_device = (NM.DeviceWifi)get_device(NM.DeviceType.WIFI); + if (wifi_device != null) + wifi = new Wifi(wifi_device); + + var ethernet = (NM.DeviceEthernet)get_device(NM.DeviceType.ETHERNET); + if (ethernet != null) + wired = new Wired(ethernet); + + sync(); + client.notify["primary-connection"].connect(sync); + client.notify["activating-connection"].connect(sync); + + client.notify["state"].connect(() => notify_property("state")); + client.notify["connectivity"].connect(() => notify_property("connectivity")); + } catch (Error err) { + critical(err.message); + } + } + + private NM.Device get_device(NM.DeviceType t) { + var valid = new GenericArray<NM.Device>(); + foreach (var device in client.get_devices()) { + if (device.device_type == t) + valid.add(device); + } + + foreach (var device in valid) { + if (device.active_connection != null) + return device; + } + + return valid.get(0); + } + + private void sync() { + var ac = client.get_primary_connection(); + + if (ac == null) + ac = client.get_activating_connection(); + + if (ac != null) + primary = Primary.from_connection_type(ac.type); + else + primary = Primary.UNKNOWN; + } +} + +public enum AstalNetwork.Primary { + UNKNOWN, + WIRED, + WIFI; + + public string to_string() { + switch (this) { + case WIFI: return "wifi"; + case WIRED: return "wired"; + default: return "unknown"; + } + } + + public static Primary from_connection_type(string type) { + switch (type) { + case "802-11-wireless": return Primary.WIFI; + case "802-3-ethernet": return Primary.WIRED; + default: return Primary.UNKNOWN; + } + } +} + +// alias for NM.State +public enum AstalNetwork.State { + UNKNOWN, + ASLEEP, + DISCONNECTED, + DISCONNECTING, + CONNECTING, + CONNECTED_LOCAL, + CONNECTED_SITE, + CONNECTED_GLOBAL; + + public string to_string() { + switch (this) { + case ASLEEP: return "asleep"; + case DISCONNECTED: return "disconnected"; + case DISCONNECTING: return "disconnecting"; + case CONNECTING: return "connecting"; + case CONNECTED_LOCAL: return "connected_local"; + case CONNECTED_SITE: return "connected_site"; + case CONNECTED_GLOBAL: return "connected_global"; + default: return "unknown"; + } + } +} + + +// alias for NM.ConnectivityState +public enum AstalNetwork.Connectivity { + UNKNOWN, + NONE, + PORTAL, + LIMITED, + FULL; + + public string to_string() { + switch (this) { + case NONE: return "none"; + case PORTAL: return "portal"; + case LIMITED: return "limited"; + case FULL: return "full"; + default: return "unknown"; + } + } +} + +// alias for NM.DeviceState +public enum AstalNetwork.DeviceState { + UNKNOWN, + UNMANAGED, + UNAVAILABLE, + DISCONNECTED, + PREPARE, + CONFIG, + NEED_AUTH, + IP_CONFIG, + IP_CHECK, + SECONDARIES, + ACTIVATED, + DEACTIVATING, + FAILED; + + public string to_string() { + switch (this) { + case UNMANAGED: return "unmanaged"; + case UNAVAILABLE: return "unavailable"; + case DISCONNECTED: return "disconnected"; + case PREPARE: return "prepare"; + case CONFIG: return "config"; + case NEED_AUTH: return "need_auth"; + case IP_CONFIG: return "ip_config"; + case IP_CHECK: return "ip_check"; + case SECONDARIES: return "secondaries"; + case ACTIVATED: return "activated"; + case DEACTIVATING: return "deactivating"; + case FAILED: return "failed"; + default: return "unknown"; + } + + } +} + +public enum AstalNetwork.Internet { + CONNECTED, + CONNECTING, + DISCONNECTED; + + public static Internet from_device(NM.Device device) { + if (device == null || device.active_connection == null) + return DISCONNECTED; + + switch (device.active_connection.state) { + case NM.ActiveConnectionState.ACTIVATED: return CONNECTED; + case NM.ActiveConnectionState.ACTIVATING: return CONNECTING; + default: return DISCONNECTED; + } + } + + public string to_string() { + switch (this) { + case CONNECTED: return "connected"; + case CONNECTING: return "connecting"; + default: return "disconnected"; + } + } +} diff --git a/network/src/vpn.vala b/network/src/vpn.vala new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/network/src/vpn.vala @@ -0,0 +1 @@ + diff --git a/network/src/wifi.vala b/network/src/wifi.vala new file mode 100644 index 0000000..c9e9881 --- /dev/null +++ b/network/src/wifi.vala @@ -0,0 +1,182 @@ +public class AstalNetwork.Wifi : Object { + internal const string ICON_EXCELLENT = "network-wireless-signal-excellent-symbolic"; + internal const string ICON_OK = "network-wireless-signal-ok-symbolic"; + internal const string ICON_GOOD = "network-wireless-signal-good-symbolic"; + internal const string ICON_WEAK = "network-wireless-signal-weak-symbolic"; + internal const string ICON_NONE = "network-wireless-signal-none-symbolic"; + internal const string ICON_ACQUIRING = "network-wireless-acquiring-symbolic"; + internal const string ICON_CONNECTED = "network-wireless-connected-symbolic"; + internal const string ICON_DISABLED = "network-wireless-disabled-symbolic"; + internal const string ICON_OFFLINE = "network-wireless-offline-symbolic"; + internal const string ICON_NO_ROUTE = "network-wireless-no-route-symbolic"; + internal const string ICON_HOTSPOT = "network-wireless-hotspot-symbolic"; + + private HashTable<string, AccessPoint> _access_points = + new HashTable<string, AccessPoint>(str_hash, str_equal); + + public NM.DeviceWifi device { get; construct set; } + + public NM.ActiveConnection? active_connection { get; private set; } + private ulong connection_handler = 0; + + public AccessPoint? active_access_point { get; private set; } + private ulong ap_handler = 0; + + public List<weak AccessPoint> access_points { + owned get { return _access_points.get_values(); } + } + + public bool enabled { + get { return device.client.wireless_enabled; } + set { device.client.wireless_enabled = value; } + } + + public Internet internet { get; private set; } + public uint bandwidth { get; private set; } + public string ssid { get; private set; } + public uint8 strength { get; private set; } + public uint frequency { get; private set; } + public DeviceState state { get; private set; } + public string icon_name { get; private set; } + public bool is_hotspot { get; private set; } + public bool scanning { get; private set; } + + internal Wifi(NM.DeviceWifi device) { + this.device = device; + + foreach (var ap in device.access_points) + _access_points.set(ap.bssid, new AccessPoint(this, ap)); + + device.access_point_added.connect((access_point) => { + var ap = (NM.AccessPoint)access_point; + _access_points.set(ap.bssid, new AccessPoint(this, ap)); + notify_property("access-points"); + }); + + device.access_point_removed.connect((access_point) => { + var ap = (NM.AccessPoint)access_point; + _access_points.remove(ap.bssid); + notify_property("access-points"); + }); + + on_active_connection(); + device.notify["active-connection"].connect(on_active_connection); + + on_active_access_point(); + device.notify["active-access-point"].connect(on_active_access_point); + + state = (DeviceState)device.state; + device.client.notify["wireless-enabled"].connect(() => notify_property("enabled")); + device.state_changed.connect((n, o, r) => { + state_changed(n, o, r); + state = (DeviceState)n; + }); + + device.notify.connect(() => { icon_name = _icon(); }); + device.client.notify.connect(() => { icon_name = _icon(); }); + icon_name = _icon(); + } + + public signal void state_changed( + DeviceState new_state, + DeviceState old_state, + NM.DeviceStateReason reaseon + ); + + public void scan() { + scanning = true; + var last_scan = device.last_scan; + device.request_scan_async.begin(null, (_, res) => { + try { + device.request_scan_async.end(res); + Timeout.add(1000, () => { + if (device.last_scan == last_scan) + return Source.CONTINUE; + + scanning = false; + return Source.REMOVE; + }, Priority.DEFAULT); + } catch (Error err) { + critical(err.message); + } + }); + } + + private void on_active_connection() { + if (connection_handler > 0 && active_connection != null) { + active_connection.disconnect(connection_handler); + connection_handler = 0; + active_connection = null; + } + + active_connection = device.active_connection; + is_hotspot = _hotspot(); + if (active_connection != null) { + connection_handler = active_connection.notify["state"].connect(() => { + internet = Internet.from_device(device); + }); + } + } + + private void on_active_access_point_notify() { + bandwidth = active_access_point.bandwidth; + frequency = active_access_point.frequency; + strength = active_access_point.strength; + ssid = active_access_point.ssid; + } + + private void on_active_access_point() { + if (ap_handler > 0 && active_access_point != null) { + active_access_point.disconnect(ap_handler); + ap_handler = 0; + active_access_point = null; + } + + var ap = device.active_access_point; + if (ap != null) { + active_access_point = _access_points.get(ap.bssid); + on_active_access_point_notify(); + ap_handler = active_access_point.notify.connect(on_active_access_point_notify); + } + } + + private string _icon() { + if (!enabled) return ICON_DISABLED; + + var full = device.client.connectivity == NM.ConnectivityState.FULL; + + if (internet == Internet.CONNECTED) { + if (is_hotspot) return ICON_HOTSPOT; + if (!full) return ICON_NO_ROUTE; + if (active_access_point == null) return ICON_CONNECTED; + + if (strength >= 80) return ICON_EXCELLENT; + if (strength >= 60) return ICON_GOOD; + if (strength >= 40) return ICON_OK; + if (strength >= 20) return ICON_WEAK; + + return ICON_NONE; + } + + if (internet == Internet.CONNECTING) { + return ICON_ACQUIRING; + } + + return ICON_OFFLINE; + } + + private bool _hotspot() { + if (device.active_connection == null) + return false; + + var conn = device.active_connection.connection; + if (conn == null) + return false; + + var ip4config = conn.get_setting_ip4_config(); + if (ip4config == null) + return false; + + return ip4config.method == NM.SettingIP4Config.METHOD_SHARED; + } +} diff --git a/network/src/wired.vala b/network/src/wired.vala new file mode 100644 index 0000000..68cf460 --- /dev/null +++ b/network/src/wired.vala @@ -0,0 +1,73 @@ +public class AstalNetwork.Wired : Object { + private const string ICON_CONNECTED = "network-wired-symbolic"; + private const string ICON_DISCONNECTED = "network-wired-disconnected-symbolic"; + private const string ICON_ACQUIRING = "network-wired-acquiring-symbolic"; + private const string ICON_NO_ROUTE = "network-wired-no-route-symbolic"; + + public NM.DeviceEthernet device { get; construct set; } + + public NM.ActiveConnection connection; + private ulong connection_handler = 0; + + internal Wired(NM.DeviceEthernet device) { + this.device = device; + + speed = device.speed; + state = (DeviceState)device.state; + icon_name = _icon(); + + device.notify.connect((pspec) => { + if (pspec.name == "speed") { + speed = device.speed; + } + if (pspec.name == "state") { + state = (DeviceState)device.state; + } + if (pspec.name == "active-connection") { + on_active_connection(); + } + icon_name = _icon(); + }); + + device.client.notify.connect(() => { icon_name = _icon(); }); + + on_active_connection(); + icon_name = _icon(); + } + + private void on_active_connection() { + if (connection_handler > 0 && connection != null) { + connection.disconnect(connection_handler); + connection_handler = 0; + connection = null; + } + + connection = device.active_connection; + if (connection != null) { + connection_handler = connection.notify["state"].connect(() => { + internet = Internet.from_device(device); + }); + } + } + + public uint speed { get; private set; } + public Internet internet { get; private set; } + public DeviceState state { get; private set; } + public string icon_name { get; private set; } + + private string _icon() { + var full = device.client.connectivity == NM.ConnectivityState.FULL; + + if (internet == Internet.CONNECTING) { + return ICON_ACQUIRING; + } + + if (internet == Internet.CONNECTED) { + if (!full) return ICON_NO_ROUTE; + + return ICON_CONNECTED; + } + + return ICON_DISCONNECTED; + } +} diff --git a/network/version b/network/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/network/version @@ -0,0 +1 @@ +0.1.0 diff --git a/notifd/LICENSE b/notifd/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/notifd/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/notifd/README.md b/notifd/README.md new file mode 100644 index 0000000..da93a6a --- /dev/null +++ b/notifd/README.md @@ -0,0 +1,7 @@ +# notifd + +A notification daemon library and cli tool + +## TODO + +- docs diff --git a/notifd/flake.lock b/notifd/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/notifd/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/notifd/flake.nix b/notifd/flake.nix new file mode 100644 index 0000000..b4e7a45 --- /dev/null +++ b/notifd/flake.nix @@ -0,0 +1,54 @@ +{ + description = "Notification daemon library and cli tool"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + gdk-pixbuf + json-glib + ]; + in { + packages.${system} = rec { + default = notifd; + notifd = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-notifd"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + notifd = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/notifd/meson.build b/notifd/meson.build new file mode 100644 index 0000000..8fabdb4 --- /dev/null +++ b/notifd/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-notifd', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/notifd/meson_options.txt b/notifd/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/notifd/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/notifd/src/cli.vala b/notifd/src/cli.vala new file mode 100644 index 0000000..afce774 --- /dev/null +++ b/notifd/src/cli.vala @@ -0,0 +1,115 @@ +static bool help; +static bool version; +static bool daemonize; +static bool list; +static string invoke; +static int close_n; +static int get_n; +static bool toggle_dnd; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "daemonize", 'd', OptionFlags.NONE, OptionArg.NONE, ref daemonize, null, null }, + { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, + { "invoke", 'i', OptionFlags.NONE, OptionArg.STRING, ref invoke, null, null }, + { "close", 'c', OptionFlags.NONE, OptionArg.INT, ref close_n, null, null }, + { "get", 'g', OptionFlags.NONE, OptionArg.INT, ref get_n, null, null }, + { "toggle-dnd", 't', OptionFlags.NONE, OptionArg.NONE, ref toggle_dnd, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (help) { + print("Cli client for astal-notifd\n\n"); + print("Usage:\n"); + print(" %s [flags]\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -l, --list Print every notification and exit\n"); + print(" -d, --daemonize Watch for new notifications\n"); + print(" -i, --invoke Invoke a notification action\n"); + print(" -c, --close Close a notification by its id\n"); + print(" -g, --get Print a notification by its id\n"); + print(" -t, --toggle-dnd Toggle do not disturb\n"); + return 0; + } + + var notifd = new AstalNotifd.Notifd(); + + if (version) { + print(AstalNotifd.VERSION); + return 0; + } + + if (list) { + var state = Environment.get_user_state_dir() + "/astal/notifd/notifications.json"; + if (FileUtils.test(state, FileTest.EXISTS)) { + try { + uint8[] json; + File.new_for_path(state).load_contents(null, out json, null); + + var obj = Json.from_string((string)json); + + var list = obj.get_object().get_member("notifications"); + stdout.printf("%s\n", Json.to_string(list, true)); + return 0; + } catch (Error err) { + stderr.printf("failed to load cache: %s", err.message); + } + } + stdout.printf("[]\n"); + return 0; + } + + if (toggle_dnd) { + notifd.dont_disturb = !notifd.dont_disturb; + return 0; + } + + if (daemonize) { + notifd.notified.connect((id) => { + stdout.printf("%s\n", notifd.get_notification_json(id)); + stdout.flush(); + }); + new MainLoop().run(); + } + + if (invoke != null) { + if (!invoke.contains(":")) { + stderr.printf("invoke format needs to be <notif-id>:<action-id>"); + return 1; + } + + var split = invoke.split(":"); + var n_id = int.parse(split[0]); + var a_id = split[1]; + + notifd.get_notification(n_id).invoke(a_id); + } + + if (close_n > 0) { + notifd.get_notification(close_n).dismiss(); + } + + if (get_n > 0) { + stdout.printf("%s", notifd.get_notification(get_n).to_json_string()); + } + + if (!daemonize && invoke == null && close_n == 0 && get_n == 0) + return 1; + + return 0; +} diff --git a/notifd/src/config.vala.in b/notifd/src/config.vala.in new file mode 100644 index 0000000..752c754 --- /dev/null +++ b/notifd/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalNotifd { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/notifd/src/daemon.vala b/notifd/src/daemon.vala new file mode 100644 index 0000000..b8fb598 --- /dev/null +++ b/notifd/src/daemon.vala @@ -0,0 +1,255 @@ +[DBus (name = "org.freedesktop.Notifications")] +internal class AstalNotifd.Daemon : Object { + public static string name = "notifd"; + public static string vendor = "astal"; + public static string version = "0.1"; + + private string state_file; + private string state_directory; + private string cache_directory; + + private uint n_id = 1; + private HashTable<uint, Notification> notifs = + new HashTable<uint, Notification>((i) => i, (a, b) => a == b); + + private bool _ignore_timeout; + public bool ignore_timeout { + get { return _ignore_timeout; } + set { + _ignore_timeout = value; + write_state(); + } + } + + private bool _dont_disturb; + public bool dont_disturb { + get { return _dont_disturb; } + set { + _dont_disturb = value; + write_state(); + } + } + + public signal void notified(uint id, bool replaced); + public signal void resolved(uint id, ClosedReason reason); + public signal void action_invoked(uint id, string action); + public signal void prop_changed(string prop); + + // emitting an event from proxy doesn't seem to work + public void emit_resolved(uint id, ClosedReason reason) { resolved(id, reason); } + public void emit_action_invoked(uint id, string action) { action_invoked(id, action); } + + construct { + cache_directory = Environment.get_user_cache_dir() + "/astal/notifd"; + state_directory = Environment.get_user_state_dir() + "/astal/notifd"; + state_file = state_directory + "/notifications.json"; + + if (FileUtils.test(state_file, FileTest.EXISTS)) { + try { + uint8[] json; + File.new_for_path(state_file).load_contents(null, out json, null); + + var obj = Json.from_string((string)json); + + var list = obj.get_object().get_array_member("notifications"); + for (var i = 0; i < list.get_length(); ++i) { + add_notification(new Notification.from_json(list.get_object_element(i))); + } + n_id = list.get_length() + 1; + + _dont_disturb = obj.get_object().get_boolean_member("dont_disturb"); + _ignore_timeout = obj.get_object().get_boolean_member("ignore_timeout"); + } catch (Error err) { + warning("failed to load cache: %s", err.message); + } + } + + notify.connect((prop) => prop_changed(prop.name)); + + notified.connect(() => { + notify_property("notifications"); + }); + + resolved.connect((id, reason) => { + notifs.get(id).resolved(reason); + notifs.remove(id); + write_state(); + notify_property("notifications"); + notification_closed(id, reason); + }); + } + + public uint[] notification_ids() throws DBusError, IOError { + var keys = notifs.get_keys(); + uint[] id = new uint[keys.length()]; + for (var i = 0; i < keys.length(); ++i) + id[i] = keys.nth_data(i); + return id; + } + + [DBus (visible = false)] + public List<weak Notification> notifications { + owned get { return notifs.get_values(); } + } + + [DBus (visible = false)] + public Notification get_notification(uint id) { + return notifs.get(id); + } + + public string get_notification_json(uint id) throws DBusError, IOError { + return notifs.get(id).to_json_string(); + } + + [DBus (name = "Notify")] + public uint Notify( + string app_name, + uint replaces_id, + string app_icon, + string summary, + string body, + string[] actions, + HashTable<string, Variant> hints, + int expire_timeout + ) throws DBusError, IOError { + if (hints.get("image-data") != null) { + var file = cache_image(hints.get("image-data"), app_name); + if (file != null) { + hints.set("image-path", new Variant.string(file)); + hints.remove("image-data"); + } + } + + // deprecated hints + hints.remove("image_data"); + hints.remove("icon_data"); + + var id = replaces_id > 0 ? replaces_id : n_id++; + + var replaced = add_notification(new Notification( + app_name, id, app_icon, summary, body, actions, hints, expire_timeout + )); + + if (!ignore_timeout && expire_timeout > 0) { + Timeout.add(expire_timeout, () => { + resolved(id, ClosedReason.EXPIRED); + return Source.REMOVE; + }, Priority.DEFAULT); + } + + notified(id, replaced); + + write_state(); + return id; + } + + private bool add_notification(Notification n) { + n.dismissed.connect(() => resolved(n.id, ClosedReason.DISMISSED_BY_USER)); + n.invoked.connect((action) => action_invoked(n.id, action)); + var replaced = notifs.contains(n.id); + notifs.set(n.id, n); + return replaced; + } + + private void write_state() { + var list = new Json.Builder().begin_array(); + foreach (var n in notifications) { + list.add_value(n.to_json()); + } + list.end_array(); + + var obj = new Json.Builder() + .begin_object() + .set_member_name("notifications").add_value(list.get_root()) + .set_member_name("ignore_timeout").add_boolean_value(ignore_timeout) + .set_member_name("dont_disturb").add_boolean_value(dont_disturb) + .end_object(); + + try { + if (!FileUtils.test(state_directory, FileTest.EXISTS)) + File.new_for_path(state_directory).make_directory_with_parents(null); + + FileUtils.set_contents_full(state_file, Json.to_string(obj.get_root(), false)); + } catch (Error err) { + warning("failed to cache notifications: %s", err.message); + } + } + + public signal void notification_closed(uint id, uint reason); + public signal void activation_token(uint id, string token); + + public void close_notification(uint id) throws DBusError, IOError { + resolved(id, ClosedReason.CLOSED); + } + + public void get_server_information( + out string name, + out string vendor, + out string version, + out string spec_version + ) throws DBusError, IOError { + name = Daemon.name; + vendor = Daemon.vendor; + version = Daemon.version; + spec_version = "1.2"; + } + + public string[] get_capabilities() throws DBusError, IOError { + return {"action-icons", "actions", "body", "icon-static", "persistence", "sound"}; + } + + private string? cache_image(Variant image, string app_name) { + int w = image.get_child_value(0).get_int32(); + int h = image.get_child_value(1).get_int32(); + int rs = image.get_child_value(2).get_int32(); + bool alpha = image.get_child_value(3).get_boolean(); + int bps = image.get_child_value(4).get_int32(); + Bytes data = image.get_child_value(6).get_data_as_bytes(); + + if (bps != 8) { + warning("Can not cache image from %s. %s", app_name, + "Currently only RGB images with 8 bits per sample are supported."); + return null; + } + + var pixbuf = new Gdk.Pixbuf.from_bytes( + data, Gdk.Colorspace.RGB, alpha, bps, w, h, rs); + + if (pixbuf == null) + return null; + + var file_name = cache_directory + "/" + data.hash().to_string("%u.png"); + + try { + if (!FileUtils.test(cache_directory, FileTest.EXISTS)) + File.new_for_path(cache_directory).make_directory_with_parents(null); + + var output_stream = File.new_for_path(file_name) + .replace(null, false, FileCreateFlags.NONE, null); + + pixbuf.save_to_streamv(output_stream, "png", null, null, null); + output_stream.close(null); + } catch (Error err) { + warning("could not cache image %s", err.message); + return null; + } + + return file_name; + } + + internal Daemon register(DBusConnection conn) { + try { + conn.register_object("/org/freedesktop/Notifications", this); + } catch (Error err) { + critical(err.message); + } + return this; + } +} + +public enum AstalNotifd.ClosedReason { + EXPIRED = 1, + DISMISSED_BY_USER = 2, + CLOSED = 3, + UNDEFINED = 4, +} diff --git a/notifd/src/meson.build b/notifd/src/meson.build new file mode 100644 index 0000000..d3efa36 --- /dev/null +++ b/notifd/src/meson.build @@ -0,0 +1,79 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalNotifd-' + api_version + '.gir' +typelib = 'AstalNotifd-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('json-glib-1.0'), + dependency('gdk-pixbuf-2.0'), +] + +sources = [ + config, + 'daemon.vala', + 'notifd.vala', + 'notification.vala', + 'proxy.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/notifd/src/notifd.vala b/notifd/src/notifd.vala new file mode 100644 index 0000000..c962862 --- /dev/null +++ b/notifd/src/notifd.vala @@ -0,0 +1,140 @@ +namespace AstalNotifd { + public Notifd get_default() { + return Notifd.get_default(); + } +} + +public class AstalNotifd.Notifd : Object { + private static Notifd _instance; + public static Notifd get_default() { + if (_instance == null) + _instance = new Notifd(); + + return _instance; + } + + private Daemon daemon; + private DaemonProxy proxy; + + public signal void active(ActiveType type); + + public bool ignore_timeout { + get { + return proxy != null ? proxy.ignore_timeout : daemon.ignore_timeout; + } + set { + if (proxy != null) + proxy.ignore_timeout = value; + else + daemon.ignore_timeout = value; + } + } + + public bool dont_disturb { + get { + return proxy != null ? proxy.dont_disturb : daemon.dont_disturb; + } + set { + if (proxy != null) + proxy.dont_disturb = value; + else + daemon.dont_disturb = value; + } + } + + public List<weak Notification> notifications { + owned get { return proxy != null ? proxy.notifications : daemon.notifications; } + } + + public uint[] notification_ids() throws Error { + return proxy != null ? proxy.notification_ids() : daemon.notification_ids(); + } + + public Notification get_notification(uint id) { + return proxy != null ? proxy.get_notification(id) : daemon.get_notification(id); + } + + public string get_notification_json(uint id) { + return get_notification(id).to_json_string(); + } + + public signal void notified(uint id, bool replaced); + public signal void resolved(uint id, ClosedReason reason); + + construct { + // hack to make it synchronous + MainLoop? loop = null; + + if (!MainContext.default().is_owner()) { + loop = new MainLoop(); + } + + bool done = false; + + Bus.own_name( + BusType.SESSION, + "org.freedesktop.Notifications", + BusNameOwnerFlags.NONE, + acquire_daemon, + on_daemon_acquired, + make_proxy + ); + + active.connect(() => { + done = true; + if (loop != null && loop.is_running()) { + loop.quit(); + } + }); + + if (loop != null) { + loop.run(); + } else { + while (!done) { + MainContext.default().iteration(false); + } + } + } + + private void acquire_daemon(DBusConnection conn) { + daemon = new Daemon().register(conn); + } + + private void on_daemon_acquired() { + if (proxy != null) { + proxy.stop(); + proxy = null; + } + daemon.notified.connect((id, replaced) => notified(id, replaced)); + daemon.resolved.connect((id, reason) => resolved(id, reason)); + daemon.notify.connect((prop) => { + if (get_class().find_property(prop.name) != null) { + notify_property(prop.name); + } + }); + active(ActiveType.DAEMON); + } + + private void make_proxy() { + proxy = new DaemonProxy(); + + if (proxy.start()) { + active(ActiveType.PROXY); + } else { + return; + } + + proxy.notified.connect((id, replaced) => notified(id, replaced)); + proxy.resolved.connect((id, reason) => resolved(id, reason)); + proxy.notify.connect((prop) => { + if (get_class().find_property(prop.name) != null) { + notify_property(prop.name); + } + }); + } +} + +public enum AstalNotifd.ActiveType { + DAEMON, + PROXY, +} diff --git a/notifd/src/notification.vala b/notifd/src/notification.vala new file mode 100644 index 0000000..0b4af06 --- /dev/null +++ b/notifd/src/notification.vala @@ -0,0 +1,160 @@ +public enum AstalNotifd.Urgency { + LOW = 0, + NORMAL = 1, + CRITICAL = 2, +} + +public struct AstalNotifd.Action { + public string id; + public string label; +} + +public class AstalNotifd.Notification : Object { + private List<Action?> _actions; + private HashTable<string, Variant> hints; + + public int64 time { construct set; get; } + public string app_name { construct set; get; } + public string app_icon { construct set; get; } + public string summary { construct set; get; } + public string body { construct set; get; } + public uint id { construct set; get; } + public int expire_timeout { construct set; get; } + public List<Action?> actions { get { return _actions; } } + + public string image { get { return get_str_hint("image-path"); } } + public bool action_icons { get { return get_bool_hint("action-icons"); } } + public string category { get { return get_str_hint("category"); } } + public string desktop_entry { get { return get_str_hint("desktop-entry"); } } + public bool resident { get { return get_bool_hint("resident"); } } + public string sound_file { get { return get_str_hint("sound-file"); } } + public string sound_name { get { return get_str_hint("sound-name"); } } + public bool suppress_sound { get { return get_bool_hint("suppress-sound"); } } + public bool transient { get { return get_bool_hint("transient"); } } + public int x { get { return get_int_hint("x"); } } + public int y { get { return get_int_hint("y"); } } + public Urgency urgency { get { return get_int_hint("urgency"); } } + + internal Notification( + string app_name, + uint id, + string app_icon, + string summary, + string body, + string[] actions, + HashTable<string, Variant> hints, + int expire_timeout + ) { + Object( + app_name: app_name, + id: id, + app_icon: app_icon, + summary: summary, + body: body, + expire_timeout: expire_timeout, + time: new DateTime.now_local().to_unix() + ); + + this.hints = hints; + _actions = new List<Action?>(); + for (var i = 0; i < actions.length; i += 2) { + _actions.append(Action() { + id = actions[i], + label = actions[i + 1] + }); + } + } + + public Variant? get_hint(string hint) { + return hints.contains(hint) ? hints.get(hint) : null; + } + + public unowned string get_str_hint(string hint) { + return hints.contains(hint) ? hints.get(hint).get_string() : null; + } + + public bool get_bool_hint(string hint) { + return hints.contains(hint) ? hints.get(hint).get_boolean() : false; + } + + public int get_int_hint(string hint) { + return hints.contains(hint) ? hints.get(hint).get_int32() : 0; + } + + public signal void resolved(ClosedReason reason); + public signal void dismissed(); + public signal void invoked(string action); + + public void dismiss() { dismissed(); } + public void invoke(string action) { invoked(action); } + + internal Notification.from_json(Json.Object root) throws GLib.Error { + foreach (var key in root.get_members()) { + var node = root.get_member(key); + switch (key) { + case "id": id = (uint)node.get_int(); break; + case "time": time = node.get_int(); break; + case "expire_timeout": expire_timeout = (int)node.get_int(); break; + case "app_name": app_name = node.get_string(); break; + case "app_icon": app_icon = node.get_string(); break; + case "summary": summary = node.get_string(); break; + case "body": body = node.get_string(); break; + case "hints": + hints = new HashTable<string, Variant>(str_hash, str_equal); + var obj = node.get_object(); + foreach (var hint in obj.get_members()) { + hints.set(hint, Json.gvariant_deserialize(obj.get_member(hint), null)); + } + break; + case "actions": + _actions = new List<Action?>(); + for (var i = 0; i < node.get_array().get_length(); ++i) { + var o = node.get_array().get_object_element(i); + _actions.append(Action() { + id = o.get_member("id").get_string(), + label = o.get_member("label").get_string() + }); + } + break; + default: break; + } + } + } + + internal static Notification from_json_string(string json) throws GLib.Error { + var parser = new Json.Parser(); + parser.load_from_data(json); + return new Notification.from_json(parser.get_root().get_object()); + } + + public string to_json_string() { + var generator = new Json.Generator(); + generator.set_root(to_json()); + return generator.to_data(null); + } + + internal Json.Node to_json() { + var acts = new Json.Builder().begin_array(); + foreach (var action in actions) { + acts.begin_object() + .set_member_name("id").add_string_value(action.id) + .set_member_name("label").add_string_value(action.label) + .end_object(); + } + acts.end_array(); + + return new Json.Builder() + .begin_object() + .set_member_name("id").add_int_value(id) + .set_member_name("time").add_int_value(time) + .set_member_name("expire_timeout").add_int_value(expire_timeout) + .set_member_name("app_name").add_string_value(app_name) + .set_member_name("app_icon").add_string_value(app_icon) + .set_member_name("summary").add_string_value(summary) + .set_member_name("body").add_string_value(body) + .set_member_name("actions").add_value(acts.get_root()) + .set_member_name("hints").add_value(Json.gvariant_serialize(hints)) + .end_object() + .get_root(); + } +} diff --git a/notifd/src/proxy.vala b/notifd/src/proxy.vala new file mode 100644 index 0000000..bedb8b9 --- /dev/null +++ b/notifd/src/proxy.vala @@ -0,0 +1,129 @@ +[DBus (name = "org.freedesktop.Notifications")] +internal interface AstalNotifd.IDaemon : DBusProxy { + public abstract bool ignore_timeout { get; set; } + public abstract bool dont_disturb { get; set; } + + public abstract uint[] notification_ids() throws DBusError, IOError; + public abstract string get_notification_json(uint id) throws DBusError, IOError; + + public signal void notified(uint id, bool replaced); + public signal void resolved(uint id, ClosedReason reason); + public signal void prop_changed(string prop); + + public abstract void emit_resolved(uint id, ClosedReason reason); + public abstract void emit_action_invoked(uint id, string action); +} + +internal class AstalNotifd.DaemonProxy : Object { + private HashTable<uint, Notification> notifs = + new HashTable<uint, Notification>((i) => i, (a, b) => a == b); + + public List<weak Notification> notifications { + owned get { return notifs.get_values(); } + } + + public bool ignore_timeout { + get { return proxy.ignore_timeout; } + set { proxy.ignore_timeout = value; } + } + + public bool dont_disturb { + get { return proxy.dont_disturb; } + set { proxy.dont_disturb = value; } + } + + public uint[] notification_ids() throws DBusError, IOError { + return proxy.notification_ids(); + } + + public Notification get_notification(uint id) { + return notifs.get(id); + } + + public signal void notified(uint id, bool replaced); + public signal void resolved(uint id, ClosedReason reason); + + private IDaemon proxy; + private List<ulong> ids = new List<ulong>(); + + public void stop() { + if (ids.length() > 0) { + foreach (var id in ids) + SignalHandler.disconnect(proxy, id); + } + } + + public bool start() { + try { + var bus = Bus.get_sync(BusType.SESSION, null); + var variant = bus.call_sync( + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "GetServerInformation", + null, + null, + DBusCallFlags.NONE, + -1, + null); + + var name = variant.get_child_value(0).get_string(); + var vendor = variant.get_child_value(1).get_string(); + var version = variant.get_child_value(2).get_string(); + + var running = name == Daemon.name + && vendor == Daemon.vendor + && version == Daemon.version; + + if (running) { + setup_proxy(); + return true; + } else { + critical("cannot get proxy: %s is already running", name); + } + } catch (Error err) { + critical("cannot get proxy: %s", err.message); + } + return false; + } + + private void setup_proxy() throws Error { + proxy = Bus.get_proxy_sync( + BusType.SESSION, + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications" + ); + + foreach (var id in proxy.notification_ids()) + add_notification(id); + + ids.append(proxy.prop_changed.connect((prop) => { + if (prop == "ignore-timeout" || prop == "dont-disturb") + notify_property(prop); + })); + + ids.append(proxy.notified.connect((id, replaced) => { + add_notification(id); + notified(id, replaced); + notify_property("notifications"); + })); + + ids.append(proxy.resolved.connect((id, reason) => { + notifs.remove(id); + resolved(id, reason); + notify_property("notifications"); + })); + } + + private void add_notification(uint id) { + try { + var n = Notification.from_json_string(proxy.get_notification_json(id)); + proxy.resolved.connect((id, reason) => n.resolved(reason)); + n.dismissed.connect(() => proxy.emit_resolved(id, ClosedReason.DISMISSED_BY_USER)); + n.invoked.connect((action) => proxy.emit_action_invoked(id, action)); + notifs.set(id, n); + } catch (Error err) { + critical(err.message); + } + } +} diff --git a/notifd/src/signals.md b/notifd/src/signals.md new file mode 100644 index 0000000..cdc6688 --- /dev/null +++ b/notifd/src/signals.md @@ -0,0 +1,35 @@ +# Signals + +ignore this, I'm just dumb and can't follow where signals go or get emitted from + +## Notification + +* resolved(reason) - by daemon/proxy +* dismissed() - by user with `.dismiss()` +* invoked(action) - by user with `.invoke()` + +## Deamon + +non-spec, used by user + +* notified(id, replaced) - by outside through dbus with `.Notify()` +* resolved(id, reason) - by `Notification.dismiss()` or outside with `.CloseNotification` + +spec, not used by user + +* notification_closed(id, reason) - sideeffect of `resolved` +* action_invoked(id, action) - by `Notification.invoke()` + +## Proxy + +mirrors Daemon + +* notified(id, replaced) +* resolved(id, reason) + +creates `Notification` objects through daemon's json strings +and hooks them up to call daemon's signals and vice versa + +## Notifd + +acts as a bridge between Proxy/Daemon, everything else is internal only diff --git a/notifd/version b/notifd/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/notifd/version @@ -0,0 +1 @@ +0.1.0 diff --git a/powerprofiles/.gitignore b/powerprofiles/.gitignore new file mode 100644 index 0000000..f047207 --- /dev/null +++ b/powerprofiles/.gitignore @@ -0,0 +1,5 @@ +build/ +result +.cache/ +test.sh +tmp/ diff --git a/powerprofiles/LICENSE b/powerprofiles/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/powerprofiles/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/powerprofiles/README.md b/powerprofiles/README.md new file mode 100644 index 0000000..3e0a1ca --- /dev/null +++ b/powerprofiles/README.md @@ -0,0 +1,7 @@ +# powerprofiles + +Library and cli to control upowerd powerprofiles + +## TODO + +- docs diff --git a/powerprofiles/flake.lock b/powerprofiles/flake.lock new file mode 100644 index 0000000..13f566b --- /dev/null +++ b/powerprofiles/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1716137900, + "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/powerprofiles/flake.nix b/powerprofiles/flake.nix new file mode 100644 index 0000000..9c2c68b --- /dev/null +++ b/powerprofiles/flake.nix @@ -0,0 +1,53 @@ +{ + description = "Library and cli to control upowerd powerprofiles"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + json-glib + ]; + in { + packages.${system} = rec { + default = power-profiles; + power-profiles = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-power-profiles"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + power-profiles = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/powerprofiles/meson.build b/powerprofiles/meson.build new file mode 100644 index 0000000..0e0ad2d --- /dev/null +++ b/powerprofiles/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-power-profiles', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/powerprofiles/meson_options.txt b/powerprofiles/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/powerprofiles/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/powerprofiles/src/cli.vala b/powerprofiles/src/cli.vala new file mode 100644 index 0000000..7be01d2 --- /dev/null +++ b/powerprofiles/src/cli.vala @@ -0,0 +1,80 @@ +static bool help; +static bool version; +static bool daemonize; +static bool list; +static string set; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "daemonize", 'd', OptionFlags.NONE, OptionArg.NONE, ref daemonize, null, null }, + { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, + { "set", 's', OptionFlags.NONE, OptionArg.STRING, ref set, null, null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (help) { + print("Usage:\n"); + print(" %s [flags]\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -d, --daemonize Monitor for changes\n"); + print(" -l, --list List available profiles\n"); + return 0; + } + + if (version) { + print(AstalPowerProfiles.VERSION); + return 0; + } + + var profiles = AstalPowerProfiles.get_default(); + if (set != null) { + profiles.active_profile = set; + } + + else if (list) { + foreach (var p in profiles.profiles) { + print("%s\n", p.profile); + } + return 0; + } + + if (daemonize) { + var loop = new MainLoop(); + + stdout.printf("%s\n", profiles.to_json_string()); + stdout.flush(); + + profiles.notify.connect(() => { + stdout.printf("%s\n", profiles.to_json_string()); + stdout.flush(); + }); + + profiles.profile_released.connect(() => { + stdout.printf("%s\n", profiles.to_json_string()); + stdout.flush(); + }); + + loop.run(); + } + + if (set == null && !daemonize) { + stdout.printf("%s\n", profiles.to_json_string()); + } + + return 0; +} diff --git a/powerprofiles/src/config.vala.in b/powerprofiles/src/config.vala.in new file mode 100644 index 0000000..79034f1 --- /dev/null +++ b/powerprofiles/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalPowerProfiles { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/powerprofiles/src/meson.build b/powerprofiles/src/meson.build new file mode 100644 index 0000000..fdb8580 --- /dev/null +++ b/powerprofiles/src/meson.build @@ -0,0 +1,75 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalPowerProfiles-' + api_version + '.gir' +typelib = 'AstalPowerProfiles-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('json-glib-1.0'), +] + +sources = [ + config, + 'power-profiles.vala', +] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + version: meson.project_version(), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/powerprofiles/src/power-profiles.vala b/powerprofiles/src/power-profiles.vala new file mode 100644 index 0000000..ab98505 --- /dev/null +++ b/powerprofiles/src/power-profiles.vala @@ -0,0 +1,205 @@ +namespace AstalPowerProfiles { +[DBus (name = "org.freedesktop.UPower.PowerProfiles")] +private interface IPowerProfiles : DBusProxy { + public abstract string[] actions { owned get; } + public abstract string active_profile { owned get; set; } + public abstract HashTable<string, Variant>[] active_profile_holds { owned get; } + public abstract string performance_degraded { owned get; } + public abstract string performance_inhibited { owned get; } + public abstract HashTable<string, Variant>[] profiles { owned get; } + public abstract string version { owned get; } + + public signal void profile_released (uint cookie); + + public abstract uint hold_profile(string profile, string reason, string application_id) throws Error; + public abstract void release_profile(uint cookie) throws Error; +} + +public PowerProfiles get_default() { + return PowerProfiles.get_default(); +} + +public class PowerProfiles : Object { + private static PowerProfiles instance; + public static PowerProfiles get_default() { + if (instance == null) + instance = new PowerProfiles(); + + return instance; + } + + private IPowerProfiles proxy; + + construct { + try { + proxy = Bus.get_proxy_sync( + GLib.BusType.SYSTEM, + "org.freedesktop.UPower.PowerProfiles", + "/org/freedesktop/UPower/PowerProfiles" + ); + + proxy.profile_released.connect((cookie) => profile_released(cookie)); + proxy.g_properties_changed.connect((props) => { + var map = (HashTable<string, Variant>)props; + foreach (var key in map.get_keys()) { + notify_property(kebab_case(key)); + if (key == "ActiveProfile") + notify_property("icon-name"); + } + }); + } catch (Error error){ + critical(error.message); + } + } + + public string active_profile { + owned get { return proxy.active_profile; } + set { proxy.active_profile = value; } + } + + public string icon_name { + owned get { return @"power-profile-$active_profile-symbolic"; } + } + + public string[] actions { + owned get { return proxy.actions.copy(); } + } + + public Hold[] active_profile_holds { + owned get { + Hold[] holds = new Hold[proxy.active_profile_holds.length]; + for (var i = 0; i < proxy.active_profile_holds.length; ++i) { + var hold = proxy.active_profile_holds[i]; + holds[i] = Hold() { + application_id = hold.get("ApplicationId").get_string(), + profile = hold.get("Profile").get_string(), + reason = hold.get("Reason").get_string() + }; + } + return holds; + } + } + + public string performance_degraded { + owned get { return proxy.performance_degraded; } + } + + public string performance_inhibited { + owned get { return proxy.performance_degraded; } + } + + public Profile[] profiles { + owned get { + Profile[] profs = new Profile[proxy.profiles.length]; + for (var i = 0; i < proxy.profiles.length; ++i) { + var prof = proxy.profiles[i]; + profs[i] = Profile() { + profile = prof.get("Profile").get_string(), + cpu_driver = prof.get("CpuDriver").get_string(), + platform_driver = prof.get("PlatformDriver").get_string(), + driver = prof.get("Driver").get_string() + }; + } + return profs; + } + } + + public string version { + owned get { return proxy.version; } + } + + public signal void profile_released (uint cookie); + + public int hold_profile(string profile, string reason, string application_id) { + try { + return (int)proxy.hold_profile(profile, reason, application_id); + } catch (Error error) { + critical(error.message); + return -1; + } + } + + public void release_profile(uint cookie) { + try { + proxy.release_profile(cookie); + } catch (Error error) { + critical(error.message); + } + } + + public string to_json_string() { + var acts = new Json.Builder().begin_array(); + foreach (var action in actions) { + acts.add_string_value(action); + } + + var active_holds = new Json.Builder().begin_array(); + foreach (var action in active_profile_holds) { + active_holds.add_value(new Json.Builder() + .begin_object() + .set_member_name("application_id").add_string_value(action.application_id) + .set_member_name("profile").add_string_value(action.profile) + .set_member_name("reason").add_string_value(action.reason) + .end_object() + .get_root()); + } + + var profs = new Json.Builder().begin_array(); + foreach (var prof in profiles) { + profs.add_value(new Json.Builder() + .begin_object() + .set_member_name("profie").add_string_value(prof.profile) + .set_member_name("driver").add_string_value(prof.driver) + .set_member_name("cpu_driver").add_string_value(prof.cpu_driver) + .set_member_name("platform_driver").add_string_value(prof.platform_driver) + .end_object() + .get_root()); + } + + return Json.to_string(new Json.Builder() + .begin_object() + .set_member_name("active_profile").add_string_value(active_profile) + .set_member_name("icon_name").add_string_value(icon_name) + .set_member_name("performance_degraded").add_string_value(performance_degraded) + .set_member_name("performance_inhibited").add_string_value(performance_inhibited) + .set_member_name("actions").add_value(acts.end_array().get_root()) + .set_member_name("active_profile_holds").add_value(active_holds.end_array().get_root()) + .set_member_name("profiles").add_value(profs.end_array().get_root()) + .end_object() + .get_root(), false); + } +} + +public struct Profile { + public string profile; + public string cpu_driver; + public string platform_driver; + public string driver; +} + +public struct Hold { + public string application_id; + public string profile; + public string reason; +} + +private string kebab_case(string pascal_case) { + StringBuilder kebab_case = new StringBuilder(); + + for (int i = 0; i < pascal_case.length; i++) { + char c = pascal_case[i]; + + if (c >= 'A' && c <= 'Z') { + if (i != 0) { + kebab_case.append_c('-'); + } + + kebab_case.append_c((char)(c + 32)); + } else { + kebab_case.append_c(c); + } + } + + return kebab_case.str; +} +} diff --git a/powerprofiles/version b/powerprofiles/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/powerprofiles/version @@ -0,0 +1 @@ +0.1.0 diff --git a/river/.gitignore b/river/.gitignore new file mode 100644 index 0000000..6bf41b5 --- /dev/null +++ b/river/.gitignore @@ -0,0 +1,3 @@ +build/ +result/ +.cache/ diff --git a/river/LICENSE b/river/LICENSE new file mode 100644 index 0000000..4ef65b3 --- /dev/null +++ b/river/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/river/README.md b/river/README.md new file mode 100644 index 0000000..3285628 --- /dev/null +++ b/river/README.md @@ -0,0 +1,30 @@ +# astal-river + +a library and cli tool for getting status information of the river wayland compositor. + +## Build from source +### Dependencies + +- meson +- glib +- gobject-introspection +- json-glib +- libwayland + +```sh +# Clone the repository +git clone https://github.com/astal-sh/river +cd river + +# Setup and build +meson setup build +meson compile -C build + +# Install +meson install -C build +``` + +## Todo +- docs + + diff --git a/river/flake.lock b/river/flake.lock new file mode 100644 index 0000000..ed5e17d --- /dev/null +++ b/river/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1720031269, + "narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9f4128e00b0ae8ec65918efeba59db998750ead6", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/river/flake.nix b/river/flake.nix new file mode 100644 index 0000000..766263f --- /dev/null +++ b/river/flake.nix @@ -0,0 +1,54 @@ +{ + description = "Library and cli tool for querying river"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + wayland + ]; + + buildInputs = with pkgs; [ + glib + json-glib + ]; + in { + packages.${system} = rec { + default = river; + river = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-river"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + river = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + pkgs.gjs + self.packages.${system}.default + ]; + }; + }; + }; +} diff --git a/river/include/astal-river.h b/river/include/astal-river.h new file mode 100644 index 0000000..6bedd94 --- /dev/null +++ b/river/include/astal-river.h @@ -0,0 +1,59 @@ +#ifndef ASTAL_RIVER_H +#define ASTAL_RIVER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_RIVER_TYPE_OUTPUT (astal_river_output_get_type()) + +G_DECLARE_FINAL_TYPE(AstalRiverOutput, astal_river_output, ASTAL_RIVER, OUTPUT, GObject) + +guint astal_river_output_get_id(AstalRiverOutput *self); + +gchar *astal_river_output_get_name(AstalRiverOutput *self); + +gchar *astal_river_output_get_layout_name(AstalRiverOutput *self); + +gchar *astal_river_output_get_focused_view(AstalRiverOutput *self); + +guint astal_river_output_get_focused_tags(AstalRiverOutput *self); + +guint astal_river_output_get_urgent_tags(AstalRiverOutput *self); + +guint astal_river_output_get_occupied_tags(AstalRiverOutput *self); + +#define ASTAL_RIVER_TYPE_RIVER (astal_river_river_get_type()) + +G_DECLARE_FINAL_TYPE(AstalRiverRiver, astal_river_river, ASTAL_RIVER, RIVER, GObject) + +AstalRiverRiver *astal_river_river_new(); + +AstalRiverRiver *astal_river_river_get_default(); +AstalRiverRiver *astal_river_get_default(); + +GList *astal_river_river_get_outputs(AstalRiverRiver *self); + +AstalRiverOutput *astal_river_river_get_output(AstalRiverRiver *self, gchar *name); + +gchar *astal_river_river_get_focused_view(AstalRiverRiver *self); + +gchar *astal_river_river_get_focused_output(AstalRiverRiver *self); + +gchar *astal_river_river_get_mode(AstalRiverRiver *self); + +/** + * AstalRiverCommandCallback: + * @success: a #gboolean indicating whether the command was executed successfully + * @msg: a string containing the result of the command + * + * A callback function that is called after a river command is run. + */ +typedef void (*AstalRiverCommandCallback)(gboolean success, const gchar *msg); + +void astal_river_river_run_command_async(AstalRiverRiver *self, gint length, const gchar **cmd, + AstalRiverCommandCallback callback); + +G_END_DECLS + +#endif // !ASTAL_RIVER_H diff --git a/river/include/meson.build b/river/include/meson.build new file mode 100644 index 0000000..28a51f6 --- /dev/null +++ b/river/include/meson.build @@ -0,0 +1,6 @@ +astal_river_inc = include_directories('.') +astal_river_headers = files( + 'astal-river.h', +) + +install_headers('astal-river.h') diff --git a/river/include/river-private.h b/river/include/river-private.h new file mode 100644 index 0000000..14cd1c5 --- /dev/null +++ b/river/include/river-private.h @@ -0,0 +1,20 @@ +#ifndef ASTAL_RIVER_OUTPUT_PRIVATE_H +#define ASTAL_RIVER_OUTPUT_PRIVATE_H + +#include <wayland-client.h> + +#include "astal-river.h" +#include "river-status-unstable-v1-client.h" + +G_BEGIN_DECLS + +AstalRiverOutput *astal_river_output_new(guint id, struct wl_output *wl_output, + struct zriver_status_manager_v1 *status_manager, + struct wl_display *wl_display); + +struct wl_output *astal_river_output_get_wl_output(AstalRiverOutput *self); +void astal_river_output_set_focused_view(AstalRiverOutput *self, const gchar *focused_view); + +G_END_DECLS + +#endif // !ASTAL_RIVER_OUTPUT_PRIVATE_H diff --git a/river/include/wayland-source.h b/river/include/wayland-source.h new file mode 100644 index 0000000..b219589 --- /dev/null +++ b/river/include/wayland-source.h @@ -0,0 +1,16 @@ +#ifndef __WAYLAND_SOURCE_H__ +#define __WAYLAND_SOURCE_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _WLSource WLSource; + +WLSource* wl_source_new(); +void wl_source_free(WLSource* self); +struct wl_display* wl_source_get_display(WLSource* source); + +G_END_DECLS + +#endif /* __WAYLAND_SOURCE_H__ */ diff --git a/river/meson.build b/river/meson.build new file mode 100644 index 0000000..0cee54d --- /dev/null +++ b/river/meson.build @@ -0,0 +1,23 @@ +project('astal_river', + 'c', + version : '0.1.0', + default_options : [ + 'c_std=gnu11', + 'warning_level=3', + 'prefix=/usr' + ] +) + +add_project_arguments( + ['-Wno-pedantic', '-Wno-unused-parameter'], + language : 'c') + +version_split = meson.project_version().split('.') +lib_so_version = version_split[0] + '.' + version_split[1] + +pkg_config = import('pkgconfig') +gnome = import('gnome') + +subdir('protocols') +subdir('include') +subdir('src') diff --git a/river/meson_options.txt b/river/meson_options.txt new file mode 100644 index 0000000..d585854 --- /dev/null +++ b/river/meson_options.txt @@ -0,0 +1,2 @@ +option('introspection', type : 'boolean', value : true, description : 'Build gobject-introspection data') +option('vapi', type : 'boolean', value : true, description : 'Generate vapi data (needs vapigen & introspection option)') diff --git a/river/protocols/meson.build b/river/protocols/meson.build new file mode 100644 index 0000000..69f01bb --- /dev/null +++ b/river/protocols/meson.build @@ -0,0 +1,23 @@ + +wayland_scanner = find_program('wayland-scanner') + +protocols = [ + 'river-status-unstable-v1.xml', + 'river-control-unstable-v1.xml' +] + +gen_client_header = generator(wayland_scanner, + output: ['@[email protected]'], + arguments: ['-c', 'client-header', '@INPUT@', '@BUILD_DIR@/@[email protected]']) + +gen_private_code = generator(wayland_scanner, + output: ['@[email protected]'], + arguments: ['-c', 'private-code', '@INPUT@', '@BUILD_DIR@/@[email protected]']) + +client_protocol_srcs = [] + +foreach protocol : protocols + client_header = gen_client_header.process(protocol) + code = gen_private_code.process(protocol) + client_protocol_srcs += [client_header, code] +endforeach diff --git a/river/protocols/river-control-unstable-v1.xml b/river/protocols/river-control-unstable-v1.xml new file mode 100644 index 0000000..c431901 --- /dev/null +++ b/river/protocols/river-control-unstable-v1.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="river_control_unstable_v1"> + <copyright> + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + </copyright> + + <interface name="zriver_control_v1" version="1"> + <description summary="run compositor commands"> + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and + executed with a run_command request. The first argument is the command + to be run. + + A complete list of commands should be made available in the man page of + the compositor. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_control object"> + This request indicates that the client will not use the + river_control object any more. Objects that have been created + through this instance are not affected. + </description> + </request> + + <request name="add_argument"> + <description summary="add an argument to the current command"> + Arguments are stored by the server in the order they were sent until + the run_command request is made. + </description> + <arg name="argument" type="string" summary="the argument to add"/> + </request> + + <request name="run_command"> + <description summary="run the current command"> + Execute the command built up using the add_argument request for the + given seat. + </description> + <arg name="seat" type="object" interface="wl_seat"/> + <arg name="callback" type="new_id" interface="zriver_command_callback_v1" + summary="callback object"/> + </request> + </interface> + + <interface name="zriver_command_callback_v1" version="1"> + <description summary="callback object"> + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed + by the compositor after one of the events is sent. + </description> + + <event name="success" type="destructor"> + <description summary="command successful"> + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + </description> + <arg name="output" type="string" summary="the output of the command"/> + </event> + + <event name="failure" type="destructor"> + <description summary="command failed"> + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + </description> + <arg name="failure_message" type="string" + summary="a message explaining why failure occurred"/> + </event> + </interface> +</protocol> + diff --git a/river/protocols/river-status-unstable-v1.xml b/river/protocols/river-status-unstable-v1.xml new file mode 100644 index 0000000..09c52c1 --- /dev/null +++ b/river/protocols/river-status-unstable-v1.xml @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="river_status_unstable_v1"> + <copyright> + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + </copyright> + + <interface name="zriver_status_manager_v1" version="4"> + <description summary="manage river status objects"> + A global factory for objects that receive status information specific + to river. It could be used to implement, for example, a status bar. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_status_manager object"> + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + </description> + </request> + + <request name="get_river_output_status"> + <description summary="create an output status object"> + This creates a new river_output_status object for the given wl_output. + </description> + <arg name="id" type="new_id" interface="zriver_output_status_v1"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + + <request name="get_river_seat_status"> + <description summary="create a seat status object"> + This creates a new river_seat_status object for the given wl_seat. + </description> + <arg name="id" type="new_id" interface="zriver_seat_status_v1"/> + <arg name="seat" type="object" interface="wl_seat"/> + </request> + </interface> + + <interface name="zriver_output_status_v1" version="4"> + <description summary="track output tags and focus"> + This interface allows clients to receive information about the current + windowing state of an output. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_output_status object"> + This request indicates that the client will not use the + river_output_status object any more. + </description> + </request> + + <event name="focused_tags"> + <description summary="focused tags of the output"> + Sent once binding the interface and again whenever the tag focus of + the output changes. + </description> + <arg name="tags" type="uint" summary="32-bit bitfield"/> + </event> + + <event name="view_tags"> + <description summary="tag state of an output's views"> + Sent once on binding the interface and again whenever the tag state + of the output changes. + </description> + <arg name="tags" type="array" summary="array of 32-bit bitfields"/> + </event> + + <event name="urgent_tags" since="2"> + <description summary="tags of the output with an urgent view"> + Sent once on binding the interface and again whenever the set of + tags with at least one urgent view changes. + </description> + <arg name="tags" type="uint" summary="32-bit bitfield"/> + </event> + + <event name="layout_name" since="4"> + <description summary="name of the layout"> + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + </description> + <arg name="name" type="string" summary="layout name"/> + </event> + + <event name="layout_name_clear" since="4"> + <description summary="name of the layout"> + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + </description> + </event> + </interface> + + <interface name="zriver_seat_status_v1" version="3"> + <description summary="track seat focus"> + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_seat_status object"> + This request indicates that the client will not use the + river_seat_status object any more. + </description> + </request> + + <event name="focused_output"> + <description summary="the seat focused an output"> + Sent on binding the interface and again whenever an output gains focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="unfocused_output"> + <description summary="the seat unfocused an output"> + Sent whenever an output loses focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="focused_view"> + <description summary="information on the focused view"> + Sent once on binding the interface and again whenever the focused + view or a property thereof changes. The title may be an empty string + if no view is focused or the focused view did not set a title. + </description> + <arg name="title" type="string" summary="title of the focused view"/> + </event> + + <event name="mode" since="3"> + <description summary="the active mode changed"> + Sent once on binding the interface and again whenever a new mode + is entered (e.g. with riverctl enter-mode foobar). + </description> + <arg name="name" type="string" summary="name of the mode"/> + </event> + </interface> +</protocol> + diff --git a/river/src/astal-river.c b/river/src/astal-river.c new file mode 100644 index 0000000..37f34d5 --- /dev/null +++ b/river/src/astal-river.c @@ -0,0 +1,51 @@ +#include <getopt.h> +#include <json-glib/json-glib.h> +#include <stdlib.h> + +#include "gio/gio.h" +#include "astal-river.h" + +GMainLoop* loop; + +void print_json(AstalRiverRiver* river) { + JsonNode* json = json_gobject_serialize(G_OBJECT(river)); + + gchar* json_str = json_to_string(json, FALSE); + g_print("%s\n", json_str); + g_free(json); +} + +int main(int argc, char** argv) { + gboolean daemon = FALSE; + + int opt; + const char* optstring = "d"; + + static struct option long_options[] = {{"daemon", no_argument, NULL, 'd'}, {NULL, 0, NULL, 0}}; + + while ((opt = getopt_long(argc, argv, optstring, long_options, NULL)) != -1) { + switch (opt) { + case 'd': + daemon = TRUE; + break; + default: + g_print("Usage: %s [-d]\n", argv[0]); + exit(EXIT_FAILURE); + } + } + + GError* error = NULL; + AstalRiverRiver* river = g_initable_new(ASTAL_RIVER_TYPE_RIVER, NULL, &error, NULL); + if (error) { + g_critical("%s\n", error->message); + exit(EXIT_FAILURE); + } + if (daemon) { + loop = g_main_loop_new(NULL, FALSE); + g_signal_connect(river, "changed", G_CALLBACK(print_json), NULL); + g_main_loop_run(loop); + } else { + print_json(river); + g_object_unref(river); + } +} diff --git a/river/src/meson.build b/river/src/meson.build new file mode 100644 index 0000000..647cc87 --- /dev/null +++ b/river/src/meson.build @@ -0,0 +1,70 @@ +srcs = files( + 'river.c', + 'river-output.c', + 'wayland-source.c', +) + +deps = [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('wayland-client'), + dependency('json-glib-1.0'), +] + +astal_river_lib = library( + 'astal-river', + sources : srcs + client_protocol_srcs, + include_directories : astal_river_inc, + dependencies : deps, + version : meson.project_version(), + install : true +) + +libastal_river = declare_dependency( + link_with : astal_river_lib, + include_directories : astal_river_inc) + +astal_river_executable = executable( + 'astal-river', + files('astal-river.c'), + dependencies : [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('json-glib-1.0'), + libastal_river + ], + install : true) + +pkg_config_name = 'astal-river-' + lib_so_version + +if get_option('introspection') + gir = gnome.generate_gir( + astal_river_lib, + sources : srcs + astal_river_headers, + nsversion : '0.1', + namespace : 'AstalRiver', + symbol_prefix : 'astal_river', + identifier_prefix : 'AstalRiver', + includes : ['GObject-2.0', 'Gio-2.0'], + header : 'astal-river.h', + export_packages : pkg_config_name, + install : true + ) + + if get_option('vapi') + gnome.generate_vapi( + pkg_config_name, + sources : [gir[0]], + packages : ['gobject-2.0', 'gio-2.0'], + install : true) + endif +endif + +pkg_config.generate( + name : 'astal-river', + version : meson.project_version(), + libraries : [astal_river_lib], + filebase : pkg_config_name, + subdirs : 'astal', + description : 'astal riverentication module', + url : 'https://github.com/astal-sh/river') diff --git a/river/src/river-output.c b/river/src/river-output.c new file mode 100644 index 0000000..6317db1 --- /dev/null +++ b/river/src/river-output.c @@ -0,0 +1,343 @@ +#include <gio/gio.h> + +#include "river-private.h" +#include "river-status-unstable-v1-client.h" + +struct _AstalRiverOutput { + GObject parent_instance; + guint focused_tags; + guint occupied_tags; + guint urgent_tags; + gchar* layout_name; + gchar* focused_view; + guint id; + gchar* name; +}; + +typedef struct { + struct zriver_status_manager_v1* river_status_manager; + struct zriver_output_status_v1* river_output_status; + struct wl_display* wl_display; + struct wl_output* wl_output; +} AstalRiverOutputPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalRiverOutput, astal_river_output, G_TYPE_OBJECT); + +typedef enum { + ASTAL_RIVER_OUTPUT_PROP_FOCUSED_TAGS = 1, + ASTAL_RIVER_OUTPUT_PROP_OCCUPIED_TAGS, + ASTAL_RIVER_OUTPUT_PROP_URGENT_TAGS, + ASTAL_RIVER_OUTPUT_PROP_LAYOUT_NAME, + ASTAL_RIVER_OUTPUT_PROP_NAME, + ASTAL_RIVER_OUTPUT_PROP_FOCUSED_VIEW, + ASTAL_RIVER_OUTPUT_PROP_ID, + ASTAL_RIVER_OUTPUT_N_PROPERTIES +} AstalRiverOutputProperties; + +typedef enum { + ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED, + ASTAL_RIVER_OUTPUT_N_SIGNALS +} AstalRiverOutputSignals; + +static guint astal_river_output_signals[ASTAL_RIVER_OUTPUT_N_SIGNALS] = { + 0, +}; + +static GParamSpec* astal_river_output_properties[ASTAL_RIVER_OUTPUT_N_PROPERTIES] = { + NULL, +}; + +/** + * astal_river_output_get_nid + * @self: the AstalRiverOutput object + * + * Returns: the id of the underlying wl_output object + */ +guint astal_river_output_get_id(AstalRiverOutput* self) { return self->id; } + +/** + * astal_river_output_get_name + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the name of the output + */ +gchar* astal_river_output_get_name(AstalRiverOutput* self) { return self->name; } + +/** + * astal_river_output_get_layout_name + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the currently used layout name of the output + */ +gchar* astal_river_output_get_layout_name(AstalRiverOutput* self) { return self->layout_name; } + +/** + * astal_river_output_get_focused_view + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the focused view on the output + */ +gchar* astal_river_output_get_focused_view(AstalRiverOutput* self) { return self->focused_view; } + +void astal_river_output_set_focused_view(AstalRiverOutput* self, const gchar* focused_view) { + g_free(self->focused_view); + self->focused_view = g_strdup(focused_view); + g_object_notify(G_OBJECT(self), "focused-view"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +/** + * astal_river_output_get_focused_tags + * @self: the AstalRiverOutput object + * + * Returns: the focused tags of the output + */ +guint astal_river_output_get_focused_tags(AstalRiverOutput* self) { return self->focused_tags; } + +/** + * astal_river_output_get_urgent_tags + * @self: the AstalRiverOutput object + * + * Returns: the urgent tags of the output + */ +guint astal_river_output_get_urgent_tags(AstalRiverOutput* self) { return self->urgent_tags; } + +/** + * astal_river_output_get_occupied_tags + * @self: the AstalRiverOutput object + * + * Returns: the occupied tags of the output + */ +guint astal_river_output_get_occupied_tags(AstalRiverOutput* self) { return self->occupied_tags; } + +struct wl_output* astal_river_output_get_wl_output(AstalRiverOutput* self) { + AstalRiverOutputPrivate* priv = astal_river_output_get_instance_private(self); + return priv->wl_output; +} + +static void astal_river_output_get_property(GObject* object, guint property_id, GValue* value, + GParamSpec* pspec) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(object); + + switch (property_id) { + case ASTAL_RIVER_OUTPUT_PROP_FOCUSED_TAGS: + g_value_set_uint(value, self->focused_tags); + break; + case ASTAL_RIVER_OUTPUT_PROP_OCCUPIED_TAGS: + g_value_set_uint(value, self->occupied_tags); + break; + case ASTAL_RIVER_OUTPUT_PROP_URGENT_TAGS: + g_value_set_uint(value, self->urgent_tags); + break; + case ASTAL_RIVER_OUTPUT_PROP_ID: + g_value_set_uint(value, self->id); + break; + case ASTAL_RIVER_OUTPUT_PROP_NAME: + g_value_set_string(value, self->name); + break; + case ASTAL_RIVER_OUTPUT_PROP_LAYOUT_NAME: + g_value_set_string(value, self->layout_name); + break; + case ASTAL_RIVER_OUTPUT_PROP_FOCUSED_VIEW: + g_value_set_string(value, self->focused_view); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_river_output_set_property(GObject* object, guint property_id, const GValue* value, + GParamSpec* pspec) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(object); + + switch (property_id) { + case ASTAL_RIVER_OUTPUT_PROP_ID: + self->id = g_value_get_uint(value); + g_object_notify(G_OBJECT(self), "id"); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void noop() {} + +static void astal_river_handle_focused_tags(void* data, struct zriver_output_status_v1* status, + uint32_t tags) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + self->focused_tags = tags; + g_object_notify(G_OBJECT(self), "focused-tags"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void astal_river_handle_urgent_tags(void* data, struct zriver_output_status_v1* status, + uint32_t tags) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + self->urgent_tags = tags; + g_object_notify(G_OBJECT(self), "urgent-tags"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void astal_river_handle_occupied_tags(void* data, struct zriver_output_status_v1* status, + struct wl_array* view_tags) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + guint tags = 0; + guint* view; + wl_array_for_each(view, view_tags) { tags |= *view; } + + self->occupied_tags = tags; + g_object_notify(G_OBJECT(self), "occupied-tags"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void astal_river_handle_layout_name(void* data, struct zriver_output_status_v1* status, + const char* name) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + g_free(self->layout_name); + self->layout_name = g_strdup(name); + g_object_notify(G_OBJECT(self), "layout-name"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void astal_river_handle_layout_name_clear(void* data, + struct zriver_output_status_v1* status) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + g_free(self->layout_name); + self->layout_name = NULL; + g_object_notify(G_OBJECT(self), "layout-name"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static void astal_river_wl_output_handle_name(void* data, struct wl_output* output, + const char* name) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(data); + g_free(self->name); + self->name = g_strdup(name); + g_object_notify(G_OBJECT(self), "name"); + g_signal_emit(self, astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED], 0); +} + +static const struct zriver_output_status_v1_listener output_status_listener = { + .focused_tags = astal_river_handle_focused_tags, + .view_tags = astal_river_handle_occupied_tags, + .urgent_tags = astal_river_handle_urgent_tags, + .layout_name = astal_river_handle_layout_name, + .layout_name_clear = astal_river_handle_layout_name_clear, +}; + +static const struct wl_output_listener wl_output_listener = { + .name = astal_river_wl_output_handle_name, + .geometry = noop, + .mode = noop, + .scale = noop, + .description = noop, + .done = noop, +}; + +static void astal_river_output_finalize(GObject* object) { + AstalRiverOutput* self = ASTAL_RIVER_OUTPUT(object); + AstalRiverOutputPrivate* priv = astal_river_output_get_instance_private(self); + + zriver_output_status_v1_destroy(priv->river_output_status); + wl_output_destroy(priv->wl_output); + + wl_display_roundtrip(priv->wl_display); + + g_free(self->layout_name); + g_free(self->name); + + G_OBJECT_CLASS(astal_river_output_parent_class)->finalize(object); +} + +static void astal_river_output_init(AstalRiverOutput* self) {} + +AstalRiverOutput* astal_river_output_new(guint id, struct wl_output* wl_output, + struct zriver_status_manager_v1* status_manager, + struct wl_display* wl_display) { + AstalRiverOutput* self = g_object_new(ASTAL_RIVER_TYPE_OUTPUT, NULL); + AstalRiverOutputPrivate* priv = astal_river_output_get_instance_private(self); + + self->id = id; + priv->wl_display = wl_display; + priv->wl_output = wl_output; + priv->river_status_manager = status_manager; + + priv->river_output_status = + zriver_status_manager_v1_get_river_output_status(priv->river_status_manager, wl_output); + + zriver_output_status_v1_add_listener(priv->river_output_status, &output_status_listener, self); + + wl_output_add_listener(wl_output, &wl_output_listener, self); + + wl_display_roundtrip(priv->wl_display); + + return self; +} + +static void astal_river_output_class_init(AstalRiverOutputClass* class) { + GObjectClass* object_class = G_OBJECT_CLASS(class); + object_class->get_property = astal_river_output_get_property; + object_class->set_property = astal_river_output_set_property; + object_class->finalize = astal_river_output_finalize; + /** + * AstalRiverOutput:focused-tags: + * + * The currently focused tags + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_FOCUSED_TAGS] = g_param_spec_uint( + "focused-tags", "focused-tags", "currently focused tags", 0, INT_MAX, 0, G_PARAM_READABLE); + /** + * AstalRiverOutput:occupied-tags: + * + * The currently occupied tags + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_OCCUPIED_TAGS] = + g_param_spec_uint("occupied-tags", "occupied-tags", "currently occupied tags", 0, INT_MAX, + 0, G_PARAM_READABLE); + /** + * AstalRiverOutput:urgent-tags: + * + * The currently tags marked as urgent + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_URGENT_TAGS] = g_param_spec_uint( + "urgent-tags", "urgent-tags", "currently urgent tags", 0, INT_MAX, 0, G_PARAM_READABLE); + /** + * AstalRiverOutput:id: + * + * The id of the underlying wl_output object + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_ID] = + g_param_spec_uint("id", "id", "id of the output object", 0, INT_MAX, 0, G_PARAM_READABLE); + /** + * AstalRiverOutput:layout-name: + * + * The name of active layout + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_LAYOUT_NAME] = g_param_spec_string( + "layout-name", "layout-name", "name of the current layout", NULL, G_PARAM_READABLE); + /** + * AstalRiverOutput:name: + * + * The name of this output + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_NAME] = + g_param_spec_string("name", "name", "name of the output", NULL, G_PARAM_READABLE); + /** + * AstalRiverOutput:focused-view: + * + * The name of currently focused view + */ + astal_river_output_properties[ASTAL_RIVER_OUTPUT_PROP_FOCUSED_VIEW] = + g_param_spec_string("focused-view", "focused-view", + "name of last focused view on this output", NULL, G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_RIVER_OUTPUT_N_PROPERTIES, + astal_river_output_properties); + + astal_river_output_signals[ASTAL_RIVER_OUTPUT_SIGNAL_CHANGED] = + g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/river/src/river.c b/river/src/river.c new file mode 100644 index 0000000..a735898 --- /dev/null +++ b/river/src/river.c @@ -0,0 +1,504 @@ +#include <gio/gio.h> +#include <json-glib/json-glib.h> +#include <wayland-client-protocol.h> +#include <wayland-client.h> + +#include "river-control-unstable-v1-client.h" +#include "river-private.h" +#include "river-status-unstable-v1-client.h" +#include "wayland-source.h" + +struct _AstalRiverRiver { + GObject parent_instance; + GList* outputs; + gchar* focused_output; + gchar* focused_view; + gchar* mode; +}; + +typedef struct { + GHashTable* signal_ids; + gboolean init; + struct wl_registry* wl_registry; + struct wl_seat* seat; + struct wl_display* display; + WLSource* wl_source; + struct zriver_status_manager_v1* river_status_manager; + struct zriver_control_v1* river_control; + struct zriver_seat_status_v1* river_seat_status; +} AstalRiverRiverPrivate; + +static JsonSerializableIface* serializable_iface = NULL; +static void astal_river_river_initable_iface_init(GInitableIface* iface); +static void astal_river_river_json_serializable_iface_init(JsonSerializableIface* g_iface); + +G_DEFINE_TYPE_WITH_CODE(AstalRiverRiver, astal_river_river, G_TYPE_OBJECT, + G_ADD_PRIVATE(AstalRiverRiver) G_IMPLEMENT_INTERFACE( + G_TYPE_INITABLE, astal_river_river_initable_iface_init) + G_IMPLEMENT_INTERFACE(JSON_TYPE_SERIALIZABLE, + astal_river_river_json_serializable_iface_init)) + +typedef enum { + ASTAL_RIVER_RIVER_PROP_FOCUSED_OUTPUT = 1, + ASTAL_RIVER_RIVER_PROP_FOCUSED_VIEW, + ASTAL_RIVER_RIVER_PROP_MODE, + ASTAL_RIVER_RIVER_PROP_OUTPUTS, + ASTAL_RIVER_RIVER_N_PROPERTIES +} AstalRiverRiverProperties; + +typedef enum { + ASTAL_RIVER_RIVER_SIGNAL_CHANGED, + ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_ADDED, + ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_REMOVED, + ASTAL_RIVER_RIVER_N_SIGNALS +} AstalRiverRiverSignals; + +static guint astal_river_river_signals[ASTAL_RIVER_RIVER_N_SIGNALS] = { + 0, +}; +static GParamSpec* astal_river_river_properties[ASTAL_RIVER_RIVER_N_PROPERTIES] = { + NULL, +}; + +static void reemit_changed(AstalRiverOutput* output, AstalRiverRiver* self) { + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); +} + +static AstalRiverOutput* find_output_by_id(AstalRiverRiver* self, uint32_t id) { + GList* output = self->outputs; + while (output != NULL) { + AstalRiverOutput* river_output = output->data; + if (astal_river_output_get_id(river_output) == id) return river_output; + output = output->next; + } + return NULL; +} + +static AstalRiverOutput* find_output_by_output(AstalRiverRiver* self, struct wl_output* wl_output) { + GList* output = self->outputs; + while (output != NULL) { + AstalRiverOutput* river_output = output->data; + if (astal_river_output_get_wl_output(river_output) == wl_output) return river_output; + output = output->next; + } + return NULL; +} + +static AstalRiverOutput* find_output_by_name(AstalRiverRiver* self, gchar* name) { + GList* output = self->outputs; + while (output != NULL) { + AstalRiverOutput* river_output = output->data; + if (strcmp(astal_river_output_get_name(river_output), name) == 0) return river_output; + output = output->next; + } + return NULL; +} + +/** + * astal_river_river_get_outputs + * @self: the AstalRiverRiver object + * + * Returns: (transfer none) (element-type AstalRiver.Output): a list of all outputs + * + */ +GList* astal_river_river_get_outputs(AstalRiverRiver* self) { return self->outputs; } + +/** + * astal_river_river_get_output + * @self: the AstalRiverRiver object + * @name: the name of the output + * + * Returns: (transfer none) (nullable): the output with the given name or null + */ +AstalRiverOutput* astal_river_river_get_output(AstalRiverRiver* self, gchar* name) { + return find_output_by_name(self, name); +} + +/** + * astal_river_river_get_focused_view + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the currently focused view + */ +gchar* astal_river_river_get_focused_view(AstalRiverRiver* self) { return self->focused_view; } + +/** + * astal_river_river_get_focused_output + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the name of the currently focused output + */ +gchar* astal_river_river_get_focused_output(AstalRiverRiver* self) { return self->focused_output; } + +/** + * astal_river_river_get_mode + * @self: the AstalRiverOutput object + * + * Returns: (transfer none) (nullable): the currently active mode + */ +gchar* astal_river_river_get_mode(AstalRiverRiver* self) { return self->mode; } + +static void astal_river_river_get_property(GObject* object, guint property_id, GValue* value, + GParamSpec* pspec) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(object); + + switch (property_id) { + case ASTAL_RIVER_RIVER_PROP_MODE: + g_value_set_string(value, self->mode); + break; + case ASTAL_RIVER_RIVER_PROP_FOCUSED_VIEW: + g_value_set_string(value, self->focused_view); + break; + case ASTAL_RIVER_RIVER_PROP_FOCUSED_OUTPUT: + g_value_set_string(value, self->focused_output); + break; + case ASTAL_RIVER_RIVER_PROP_OUTPUTS: + g_value_set_pointer(value, self->outputs); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static JsonNode* astal_river_river_serialize_property(JsonSerializable* serializable, + const gchar* name, const GValue* value, + GParamSpec* pspec) { + JsonNode* retval = NULL; + if (strcmp(name, "outputs") == 0) { + JsonArray* outputs = json_array_new(); + retval = json_node_new(JSON_NODE_ARRAY); + GList* output = g_value_get_pointer(value); + while (output != NULL) { + AstalRiverOutput* river_output = output->data; + json_array_add_element(outputs, json_gobject_serialize(G_OBJECT(river_output))); + output = output->next; + } + json_node_take_array(retval, outputs); + } else + retval = serializable_iface->serialize_property(serializable, name, value, pspec); + + return retval; +} + +static void noop() {} + +static void river_seat_status_handle_focused_output(void* data, + struct zriver_seat_status_v1* seat_status, + struct wl_output* focused_output) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(data); + g_free(self->focused_output); + AstalRiverOutput* output = find_output_by_output(self, focused_output); + if (output == NULL) return; + self->focused_output = g_strdup(astal_river_output_get_name(output)); + g_object_notify(G_OBJECT(self), "focused-output"); + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); +} + +static void river_seat_status_handle_focused_view(void* data, + struct zriver_seat_status_v1* seat_status, + const char* focused_view) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(data); + g_free(self->focused_view); + self->focused_view = g_strdup(focused_view); + AstalRiverOutput* output = find_output_by_name(self, self->focused_output); + astal_river_output_set_focused_view(output, focused_view); + g_object_notify(G_OBJECT(self), "focused-view"); + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); +} + +static void river_seat_status_handle_mode(void* data, struct zriver_seat_status_v1* seat_status, + const char* mode) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(data); + g_free(self->mode); + self->mode = g_strdup(mode); + g_object_notify(G_OBJECT(self), "mode"); + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); +} + +static const struct zriver_seat_status_v1_listener river_seat_status_listener = { + .focused_output = river_seat_status_handle_focused_output, + .unfocused_output = noop, + .focused_view = river_seat_status_handle_focused_view, + .mode = river_seat_status_handle_mode, +}; + +static void global_registry_handler(void* data, struct wl_registry* registry, uint32_t id, + const char* interface, uint32_t version) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(data); + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + if (strcmp(interface, wl_output_interface.name) == 0) { + if (priv->river_status_manager == NULL) return; + struct wl_output* wl_out = wl_registry_bind(registry, id, &wl_output_interface, 4); + AstalRiverOutput* output = + astal_river_output_new(id, wl_out, priv->river_status_manager, priv->display); + + self->outputs = g_list_append(self->outputs, output); + g_object_notify(G_OBJECT(self), "outputs"); + g_signal_emit(G_OBJECT(self), + astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_ADDED], 0, + astal_river_output_get_name(output)); + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); + + guint signal_id = g_signal_connect(output, "changed", G_CALLBACK(reemit_changed), self); + g_hash_table_insert(priv->signal_ids, GUINT_TO_POINTER(id), GUINT_TO_POINTER(signal_id)); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + priv->seat = wl_registry_bind(registry, id, &wl_seat_interface, 4); + } else if (strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { + priv->river_status_manager = + wl_registry_bind(registry, id, &zriver_status_manager_v1_interface, 4); + } else if (strcmp(interface, zriver_control_v1_interface.name) == 0) { + priv->river_control = wl_registry_bind(registry, id, &zriver_control_v1_interface, 1); + } +} + +static void astal_river_river_callback_success(void* data, struct zriver_command_callback_v1* cb, + const char* msg) { + AstalRiverCommandCallback callback = (AstalRiverCommandCallback)(data); + callback(TRUE, msg); +} + +static void astal_river_river_callback_failure(void* data, struct zriver_command_callback_v1* cb, + const char* msg) { + AstalRiverCommandCallback callback = (AstalRiverCommandCallback)(data); + callback(FALSE, msg); +} + +const struct zriver_command_callback_v1_listener cb_listener = { + .success = astal_river_river_callback_success, .failure = astal_river_river_callback_failure}; + +/** + * astal_river_river_run_command_async: + * @self: the AstalRiverRiver object + * @length: the length of the cmd array + * @cmd: (array length=length): the command to execute + * @callback: (scope async) (nullable): the callback to invoke. + * + * Calls the given callback with the provided parameters. + */ +void astal_river_river_run_command_async(AstalRiverRiver* self, gint length, const gchar** cmd, + AstalRiverCommandCallback callback) { + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + + for (gint i = 0; i < length; ++i) { + zriver_control_v1_add_argument(priv->river_control, cmd[i]); + } + + struct zriver_command_callback_v1* cb = + zriver_control_v1_run_command(priv->river_control, priv->seat); + if (callback != NULL) zriver_command_callback_v1_add_listener(cb, &cb_listener, callback); +} + +static void global_registry_remover(void* data, struct wl_registry* registry, uint32_t id) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(data); + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + AstalRiverOutput* output = find_output_by_id(self, id); + if (output != NULL) { + guint signal_id = + GPOINTER_TO_UINT(g_hash_table_lookup(priv->signal_ids, GUINT_TO_POINTER(id))); + g_hash_table_remove(priv->signal_ids, GUINT_TO_POINTER(id)); + g_signal_handler_disconnect(output, signal_id); + g_signal_emit(G_OBJECT(self), + astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_ADDED], 0, + astal_river_output_get_name(output)); + self->outputs = g_list_remove(self->outputs, output); + g_object_notify(G_OBJECT(self), "outputs"); + g_object_unref(output); + return; + } + g_signal_emit(self, astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED], 0); +} + +static const struct wl_registry_listener registry_listener = {global_registry_handler, + global_registry_remover}; + +static void astal_river_river_json_serializable_iface_init(JsonSerializableIface* iface) { + iface->serialize_property = astal_river_river_serialize_property; + serializable_iface = g_type_default_interface_peek(JSON_TYPE_SERIALIZABLE); +} + +static gboolean astal_river_river_initable_init(GInitable* initable, GCancellable* cancellable, + GError** error) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(initable); + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + + if (priv->init) return TRUE; + + priv->wl_source = wl_source_new(NULL, NULL); + + if (priv->wl_source == NULL) { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can not connect to wayland display"); + return FALSE; + } + + priv->display = wl_source_get_display(priv->wl_source); + + priv->wl_registry = wl_display_get_registry(priv->display); + wl_registry_add_listener(priv->wl_registry, ®istry_listener, self); + + wl_display_roundtrip(priv->display); + + if (priv->river_status_manager == NULL) { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Can not connect river status protocol"); + return FALSE; + } + + priv->river_seat_status = + zriver_status_manager_v1_get_river_seat_status(priv->river_status_manager, priv->seat); + zriver_seat_status_v1_add_listener(priv->river_seat_status, &river_seat_status_listener, self); + + wl_display_roundtrip(priv->display); + + priv->init = TRUE; + return TRUE; +} + +static void astal_river_river_constructed(GObject* object) { + astal_river_river_initable_init(G_INITABLE(object), NULL, NULL); +} + +static void astal_river_river_initable_iface_init(GInitableIface* iface) { + iface->init = astal_river_river_initable_init; +} + +static void astal_river_river_init(AstalRiverRiver* self) { + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + self->outputs = NULL; + priv->init = FALSE; + priv->seat = NULL; + priv->display = NULL; + priv->river_status_manager = NULL; + priv->signal_ids = g_hash_table_new(g_direct_hash, g_direct_equal); +} + +/** + * astal_river_river_new + * + * creates a new River object. It is recommended to use the get_default() method + * instead of this method. + * + * Returns: (nullable): a newly created connection to river + */ +AstalRiverRiver* astal_river_river_new() { + return g_initable_new(ASTAL_RIVER_TYPE_RIVER, NULL, NULL, NULL); +} + +static void disconnect_signal(gpointer key, gpointer value, gpointer user_data) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(user_data); + + AstalRiverOutput* output = find_output_by_id(self, GPOINTER_TO_UINT(key)); + g_signal_handler_disconnect(output, GPOINTER_TO_UINT(value)); +} + +/** + * astal_river_river_get_default + * + * Returns: (nullable) (transfer none): gets the default River object. + */ +AstalRiverRiver* astal_river_river_get_default() { + static AstalRiverRiver* self = NULL; + + if (self == NULL) self = astal_river_river_new(); + + return self; +} + +/** + * astal_river_get_default + * + * Returns: (nullable) (transfer none): gets the default River object. + */ +AstalRiverRiver* astal_river_get_default() { return astal_river_river_get_default(); } + +static void astal_river_river_finalize(GObject* object) { + AstalRiverRiver* self = ASTAL_RIVER_RIVER(object); + AstalRiverRiverPrivate* priv = astal_river_river_get_instance_private(self); + + g_hash_table_foreach(priv->signal_ids, disconnect_signal, self); + g_hash_table_destroy(priv->signal_ids); + + if (priv->display != NULL) wl_display_roundtrip(priv->display); + + g_clear_list(&self->outputs, g_object_unref); + self->outputs = NULL; + + if (priv->wl_registry != NULL) wl_registry_destroy(priv->wl_registry); + if (priv->river_status_manager != NULL) + zriver_status_manager_v1_destroy(priv->river_status_manager); + if (priv->river_seat_status != NULL) zriver_seat_status_v1_destroy(priv->river_seat_status); + if (priv->seat != NULL) wl_seat_destroy(priv->seat); + if (priv->display != NULL) wl_display_flush(priv->display); + + if (priv->wl_source != NULL) wl_source_free(priv->wl_source); + + g_free(self->focused_view); + g_free(self->focused_output); + g_free(self->mode); + + G_OBJECT_CLASS(astal_river_river_parent_class)->finalize(object); +} + +static void astal_river_river_class_init(AstalRiverRiverClass* class) { + GObjectClass* object_class = G_OBJECT_CLASS(class); + object_class->get_property = astal_river_river_get_property; + object_class->finalize = astal_river_river_finalize; + object_class->constructed = astal_river_river_constructed; + + /** + * AstalRiverRiver:mode: + * + * The currently active mode + */ + astal_river_river_properties[ASTAL_RIVER_RIVER_PROP_MODE] = + g_param_spec_string("mode", "mode", "currently active mode", NULL, G_PARAM_READABLE); + /** + * AstalRiverRiver:focused-view: + * + * The name of the currently focused view + */ + astal_river_river_properties[ASTAL_RIVER_RIVER_PROP_FOCUSED_VIEW] = g_param_spec_string( + "focused-view", "focused-view", "currently focused view", NULL, G_PARAM_READABLE); + /** + * AstalRiverRiver:focused-output: + * + * The name of the currently focused output + */ + astal_river_river_properties[ASTAL_RIVER_RIVER_PROP_FOCUSED_OUTPUT] = g_param_spec_string( + "focused-output", "focused-output", "currently focused-output", NULL, G_PARAM_READABLE); + /** + * AstalRiverRiver:outputs: (type GList(AstalRiverOutput)) + * + * A list of AstalRiverOutput objects + */ + astal_river_river_properties[ASTAL_RIVER_RIVER_PROP_OUTPUTS] = + g_param_spec_pointer("outputs", "outputs", "a list of all outputs", G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_RIVER_RIVER_N_PROPERTIES, + astal_river_river_properties); + /** + * AstalRiverRiver::output-added: + * @river: the object which received the signal. + * @output: the name of the added output + * + * This signal is emitted when a new output was connected + */ + astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_ADDED] = + g_signal_new("output-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * AstalRiverRiver::output-removed: + * @river: the object which received the signal. + * @output: the name of the removed output + * + * This signal is emitted when a new output was disconnected + */ + astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_OUTPUT_REMOVED] = + g_signal_new("output-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + + astal_river_river_signals[ASTAL_RIVER_RIVER_SIGNAL_CHANGED] = + g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/river/src/wayland-source.c b/river/src/wayland-source.c new file mode 100644 index 0000000..875c32c --- /dev/null +++ b/river/src/wayland-source.c @@ -0,0 +1,104 @@ + +#include "wayland-source.h" + +#include <errno.h> +#include <glib.h> +#include <wayland-client.h> + +struct _WLSource { + GSource source; + struct wl_display *display; + gpointer fd; + int error; +}; + +static gboolean wl_source_prepare(GSource *source, gint *timeout) { + WLSource *self = (WLSource *)source; + + *timeout = 0; + if (wl_display_prepare_read(self->display) != 0) + return TRUE; + else if (wl_display_flush(self->display) < 0) { + self->error = errno; + return TRUE; + } + *timeout = -1; + return FALSE; +} + +static gboolean wl_source_check(GSource *source) { + WLSource *self = (WLSource *)source; + + if (self->error > 0) return TRUE; + + GIOCondition revents; + revents = g_source_query_unix_fd(source, self->fd); + + if (revents & G_IO_IN) { + if (wl_display_read_events(self->display) < 0) self->error = errno; + } else + wl_display_cancel_read(self->display); + + return revents > 0; +} + +static gboolean wl_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { + WLSource *self = (WLSource *)source; + GIOCondition revents; + + revents = g_source_query_unix_fd(source, self->fd); + if ((self->error > 0) || (revents & (G_IO_ERR | G_IO_HUP))) { + errno = self->error; + self->error = 0; + if (callback != NULL) return callback(user_data); + return G_SOURCE_REMOVE; + } + + if (wl_display_dispatch_pending(self->display) < 0) { + if (callback != NULL) return callback(user_data); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void wl_source_finalize(GSource *source) { + WLSource *self = (WLSource *)source; + wl_display_disconnect(self->display); +} + +static GSourceFuncs wl_source_funcs = { + .prepare = wl_source_prepare, + .check = wl_source_check, + .dispatch = wl_source_dispatch, + .finalize = wl_source_finalize, +}; + +WLSource *wl_source_new() { + struct wl_display *display; + WLSource *self; + GSource *source; + + display = wl_display_connect(NULL); + if (display == NULL) return NULL; + + source = g_source_new(&wl_source_funcs, sizeof(WLSource)); + self = (WLSource *)source; + self->display = display; + + self->fd = g_source_add_unix_fd(source, wl_display_get_fd(self->display), + G_IO_IN | G_IO_ERR | G_IO_HUP); + + g_source_attach(source, NULL); + + return self; +} + +void wl_source_free(WLSource *self) { + GSource *source = (GSource *)self; + g_return_if_fail(source != NULL); + g_source_destroy(source); + g_source_unref(source); +} + +struct wl_display *wl_source_get_display(WLSource *self) { return self->display; } diff --git a/river/version b/river/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/river/version @@ -0,0 +1 @@ +0.1.0 diff --git a/tray/LICENSE b/tray/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/tray/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/tray/README.md b/tray/README.md new file mode 100644 index 0000000..c436746 --- /dev/null +++ b/tray/README.md @@ -0,0 +1,30 @@ +# libastal-tray +a library for managing the systemtray by implementing the StatusNotifierItem dbus protocol. + +## Build from source +### Dependencies + +- meson +- glib +- gdk-pixbuf +- gtk3 +- gobject-introspection +- dbusemenu-gtk3 +- vala + +```sh +# Clone the repository +git clone https://github.com/astal-sh/tray +cd tray + +# Setup and build +meson setup build +meson compile -C build + +# Install +meson install -C build +``` + +## Todo +- docs +- cli tool diff --git a/tray/flake.lock b/tray/flake.lock new file mode 100644 index 0000000..8d2ab6e --- /dev/null +++ b/tray/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1719254875, + "narHash": "sha256-ECni+IkwXjusHsm9Sexdtq8weAq/yUyt1TWIemXt3Ko=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2893f56de08021cffd9b6b6dfc70fd9ccd51eb60", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/tray/flake.nix b/tray/flake.nix new file mode 100644 index 0000000..c78cbaa --- /dev/null +++ b/tray/flake.nix @@ -0,0 +1,56 @@ +{ + description = "Library and cli for the StatusNotifierItem protocol"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + gtk3 + glib + gdk-pixbuf + libdbusmenu-gtk3 + json-glib + ]; + in { + packages.${system} = rec { + default = tray; + tray = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-tray"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + tray = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/tray/meson.build b/tray/meson.build new file mode 100644 index 0000000..4a52d56 --- /dev/null +++ b/tray/meson.build @@ -0,0 +1,19 @@ +project( + 'astal-tray', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +assert( + get_option('lib') or get_option('cli'), + 'Either lib or cli option must be set to true.', +) + +subdir('src') diff --git a/tray/meson_options.txt b/tray/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/tray/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/tray/src/cli.vala b/tray/src/cli.vala new file mode 100644 index 0000000..3147fb5 --- /dev/null +++ b/tray/src/cli.vala @@ -0,0 +1,54 @@ +static bool version; +static bool daemonize; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, "Print version number", null }, + { "daemonize", 'd', OptionFlags.NONE, OptionArg.NONE, ref daemonize, "Monitor the systemtray", null }, + { null }, +}; + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(true); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError err) { + printerr (err.message); + return 1; + } + + if (version) { + print(AstalTray.VERSION); + return 0; + } + + if (daemonize) { + var loop = new MainLoop(); + var tray = new AstalTray.Tray(); + + tray.item_added.connect((id) => { + AstalTray.TrayItem item = tray.get_item(id); + + stdout.printf("{\"event\":\"item_added\",\"id\":\"%s\",\"item\":%s}\n", + id, item.to_json_string()); + stdout.flush(); + + item.changed.connect(() => { + stdout.printf("{\"event\":\"item_changed\",\"id\":\"%s\",\"item\":%s}\n", + id, item.to_json_string()); + stdout.flush(); + }); + }); + + tray.item_removed.connect((id) => { + stdout.printf("{\"event\":\"item_removed\",\"id\":\"%s\"}\n", id); + stdout.flush(); + }); + + loop.run(); + } + + return 0; +} diff --git a/tray/src/config.vala.in b/tray/src/config.vala.in new file mode 100644 index 0000000..8ef8498 --- /dev/null +++ b/tray/src/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalTray { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/tray/src/meson.build b/tray/src/meson.build new file mode 100644 index 0000000..b2229b0 --- /dev/null +++ b/tray/src/meson.build @@ -0,0 +1,100 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalTray-' + api_version + '.gir' +typelib = 'AstalTray-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('json-glib-1.0'), + dependency('gdk-pixbuf-2.0'), + dependency('gtk+-3.0'), +] + +dbusmenu_cflags = run_command( + find_program('pkg-config', required: true), + '--cflags', 'dbusmenu-gtk3-0.4', + 'gobject-introspection-1.0', + 'gobject-2.0', + 'glib-2.0', + capture: true, + check: true, +).stdout().strip() + +dbusmenu_libs = run_command( + find_program('pkg-config', required: true), + '--libs', 'dbusmenu-gtk3-0.4', + 'gobject-introspection-1.0', + 'gobject-2.0', + 'glib-2.0', + capture: true, + check: true, +).stdout().strip() + +sources = [config, 'tray.vala', 'watcher.vala', 'trayItem.vala'] + +if get_option('lib') + lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + vala_gir: gir, + vala_args: ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], + version: meson.project_version(), + c_args: dbusmenu_cflags.split(' '), + link_args: dbusmenu_libs.split(' '), + install: true, + install_dir: [true, true, true, true], + ) + + import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) + + custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: lib, + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + vala_args: ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], + c_args: dbusmenu_cflags.split(' '), + link_args: dbusmenu_libs.split(' '), + install: true, + ) +endif diff --git a/tray/src/tray.vala b/tray/src/tray.vala new file mode 100644 index 0000000..09b0643 --- /dev/null +++ b/tray/src/tray.vala @@ -0,0 +1,135 @@ +namespace AstalTray { +[DBus (name="org.kde.StatusNotifierWatcher")] +internal interface IWatcher : Object { + public abstract string[] RegisteredStatusNotifierItems { owned get; } + public abstract int ProtocolVersion { owned get; } + + public abstract void RegisterStatusNotifierItem(string service, BusName sender) throws DBusError, IOError; + public abstract void RegisterStatusNotifierHost(string service) throws DBusError, IOError; + + public signal void StatusNotifierItemRegistered(string service); + public signal void StatusNotifierItemUnregistered(string service); + public signal void StatusNotifierHostRegistered(); + public signal void StatusNotifierHostUnregistered(); +} + +public Tray get_default() { + return Tray.get_default(); +} + +public class Tray : Object { + private static Tray? instance; + public static unowned Tray get_default() { + if (instance == null) + instance = new Tray(); + + return instance; + } + + private StatusNotifierWatcher watcher; + private IWatcher proxy; + + private HashTable<string, TrayItem> _items = + new HashTable<string, TrayItem>(str_hash, str_equal); + + public List<weak TrayItem> items { owned get { return _items.get_values(); }} + + public signal void item_added(string service) { + notify_property("items"); + } + + public signal void item_removed(string service) { + notify_property("items"); + } + + construct { + try { + Bus.own_name( + BusType.SESSION, + "org.kde.StatusNotifierWatcher", + BusNameOwnerFlags.NONE, + start_watcher, + () => { + if (proxy != null) { + proxy = null; + } + }, + start_host + ); + } catch (Error err) { + critical(err.message); + } + + } + + private void start_watcher(DBusConnection conn) { + try { + watcher = new StatusNotifierWatcher(); + conn.register_object("/StatusNotifierWatcher", watcher); + watcher.StatusNotifierItemRegistered.connect(on_item_register); + watcher.StatusNotifierItemUnregistered.connect(on_item_unregister); + } catch (Error err) { + critical(err.message); + } + } + + private void start_host() { + if (proxy != null) + return; + + try { + proxy = Bus.get_proxy_sync(BusType.SESSION, + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + + proxy.StatusNotifierItemRegistered.connect(on_item_register); + proxy.StatusNotifierItemUnregistered.connect(on_item_unregister); + + proxy.notify["g-name-owner"].connect(() => { + _items.foreach((service, _) => { + item_removed(service); + }); + + _items.remove_all(); + + if(proxy != null) { + foreach (string item in proxy.RegisteredStatusNotifierItems) { + on_item_register(item); + } + } else { + foreach (string item in watcher.RegisteredStatusNotifierItems) { + on_item_register(item); + } + } + }); + + foreach (string item in proxy.RegisteredStatusNotifierItems) { + on_item_register(item); + } + } catch (Error err) { + critical("cannot get proxy: %s", err.message); + } + } + + private void on_item_register(string service) { + if (_items.contains(service)) + return; + + var parts = service.split("/", 2); + TrayItem item = new TrayItem(parts[0], "/" + parts[1]); + item.ready.connect(() => { + _items.set(service, item); + item_added(service); + }); + } + + private void on_item_unregister(string service) { + _items.remove(service); + item_removed(service); + } + + public TrayItem get_item(string service) { + return _items.get(service); + } +} +} diff --git a/tray/src/trayItem.vala b/tray/src/trayItem.vala new file mode 100644 index 0000000..b6b9da0 --- /dev/null +++ b/tray/src/trayItem.vala @@ -0,0 +1,363 @@ +using DbusmenuGtk; + +namespace AstalTray { +public struct Pixmap { + int width; + int height; + uint8[] bytes; +} + +public struct Tooltip { + string icon_name; + Pixmap[] icon; + string title; + string description; +} + +[DBus (use_string_marshalling = true)] +public enum Category { + [DBus (value = "ApplicationStatus"), Description (nick = "ApplicationStatus")] + APPLICATION, + + [DBus (value = "Communications"), Description (nick = "Communications")] + COMMUNICATIONS, + + [DBus (value = "SystemServices"), Description (nick = "SystemServices")] + SYSTEM, + + [DBus (value = "Hardware"), Description (nick = "Hardware")] + HARDWARE; + + public string to_nick () { + var enumc = (EnumClass)typeof (Category).class_ref(); + unowned var eval = enumc.get_value(this); + return eval.value_nick; + } +} + + +[DBus (use_string_marshalling = true)] +public enum Status { + [DBus (value = "Passive"), Description (nick = "Passive")] + PASSIVE, + + [DBus (value = "Active"), Description (nick = "Active")] + ACTIVE, + + [DBus (value = "NeedsAttention"), Description (nick = "NeedsAttention")] + NEEDS_ATTENTION; + + public string to_nick () { + var enumc = (EnumClass)typeof (Status).class_ref(); + unowned var eval = enumc.get_value(this); + return eval.value_nick; + } +} + +[DBus (name="org.kde.StatusNotifierItem")] +internal interface IItem : DBusProxy { + public abstract string Title { owned get; } + public abstract Category Category { owned get; } + public abstract Status Status { owned get; } + public abstract Tooltip? ToolTip { owned get; } + public abstract string Id { owned get; } + public abstract string? IconThemePath { owned get; } + public abstract bool ItemIsMenu { owned get; } + public abstract ObjectPath? Menu { owned get; } + public abstract string IconName { owned get; } + public abstract Pixmap[] IconPixmap { owned get; } + public abstract string AttentionIconName { owned get; } + public abstract Pixmap[] AttentionIconPixmap { owned get; } + public abstract string OverlayIconName { owned get; } + public abstract Pixmap[] OverlayIconPixmap { owned get; } + + public abstract void ContexMenu(int x, int y) throws DBusError, IOError; + public abstract void Activate(int x, int y) throws DBusError, IOError; + public abstract void SecondaryActivate(int x, int y) throws DBusError, IOError; + public abstract void Scroll(int delta, string orientation) throws DBusError, IOError; + + public signal void NewTitle(); + public signal void NewIcon(); + public signal void NewAttentionIcon(); + public signal void NewOverlayIcon(); + public signal void NewToolTip(); + public signal void NewStatus(string status); +} + +public class TrayItem : Object { + private IItem proxy; + private List<ulong> connection_ids; + + public string title { owned get { return proxy.Title; } } + public Category category { get { return proxy.Category; } } + public Status status { get { return proxy.Status; } } + public Tooltip? tooltip { owned get { return proxy.ToolTip; } } + + public string tooltip_markup { + owned get { + if (proxy.ToolTip == null) + return ""; + + var tt = proxy.ToolTip.title; + if (proxy.ToolTip.description != "") + tt += "\n" + proxy.ToolTip.description; + + return tt; + } + } + + public string id { owned get { return proxy.Id ;} } + public string icon_theme_path { owned get { return proxy.IconThemePath ;} } + public bool is_menu { get { return proxy.ItemIsMenu ;} } + + public string icon_name { + owned get { + return proxy.Status == Status.NEEDS_ATTENTION + ? proxy.AttentionIconName + : proxy.IconName; + } + } + + public Gdk.Pixbuf icon_pixbuf { owned get { return _get_icon_pixbuf(); } } + + public GLib.Icon gicon { get; private set; } + + public string item_id { get; private set; } + + public signal void changed(); + public signal void ready(); + + public TrayItem(string service, string path) { + connection_ids = new List<ulong>(); + item_id = service + path; + setup_proxy.begin(service, path, (_, res) => setup_proxy.end(res)); + } + + private async void setup_proxy(string service, string path) { + try { + proxy = yield Bus.get_proxy( + BusType.SESSION, + service, + path); + + connection_ids.append(proxy.NewStatus.connect(refresh_all_properties)); + connection_ids.append(proxy.NewToolTip.connect(refresh_all_properties)); + connection_ids.append(proxy.NewTitle.connect(refresh_all_properties)); + connection_ids.append(proxy.NewIcon.connect(refresh_all_properties)); + + proxy.notify["g-name-owner"].connect(() => { + if (proxy.g_name_owner == null) { + foreach (var id in connection_ids) + SignalHandler.disconnect(proxy, id); + } + }); + + update_gicon(); + + ready(); + } catch (Error err) { + critical(err.message); + } + } + + private void _notify() { + string[] props = { "category", "id", "title", "status", "is-menu", "tooltip-markup", "icon-name", "icon-pixbuf" }; + + foreach (string prop in props) + notify_property(prop); + + changed(); + } + + private void update_gicon() { + if(icon_name != null && icon_name != "") { + if(icon_theme_path != null && icon_theme_path != "") { + + Gtk.IconTheme icon_theme = new Gtk.IconTheme(); + string[] paths = {icon_theme_path}; + icon_theme.set_search_path(paths); + + int size = icon_theme.get_icon_sizes(icon_name)[0]; + Gtk.IconInfo icon_info = icon_theme.lookup_icon( + icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE); + + if (icon_info != null) + gicon = new GLib.FileIcon(GLib.File.new_for_path(icon_info.get_filename())); + } else { + gicon = new GLib.ThemedIcon(icon_name); + } + } + else { + Pixmap[] pixmaps = proxy.Status == Status.NEEDS_ATTENTION + ? proxy.AttentionIconPixmap + : proxy.IconPixmap; + gicon = pixmap_to_pixbuf(pixmaps); + } + } + + + private void refresh_all_properties() { + proxy.g_connection.call.begin( + proxy.g_name, + proxy.g_object_path, + "org.freedesktop.DBus.Properties", + "GetAll", + new Variant("(s)", proxy.g_interface_name), + new VariantType("(a{sv})"), + DBusCallFlags.NONE, + -1, + null, + (_, result) => { + try { + Variant parameters = proxy.g_connection.call.end(result); + VariantIter prop_iter; + parameters.get("(a{sv})", out prop_iter); + + string prop_key; + Variant prop_value; + + while (prop_iter.next ("{sv}", out prop_key, out prop_value)) { + proxy.set_cached_property(prop_key, prop_value); + } + + update_gicon(); + + _notify(); + } catch(Error e) { + //silently ignore + } + } + ); + } + + public void activate(int x, int y) { + try { + proxy.Activate(x, y); + } catch (Error e) { + if(e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) + warning(e.message); + } + } + + public void secondary_activate(int x, int y) { + try { + proxy.SecondaryActivate(x, y); + } catch (Error e) { + if(e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) + warning(e.message); + } + } + + public void scroll(int delta, string orientation) { + try { + proxy.Scroll(delta, orientation); + } catch (Error e) { + if(e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) + warning("%s\n", e.message); + } + } + + + public DbusmenuGtk.Menu? create_menu() { + if (proxy.Menu == null) + return null; + + return new DbusmenuGtk.Menu( + proxy.get_name_owner(), + proxy.Menu); + } + + public Gdk.Pixbuf? _get_icon_pixbuf() { + Pixmap[] pixmaps = proxy.Status == Status.NEEDS_ATTENTION + ? proxy.AttentionIconPixmap + : proxy.IconPixmap; + + + string icon_name = proxy.Status == Status.NEEDS_ATTENTION + ? proxy.AttentionIconName + : proxy.IconName; + + Gdk.Pixbuf pixbuf = null; + + if (icon_name != null && proxy.IconThemePath != null) + pixbuf = load_from_theme(icon_name, proxy.IconThemePath); + + if (pixbuf == null) + pixbuf = pixmap_to_pixbuf(pixmaps); + + return pixbuf; + } + + private Gdk.Pixbuf? load_from_theme(string icon_name, string theme_path) { + if (theme_path == "" || theme_path == null) + return null; + + if (icon_name == "" || icon_name == null) + return null; + + Gtk.IconTheme icon_theme = new Gtk.IconTheme(); + string[] paths = {theme_path}; + icon_theme.set_search_path(paths); + + int size = icon_theme.get_icon_sizes(icon_name)[0]; + Gtk.IconInfo icon_info = icon_theme.lookup_icon( + icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE); + + if (icon_info != null) + return icon_info.load_icon(); + + return null; + } + + private Gdk.Pixbuf? pixmap_to_pixbuf(Pixmap[] pixmaps) { + if (pixmaps == null || pixmaps.length == 0) + return null; + + Pixmap pixmap = pixmaps[0]; + uint8[] image_data = pixmap.bytes.copy(); + + for (int i = 0; i < pixmap.width * pixmap.height * 4; i += 4) { + uint8 alpha = image_data[i]; + image_data[i] = image_data[i + 1]; + image_data[i + 1] = image_data[i + 2]; + image_data[i + 2] = image_data[i + 3]; + image_data[i + 3] = alpha; + } + + return new Gdk.Pixbuf.from_bytes( + new Bytes(image_data), + Gdk.Colorspace.RGB, + true, + 8, + (int)pixmap.width, + (int)pixmap.height, + (int)(pixmap.width * 4) + ); + } + + public string to_json_string() { + var generator = new Json.Generator(); + generator.set_root(to_json()); + return generator.to_data(null); + } + + internal Json.Node to_json() { + return new Json.Builder() + .begin_object() + .set_member_name("item_id").add_string_value(item_id) + .set_member_name("id").add_string_value(id) + .set_member_name("bus_name").add_string_value(proxy.g_name) + .set_member_name("object_path").add_string_value(proxy.g_object_path) + .set_member_name("title").add_string_value(title) + .set_member_name("status").add_string_value(status.to_nick()) + .set_member_name("category").add_string_value(category.to_nick()) + .set_member_name("tooltip").add_string_value(tooltip_markup) + .set_member_name("icon_theme_path").add_string_value(proxy.IconThemePath) + .set_member_name("icon_name").add_string_value(icon_name) + .set_member_name("menu_path").add_string_value(proxy.Menu) + .set_member_name("is_menu").add_boolean_value(is_menu) + .end_object() + .get_root(); + } +} +} diff --git a/tray/src/watcher.vala b/tray/src/watcher.vala new file mode 100644 index 0000000..974cd02 --- /dev/null +++ b/tray/src/watcher.vala @@ -0,0 +1,59 @@ +namespace AstalTray { +[DBus (name="org.kde.StatusNotifierWatcher")] +internal class StatusNotifierWatcher : Object { + private HashTable<string, string> _items = + new HashTable<string, string>(str_hash, str_equal); + + public string[] RegisteredStatusNotifierItems { owned get { return _items.get_values_as_ptr_array().data; } } + public bool IsStatusNotifierHostRegistered { get; default = true; } + public int ProtocolVersion { get; default = 0; } + + public signal void StatusNotifierItemRegistered(string service); + public signal void StatusNotifierItemUnregistered(string service); + public signal void StatusNotifierHostRegistered(); + public signal void StatusNotifierHostUnregistered(); + + public void RegisterStatusNotifierItem(string service, BusName sender) throws DBusError, IOError { + string busName; + string path; + if (service[0] == '/') { + path = service; + busName = sender; + } else { + busName = service; + path = "/StatusNotifierItem"; + } + + Bus.get_sync(BusType.SESSION).signal_subscribe( + null, + "org.freedesktop.DBus", + "NameOwnerChanged", + null, + null, + DBusSignalFlags.NONE, + (connection, sender_name, path, interface_name, signal_name, parameters) => { + string name = null; + string new_owner = null; + string old_owner = null; + parameters.get("(sss)", &name, &old_owner, &new_owner); + if (new_owner == "" && _items.contains(old_owner)) { + string full_path = _items.take(old_owner); + StatusNotifierItemUnregistered(full_path); + } + } + ); + + _items.set(busName, busName+path); + StatusNotifierItemRegistered(busName+path); + } + + public void RegisterStatusNotifierHost(string service) throws DBusError, IOError { + /* NOTE: + usually the watcher should keep track of registered host + but some tray applications do net register their trayitem properly + when hosts register/deregister. This is fixed by setting isHostRegistered + always to true, this also make host handling logic unneccessary. + */ + } +} +} diff --git a/tray/version b/tray/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/tray/version @@ -0,0 +1 @@ +0.1.0 @@ -0,0 +1 @@ +0.1.0 diff --git a/wireplumber/.gitignore b/wireplumber/.gitignore new file mode 100644 index 0000000..6bf41b5 --- /dev/null +++ b/wireplumber/.gitignore @@ -0,0 +1,3 @@ +build/ +result/ +.cache/ diff --git a/wireplumber/LICENSE b/wireplumber/LICENSE new file mode 100644 index 0000000..67cd97b --- /dev/null +++ b/wireplumber/LICENSE @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/wireplumber/README.md b/wireplumber/README.md new file mode 100644 index 0000000..cfbb6db --- /dev/null +++ b/wireplumber/README.md @@ -0,0 +1,34 @@ +# astal-wireplumber + +A libwireplumber wrapper. + +## Build from source +### Dependencies + +- meson +- libwireplumber +- glib +- gobject-introspection +- vala (only required for the vapi option) + +### Meson options + +* `-Dintrospection` (default: `true`): build GObject Introspection data (needed for language bindings) +* `-Dvapi` (default: `true`): build VAPI data (required to make this lib usable in vala). Requires `-Dintrospection=true` + +### build instructions + +```sh +# Clone the repository +git clone https://github.com/astal-sh/wireplumber +cd wireplumber + +# Setup and build +meson setup build +meson compile -C build + +# Install +meson install -C build +``` + + diff --git a/wireplumber/flake.lock b/wireplumber/flake.lock new file mode 100644 index 0000000..f1034b0 --- /dev/null +++ b/wireplumber/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1720957393, + "narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "693bc46d169f5af9c992095736e82c3488bf7dbb", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/wireplumber/flake.nix b/wireplumber/flake.nix new file mode 100644 index 0000000..96ffc6f --- /dev/null +++ b/wireplumber/flake.nix @@ -0,0 +1,54 @@ +{ + description = "Wrapper library for WirePlumber"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { + self, + nixpkgs, + }: let + version = builtins.replaceStrings ["\n"] [""] (builtins.readFile ./version); + system = "x86_64-linux"; + pkgs = import nixpkgs {inherit system;}; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + + buildInputs = with pkgs; [ + glib + wireplumber + # json-glib + ]; + in { + packages.${system} = rec { + default = wireplumber; + wireplumber = pkgs.stdenv.mkDerivation { + inherit nativeBuildInputs buildInputs; + pname = "astal-wireplumber"; + version = version; + src = ./.; + outputs = ["out" "dev"]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + inherit nativeBuildInputs buildInputs; + }; + wireplumber = pkgs.mkShell { + inherit nativeBuildInputs; + buildInputs = + buildInputs + ++ [ + self.packages.${system}.default + pkgs.gjs + ]; + }; + }; + }; +} diff --git a/wireplumber/include/astal-wp.h b/wireplumber/include/astal-wp.h new file mode 100644 index 0000000..6c48211 --- /dev/null +++ b/wireplumber/include/astal-wp.h @@ -0,0 +1,4 @@ + +#include "astal/wireplumber/audio.h" +#include "astal/wireplumber/endpoint.h" +#include "astal/wireplumber/wp.h" diff --git a/wireplumber/include/astal/wireplumber/audio.h b/wireplumber/include/astal/wireplumber/audio.h new file mode 100644 index 0000000..c1176e2 --- /dev/null +++ b/wireplumber/include/astal/wireplumber/audio.h @@ -0,0 +1,33 @@ +#ifndef ASTAL_WIREPLUMBER_AUDIO_H +#define ASTAL_WIREPLUMBER_AUDIO_H + +#include <glib-object.h> + +#include "device.h" +#include "endpoint.h" + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_AUDIO (astal_wp_audio_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpAudio, astal_wp_audio, ASTAL_WP, AUDIO, GObject) + +AstalWpEndpoint *astal_wp_audio_get_speaker(AstalWpAudio *self, guint id); +AstalWpEndpoint *astal_wp_audio_get_microphone(AstalWpAudio *self, guint id); +AstalWpEndpoint *astal_wp_audio_get_recorder(AstalWpAudio *self, guint id); +AstalWpEndpoint *astal_wp_audio_get_stream(AstalWpAudio *self, guint id); +AstalWpEndpoint *astal_wp_audio_get_endpoint(AstalWpAudio *self, guint id); +AstalWpDevice *astal_wp_audio_get_device(AstalWpAudio *self, guint id); + +AstalWpEndpoint *astal_wp_audio_get_default_speaker(AstalWpAudio *self); +AstalWpEndpoint *astal_wp_audio_get_default_microphone(AstalWpAudio *self); + +GList *astal_wp_audio_get_microphones(AstalWpAudio *self); +GList *astal_wp_audio_get_speakers(AstalWpAudio *self); +GList *astal_wp_audio_get_recorders(AstalWpAudio *self); +GList *astal_wp_audio_get_streams(AstalWpAudio *self); +GList *astal_wp_audio_get_devices(AstalWpAudio *self); + +G_END_DECLS + +#endif // !ASTAL_WIREPLUMBER_AUDIO_H diff --git a/wireplumber/include/astal/wireplumber/device.h b/wireplumber/include/astal/wireplumber/device.h new file mode 100644 index 0000000..9f633e3 --- /dev/null +++ b/wireplumber/include/astal/wireplumber/device.h @@ -0,0 +1,29 @@ +#ifndef ASTAL_WP_DEVICE_H +#define ASTAL_WP_DEVICE_H + +#include <glib-object.h> + +#include "profile.h" + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_DEVICE (astal_wp_device_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpDevice, astal_wp_device, ASTAL_WP, DEVICE, GObject) + +#define ASTAL_WP_TYPE_DEVICE_TYPE (astal_wp_device_type_get_type()) + +typedef enum { ASTAL_WP_DEVICE_TYPE_AUDIO, ASTAL_WP_DEVICE_TYPE_VIDEO } AstalWpDeviceType; + +guint astal_wp_device_get_id(AstalWpDevice *self); +const gchar *astal_wp_device_get_description(AstalWpDevice *self); +const gchar *astal_wp_device_get_icon(AstalWpDevice *self); +AstalWpProfile *astal_wp_device_get_profile(AstalWpDevice *self, gint id); +GList *astal_wp_device_get_profiles(AstalWpDevice *self); +void astal_wp_device_set_active_profile(AstalWpDevice *self, int profile_id); +gint astal_wp_device_get_active_profile(AstalWpDevice *self); +AstalWpDeviceType astal_wp_device_get_device_type(AstalWpDevice *self); + +G_END_DECLS + +#endif // !ASTAL_WP_DEVICE_H diff --git a/wireplumber/include/astal/wireplumber/endpoint.h b/wireplumber/include/astal/wireplumber/endpoint.h new file mode 100644 index 0000000..6ef0329 --- /dev/null +++ b/wireplumber/include/astal/wireplumber/endpoint.h @@ -0,0 +1,43 @@ +#ifndef ASTAL_WP_ENDPOINT_H +#define ASTAL_WP_ENDPOINT_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_ENDPOINT (astal_wp_endpoint_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpEndpoint, astal_wp_endpoint, ASTAL_WP, ENDPOINT, GObject) + +#define ASTAL_WP_TYPE_MEDIA_CLASS (astal_wp_media_class_get_type()) + +typedef enum { + ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE, + ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER, + ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER, + ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM, + ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE, + ASTAL_WP_MEDIA_CLASS_VIDEO_SINK, + ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER, + ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM, +} AstalWpMediaClass; + +void astal_wp_endpoint_set_volume(AstalWpEndpoint *self, gdouble volume); +void astal_wp_endpoint_set_mute(AstalWpEndpoint *self, gboolean mute); +gboolean astal_wp_endpoint_get_is_default(AstalWpEndpoint *self); +void astal_wp_endpoint_set_is_default(AstalWpEndpoint *self, gboolean is_default); +gboolean astal_wp_endpoint_get_lock_channels(AstalWpEndpoint *self); +void astal_wp_endpoint_set_lock_channels(AstalWpEndpoint *self, gboolean lock_channels); + +AstalWpMediaClass astal_wp_endpoint_get_media_class(AstalWpEndpoint *self); +guint astal_wp_endpoint_get_id(AstalWpEndpoint *self); +gboolean astal_wp_endpoint_get_mute(AstalWpEndpoint *self); +gdouble astal_wp_endpoint_get_volume(AstalWpEndpoint *self); +const gchar *astal_wp_endpoint_get_description(AstalWpEndpoint *self); +const gchar *astal_wp_endpoint_get_name(AstalWpEndpoint *self); +const gchar *astal_wp_endpoint_get_icon(AstalWpEndpoint *self); +const gchar *astal_wp_endpoint_get_volume_icon(AstalWpEndpoint *self); + +G_END_DECLS + +#endif // !ASTAL_WP_ENDPOINT_H diff --git a/wireplumber/include/astal/wireplumber/meson.build b/wireplumber/include/astal/wireplumber/meson.build new file mode 100644 index 0000000..d02563c --- /dev/null +++ b/wireplumber/include/astal/wireplumber/meson.build @@ -0,0 +1,10 @@ +astal_wireplumber_subheaders = files( + 'wp.h', + 'endpoint.h', + 'device.h', + 'video.h', + 'audio.h', + 'profile.h', +) + +install_headers(astal_wireplumber_subheaders, subdir : 'astal/wireplumber') diff --git a/wireplumber/include/astal/wireplumber/profile.h b/wireplumber/include/astal/wireplumber/profile.h new file mode 100644 index 0000000..2ec768e --- /dev/null +++ b/wireplumber/include/astal/wireplumber/profile.h @@ -0,0 +1,17 @@ +#ifndef ASTAL_WP_PROFILE_H +#define ASTAL_WP_PROFILE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_PROFILE (astal_wp_profile_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpProfile, astal_wp_profile, ASTAL_WP, PROFILE, GObject) + +gint astal_wp_profile_get_index(AstalWpProfile *self); +const gchar *astal_wp_profile_get_description(AstalWpProfile *self); + +G_END_DECLS + +#endif // !ASTAL_WP_PROFILE_H diff --git a/wireplumber/include/astal/wireplumber/video.h b/wireplumber/include/astal/wireplumber/video.h new file mode 100644 index 0000000..3c4ae74 --- /dev/null +++ b/wireplumber/include/astal/wireplumber/video.h @@ -0,0 +1,29 @@ +#ifndef ASTAL_WIREPLUMBER_VIDEO_H +#define ASTAL_WIREPLUMBER_VIDEO_H + +#include <glib-object.h> + +#include "device.h" +#include "endpoint.h" + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_VIDEO (astal_wp_video_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpVideo, astal_wp_video, ASTAL_WP, VIDEO, GObject) + +AstalWpEndpoint *astal_wp_video_get_source(AstalWpVideo *self, guint id); +AstalWpEndpoint *astal_wp_video_get_sink(AstalWpVideo *self, guint id); +AstalWpEndpoint *astal_wp_video_get_recorder(AstalWpVideo *self, guint id); +AstalWpEndpoint *astal_wp_video_get_stream(AstalWpVideo *self, guint id); +AstalWpDevice *astal_wp_video_get_device(AstalWpVideo *self, guint id); + +GList *astal_wp_video_get_sources(AstalWpVideo *self); +GList *astal_wp_video_get_sinks(AstalWpVideo *self); +GList *astal_wp_video_get_recorders(AstalWpVideo *self); +GList *astal_wp_video_get_streams(AstalWpVideo *self); +GList *astal_wp_video_get_devices(AstalWpVideo *self); + +G_END_DECLS + +#endif // !ASTAL_WIREPLUMBER_VIDEO_H diff --git a/wireplumber/include/astal/wireplumber/wp.h b/wireplumber/include/astal/wireplumber/wp.h new file mode 100644 index 0000000..1ff341c --- /dev/null +++ b/wireplumber/include/astal/wireplumber/wp.h @@ -0,0 +1,47 @@ +#ifndef ASTAL_WIREPLUMBER_H +#define ASTAL_WIREPLUMBER_H + +#include <glib-object.h> + +#include "audio.h" +#include "device.h" +#include "endpoint.h" +#include "video.h" + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_SCALE (astal_wp_scale_get_type()) + +typedef enum { + ASTAL_WP_SCALE_LINEAR, + ASTAL_WP_SCALE_CUBIC, +} AstalWpScale; + +#define ASTAL_WP_TYPE_WP (astal_wp_wp_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpWp, astal_wp_wp, ASTAL_WP, WP, GObject) + +AstalWpWp* astal_wp_wp_get_default(); +AstalWpWp* astal_wp_get_default_wp(); + +AstalWpAudio* astal_wp_wp_get_audio(AstalWpWp* self); +AstalWpVideo* astal_wp_wp_get_video(AstalWpWp* self); + +AstalWpEndpoint* astal_wp_wp_get_endpoint(AstalWpWp* self, guint id); +GList* astal_wp_wp_get_endpoints(AstalWpWp* self); + +AstalWpDevice* astal_wp_wp_get_device(AstalWpWp* self, guint id); +GList* astal_wp_wp_get_devices(AstalWpWp* self); + +AstalWpEndpoint* astal_wp_wp_get_default_speaker(AstalWpWp* self); +AstalWpEndpoint* astal_wp_wp_get_default_microphone(AstalWpWp* self); + +AstalWpScale astal_wp_wp_get_scale(AstalWpWp* self); +void astal_wp_wp_set_scale(AstalWpWp* self, AstalWpScale scale); + +AstalWpVideo* astal_wp_video_new(AstalWpWp* wp); +AstalWpAudio* astal_wp_audio_new(AstalWpWp* wp); + +G_END_DECLS + +#endif // !ASTAL_WIREPLUMBER_H diff --git a/wireplumber/include/meson.build b/wireplumber/include/meson.build new file mode 100644 index 0000000..441097c --- /dev/null +++ b/wireplumber/include/meson.build @@ -0,0 +1,8 @@ +astal_wireplumber_inc = include_directories('.', 'astal/wireplumber', 'private') +astal_wireplumber_headers = files( + 'astal-wp.h', +) + +install_headers(astal_wireplumber_headers) + +subdir('astal/wireplumber') diff --git a/wireplumber/include/private/device-private.h b/wireplumber/include/private/device-private.h new file mode 100644 index 0000000..e98a7f7 --- /dev/null +++ b/wireplumber/include/private/device-private.h @@ -0,0 +1,15 @@ +#ifndef ASTAL_WP_DEVICE_PRIVATE_H +#define ASTAL_WP_DEVICE_PRIVATE_H + +#include <glib-object.h> +#include <wp/wp.h> + +#include "device.h" + +G_BEGIN_DECLS + +AstalWpDevice *astal_wp_device_create(WpDevice *device); + +G_END_DECLS + +#endif // !ASTAL_WP_DEVICE_PRIATE_H diff --git a/wireplumber/include/private/endpoint-private.h b/wireplumber/include/private/endpoint-private.h new file mode 100644 index 0000000..7431c78 --- /dev/null +++ b/wireplumber/include/private/endpoint-private.h @@ -0,0 +1,22 @@ +#ifndef ASTAL_WP_ENDPOINT_PRIV_H +#define ASTAL_WP_ENDPOINT_PRIV_H + +#include <glib-object.h> +#include <wp/wp.h> + +#include "endpoint.h" +#include "wp.h" + +G_BEGIN_DECLS + +AstalWpEndpoint *astal_wp_endpoint_create(WpNode *node, WpPlugin *mixer, WpPlugin *defaults, + AstalWpWp *wp); +AstalWpEndpoint *astal_wp_endpoint_init_as_default(AstalWpEndpoint *self, WpPlugin *mixer, + WpPlugin *defaults, AstalWpMediaClass type, + AstalWpWp *wp); +void astal_wp_endpoint_update_default(AstalWpEndpoint *self, gboolean is_default); +void astal_wp_endpoint_update_volume(AstalWpEndpoint *self); + +G_END_DECLS + +#endif // !ASTAL_WP_ENDPOINT_PRIV_H diff --git a/wireplumber/meson.build b/wireplumber/meson.build new file mode 100644 index 0000000..b295b31 --- /dev/null +++ b/wireplumber/meson.build @@ -0,0 +1,22 @@ +project('astal_wireplumber', + 'c', + version : '0.1.0', + default_options : [ + 'c_std=gnu11', + 'warning_level=3', + 'prefix=/usr' + ] +) + +add_project_arguments( + ['-Wno-pedantic', '-Wno-unused-parameter'], + language : 'c') + +version_split = meson.project_version().split('.') +lib_so_version = version_split[0] + '.' + version_split[1] + +pkg_config = import('pkgconfig') +gnome = import('gnome') + +subdir('include') +subdir('src') diff --git a/wireplumber/meson_options.txt b/wireplumber/meson_options.txt new file mode 100644 index 0000000..d585854 --- /dev/null +++ b/wireplumber/meson_options.txt @@ -0,0 +1,2 @@ +option('introspection', type : 'boolean', value : true, description : 'Build gobject-introspection data') +option('vapi', type : 'boolean', value : true, description : 'Generate vapi data (needs vapigen & introspection option)') diff --git a/wireplumber/src/audio.c b/wireplumber/src/audio.c new file mode 100644 index 0000000..15582d7 --- /dev/null +++ b/wireplumber/src/audio.c @@ -0,0 +1,503 @@ +#include "audio.h" + +#include <wp/wp.h> + +#include "device.h" +#include "endpoint.h" +#include "glib-object.h" +#include "wp.h" + +struct _AstalWpAudio { + GObject parent_instance; +}; + +typedef struct { + AstalWpWp *wp; +} AstalWpAudioPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpAudio, astal_wp_audio, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_ADDED, + ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_REMOVED, + ASTAL_WP_AUDIO_SIGNAL_SPEAKER_ADDED, + ASTAL_WP_AUDIO_SIGNAL_SPEAKER_REMOVED, + ASTAL_WP_AUDIO_SIGNAL_STREAM_ADDED, + ASTAL_WP_AUDIO_SIGNAL_STREAM_REMOVED, + ASTAL_WP_AUDIO_SIGNAL_RECORDER_ADDED, + ASTAL_WP_AUDIO_SIGNAL_RECORDER_REMOVED, + ASTAL_WP_AUDIO_SIGNAL_DEVICE_ADDED, + ASTAL_WP_AUDIO_SIGNAL_DEVICE_REMOVED, + ASTAL_WP_AUDIO_N_SIGNALS +} AstalWpWpSignals; + +static guint astal_wp_audio_signals[ASTAL_WP_AUDIO_N_SIGNALS] = { + 0, +}; + +typedef enum { + ASTAL_WP_AUDIO_PROP_MICROPHONES = 1, + ASTAL_WP_AUDIO_PROP_SPEAKERS, + ASTAL_WP_AUDIO_PROP_STREAMS, + ASTAL_WP_AUDIO_PROP_RECORDERS, + ASTAL_WP_AUDIO_PROP_DEVICES, + ASTAL_WP_AUDIO_PROP_DEFAULT_SPEAKER, + ASTAL_WP_AUDIO_PROP_DEFAULT_MICROPHONE, + ASTAL_WP_AUDIO_N_PROPERTIES, +} AstalWpAudioProperties; + +static GParamSpec *astal_wp_audio_properties[ASTAL_WP_AUDIO_N_PROPERTIES] = { + NULL, +}; + +/** + * astal_wp_audio_get_speaker: + * @self: the AstalWpAudio object + * @id: the id of the endpoint + * + * gets the speaker with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpEndpoint *astal_wp_audio_get_speaker(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER) + return endpoint; + return NULL; +} + +/** + * astal_wp_audio_get_microphone: + * @self: the AstalWpAudio object + * @id: the id of the endpoint + * + * gets the microphone with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpEndpoint *astal_wp_audio_get_microphone(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE) + return endpoint; + return NULL; +} + +/** + * astal_wp_audio_get_recorder: + * @self: the AstalWpAudio object + * @id: the id of the endpoint + * + * gets the recorder with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpEndpoint *astal_wp_audio_get_recorder(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER) + return endpoint; + return NULL; +} + +/** + * astal_wp_audio_get_stream: + * @self: the AstalWpAudio object + * @id: the id of the endpoint + * + * gets the stream with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpEndpoint *astal_wp_audio_get_stream(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM) + return endpoint; + return NULL; +} + +/** + * astal_wp_audio_get_device: + * @self: the AstalWpAudio object + * @id: the id of the device + * + * gets the device with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpDevice *astal_wp_audio_get_device(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + return astal_wp_wp_get_device(priv->wp, id); +} + +/** + * astal_wp_audio_get_microphones: + * @self: the AstalWpAudio object + * + * a GList containing the microphones + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)) + */ +GList *astal_wp_audio_get_microphones(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *mics = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE) { + mics = g_list_append(mics, l->data); + } + } + g_list_free(eps); + return mics; +} + +/** + * astal_wp_audio_get_speakers: + * @self: the AstalWpAudio object + * + * a GList containing the speakers + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)) + */ +GList *astal_wp_audio_get_speakers(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *speakers = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER) { + speakers = g_list_append(speakers, l->data); + } + } + g_list_free(eps); + return speakers; +} + +/** + * astal_wp_audio_get_recorders: + * @self: the AstalWpAudio object + * + * a GList containing the recorders + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)) + */ +GList *astal_wp_audio_get_recorders(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *recorders = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER) { + recorders = g_list_append(recorders, l->data); + } + } + g_list_free(eps); + return recorders; +} + +/** + * astal_wp_audio_get_streams: + * @self: the AstalWpAudio object + * + * a GList containing the streams + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)) + */ +GList *astal_wp_audio_get_streams(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *streams = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM) { + streams = g_list_append(streams, l->data); + } + } + g_list_free(eps); + return streams; +} + +/** + * astal_wp_audio_get_devices: + * @self: the AstalWpAudio object + * + * a GList containing the devices + * + * Returns: (transfer container) (nullable) (type GList(AstalWpDevice)) + */ +GList *astal_wp_audio_get_devices(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_devices(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_device_get_device_type(l->data) == ASTAL_WP_DEVICE_TYPE_AUDIO) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +/** + * astal_wp_audio_get_endpoint: + * @self: the AstalWpAudio object + * @id: the id of the endpoint + * + * the endpoint with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpEndpoint *astal_wp_audio_get_endpoint(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + return endpoint; +} + +/** + * astal_wp_audio_get_default_speaker + * + * gets the default speaker object + * + * Returns: (nullable) (transfer none) + */ +AstalWpEndpoint *astal_wp_audio_get_default_speaker(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + return astal_wp_wp_get_default_speaker(priv->wp); +} + +/** + * astal_wp_audio_get_default_microphone + * + * gets the default microphone object + * + * Returns: (nullable) (transfer none) + */ +AstalWpEndpoint *astal_wp_audio_get_default_microphone(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + return astal_wp_wp_get_default_microphone(priv->wp); +} + +static void astal_wp_audio_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpAudio *self = ASTAL_WP_AUDIO(object); + + switch (property_id) { + case ASTAL_WP_AUDIO_PROP_MICROPHONES: + g_value_set_pointer(value, astal_wp_audio_get_microphones(self)); + break; + case ASTAL_WP_AUDIO_PROP_SPEAKERS: + g_value_set_pointer(value, astal_wp_audio_get_speakers(self)); + break; + case ASTAL_WP_AUDIO_PROP_STREAMS: + g_value_set_pointer(value, astal_wp_audio_get_streams(self)); + break; + case ASTAL_WP_AUDIO_PROP_RECORDERS: + g_value_set_pointer(value, astal_wp_audio_get_recorders(self)); + break; + case ASTAL_WP_AUDIO_PROP_DEFAULT_SPEAKER: + g_value_set_object(value, astal_wp_audio_get_default_speaker(self)); + break; + case ASTAL_WP_AUDIO_PROP_DEVICES: + g_value_set_pointer(value, astal_wp_audio_get_devices(self)); + break; + case ASTAL_WP_AUDIO_PROP_DEFAULT_MICROPHONE: + g_value_set_object(value, astal_wp_audio_get_default_microphone(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_audio_device_added(AstalWpAudio *self, gpointer object) { + AstalWpDevice *device = ASTAL_WP_DEVICE(object); + if (astal_wp_device_get_device_type(device) == ASTAL_WP_DEVICE_TYPE_AUDIO) { + g_signal_emit_by_name(self, "device-added", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_audio_device_removed(AstalWpAudio *self, gpointer object) { + AstalWpDevice *device = ASTAL_WP_DEVICE(object); + if (astal_wp_device_get_device_type(device) == ASTAL_WP_DEVICE_TYPE_AUDIO) { + g_signal_emit_by_name(self, "device-removed", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_audio_object_added(AstalWpAudio *self, gpointer object) { + AstalWpEndpoint *endpoint = ASTAL_WP_ENDPOINT(object); + switch (astal_wp_endpoint_get_media_class(endpoint)) { + case ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE: + g_signal_emit_by_name(self, "microphone-added", endpoint); + g_object_notify(G_OBJECT(self), "microphones"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER: + g_signal_emit_by_name(self, "speaker-added", endpoint); + g_object_notify(G_OBJECT(self), "speakers"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM: + g_signal_emit_by_name(self, "stream-added", endpoint); + g_object_notify(G_OBJECT(self), "streams"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER: + g_signal_emit_by_name(self, "recorder-added", endpoint); + g_object_notify(G_OBJECT(self), "recorders"); + break; + default: + break; + } +} + +static void astal_wp_audio_object_removed(AstalWpAudio *self, gpointer object) { + AstalWpEndpoint *endpoint = ASTAL_WP_ENDPOINT(object); + switch (astal_wp_endpoint_get_media_class(endpoint)) { + case ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE: + g_signal_emit_by_name(self, "microphone-removed", endpoint); + g_object_notify(G_OBJECT(self), "microphones"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER: + g_signal_emit_by_name(self, "speaker-removed", endpoint); + g_object_notify(G_OBJECT(self), "speakers"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM: + g_signal_emit_by_name(self, "stream-removed", endpoint); + g_object_notify(G_OBJECT(self), "streams"); + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER: + g_signal_emit_by_name(self, "recorder-removed", endpoint); + g_object_notify(G_OBJECT(self), "recorders"); + break; + default: + break; + } +} + +AstalWpAudio *astal_wp_audio_new(AstalWpWp *wp) { + AstalWpAudio *self = g_object_new(ASTAL_WP_TYPE_AUDIO, NULL); + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + priv->wp = g_object_ref(wp); + + g_signal_connect_swapped(priv->wp, "endpoint-added", G_CALLBACK(astal_wp_audio_object_added), + self); + g_signal_connect_swapped(priv->wp, "endpoint-removed", + G_CALLBACK(astal_wp_audio_object_removed), self); + g_signal_connect_swapped(priv->wp, "device-added", G_CALLBACK(astal_wp_audio_device_added), + self); + g_signal_connect_swapped(priv->wp, "device-removed", G_CALLBACK(astal_wp_audio_device_removed), + self); + + return self; +} + +static void astal_wp_audio_dispose(GObject *object) { + AstalWpAudio *self = ASTAL_WP_AUDIO(object); + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + g_clear_object(&priv->wp); +} + +static void astal_wp_audio_init(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); +} + +static void astal_wp_audio_class_init(AstalWpAudioClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->get_property = astal_wp_audio_get_property; + object_class->dispose = astal_wp_audio_dispose; + + /** + * AstalWpAudio:microphones: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_MICROPHONES] = + g_param_spec_pointer("microphones", "microphones", "microphones", G_PARAM_READABLE); + /** + * AstalWpAudio:speakers: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_SPEAKERS] = + g_param_spec_pointer("speakers", "speakers", "speakers", G_PARAM_READABLE); + /** + * AstalWpAudio:recorders: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_RECORDERS] = + g_param_spec_pointer("recorders", "recorders", "recorders", G_PARAM_READABLE); + /** + * AstalWpAudio:streams: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_STREAMS] = + g_param_spec_pointer("streams", "streams", "streams", G_PARAM_READABLE); + /** + * AstalWpAudio:devices: (type GList(AstalWpDevice)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_DEVICES] = + g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE); + /** + * AstalWpAudio:default-speaker: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_DEFAULT_SPEAKER] = + g_param_spec_object("default-speaker", "default-speaker", "default-speaker", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + /** + * AstalWpAudio:default-microphone: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_DEFAULT_MICROPHONE] = + g_param_spec_object("default-microphone", "default-microphone", "default-microphone", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_WP_AUDIO_N_PROPERTIES, + astal_wp_audio_properties); + + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_ADDED] = + g_signal_new("microphone-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_REMOVED] = + g_signal_new("microphone-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_SPEAKER_ADDED] = + g_signal_new("speaker-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_SPEAKER_REMOVED] = + g_signal_new("speaker-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_STREAM_ADDED] = + g_signal_new("stream-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_STREAM_REMOVED] = + g_signal_new("stream-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_RECORDER_ADDED] = + g_signal_new("recorder-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_RECORDER_REMOVED] = + g_signal_new("recorder-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_DEVICE_ADDED] = + g_signal_new("device-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); + astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_REMOVED] = + g_signal_new("device-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); +} diff --git a/wireplumber/src/device.c b/wireplumber/src/device.c new file mode 100644 index 0000000..af0760c --- /dev/null +++ b/wireplumber/src/device.c @@ -0,0 +1,371 @@ +#include <wp/wp.h> + +#include "device-private.h" +#include "profile.h" + +struct _AstalWpDevice { + GObject parent_instance; + + guint id; + gchar *description; + gchar *icon; + gint active_profile; + AstalWpDeviceType type; +}; + +typedef struct { + WpDevice *device; + GHashTable *profiles; +} AstalWpDevicePrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpDevice, astal_wp_device, G_TYPE_OBJECT); + +G_DEFINE_ENUM_TYPE(AstalWpDeviceType, astal_wp_device_type, + G_DEFINE_ENUM_VALUE(ASTAL_WP_DEVICE_TYPE_AUDIO, "Audio/Device"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_DEVICE_TYPE_VIDEO, "Video/Device")); + +typedef enum { + ASTAL_WP_DEVICE_PROP_ID = 1, + ASTAL_WP_DEVICE_PROP_DESCRIPTION, + ASTAL_WP_DEVICE_PROP_ICON, + ASTAL_WP_DEVICE_PROP_PROFILES, + ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE, + ASTAL_WP_DEVICE_PROP_DEVICE_TYPE, + ASTAL_WP_DEVICE_N_PROPERTIES, +} AstalWpDeviceProperties; + +static GParamSpec *astal_wp_device_properties[ASTAL_WP_DEVICE_N_PROPERTIES] = { + NULL, +}; + +/** + * astal_wp_device_get_id + * @self: the AstalWpDevice object + * + * gets the id of this device + * + */ +guint astal_wp_device_get_id(AstalWpDevice *self) { return self->id; } + +/** + * astal_wp_device_get_description + * @self: the AstalWpDevice object + * + * gets the description of this device + * + */ +const gchar *astal_wp_device_get_description(AstalWpDevice *self) { return self->description; } + +/** + * astal_wp_device_get_icon + * @self: the AstalWpDevice object + * + * gets the icon of this device + * + */ +const gchar *astal_wp_device_get_icon(AstalWpDevice *self) { + g_return_val_if_fail(self != NULL, "audio-card-symbolic"); + return self->icon; +} + +/** + * astal_wp_device_get_device_type + * @self: the AstalWpDevice object + * + * gets the type of this device + * + */ +AstalWpDeviceType astal_wp_device_get_device_type(AstalWpDevice *self) { return self->type; } + +/** + * astal_wp_device_get_active_profile + * @self: the AstalWpDevice object + * + * gets the currently active profile of this device + * + */ +gint astal_wp_device_get_active_profile(AstalWpDevice *self) { return self->active_profile; } + +/** + * astal_wp_device_set_active_profile + * @self: the AstalWpDevice object + * @profile_id: the id of the profile + * + * sets the profile for this device + * + */ +void astal_wp_device_set_active_profile(AstalWpDevice *self, int profile_id) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + WpSpaPodBuilder *builder = + wp_spa_pod_builder_new_object("Spa:Pod:Object:Param:Profile", "Profile"); + wp_spa_pod_builder_add_property(builder, "index"); + wp_spa_pod_builder_add_int(builder, profile_id); + WpSpaPod *pod = wp_spa_pod_builder_end(builder); + wp_pipewire_object_set_param(WP_PIPEWIRE_OBJECT(priv->device), "Profile", 0, pod); + + wp_spa_pod_builder_unref(builder); +} + +/** + * astal_wp_device_get_profile: + * @self: the AstalWpDevice object + * @id: the id of the profile + * + * gets the profile with the given id + * + * Returns: (transfer none) (nullable) + */ +AstalWpProfile *astal_wp_device_get_profile(AstalWpDevice *self, gint id) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + return g_hash_table_lookup(priv->profiles, GINT_TO_POINTER(id)); +} + +/** + * astal_wp_device_get_profiles: + * @self: the AstalWpDevice object + * + * gets a GList containing the profiles + * + * Returns: (transfer container) (nullable) (type GList(AstalWpProfile)) + */ +GList *astal_wp_device_get_profiles(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + return g_hash_table_get_values(priv->profiles); +} + +static void astal_wp_device_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + + switch (property_id) { + case ASTAL_WP_DEVICE_PROP_ID: + g_value_set_uint(value, self->id); + break; + case ASTAL_WP_DEVICE_PROP_DESCRIPTION: + g_value_set_string(value, self->description); + break; + case ASTAL_WP_DEVICE_PROP_ICON: + g_value_set_string(value, self->icon); + break; + case ASTAL_WP_DEVICE_PROP_PROFILES: + g_value_set_pointer(value, astal_wp_device_get_profiles(self)); + break; + case ASTAL_WP_DEVICE_PROP_DEVICE_TYPE: + g_value_set_enum(value, astal_wp_device_get_device_type(self)); + break; + case ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE: + g_value_set_int(value, self->active_profile); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_device_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + + switch (property_id) { + case ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE: + astal_wp_device_set_active_profile(self, g_value_get_int(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_device_update_profiles(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + g_hash_table_remove_all(priv->profiles); + + WpIterator *iter = + wp_pipewire_object_enum_params_sync(WP_PIPEWIRE_OBJECT(priv->device), "EnumProfile", NULL); + if (iter == NULL) return; + GValue profile = G_VALUE_INIT; + while (wp_iterator_next(iter, &profile)) { + WpSpaPod *pod = g_value_get_boxed(&profile); + + gint index; + gchar *description; + wp_spa_pod_get_object(pod, NULL, "index", "i", &index, "description", "s", &description, + NULL); + + g_hash_table_insert( + priv->profiles, GINT_TO_POINTER(index), + g_object_new(ASTAL_WP_TYPE_PROFILE, "index", index, "description", description, NULL)); + g_value_unset(&profile); + } + wp_iterator_unref(iter); + + g_object_notify(G_OBJECT(self), "profiles"); +} + +static void astal_wp_device_update_active_profile(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + WpIterator *iter = + wp_pipewire_object_enum_params_sync(WP_PIPEWIRE_OBJECT(priv->device), "Profile", NULL); + if (iter == NULL) return; + GValue profile = G_VALUE_INIT; + while (wp_iterator_next(iter, &profile)) { + WpSpaPod *pod = g_value_get_boxed(&profile); + + gint index; + gchar *description; + wp_spa_pod_get_object(pod, NULL, "index", "i", &index, "description", "s", &description, + NULL); + + g_hash_table_insert( + priv->profiles, GINT_TO_POINTER(index), + g_object_new(ASTAL_WP_TYPE_PROFILE, "index", index, "description", description, NULL)); + + self->active_profile = index; + g_value_unset(&profile); + } + wp_iterator_unref(iter); + + g_object_notify(G_OBJECT(self), "active-profile-id"); +} + +static void astal_wp_device_params_changed(AstalWpDevice *self, const gchar *prop) { + if (g_strcmp0(prop, "EnumProfile") == 0) { + astal_wp_device_update_profiles(self); + } else if (g_strcmp0(prop, "Profile") == 0) { + astal_wp_device_update_active_profile(self); + } +} + +static void astal_wp_device_update_properties(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + if (priv->device == NULL) return; + self->id = wp_proxy_get_bound_id(WP_PROXY(priv->device)); + const gchar *description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.description"); + if (description == NULL) { + description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.name"); + } + if (description == NULL) { + description = "unknown"; + } + g_free(self->description); + self->description = g_strdup(description); + + const gchar *icon = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.icon-name"); + if (icon == NULL) { + icon = "audio-card-symbolic"; + } + g_free(self->icon); + self->icon = g_strdup(icon); + + const gchar *type = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "media.class"); + GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_DEVICE_TYPE); + if (g_enum_get_value_by_nick(enum_class, type) != NULL) + self->type = g_enum_get_value_by_nick(enum_class, type)->value; + g_type_class_unref(enum_class); + + astal_wp_device_update_profiles(self); + astal_wp_device_update_active_profile(self); + + g_object_notify(G_OBJECT(self), "id"); + g_object_notify(G_OBJECT(self), "device-type"); + g_object_notify(G_OBJECT(self), "icon"); + g_object_notify(G_OBJECT(self), "description"); +} + +AstalWpDevice *astal_wp_device_create(WpDevice *device) { + AstalWpDevice *self = g_object_new(ASTAL_WP_TYPE_DEVICE, NULL); + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + priv->device = g_object_ref(device); + + g_signal_connect_swapped(priv->device, "params-changed", + G_CALLBACK(astal_wp_device_params_changed), self); + + astal_wp_device_update_properties(self); + return self; +} + +static void astal_wp_device_init(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + priv->device = NULL; + + priv->profiles = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + + self->description = NULL; + self->icon = NULL; +} + +static void astal_wp_device_dispose(GObject *object) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + g_clear_object(&priv->device); +} + +static void astal_wp_device_finalize(GObject *object) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + g_free(self->description); + g_free(self->icon); +} + +static void astal_wp_device_class_init(AstalWpDeviceClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->dispose = astal_wp_device_dispose; + object_class->finalize = astal_wp_device_finalize; + object_class->get_property = astal_wp_device_get_property; + object_class->set_property = astal_wp_device_set_property; + /** + * AstalWpDevice:id + * + * The id of this device. + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ID] = + g_param_spec_uint("id", "id", "id", 0, UINT_MAX, 0, G_PARAM_READABLE); + /** + * AstalWpDevice:description + * + * The description of this device. + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, G_PARAM_READABLE); + /** + * AstalWpDevice:icon + * + * The icon name for this device. + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ICON] = + g_param_spec_string("icon", "icon", "icon", NULL, G_PARAM_READABLE); + /** + * AstalWpDevice:device-type: (type AstalWpDeviceType) + * + * The type of this device + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_DEVICE_TYPE] = + g_param_spec_enum("device-type", "device-type", "device-type", ASTAL_WP_TYPE_DEVICE_TYPE, 1, + G_PARAM_READABLE); + /** + * AstalWpDevice:profiles: (type GList(AstalWpProfile)) (transfer container) + * + * A list of available profiles + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_PROFILES] = + g_param_spec_pointer("profiles", "profiles", "profiles", G_PARAM_READABLE); + /** + * AstalWpDevice:active-profile-id + * + * The id of the currently active profile. + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE] = + g_param_spec_int("active-profile-id", "active-profile-id", "active-profile-id", G_MININT, + G_MAXINT, 0, G_PARAM_READWRITE); + + g_object_class_install_properties(object_class, ASTAL_WP_DEVICE_N_PROPERTIES, + astal_wp_device_properties); +} diff --git a/wireplumber/src/endpoint.c b/wireplumber/src/endpoint.c new file mode 100644 index 0000000..13979d1 --- /dev/null +++ b/wireplumber/src/endpoint.c @@ -0,0 +1,554 @@ +#include "endpoint.h" + +#include <wp/wp.h> + +#include "device.h" +#include "endpoint-private.h" +#include "glib.h" +#include "wp.h" + +struct _AstalWpEndpoint { + GObject parent_instance; + + guint id; + gdouble volume; + gboolean mute; + gchar *description; + gchar *name; + AstalWpMediaClass type; + gboolean is_default; + gboolean lock_channels; + + gchar *icon; +}; + +typedef struct { + WpNode *node; + WpPlugin *mixer; + WpPlugin *defaults; + AstalWpWp *wp; + + gboolean is_default_node; + AstalWpMediaClass media_class; + + gulong default_signal_handler_id; + gulong mixer_signal_handler_id; + +} AstalWpEndpointPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpEndpoint, astal_wp_endpoint, G_TYPE_OBJECT); + +G_DEFINE_ENUM_TYPE(AstalWpMediaClass, astal_wp_media_class, + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE, "Audio/Source"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER, "Audio/Sink"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER, "Stream/Input/Audio"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM, "Stream/Output/Audio"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE, "Video/Source"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_VIDEO_SINK, "Video/Sink"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER, "Stream/Input/Video"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM, "Stream/Output/Video")); + +typedef enum { + ASTAL_WP_ENDPOINT_PROP_ID = 1, + ASTAL_WP_ENDPOINT_PROP_VOLUME, + ASTAL_WP_ENDPOINT_PROP_MUTE, + ASTAL_WP_ENDPOINT_PROP_DESCRIPTION, + ASTAL_WP_ENDPOINT_PROP_NAME, + ASTAL_WP_ENDPOINT_PROP_MEDIA_CLASS, + ASTAL_WP_ENDPOINT_PROP_DEFAULT, + ASTAL_WP_ENDPOINT_PROP_ICON, + ASTAL_WP_ENDPOINT_PROP_VOLUME_ICON, + ASTAL_WP_ENDPOINT_PROP_LOCK_CHANNELS, + ASTAL_WP_ENDPOINT_N_PROPERTIES, +} AstalWpEndpointProperties; + +static GParamSpec *astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_N_PROPERTIES] = { + NULL, +}; + +void astal_wp_endpoint_update_volume(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + gdouble volume = 0; + gboolean mute; + GVariant *variant = NULL; + GVariantIter *channels = NULL; + + g_signal_emit_by_name(priv->mixer, "get-volume", self->id, &variant); + + if (variant == NULL) return; + + g_variant_lookup(variant, "volume", "d", &volume); + g_variant_lookup(variant, "mute", "b", &mute); + g_variant_lookup(variant, "channelVolumes", "a{sv}", &channels); + + if (channels != NULL) { + const gchar *key; + const gchar *channel_str; + gdouble channel_volume; + GVariant *varvol; + + while (g_variant_iter_loop(channels, "{&sv}", &key, &varvol)) { + g_variant_lookup(varvol, "volume", "d", &channel_volume); + g_variant_lookup(varvol, "channel", "&s", &channel_str); + if (channel_volume > volume) volume = channel_volume; + } + } + + if (mute != self->mute) { + self->mute = mute; + g_object_notify(G_OBJECT(self), "mute"); + } + + if (volume != self->volume) { + self->volume = volume; + g_object_notify(G_OBJECT(self), "volume"); + } + + g_object_notify(G_OBJECT(self), "volume-icon"); +} + +/** + * astal_wp_endpoint_set_volume: + * @self: the AstalWpEndpoint object + * @volume: The new volume level to set. + * + * Sets the volume level for this endpoint. The volume is clamped to be between + * 0 and 1.5. + */ +void astal_wp_endpoint_set_volume(AstalWpEndpoint *self, gdouble volume) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + gboolean ret; + if (volume >= 1.5) volume = 1.5; + if (volume <= 0) volume = 0; + + gboolean mute; + GVariant *variant = NULL; + GVariantIter *channels = NULL; + + g_auto(GVariantBuilder) vol_b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + g_signal_emit_by_name(priv->mixer, "get-volume", self->id, &variant); + + if (variant == NULL) return; + + g_variant_lookup(variant, "mute", "b", &mute); + g_variant_lookup(variant, "channelVolumes", "a{sv}", &channels); + + if (channels != NULL && !self->lock_channels) { + g_auto(GVariantBuilder) channel_volumes_b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + + const gchar *key; + const gchar *channel_str; + gdouble channel_volume; + GVariant *varvol; + + while (g_variant_iter_loop(channels, "{&sv}", &key, &varvol)) { + g_auto(GVariantBuilder) channel_b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + g_variant_lookup(varvol, "volume", "d", &channel_volume); + g_variant_lookup(varvol, "channel", "&s", &channel_str); + gdouble vol = self->volume == 0 ? volume : channel_volume * volume / self->volume; + g_variant_builder_add(&channel_b, "{sv}", "volume", g_variant_new_double(vol)); + g_variant_builder_add(&channel_volumes_b, "{sv}", key, + g_variant_builder_end(&channel_b)); + } + + g_variant_builder_add(&vol_b, "{sv}", "channelVolumes", + g_variant_builder_end(&channel_volumes_b)); + } else { + GVariant *volume_variant = g_variant_new_double(volume); + g_variant_builder_add(&vol_b, "{sv}", "volume", volume_variant); + } + + g_signal_emit_by_name(priv->mixer, "set-volume", self->id, g_variant_builder_end(&vol_b), &ret); +} + +/** + * astal_wp_endpoint_set_mute: + * @self: the AstalWpEndpoint instance. + * @mute: A boolean indicating whether to mute the endpoint. + * + * Sets the mute status for the endpoint. + */ +void astal_wp_endpoint_set_mute(AstalWpEndpoint *self, gboolean mute) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + gboolean ret; + GVariant *variant = NULL; + GVariantBuilder b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&b, "{sv}", "mute", g_variant_new_boolean(mute)); + variant = g_variant_builder_end(&b); + + g_signal_emit_by_name(priv->mixer, "set-volume", self->id, variant, &ret); +} + +/** + * astal_wp_endpoint_get_media_class: + * @self: the AstalWpEndpoint instance. + * + * gets the media class of the endpoint. + */ +AstalWpMediaClass astal_wp_endpoint_get_media_class(AstalWpEndpoint *self) { return self->type; } + +/** + * astal_wp_endpoint_get_id: + * @self: the AstalWpEndpoint instance. + * + * gets the id of the endpoint. + */ +guint astal_wp_endpoint_get_id(AstalWpEndpoint *self) { return self->id; } + +/** + * astal_wp_endpoint_get_mute: + * @self: the AstalWpEndpoint instance. + * + * gets the mute status of the endpoint. + */ +gboolean astal_wp_endpoint_get_mute(AstalWpEndpoint *self) { return self->mute; } + +/** + * astal_wp_endpoint_get_volume: + * @self: the AstalWpEndpoint instance. + * + * gets the volume + */ +gdouble astal_wp_endpoint_get_volume(AstalWpEndpoint *self) { return self->volume; } + +const gchar *astal_wp_endpoint_get_description(AstalWpEndpoint *self) { return self->description; } + +const gchar *astal_wp_endpoint_get_name(AstalWpEndpoint *self) { return self->name; } + +const gchar *astal_wp_endpoint_get_icon(AstalWpEndpoint *self) { return self->icon; } + +gboolean astal_wp_endpoint_get_is_default(AstalWpEndpoint *self) { return self->is_default; } + +void astal_wp_endpoint_set_is_default(AstalWpEndpoint *self, gboolean is_default) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + if (!is_default) return; + gboolean ret; + const gchar *name = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "node.name"); + const gchar *media_class = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.class"); + g_signal_emit_by_name(priv->defaults, "set-default-configured-node-name", media_class, name, + &ret); +} + +gboolean astal_wp_endpoint_get_lock_channels(AstalWpEndpoint *self) { return self->lock_channels; } + +void astal_wp_endpoint_set_lock_channels(AstalWpEndpoint *self, gboolean lock_channels) { + self->lock_channels = lock_channels; + astal_wp_endpoint_set_volume(self, self->volume); +} + +const gchar *astal_wp_endpoint_get_volume_icon(AstalWpEndpoint *self) { + if (self->type == ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE) { + if (self->mute) return "microphone-sensitivity-muted-symbolic"; + if (self->volume <= 0.33) return "microphone-sensitivity-low-symbolic"; + if (self->volume <= 0.66) return "microphone-sensitivity-medium-symbolic"; + return "microphone-sensitivity-high-symbolic"; + + } else { + if (self->mute) return "audio-volume-muted-symbolic"; + if (self->volume <= 0.33) return "audio-volume-low-symbolic"; + if (self->volume <= 0.66) return "audio-volume-medium-symbolic"; + if (self->volume <= 1) return "audio-volume-high-symbolic"; + return "audio-volume-overamplified-symbolic"; + } +} + +static void astal_wp_endpoint_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + + switch (property_id) { + case ASTAL_WP_ENDPOINT_PROP_ID: + g_value_set_uint(value, self->id); + break; + case ASTAL_WP_ENDPOINT_PROP_MUTE: + g_value_set_boolean(value, self->mute); + break; + case ASTAL_WP_ENDPOINT_PROP_VOLUME: + g_value_set_double(value, self->volume); + break; + case ASTAL_WP_ENDPOINT_PROP_DESCRIPTION: + g_value_set_string(value, self->description); + break; + case ASTAL_WP_ENDPOINT_PROP_NAME: + g_value_set_string(value, self->name); + break; + case ASTAL_WP_ENDPOINT_PROP_ICON: + g_value_set_string(value, self->icon); + break; + case ASTAL_WP_ENDPOINT_PROP_VOLUME_ICON: + g_value_set_string(value, astal_wp_endpoint_get_volume_icon(self)); + break; + case ASTAL_WP_ENDPOINT_PROP_MEDIA_CLASS: + g_value_set_enum(value, self->type); + break; + case ASTAL_WP_ENDPOINT_PROP_DEFAULT: + g_value_set_boolean(value, self->is_default); + break; + case ASTAL_WP_ENDPOINT_PROP_LOCK_CHANNELS: + g_value_set_boolean(value, self->lock_channels); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_endpoint_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + + switch (property_id) { + case ASTAL_WP_ENDPOINT_PROP_MUTE: + astal_wp_endpoint_set_mute(self, g_value_get_boolean(value)); + break; + case ASTAL_WP_ENDPOINT_PROP_VOLUME: + astal_wp_endpoint_set_volume(self, g_value_get_double(value)); + break; + case ASTAL_WP_ENDPOINT_PROP_DEFAULT: + astal_wp_endpoint_set_is_default(self, g_value_get_boolean(value)); + break; + case ASTAL_WP_ENDPOINT_PROP_ICON: + g_free(self->icon); + self->icon = g_strdup(g_value_get_string(value)); + break; + case ASTAL_WP_ENDPOINT_PROP_LOCK_CHANNELS: + astal_wp_endpoint_set_lock_channels(self, g_value_get_boolean(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_endpoint_update_properties(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + if (priv->node == NULL) return; + self->id = wp_proxy_get_bound_id(WP_PROXY(priv->node)); + astal_wp_endpoint_update_volume(self); + + const gchar *description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "node.description"); + if (description == NULL) { + description = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "node.nick"); + } + if (description == NULL) { + description = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "node.name"); + } + g_free(self->description); + self->description = g_strdup(description); + + const gchar *name = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.name"); + g_free(self->name); + self->name = g_strdup(name); + + const gchar *type = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.class"); + GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); + if (g_enum_get_value_by_nick(enum_class, type) != NULL) + self->type = g_enum_get_value_by_nick(enum_class, type)->value; + g_type_class_unref(enum_class); + + const gchar *icon = NULL; + switch (self->type) { + case ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER: + case ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE: + const gchar *dev = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "device.id"); + guint device_id = g_ascii_strtoull(dev, NULL, 10); + AstalWpDevice *device = astal_wp_wp_get_device(priv->wp, device_id); + icon = astal_wp_device_get_icon(device); + if (icon == NULL) { + icon = self->type == ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER + ? "audio-card-symbolic" + : "audio-input-microphone-symbolic"; + } + break; + case ASTAL_WP_MEDIA_CLASS_AUDIO_STREAM: + case ASTAL_WP_MEDIA_CLASS_AUDIO_RECORDER: + icon = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.icon-name"); + if (icon == NULL) + icon = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), + "window.icon-name"); + if (icon == NULL) + icon = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), + "application.icon-name"); + if (icon == NULL) icon = "application-x-executable-symbolic"; + break; + default: + icon = "audio-card-symbolic"; + } + g_free(self->icon); + self->icon = g_strdup(icon); + + g_object_notify(G_OBJECT(self), "id"); + g_object_notify(G_OBJECT(self), "description"); + g_object_notify(G_OBJECT(self), "name"); + g_object_notify(G_OBJECT(self), "icon"); + g_object_notify(G_OBJECT(self), "media-class"); +} + +static void astal_wp_endpoint_default_changed_as_default(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); + const gchar *media_class = g_enum_get_value(enum_class, priv->media_class)->value_nick; + guint defaultId; + g_signal_emit_by_name(priv->defaults, "get-default-node", media_class, &defaultId); + g_type_class_unref(enum_class); + + if (defaultId != self->id) { + if (priv->node != NULL) g_object_unref(priv->node); + AstalWpEndpoint *default_endpoint = astal_wp_wp_get_endpoint(priv->wp, defaultId); + if (default_endpoint != NULL && + astal_wp_endpoint_get_media_class(default_endpoint) == priv->media_class) { + AstalWpEndpointPrivate *default_endpoint_priv = + astal_wp_endpoint_get_instance_private(default_endpoint); + priv->node = g_object_ref(default_endpoint_priv->node); + astal_wp_endpoint_update_properties(self); + } + } +} + +static void astal_wp_endpoint_default_changed(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + guint defaultId; + const gchar *media_class = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.class"); + g_signal_emit_by_name(priv->defaults, "get-default-node", media_class, &defaultId); + + if (self->is_default && defaultId != self->id) { + self->is_default = FALSE; + g_object_notify(G_OBJECT(self), "is-default"); + } else if (!self->is_default && defaultId == self->id) { + self->is_default = TRUE; + g_object_notify(G_OBJECT(self), "is-default"); + } +} + +static void astal_wp_endpoint_mixer_changed(AstalWpEndpoint *self, guint node_id) { + if (self->id != node_id) return; + astal_wp_endpoint_update_volume(self); +} + +AstalWpEndpoint *astal_wp_endpoint_init_as_default(AstalWpEndpoint *self, WpPlugin *mixer, + WpPlugin *defaults, AstalWpMediaClass type, + AstalWpWp *wp) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + priv->mixer = g_object_ref(mixer); + priv->defaults = g_object_ref(defaults); + + priv->media_class = type; + priv->is_default_node = TRUE; + self->is_default = TRUE; + priv->wp = g_object_ref(wp); + + priv->default_signal_handler_id = g_signal_connect_swapped( + priv->defaults, "changed", G_CALLBACK(astal_wp_endpoint_default_changed_as_default), self); + priv->mixer_signal_handler_id = g_signal_connect_swapped( + priv->mixer, "changed", G_CALLBACK(astal_wp_endpoint_mixer_changed), self); + + astal_wp_endpoint_default_changed_as_default(self); + astal_wp_endpoint_update_properties(self); + return self; +} + +AstalWpEndpoint *astal_wp_endpoint_create(WpNode *node, WpPlugin *mixer, WpPlugin *defaults, + AstalWpWp *wp) { + AstalWpEndpoint *self = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + priv->mixer = g_object_ref(mixer); + priv->defaults = g_object_ref(defaults); + priv->node = g_object_ref(node); + priv->is_default_node = FALSE; + priv->wp = g_object_ref(wp); + + priv->default_signal_handler_id = g_signal_connect_swapped( + priv->defaults, "changed", G_CALLBACK(astal_wp_endpoint_default_changed), self); + priv->mixer_signal_handler_id = g_signal_connect_swapped( + priv->mixer, "changed", G_CALLBACK(astal_wp_endpoint_mixer_changed), self); + + astal_wp_endpoint_update_properties(self); + astal_wp_endpoint_default_changed(self); + return self; +} + +static void astal_wp_endpoint_init(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + priv->node = NULL; + priv->mixer = NULL; + priv->defaults = NULL; + priv->wp = NULL; + + self->volume = 0; + self->mute = TRUE; + self->description = NULL; + self->name = NULL; +} + +static void astal_wp_endpoint_dispose(GObject *object) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + g_signal_handler_disconnect(priv->defaults, priv->default_signal_handler_id); + g_signal_handler_disconnect(priv->mixer, priv->mixer_signal_handler_id); + + g_clear_object(&priv->node); + g_clear_object(&priv->mixer); + g_clear_object(&priv->defaults); + g_clear_object(&priv->wp); +} + +static void astal_wp_endpoint_finalize(GObject *object) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + g_free(self->description); + g_free(self->name); +} + +static void astal_wp_endpoint_class_init(AstalWpEndpointClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->dispose = astal_wp_endpoint_dispose; + object_class->finalize = astal_wp_endpoint_finalize; + object_class->get_property = astal_wp_endpoint_get_property; + object_class->set_property = astal_wp_endpoint_set_property; + + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_ID] = + g_param_spec_uint("id", "id", "id", 0, UINT_MAX, 0, G_PARAM_READABLE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_VOLUME] = + g_param_spec_double("volume", "volume", "volume", 0, G_MAXFLOAT, 0, G_PARAM_READWRITE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_MUTE] = + g_param_spec_boolean("mute", "mute", "mute", TRUE, G_PARAM_READWRITE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, G_PARAM_READABLE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_NAME] = + g_param_spec_string("name", "name", "name", NULL, G_PARAM_READABLE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_ICON] = g_param_spec_string( + "icon", "icon", "icon", "audio-card-symbolic", G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_VOLUME_ICON] = g_param_spec_string( + "volume-icon", "volume-icon", "volume-icon", "audio-volume-muted", G_PARAM_READABLE); + /** + * AstalWpEndpoint:media-class: (type AstalWpMediaClass) + * + * The media class of this endpoint + */ + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_MEDIA_CLASS] = + g_param_spec_enum("media-class", "media-class", "media-class", ASTAL_WP_TYPE_MEDIA_CLASS, 1, + G_PARAM_READABLE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_DEFAULT] = + g_param_spec_boolean("is_default", "is_default", "is_default", FALSE, G_PARAM_READWRITE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_LOCK_CHANNELS] = g_param_spec_boolean( + "lock_channels", "lock_channels", "lock channels", FALSE, G_PARAM_READWRITE); + + g_object_class_install_properties(object_class, ASTAL_WP_ENDPOINT_N_PROPERTIES, + astal_wp_endpoint_properties); +} diff --git a/wireplumber/src/meson.build b/wireplumber/src/meson.build new file mode 100644 index 0000000..87a5ae8 --- /dev/null +++ b/wireplumber/src/meson.build @@ -0,0 +1,73 @@ +srcs = files( + 'wireplumber.c', + 'endpoint.c', + 'device.c', + 'video.c', + 'profile.c', + 'audio.c', +) + +deps = [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('wireplumber-0.5'), + # dependency('json-glib-1.0'), +] + +astal_wireplumber_lib = library( + 'astal-wireplumber', + sources : srcs, + include_directories : astal_wireplumber_inc, + dependencies : deps, + version : meson.project_version(), + install : true +) + +libastal_wireplumber = declare_dependency( + link_with : astal_wireplumber_lib, + include_directories : astal_wireplumber_inc) + +# astal_wireplumber_executable = executable( +# 'astal-wireplumber', +# files('astal-wireplumber.c'), +# dependencies : [ +# dependency('gobject-2.0'), +# dependency('gio-2.0'), +# dependency('json-glib-1.0'), +# libastal_wireplumber +# ], +# install : true) + +pkg_config_name = 'astal-wireplumber-' + lib_so_version + +if get_option('introspection') + gir = gnome.generate_gir( + astal_wireplumber_lib, + sources : srcs + astal_wireplumber_headers + astal_wireplumber_subheaders, + nsversion : '0.1', + namespace : 'AstalWp', + symbol_prefix : 'astal_wp', + identifier_prefix : 'AstalWp', + includes : ['GObject-2.0', 'Gio-2.0'], + header : 'astal-wp.h', + export_packages : pkg_config_name, + install : true + ) + + if get_option('vapi') + gnome.generate_vapi( + pkg_config_name, + sources : [gir[0]], + packages : ['gobject-2.0', 'gio-2.0'], + install : true) + endif +endif + +pkg_config.generate( + name : 'astal-wireplumber', + version : meson.project_version(), + libraries : [astal_wireplumber_lib], + filebase : pkg_config_name, + subdirs : 'astal', + description : 'astal wireplumber module', + url : 'https://github.com/astal-sh/wireplumber') diff --git a/wireplumber/src/profile.c b/wireplumber/src/profile.c new file mode 100644 index 0000000..291dc7f --- /dev/null +++ b/wireplumber/src/profile.c @@ -0,0 +1,84 @@ +#include "profile.h" + +#include <wp/wp.h> + +struct _AstalWpProfile { + GObject parent_instance; + + gint index; + gchar *description; +}; + +G_DEFINE_FINAL_TYPE(AstalWpProfile, astal_wp_profile, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_PROFILE_PROP_INDEX = 1, + ASTAL_WP_PROFILE_PROP_DESCRIPTION, + ASTAL_WP_PROFILE_N_PROPERTIES, +} AstalWpProfileProperties; + +static GParamSpec *astal_wp_profile_properties[ASTAL_WP_PROFILE_N_PROPERTIES] = { + NULL, +}; + +gint astal_wp_profile_get_index(AstalWpProfile *self) { return self->index; } + +const gchar *astal_wp_profile_get_description(AstalWpProfile *self) { return self->description; } + +static void astal_wp_profile_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + + switch (property_id) { + case ASTAL_WP_PROFILE_PROP_INDEX: + g_value_set_int(value, self->index); + break; + case ASTAL_WP_PROFILE_PROP_DESCRIPTION: + g_value_set_string(value, self->description); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_profile_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + + switch (property_id) { + case ASTAL_WP_PROFILE_PROP_INDEX: + self->index = g_value_get_int(value); + break; + case ASTAL_WP_PROFILE_PROP_DESCRIPTION: + g_free(self->description); + self->description = g_strdup(g_value_get_string(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_profile_init(AstalWpProfile *self) { self->description = NULL; } + +static void astal_wp_profile_finalize(GObject *object) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + g_free(self->description); +} + +static void astal_wp_profile_class_init(AstalWpProfileClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->finalize = astal_wp_profile_finalize; + object_class->get_property = astal_wp_profile_get_property; + object_class->set_property = astal_wp_profile_set_property; + + astal_wp_profile_properties[ASTAL_WP_PROFILE_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + astal_wp_profile_properties[ASTAL_WP_PROFILE_PROP_INDEX] = + g_param_spec_int("index", "index", "index", G_MININT, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties(object_class, ASTAL_WP_PROFILE_N_PROPERTIES, + astal_wp_profile_properties); +} diff --git a/wireplumber/src/video.c b/wireplumber/src/video.c new file mode 100644 index 0000000..00cdd82 --- /dev/null +++ b/wireplumber/src/video.c @@ -0,0 +1,428 @@ +#include "video.h" + +#include <wp/wp.h> + +#include "device.h" +#include "endpoint.h" +#include "wp.h" + +struct _AstalWpVideo { + GObject parent_instance; +}; + +typedef struct { + AstalWpWp *wp; +} AstalWpVideoPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpVideo, astal_wp_video, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_VIDEO_SIGNAL_SOURCE_ADDED, + ASTAL_WP_VIDEO_SIGNAL_SOURCE_REMOVED, + ASTAL_WP_VIDEO_SIGNAL_SINK_ADDED, + ASTAL_WP_VIDEO_SIGNAL_SINK_REMOVED, + ASTAL_WP_VIDEO_SIGNAL_STREAM_ADDED, + ASTAL_WP_VIDEO_SIGNAL_STREAM_REMOVED, + ASTAL_WP_VIDEO_SIGNAL_RECORDER_ADDED, + ASTAL_WP_VIDEO_SIGNAL_RECORDER_REMOVED, + ASTAL_WP_VIDEO_SIGNAL_DEVICE_ADDED, + ASTAL_WP_VIDEO_SIGNAL_DEVICE_REMOVED, + ASTAL_WP_VIDEO_N_SIGNALS +} AstalWpWpSignals; + +static guint astal_wp_video_signals[ASTAL_WP_VIDEO_N_SIGNALS] = { + 0, +}; + +typedef enum { + ASTAL_WP_VIDEO_PROP_SOURCE = 1, + ASTAL_WP_VIDEO_PROP_SINK, + ASTAL_WP_VIDEO_PROP_STREAMS, + ASTAL_WP_VIDEO_PROP_RECORDERS, + ASTAL_WP_VIDEO_PROP_DEVICES, + ASTAL_WP_VIDEO_N_PROPERTIES, +} AstalWpVideoProperties; + +static GParamSpec *astal_wp_video_properties[ASTAL_WP_VIDEO_N_PROPERTIES] = { + NULL, +}; + +/** + * astal_wp_video_get_source: + * @self: the AstalWpVideo object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the source with the given id + */ +AstalWpEndpoint *astal_wp_video_get_speaker(AstalWpVideo *self, guint id) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE) + return endpoint; + return NULL; +} + +/** + * astal_wp_video_get_sink: + * @self: the AstalWpVideo object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the sink with the given id + */ +AstalWpEndpoint *astal_wp_video_get_sink(AstalWpVideo *self, guint id) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_VIDEO_SINK) + return endpoint; + return NULL; +} + +/** + * astal_wp_video_get_stream: + * @self: the AstalWpVideo object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the stream with the given id + */ +AstalWpEndpoint *astal_wp_video_get_stream(AstalWpVideo *self, guint id) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM) + return endpoint; + return NULL; +} + +/** + * astal_wp_video_get_recorder: + * @self: the AstalWpVideo object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the recorder with the given id + */ +AstalWpEndpoint *astal_wp_video_get_recorder(AstalWpVideo *self, guint id) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + + AstalWpEndpoint *endpoint = astal_wp_wp_get_endpoint(priv->wp, id); + if (astal_wp_endpoint_get_media_class(endpoint) == ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER) + return endpoint; + return NULL; +} + +/** + * astal_wp_video_get_device: + * @self: the AstalWpVideo object + * @id: the id of the device + * + * Returns: (transfer none) (nullable): the device with the given id + */ +AstalWpDevice *astal_wp_video_get_device(AstalWpVideo *self, guint id) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + + AstalWpDevice *device = astal_wp_wp_get_device(priv->wp, id); + if (astal_wp_device_get_device_type(device) == ASTAL_WP_DEVICE_TYPE_VIDEO) return device; + return NULL; +} + +/** + * astal_wp_video_get_sources: + * @self: the AstalWpVideo object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * video sources + */ +GList *astal_wp_video_get_sources(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +/** + * astal_wp_video_get_sinks + * @self: the AstalWpVideo object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * video sinks + */ +GList *astal_wp_video_get_sinks(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_VIDEO_SINK) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +/** + * astal_wp_video_get_recorders: + * @self: the AstalWpVideo object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * video recorders + */ +GList *astal_wp_video_get_recorders(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +/** + * astal_wp_video_get_streams: + * @self: the AstalWpVideo object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * video streams + */ +GList *astal_wp_video_get_streams(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + GList *eps = astal_wp_wp_get_endpoints(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +/** + * astal_wp_video_get_devices: + * @self: the AstalWpAudio object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpVideo)): a GList containing the + * devices + */ +GList *astal_wp_video_get_devices(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + GList *eps = astal_wp_wp_get_devices(priv->wp); + GList *list = NULL; + + for (GList *l = eps; l != NULL; l = l->next) { + if (astal_wp_device_get_device_type(l->data) == ASTAL_WP_DEVICE_TYPE_VIDEO) { + list = g_list_append(list, l->data); + } + } + g_list_free(eps); + return list; +} + +static void astal_wp_video_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpVideo *self = ASTAL_WP_VIDEO(object); + + switch (property_id) { + case ASTAL_WP_VIDEO_PROP_SOURCE: + g_value_set_pointer(value, astal_wp_video_get_sources(self)); + break; + case ASTAL_WP_VIDEO_PROP_SINK: + g_value_set_pointer(value, astal_wp_video_get_sinks(self)); + break; + case ASTAL_WP_VIDEO_PROP_RECORDERS: + g_value_set_pointer(value, astal_wp_video_get_recorders(self)); + break; + case ASTAL_WP_VIDEO_PROP_STREAMS: + g_value_set_pointer(value, astal_wp_video_get_streams(self)); + break; + case ASTAL_WP_VIDEO_PROP_DEVICES: + g_value_set_pointer(value, astal_wp_video_get_devices(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +void astal_wp_video_device_added(AstalWpVideo *self, gpointer object) { + AstalWpDevice *device = ASTAL_WP_DEVICE(object); + if (astal_wp_device_get_device_type(device) == ASTAL_WP_DEVICE_TYPE_VIDEO) { + g_signal_emit_by_name(self, "device-added", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_video_device_removed(AstalWpVideo *self, gpointer object) { + AstalWpDevice *device = ASTAL_WP_DEVICE(object); + if (astal_wp_device_get_device_type(device) == ASTAL_WP_DEVICE_TYPE_VIDEO) { + g_signal_emit_by_name(self, "device-removed", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_video_object_added(AstalWpVideo *self, gpointer object) { + AstalWpEndpoint *endpoint = ASTAL_WP_ENDPOINT(object); + switch (astal_wp_endpoint_get_media_class(endpoint)) { + case ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE: + g_signal_emit_by_name(self, "source-added", endpoint); + g_object_notify(G_OBJECT(self), "sources"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_SINK: + g_signal_emit_by_name(self, "sink-added", endpoint); + g_object_notify(G_OBJECT(self), "sinks"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM: + g_signal_emit_by_name(self, "stream-added", endpoint); + g_object_notify(G_OBJECT(self), "streams"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER: + g_signal_emit_by_name(self, "recorder-added", endpoint); + g_object_notify(G_OBJECT(self), "recorders"); + break; + default: + break; + } +} + +static void astal_wp_video_object_removed(AstalWpAudio *self, gpointer object) { + AstalWpEndpoint *endpoint = ASTAL_WP_ENDPOINT(object); + switch (astal_wp_endpoint_get_media_class(endpoint)) { + case ASTAL_WP_MEDIA_CLASS_VIDEO_SOURCE: + g_signal_emit_by_name(self, "source-removed", endpoint); + g_object_notify(G_OBJECT(self), "sources"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_SINK: + g_signal_emit_by_name(self, "sink-removed", endpoint); + g_object_notify(G_OBJECT(self), "sinks"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_STREAM: + g_signal_emit_by_name(self, "stream-removed", endpoint); + g_object_notify(G_OBJECT(self), "streams"); + break; + case ASTAL_WP_MEDIA_CLASS_VIDEO_RECORDER: + g_signal_emit_by_name(self, "recorder-removed", endpoint); + g_object_notify(G_OBJECT(self), "recorders"); + break; + default: + break; + } +} + +AstalWpVideo *astal_wp_video_new(AstalWpWp *wp) { + AstalWpVideo *self = g_object_new(ASTAL_WP_TYPE_VIDEO, NULL); + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + priv->wp = g_object_ref(wp); + g_signal_connect_swapped(priv->wp, "endpoint-added", G_CALLBACK(astal_wp_video_object_added), + self); + g_signal_connect_swapped(priv->wp, "endpoint-removed", + G_CALLBACK(astal_wp_video_object_removed), self); + g_signal_connect_swapped(priv->wp, "device-added", G_CALLBACK(astal_wp_video_device_added), + self); + g_signal_connect_swapped(priv->wp, "device-removed", G_CALLBACK(astal_wp_video_device_removed), + self); + + return self; +} + +static void astal_wp_video_dispose(GObject *object) { + AstalWpVideo *self = ASTAL_WP_VIDEO(object); + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); + g_clear_object(&priv->wp); +} + +static void astal_wp_video_init(AstalWpVideo *self) { + AstalWpVideoPrivate *priv = astal_wp_video_get_instance_private(self); +} + +static void astal_wp_video_class_init(AstalWpVideoClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->get_property = astal_wp_video_get_property; + object_class->dispose = astal_wp_video_dispose; + + /** + * AstalWpVideo:sources: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_video_properties[ASTAL_WP_VIDEO_PROP_SOURCE] = + g_param_spec_pointer("sources", "sources", "sources", G_PARAM_READABLE); + + /** + * AstalWpVideo:sinks: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_video_properties[ASTAL_WP_VIDEO_PROP_SINK] = + g_param_spec_pointer("sinks", "sinks", "sinks", G_PARAM_READABLE); + + /** + * AstalWpVideo:recorder: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_video_properties[ASTAL_WP_VIDEO_PROP_RECORDERS] = + g_param_spec_pointer("recorders", "recorders", "recorders", G_PARAM_READABLE); + + /** + * AstalWpVideo:streams: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_video_properties[ASTAL_WP_VIDEO_PROP_STREAMS] = + g_param_spec_pointer("streams", "streams", "streams", G_PARAM_READABLE); + + /** + * AstalWpVideo:devices: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_video_properties[ASTAL_WP_VIDEO_PROP_DEVICES] = + g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_WP_VIDEO_N_PROPERTIES, + astal_wp_video_properties); + + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_SOURCE_ADDED] = + g_signal_new("source-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_SOURCE_REMOVED] = + g_signal_new("source-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_SINK_ADDED] = + g_signal_new("sink-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_SINK_REMOVED] = + g_signal_new("sink-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_STREAM_ADDED] = + g_signal_new("stream-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_SOURCE_REMOVED] = + g_signal_new("stream-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_RECORDER_ADDED] = + g_signal_new("recorder-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_RECORDER_REMOVED] = + g_signal_new("recorder-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_DEVICE_ADDED] = + g_signal_new("device-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); + astal_wp_video_signals[ASTAL_WP_VIDEO_SIGNAL_DEVICE_REMOVED] = + g_signal_new("device-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); +} diff --git a/wireplumber/src/wireplumber.c b/wireplumber/src/wireplumber.c new file mode 100644 index 0000000..f1fa516 --- /dev/null +++ b/wireplumber/src/wireplumber.c @@ -0,0 +1,503 @@ +#include <wp/wp.h> + +#include "audio.h" +#include "device-private.h" +#include "endpoint-private.h" +#include "glib-object.h" +#include "glib.h" +#include "video.h" +#include "wp.h" + +struct _AstalWpWp { + GObject parent_instance; + + AstalWpEndpoint *default_speaker; + AstalWpEndpoint *default_microphone; + + AstalWpAudio *audio; + AstalWpVideo *video; + + AstalWpScale scale; +}; + +typedef struct { + WpCore *core; + WpObjectManager *obj_manager; + + WpPlugin *mixer; + WpPlugin *defaults; + gint pending_plugins; + + GHashTable *endpoints; + GHashTable *devices; +} AstalWpWpPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpWp, astal_wp_wp, G_TYPE_OBJECT); + +G_DEFINE_ENUM_TYPE(AstalWpScale, astal_wp_scale, + G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_LINEAR, "linear"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_CUBIC, "cubic")); + +typedef enum { + ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED, + ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED, + ASTAL_WP_WP_SIGNAL_DEVICE_ADDED, + ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED, + ASTAL_WP_WP_N_SIGNALS +} AstalWpWpSignals; + +static guint astal_wp_wp_signals[ASTAL_WP_WP_N_SIGNALS] = { + 0, +}; + +typedef enum { + ASTAL_WP_WP_PROP_AUDIO = 1, + ASTAL_WP_WP_PROP_VIDEO, + ASTAL_WP_WP_PROP_ENDPOINTS, + ASTAL_WP_WP_PROP_DEVICES, + ASTAL_WP_WP_PROP_DEFAULT_SPEAKER, + ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE, + ASTAL_WP_WP_PROP_SCALE, + ASTAL_WP_WP_N_PROPERTIES, +} AstalWpWpProperties; + +static GParamSpec *astal_wp_wp_properties[ASTAL_WP_WP_N_PROPERTIES] = { + NULL, +}; + +/** + * astal_wp_wp_get_endpoint: + * @self: the AstalWpWp object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the endpoint with the given id + */ +AstalWpEndpoint *astal_wp_wp_get_endpoint(AstalWpWp *self, guint id) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + AstalWpEndpoint *endpoint = g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id)); + return endpoint; +} + +/** + * astal_wp_wp_get_endpoints: + * @self: the AstalWpWp object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * endpoints + */ +GList *astal_wp_wp_get_endpoints(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + return g_hash_table_get_values(priv->endpoints); +} + +/** + * astal_wp_wp_get_device: + * @self: the AstalWpWp object + * @id: the id of the device + * + * Returns: (transfer none) (nullable): the device with the given id + */ +AstalWpDevice *astal_wp_wp_get_device(AstalWpWp *self, guint id) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + AstalWpDevice *device = g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id)); + return device; +} + +/** + * astal_wp_wp_get_devices: + * @self: the AstalWpWp object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpDevice)): a GList containing the + * devices + */ +GList *astal_wp_wp_get_devices(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + return g_hash_table_get_values(priv->devices); +} + +/** + * astal_wp_wp_get_audio + * + * Returns: (nullable) (transfer none): gets the audio object + */ +AstalWpAudio *astal_wp_wp_get_audio(AstalWpWp *self) { return self->audio; } + +/** + * astal_wp_wp_get_video + * + * Returns: (nullable) (transfer none): gets the video object + */ +AstalWpVideo *astal_wp_wp_get_video(AstalWpWp *self) { return self->video; } + +/** + * astal_wp_wp_get_default_speaker + * + * Returns: (nullable) (transfer none): gets the default speaker object + */ +AstalWpEndpoint *astal_wp_wp_get_default_speaker(AstalWpWp *self) { return self->default_speaker; } + +/** + * astal_wp_wp_get_default_microphone + * + * Returns: (nullable) (transfer none): gets the default microphone object + */ +AstalWpEndpoint *astal_wp_wp_get_default_microphone(AstalWpWp *self) { + return self->default_microphone; +} + +AstalWpScale astal_wp_wp_get_scale(AstalWpWp *self) { return self->scale; } + +void astal_wp_wp_set_scale(AstalWpWp *self, AstalWpScale scale) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + self->scale = scale; + + if (priv->mixer == NULL) return; + + g_object_set(priv->mixer, "scale", self->scale, NULL); + + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, priv->endpoints); + while (g_hash_table_iter_next(&iter, &key, &value)) { + AstalWpEndpoint *ep = ASTAL_WP_ENDPOINT(value); + astal_wp_endpoint_update_volume(ep); + } + + astal_wp_endpoint_update_volume(self->default_speaker); + astal_wp_endpoint_update_volume(self->default_microphone); +} + +static void astal_wp_wp_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + switch (property_id) { + case ASTAL_WP_WP_PROP_AUDIO: + g_value_set_object(value, astal_wp_wp_get_audio(self)); + break; + case ASTAL_WP_WP_PROP_VIDEO: + g_value_set_object(value, astal_wp_wp_get_video(self)); + break; + case ASTAL_WP_WP_PROP_ENDPOINTS: + g_value_set_pointer(value, g_hash_table_get_values(priv->endpoints)); + break; + case ASTAL_WP_WP_PROP_DEVICES: + g_value_set_pointer(value, g_hash_table_get_values(priv->devices)); + break; + case ASTAL_WP_WP_PROP_DEFAULT_SPEAKER: + g_value_set_object(value, self->default_speaker); + break; + case ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE: + g_value_set_object(value, self->default_microphone); + break; + case ASTAL_WP_WP_PROP_SCALE: + g_value_set_enum(value, self->scale); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_wp_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpWp *self = ASTAL_WP_WP(object); + + switch (property_id) { + case ASTAL_WP_WP_PROP_SCALE: + astal_wp_wp_set_scale(self, g_value_get_enum(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_wp_object_added(AstalWpWp *self, gpointer object) { + // print pipewire properties + // WpIterator *iter = wp_pipewire_object_new_properties_iterator(WP_PIPEWIRE_OBJECT(object)); + // GValue item = G_VALUE_INIT; + // const gchar *key, *value; + // + // g_print("\n\n"); + // while (wp_iterator_next (iter, &item)) { + // WpPropertiesItem *pi = g_value_get_boxed (&item); + // key = wp_properties_item_get_key (pi); + // value = wp_properties_item_get_value (pi); + // g_print("%s: %s\n", key, value); + // g_value_unset(&item); + // } + + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + if (WP_IS_NODE(object)) { + WpNode *node = WP_NODE(object); + AstalWpEndpoint *endpoint = + astal_wp_endpoint_create(node, priv->mixer, priv->defaults, self); + + g_hash_table_insert(priv->endpoints, + GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))), endpoint); + + g_signal_emit_by_name(self, "endpoint-added", endpoint); + g_object_notify(G_OBJECT(self), "endpoints"); + } else if (WP_IS_DEVICE(object)) { + WpDevice *node = WP_DEVICE(object); + AstalWpDevice *device = astal_wp_device_create(node); + g_hash_table_insert(priv->devices, GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))), + device); + g_signal_emit_by_name(self, "device-added", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_wp_object_removed(AstalWpWp *self, gpointer object) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + if (WP_IS_NODE(object)) { + guint id = wp_proxy_get_bound_id(WP_PROXY(object)); + AstalWpEndpoint *endpoint = + g_object_ref(g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id))); + + g_hash_table_remove(priv->endpoints, GUINT_TO_POINTER(id)); + + g_signal_emit_by_name(self, "endpoint-removed", endpoint); + g_object_notify(G_OBJECT(self), "endpoints"); + g_object_unref(endpoint); + } else if (WP_IS_DEVICE(object)) { + guint id = wp_proxy_get_bound_id(WP_PROXY(object)); + AstalWpDevice *device = + g_object_ref(g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id))); + g_hash_table_remove(priv->devices, GUINT_TO_POINTER(id)); + + g_signal_emit_by_name(self, "device-removed", device); + g_object_notify(G_OBJECT(self), "devices"); + g_object_unref(device); + } +} + +static void astal_wp_wp_objm_installed(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + astal_wp_endpoint_init_as_default(self->default_speaker, priv->mixer, priv->defaults, + ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER, self); + astal_wp_endpoint_init_as_default(self->default_microphone, priv->mixer, priv->defaults, + ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE, self); +} + +static void astal_wp_wp_plugin_activated(WpObject *obj, GAsyncResult *result, AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + GError *error = NULL; + wp_object_activate_finish(obj, result, &error); + if (error) { + g_critical("Failed to activate component: %s\n", error->message); + return; + } + + if (--priv->pending_plugins == 0) { + priv->defaults = wp_plugin_find(priv->core, "default-nodes-api"); + priv->mixer = wp_plugin_find(priv->core, "mixer-api"); + g_object_set(priv->mixer, "scale", self->scale, NULL); + + g_signal_connect_swapped(priv->obj_manager, "object-added", + G_CALLBACK(astal_wp_wp_object_added), self); + g_signal_connect_swapped(priv->obj_manager, "object-removed", + G_CALLBACK(astal_wp_wp_object_removed), self); + + wp_core_install_object_manager(priv->core, priv->obj_manager); + } +} + +static void astal_wp_wp_plugin_loaded(WpObject *obj, GAsyncResult *result, AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + GError *error = NULL; + wp_core_load_component_finish(priv->core, result, &error); + if (error) { + g_critical("Failed to load component: %s\n", error->message); + return; + } + + wp_object_activate(obj, WP_PLUGIN_FEATURE_ENABLED, NULL, + (GAsyncReadyCallback)astal_wp_wp_plugin_activated, self); +} + +/** + * astal_wp_wp_get_default + * + * Returns: (nullable) (transfer none): gets the default wireplumber object. + */ +AstalWpWp *astal_wp_wp_get_default() { + static AstalWpWp *self = NULL; + + if (self == NULL) self = g_object_new(ASTAL_WP_TYPE_WP, NULL); + + return self; +} + +/** + * astal_wp_get_default_wp + * + * Returns: (nullable) (transfer none): gets the default wireplumber object. + */ +AstalWpWp *astal_wp_get_default_wp() { return astal_wp_wp_get_default(); } + +static void astal_wp_wp_dispose(GObject *object) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + g_clear_object(&self->video); + g_clear_object(&self->audio); + + wp_core_disconnect(priv->core); + g_clear_object(&self->default_speaker); + g_clear_object(&self->default_microphone); + g_clear_object(&priv->mixer); + g_clear_object(&priv->defaults); + g_clear_object(&priv->obj_manager); + g_clear_object(&priv->core); + + if (priv->endpoints != NULL) { + g_hash_table_destroy(priv->endpoints); + priv->endpoints = NULL; + } +} + +static void astal_wp_wp_finalize(GObject *object) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); +} + +static void astal_wp_wp_init(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + priv->endpoints = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + priv->devices = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + + wp_init(7); + priv->core = wp_core_new(NULL, NULL, NULL); + + if (!wp_core_connect(priv->core)) { + g_critical("could not connect to PipeWire\n"); + return; + } + + priv->obj_manager = wp_object_manager_new(); + wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_NODE, + WP_OBJECT_FEATURES_ALL); + wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_GLOBAL_PROXY, + WP_OBJECT_FEATURES_ALL); + + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Audio/Sink", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Audio/Source", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Output/Audio", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Input/Audio", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", + "Audio/Device", NULL); + + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Video/Sink", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Video/Source", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Output/Video", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Input/Video", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", + "Video/Device", NULL); + // wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_CLIENT, NULL); + + g_signal_connect_swapped(priv->obj_manager, "installed", (GCallback)astal_wp_wp_objm_installed, + self); + + self->default_speaker = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + self->default_microphone = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + + self->audio = astal_wp_audio_new(self); + self->video = astal_wp_video_new(self); + + priv->pending_plugins = 2; + wp_core_load_component(priv->core, "libwireplumber-module-default-nodes-api", "module", NULL, + "default-nodes-api", NULL, + (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self); + wp_core_load_component(priv->core, "libwireplumber-module-mixer-api", "module", NULL, + "mixer-api", NULL, (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self); +} + +static void astal_wp_wp_class_init(AstalWpWpClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->finalize = astal_wp_wp_finalize; + object_class->dispose = astal_wp_wp_dispose; + object_class->get_property = astal_wp_wp_get_property; + object_class->set_property = astal_wp_wp_set_property; + + astal_wp_wp_properties[ASTAL_WP_WP_PROP_AUDIO] = + g_param_spec_object("audio", "audio", "audio", ASTAL_WP_TYPE_AUDIO, G_PARAM_READABLE); + astal_wp_wp_properties[ASTAL_WP_WP_PROP_VIDEO] = + g_param_spec_object("video", "video", "video", ASTAL_WP_TYPE_VIDEO, G_PARAM_READABLE); + /** + * AstalWpWp:scale: (type AstalWpScale) + * + * The scale used for the volume + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_SCALE] = + g_param_spec_enum("scale", "scale", "scale", ASTAL_WP_TYPE_SCALE, ASTAL_WP_SCALE_CUBIC, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * AstalWpWp:endpoints: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_ENDPOINTS] = + g_param_spec_pointer("endpoints", "endpoints", "endpoints", G_PARAM_READABLE); + /** + * AstalWpWp:devices: (type GList(AstalWpDevice)) (transfer container) + * + * A list of AstalWpDevice objects + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEVICES] = + g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE); + /** + * AstalWpWp:default-speaker: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_SPEAKER] = + g_param_spec_object("default-speaker", "default-speaker", "default-speaker", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + /** + * AstalWpWp:default-microphone: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE] = + g_param_spec_object("default-microphone", "default-microphone", "default-microphone", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_WP_WP_N_PROPERTIES, + astal_wp_wp_properties); + + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED] = + g_signal_new("endpoint-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED] = + g_signal_new("endpoint-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_DEVICE_ADDED] = + g_signal_new("device-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED] = + g_signal_new("device-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); +} diff --git a/wireplumber/version b/wireplumber/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/wireplumber/version @@ -0,0 +1 @@ +0.1.0 |