summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--LICENSE503
-rw-r--r--README.md206
-rw-r--r--apps/.gitignore5
-rw-r--r--apps/LICENSE502
-rw-r--r--apps/README.md8
-rw-r--r--apps/flake.lock27
-rw-r--r--apps/flake.nix49
-rw-r--r--apps/meson-install.sh16
-rw-r--r--apps/meson.build19
-rw-r--r--apps/meson_options.txt11
-rw-r--r--apps/src/application.vala118
-rw-r--r--apps/src/apps.vala172
-rw-r--r--apps/src/cli.vala66
-rw-r--r--apps/src/config.vala.in6
-rw-r--r--apps/src/meson.build77
-rw-r--r--apps/version1
-rw-r--r--auth/.gitignore3
-rw-r--r--auth/LICENSE503
-rw-r--r--auth/README.md66
-rw-r--r--auth/examples/full_example.c66
-rw-r--r--auth/examples/full_example.js38
-rw-r--r--auth/examples/meson.build18
-rw-r--r--auth/examples/simple_example.c31
-rw-r--r--auth/examples/simple_example.js9
-rw-r--r--auth/flake.lock27
-rw-r--r--auth/flake.nix42
-rw-r--r--auth/include/astal-auth.h32
-rw-r--r--auth/include/meson.build4
-rw-r--r--auth/meson.build33
-rw-r--r--auth/meson_options.txt3
-rw-r--r--auth/pam/astal-auth5
-rw-r--r--auth/src/astal-auth.c153
-rw-r--r--auth/src/meson.build65
-rw-r--r--auth/src/pam.c524
-rw-r--r--auth/version1
-rw-r--r--bluetooth/.gitignore5
-rw-r--r--bluetooth/LICENSE503
-rw-r--r--bluetooth/README.md7
-rw-r--r--bluetooth/flake.lock27
-rw-r--r--bluetooth/flake.nix52
-rw-r--r--bluetooth/meson.build14
-rw-r--r--bluetooth/src/adapter.vala89
-rw-r--r--bluetooth/src/bluetooth.vala181
-rw-r--r--bluetooth/src/config.vala.in6
-rw-r--r--bluetooth/src/device.vala106
-rw-r--r--bluetooth/src/meson.build66
-rw-r--r--bluetooth/src/utils.vala21
-rw-r--r--bluetooth/version1
-rw-r--r--core/default.nix0
-rw-r--r--core/gjs/.gitignore3
-rw-r--r--core/gjs/eslint.config.mjs18
-rw-r--r--core/gjs/index.ts13
-rw-r--r--core/gjs/package-lock.json3209
-rw-r--r--core/gjs/package.json46
-rw-r--r--core/gjs/src/application.ts105
-rw-r--r--core/gjs/src/astalify.ts331
-rw-r--r--core/gjs/src/binding.ts88
-rw-r--r--core/gjs/src/file.ts44
-rw-r--r--core/gjs/src/imports.ts10
-rw-r--r--core/gjs/src/jsx/jsx-runtime.ts87
-rw-r--r--core/gjs/src/process.ts69
-rw-r--r--core/gjs/src/time.ts13
-rw-r--r--core/gjs/src/variable.ts227
-rw-r--r--core/gjs/src/widgets.ts109
-rw-r--r--core/gjs/tsconfig.json23
-rw-r--r--core/lua/astal-dev-1.rockspec31
-rw-r--r--core/lua/astal/application.lua94
-rw-r--r--core/lua/astal/binding.lua65
-rw-r--r--core/lua/astal/file.lua45
-rw-r--r--core/lua/astal/init.lua41
-rw-r--r--core/lua/astal/process.lua94
-rw-r--r--core/lua/astal/time.lua27
-rw-r--r--core/lua/astal/variable.lua276
-rw-r--r--core/lua/astal/widget.lua276
-rw-r--r--core/lua/stylua.toml3
-rw-r--r--core/lua/test.lua8
-rw-r--r--core/meson.build30
-rw-r--r--core/meson_options.txt17
-rw-r--r--core/src/astal.vala342
-rw-r--r--core/src/cli.vala87
-rw-r--r--core/src/config.vala.in6
-rw-r--r--core/src/file.vala81
-rw-r--r--core/src/meson.build98
-rw-r--r--core/src/process.vala119
-rw-r--r--core/src/time.vala73
-rw-r--r--core/src/variable.vala196
-rw-r--r--core/src/widget/box.vala70
-rw-r--r--core/src/widget/button.vala101
-rw-r--r--core/src/widget/centerbox.vala54
-rw-r--r--core/src/widget/circularprogress.vala173
-rw-r--r--core/src/widget/eventbox.vala66
-rw-r--r--core/src/widget/icon.vala95
-rw-r--r--core/src/widget/label.vala18
-rw-r--r--core/src/widget/levelbar.vala15
-rw-r--r--core/src/widget/overlay.vala59
-rw-r--r--core/src/widget/scrollable.vala42
-rw-r--r--core/src/widget/slider.vala71
-rw-r--r--core/src/widget/widget.vala157
-rw-r--r--core/src/widget/window.vala227
-rw-r--r--core/version1
-rw-r--r--flake.lock27
-rw-r--r--flake.nix54
-rw-r--r--hyprland/.gitignore4
-rw-r--r--hyprland/LICENSE503
-rw-r--r--hyprland/README.md7
-rw-r--r--hyprland/flake.lock27
-rw-r--r--hyprland/flake.nix49
-rw-r--r--hyprland/meson.build19
-rw-r--r--hyprland/meson_options.txt11
-rw-r--r--hyprland/src/cli.vala42
-rw-r--r--hyprland/src/client.vala75
-rw-r--r--hyprland/src/config.vala.in6
-rw-r--r--hyprland/src/hyprland.vala451
-rw-r--r--hyprland/src/meson.build81
-rw-r--r--hyprland/src/monitor.vala71
-rw-r--r--hyprland/src/structs.vala42
-rw-r--r--hyprland/src/workspace.vala57
-rw-r--r--hyprland/version1
-rw-r--r--mpris/.gitignore5
-rw-r--r--mpris/LICENSE503
-rw-r--r--mpris/README.md7
-rw-r--r--mpris/flake.lock27
-rw-r--r--mpris/flake.nix49
-rw-r--r--mpris/meson.build19
-rw-r--r--mpris/meson_options.txt11
-rw-r--r--mpris/src/cli.vala331
-rw-r--r--mpris/src/config.vala.in6
-rw-r--r--mpris/src/ifaces.vala60
-rw-r--r--mpris/src/meson.build77
-rw-r--r--mpris/src/mpris.vala66
-rw-r--r--mpris/src/player.vala467
-rw-r--r--mpris/version1
-rw-r--r--network/.gitignore5
-rw-r--r--network/LICENSE503
-rw-r--r--network/README.md7
-rw-r--r--network/flake.lock27
-rw-r--r--network/flake.nix53
-rw-r--r--network/meson.build14
-rw-r--r--network/src/accesspoint.vala49
-rw-r--r--network/src/config.vala.in6
-rw-r--r--network/src/meson.build67
-rw-r--r--network/src/network.vala206
-rw-r--r--network/src/vpn.vala1
-rw-r--r--network/src/wifi.vala182
-rw-r--r--network/src/wired.vala73
-rw-r--r--network/version1
-rw-r--r--notifd/LICENSE503
-rw-r--r--notifd/README.md7
-rw-r--r--notifd/flake.lock27
-rw-r--r--notifd/flake.nix54
-rw-r--r--notifd/meson.build19
-rw-r--r--notifd/meson_options.txt11
-rw-r--r--notifd/src/cli.vala115
-rw-r--r--notifd/src/config.vala.in6
-rw-r--r--notifd/src/daemon.vala255
-rw-r--r--notifd/src/meson.build79
-rw-r--r--notifd/src/notifd.vala140
-rw-r--r--notifd/src/notification.vala160
-rw-r--r--notifd/src/proxy.vala129
-rw-r--r--notifd/src/signals.md35
-rw-r--r--notifd/version1
-rw-r--r--powerprofiles/.gitignore5
-rw-r--r--powerprofiles/LICENSE503
-rw-r--r--powerprofiles/README.md7
-rw-r--r--powerprofiles/flake.lock27
-rw-r--r--powerprofiles/flake.nix53
-rw-r--r--powerprofiles/meson.build19
-rw-r--r--powerprofiles/meson_options.txt11
-rw-r--r--powerprofiles/src/cli.vala80
-rw-r--r--powerprofiles/src/config.vala.in6
-rw-r--r--powerprofiles/src/meson.build75
-rw-r--r--powerprofiles/src/power-profiles.vala205
-rw-r--r--powerprofiles/version1
-rw-r--r--river/.gitignore3
-rw-r--r--river/LICENSE504
-rw-r--r--river/README.md30
-rw-r--r--river/flake.lock27
-rw-r--r--river/flake.nix54
-rw-r--r--river/include/astal-river.h59
-rw-r--r--river/include/meson.build6
-rw-r--r--river/include/river-private.h20
-rw-r--r--river/include/wayland-source.h16
-rw-r--r--river/meson.build23
-rw-r--r--river/meson_options.txt2
-rw-r--r--river/protocols/meson.build23
-rw-r--r--river/protocols/river-control-unstable-v1.xml86
-rw-r--r--river/protocols/river-status-unstable-v1.xml149
-rw-r--r--river/src/astal-river.c51
-rw-r--r--river/src/meson.build70
-rw-r--r--river/src/river-output.c343
-rw-r--r--river/src/river.c504
-rw-r--r--river/src/wayland-source.c104
-rw-r--r--river/version1
-rw-r--r--tray/LICENSE503
-rw-r--r--tray/README.md30
-rw-r--r--tray/flake.lock27
-rw-r--r--tray/flake.nix56
-rw-r--r--tray/meson.build19
-rw-r--r--tray/meson_options.txt11
-rw-r--r--tray/src/cli.vala54
-rw-r--r--tray/src/config.vala.in6
-rw-r--r--tray/src/meson.build100
-rw-r--r--tray/src/tray.vala135
-rw-r--r--tray/src/trayItem.vala363
-rw-r--r--tray/src/watcher.vala59
-rw-r--r--tray/version1
-rw-r--r--version1
-rw-r--r--wireplumber/.gitignore3
-rw-r--r--wireplumber/LICENSE503
-rw-r--r--wireplumber/README.md34
-rw-r--r--wireplumber/flake.lock27
-rw-r--r--wireplumber/flake.nix54
-rw-r--r--wireplumber/include/astal-wp.h4
-rw-r--r--wireplumber/include/astal/wireplumber/audio.h33
-rw-r--r--wireplumber/include/astal/wireplumber/device.h29
-rw-r--r--wireplumber/include/astal/wireplumber/endpoint.h43
-rw-r--r--wireplumber/include/astal/wireplumber/meson.build10
-rw-r--r--wireplumber/include/astal/wireplumber/profile.h17
-rw-r--r--wireplumber/include/astal/wireplumber/video.h29
-rw-r--r--wireplumber/include/astal/wireplumber/wp.h47
-rw-r--r--wireplumber/include/meson.build8
-rw-r--r--wireplumber/include/private/device-private.h15
-rw-r--r--wireplumber/include/private/endpoint-private.h22
-rw-r--r--wireplumber/meson.build22
-rw-r--r--wireplumber/meson_options.txt2
-rw-r--r--wireplumber/src/audio.c503
-rw-r--r--wireplumber/src/device.c371
-rw-r--r--wireplumber/src/endpoint.c554
-rw-r--r--wireplumber/src/meson.build73
-rw-r--r--wireplumber/src/profile.c84
-rw-r--r--wireplumber/src/video.c428
-rw-r--r--wireplumber/src/wireplumber.c503
-rw-r--r--wireplumber/version1
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/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..67cd97b
--- /dev/null
+++ b/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/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, &registry_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
diff --git a/version b/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/version
@@ -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