diff options
author | Aylur <[email protected]> | 2024-11-02 23:53:41 +0100 |
---|---|---|
committer | Aylur <[email protected]> | 2024-11-02 23:57:22 +0100 |
commit | cdaf8905ac3d566284719a29af6e4eddc10bb857 (patch) | |
tree | 6bd3ffc4848cae29dede4c489c8b5f5a17d0e085 /lib | |
parent | 031321b3f418369a6c4ce578ba2673b7631117c1 (diff) | |
parent | d47b470f68a8e5f2d19f32fbfb1de95752ba8eb8 (diff) |
Merge branch 'main' into feat/gtk4
Diffstat (limited to 'lib')
-rw-r--r-- | lib/apps/application.vala | 52 | ||||
-rw-r--r-- | lib/apps/apps.vala | 58 | ||||
-rw-r--r-- | lib/astal/gtk3/src/widget/window.vala | 13 | ||||
-rw-r--r-- | lib/astal/io/application.vala | 69 | ||||
-rw-r--r-- | lib/astal/io/cli.vala | 53 | ||||
-rw-r--r-- | lib/auth/src/pam.c | 1 | ||||
-rw-r--r-- | lib/bluetooth/adapter.vala | 117 | ||||
-rw-r--r-- | lib/bluetooth/bluetooth.vala | 50 | ||||
-rw-r--r-- | lib/bluetooth/device.vala | 169 | ||||
l--------- | lib/bluetooth/gir.py | 1 | ||||
-rw-r--r-- | lib/bluetooth/interfaces.vala | 46 | ||||
-rw-r--r-- | lib/bluetooth/meson.build | 49 | ||||
-rw-r--r-- | lib/cava/.gitignore | 1 | ||||
-rw-r--r-- | lib/cava/astal-cava.h | 70 | ||||
-rw-r--r-- | lib/cava/cava.c | 659 | ||||
-rw-r--r-- | lib/cava/meson.build | 80 | ||||
-rw-r--r-- | lib/cava/meson_options.txt | 13 | ||||
-rw-r--r-- | lib/cava/subprojects/cava.wrap | 7 | ||||
-rw-r--r-- | lib/cava/version | 1 | ||||
-rw-r--r-- | lib/hyprland/client.vala | 5 | ||||
l--------- | lib/mpris/gir.py | 1 | ||||
-rw-r--r-- | lib/mpris/ifaces.vala | 40 | ||||
-rw-r--r-- | lib/mpris/meson.build | 46 | ||||
-rw-r--r-- | lib/mpris/mpris.vala | 35 | ||||
-rw-r--r-- | lib/mpris/player.vala | 365 | ||||
-rw-r--r-- | lib/notifd/notification.vala | 12 | ||||
-rw-r--r-- | lib/notifd/proxy.vala | 4 | ||||
-rw-r--r-- | lib/notifd/signals.md | 2 |
28 files changed, 1676 insertions, 343 deletions
diff --git a/lib/apps/application.vala b/lib/apps/application.vala index 0a2f73c..3a9900a 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -1,3 +1,6 @@ +/** + * Object representing an applications .desktop file. + */ public class AstalApps.Application : Object { /** * The underlying DesktopAppInfo. @@ -47,6 +50,20 @@ public class AstalApps.Application : Object { */ public string[] keywords { owned get { return app.get_keywords(); } } + /** + * `Categories` field from the desktop file. + */ + public string[] categories { + owned get { + if (app.get_categories() == null) + return {}; + + var categories = app.get_categories(); + var arr = categories.split(";"); + return categories.has_suffix(";") ? arr[0:arr.length-1] : arr; + } + } + internal Application(string id, int? frequency = 0) { Object(app: new DesktopAppInfo(id)); this.frequency = frequency; @@ -94,6 +111,12 @@ public class AstalApps.Application : Object { score.keywords = s; } } + foreach (var category in categories) { + var s = fuzzy_match_string(term, category); + if (s > score.categories) { + score.categories = s; + } + } return score; } @@ -117,12 +140,27 @@ public class AstalApps.Application : Object { score.keywords = keyword.down().contains(term.down()) ? 1 : 0; } } + foreach (var category in categories) { + if (score.categories == 0) { + score.categories = category.down().contains(term.down()) ? 1 : 0; + } + } return score; } internal Json.Node to_json() { - var builder = new Json.Builder() + var keyword_arr = new Json.Builder().begin_array(); + foreach (string keyword in keywords) { + keyword_arr.add_string_value(keyword); + } + + var category_arr = new Json.Builder().begin_array(); + foreach (string category in categories) { + category_arr.add_string_value(category); + } + + return new Json.Builder() .begin_object() .set_member_name("name").add_string_value(name) .set_member_name("entry").add_string_value(entry) @@ -130,15 +168,8 @@ public class AstalApps.Application : Object { .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) - .set_member_name("keywords") - .begin_array(); - - foreach (string keyword in keywords) { - builder.add_string_value(keyword); - } - - return builder - .end_array() + .set_member_name("keywords").add_value(keyword_arr.end_array().get_root()) + .set_member_name("categories").add_value(category_arr.end_array().get_root()) .end_object() .get_root(); } @@ -150,4 +181,5 @@ public struct AstalApps.Score { int executable; int description; int keywords; + int categories; } diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index dde7d44..999643c 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -1,3 +1,8 @@ +/** + * This object can be used to query applications. + * Multipliers can be set to customize [[email protected]] results + * from queries which then are summed and sorted accordingly. + */ public class AstalApps.Apps : Object { private string cache_directory; private string cache_file; @@ -27,21 +32,21 @@ public class AstalApps.Apps : Object { /** * Extra multiplier to apply when matching the entry of an application. - * Defaults to `1` + * Defaults to `0` */ - public double entry_multiplier { get; set; default = 1; } + public double entry_multiplier { get; set; default = 0; } /** * Extra multiplier to apply when matching the executable of an application. - * Defaults to `1` + * Defaults to `0.5` */ - public double executable_multiplier { get; set; default = 1; } + public double executable_multiplier { get; set; default = 0.5; } /** * Extra multiplier to apply when matching the description of an application. - * Defaults to `0.5` + * Defaults to `0` */ - public double description_multiplier { get; set; default = 0.5; } + public double description_multiplier { get; set; default = 0; } /** * Extra multiplier to apply when matching the keywords of an application. @@ -50,34 +55,10 @@ public class AstalApps.Apps : Object { public double keywords_multiplier { get; set; default = 0.5; } /** - * Consider the name of an application during queries. - * Defaults to `true` - */ - public bool include_name { get; set; default = true; } - - /** - * Consider the entry of an application during queries. - * Defaults to `false` - */ - public bool include_entry { get; set; default = false; } - - /** - * Consider the executable of an application during queries. - * Defaults to `false` - */ - public bool include_executable { get; set; default = false; } - - /** - * Consider the description of an application during queries. - * Defaults to `false` - */ - public bool include_description { get; set; default = false; } - - /** - * Consider the keywords of an application during queries. - * Defaults to `false` + * Extra multiplier to apply when matching the categories of an application. + * Defaults to `0` */ - public bool include_keywords { get; set; default = false; } + public double categories_multiplier { get; set; default = 0; } construct { cache_directory = Environment.get_user_cache_dir() + "/astal"; @@ -115,11 +96,12 @@ public class AstalApps.Apps : Object { if (alg == FUZZY) s = a.fuzzy_match(search); if (alg == EXACT) s = a.exact_match(search); - if (include_name) r += s.name * name_multiplier; - if (include_entry) r += s.entry * entry_multiplier; - if (include_executable) r += s.executable * executable_multiplier; - if (include_description) r += s.description * description_multiplier; - if (include_keywords) r += s.keywords * keywords_multiplier; + r += s.name * name_multiplier; + r += s.entry * entry_multiplier; + r += s.executable * executable_multiplier; + r += s.description * description_multiplier; + r += s.keywords * keywords_multiplier; + r += s.categories * categories_multiplier; return r; } diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala index e513242..9287200 100644 --- a/lib/astal/gtk3/src/widget/window.vala +++ b/lib/astal/gtk3/src/widget/window.vala @@ -1,11 +1,12 @@ using GtkLayerShell; +[Flags] public enum Astal.WindowAnchor { - NONE = 0, - TOP = 1, - RIGHT = 2, - LEFT = 4, - BOTTOM = 8, + NONE, + TOP, + RIGHT, + LEFT, + BOTTOM, } public enum Astal.Exclusivity { @@ -112,7 +113,7 @@ public class Astal.Window : Gtk.Window { * If two perpendicular edges are anchored, the surface will be anchored to that corner. * If two opposite edges are anchored, the window will be stretched across the screen in that direction. */ - public int anchor { + public WindowAnchor anchor { set { if (check("set anchor")) return; diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index c7bd311..09b61b5 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -103,75 +103,58 @@ public static List<string> get_instances() { * Quit an an Astal instances. * It is the equivalent of `astal --quit -i instance`. */ -public static void quit_instance(string instance) { - try { - IApplication proxy = Bus.get_proxy_sync( - BusType.SESSION, - "io.Astal." + instance, - "/io/Astal/Application" - ); +public static void quit_instance(string instance) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); - proxy.quit(); - } catch (Error err) { - critical(err.message); - } + proxy.quit(); } /** * Open the Gtk debug tool of an an Astal instances. * It is the equivalent of `astal --inspector -i instance`. */ -public static void open_inspector(string instance) { - try { - IApplication proxy = Bus.get_proxy_sync( - BusType.SESSION, - "io.Astal." + instance, - "/io/Astal/Application" - ); +public static void open_inspector(string instance) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); - proxy.inspector(); - } catch (Error err) { - critical(err.message); - } + proxy.inspector(); } /** * Toggle a Window of an Astal instances. * It is the equivalent of `astal -i instance --toggle window`. */ -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" - ); +public static void toggle_window_by_name(string instance, string window) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); - proxy.toggle_window(window); - } catch (Error err) { - critical(err.message); - } + proxy.toggle_window(window); } /** * Send a message to an Astal instances. * It is the equivalent of `astal -i instance content of the message`. */ -public static string send_message(string instance, string msg) { +public static string send_message(string instance, string msg) throws Error { var rundir = Environment.get_user_runtime_dir(); var socket_path = @"$rundir/astal/$instance.sock"; var client = new SocketClient(); - try { - var conn = client.connect(new UnixSocketAddress(socket_path), null); - conn.output_stream.write(msg.concat("\x04").data); + 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 ""; - } + var stream = new DataInputStream(conn.input_stream); + return stream.read_upto("\x04", -1, null, null); } /** diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala index 8fc0523..9e47b53 100644 --- a/lib/astal/io/cli.vala +++ b/lib/astal/io/cli.vala @@ -11,13 +11,19 @@ const OptionEntry[] options = { { "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 err(string msg) { + var red = "\x1b[31m"; + var r = "\x1b[0m"; + printerr(@"$(red)error: $(r)$msg"); + return 1; +} + int main(string[] argv) { try { var opts = new OptionContext(); @@ -25,9 +31,8 @@ int main(string[] argv) { opts.set_help_enabled(false); opts.set_ignore_unknown_options(false); opts.parse(ref argv); - } catch (OptionError err) { - printerr (err.message); - return 1; + } catch (OptionError e) { + return err(e.message); } if (help) { @@ -55,24 +60,30 @@ int main(string[] argv) { if (list) { foreach (var name in AstalIO.get_instances()) - stdout.printf("%s\n", name); + print(@"$name\n"); return 0; } - if (quit) { - AstalIO.quit_instance(instance_name); - return 0; - } + try { + if (quit) { + AstalIO.quit_instance(instance_name); + return 0; + } - if (inspector) { - AstalIO.open_inspector(instance_name); - return 0; - } + if (inspector) { + AstalIO.open_inspector(instance_name); + return 0; + } - if (toggle_window != null) { - AstalIO.toggle_window_by_name(instance_name, toggle_window); - return 0; + if (toggle_window != null) { + AstalIO.toggle_window_by_name(instance_name, toggle_window); + return 0; + } + } catch (DBusError.SERVICE_UNKNOWN e) { + return err(@"there is no \"$instance_name\" instance runnning\n"); + } catch (Error e) { + return err(e.message); } var request = ""; @@ -80,8 +91,14 @@ int main(string[] argv) { request = request.concat(" ", argv[i]); } - var reply = AstalIO.send_message(instance_name, request); - print("%s\n", reply); + try { + var reply = AstalIO.send_message(instance_name, request); + print("%s\n", reply); + } catch (IOError.NOT_FOUND e) { + return err(@"there is no \"$instance_name\" instance runnning\n"); + } catch (Error e) { + return err(e.message); + } return 0; } diff --git a/lib/auth/src/pam.c b/lib/auth/src/pam.c index d0afec4..f50107e 100644 --- a/lib/auth/src/pam.c +++ b/lib/auth/src/pam.c @@ -58,7 +58,6 @@ static GParamSpec *astal_auth_pam_properties[ASTAL_AUTH_PAM_N_PROPERTIES] = { G_DEFINE_TYPE_WITH_PRIVATE(AstalAuthPam, astal_auth_pam, G_TYPE_OBJECT); /** - * * AstalAuthPam * * For simple authentication using only a password, using the [[email protected]] diff --git a/lib/bluetooth/adapter.vala b/lib/bluetooth/adapter.vala index 0c9d00e..99a59fb 100644 --- a/lib/bluetooth/adapter.vala +++ b/lib/bluetooth/adapter.vala @@ -1,27 +1,10 @@ -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 { +/** + * Object representing an [[https://github.com/RadiusNetworks/bluez/blob/master/doc/adapter-api.txt|adapter]]. + */ +public class AstalBluetooth.Adapter : Object { private IAdapter proxy; - public string object_path { owned get; construct set; } + + internal string object_path { owned get; private set; } internal Adapter(IAdapter proxy) { this.proxy = proxy; @@ -37,53 +20,127 @@ public class Adapter : Object { }); } + /** + * List of 128-bit UUIDs that represents the available local services. + */ public string[] uuids { owned get { return proxy.uuids; } } + + /** + * Indicates that a device discovery procedure is active. + */ public bool discovering { get { return proxy.discovering; } } + + /** + * Local Device ID information in modalias format used by the kernel and udev. + */ public string modalias { owned get { return proxy.modalias; } } + + /** + * The Bluetooth system name (pretty hostname). + */ public string name { owned get { return proxy.name; } } + + /** + * The Bluetooth class of device. + */ public uint class { get { return proxy.class; } } + + /** + * The Bluetooth device address. + */ public string address { owned get { return proxy.address; } } + + /** + * Switch an adapter to discoverable or non-discoverable + * to either make it visible or hide it. + */ public bool discoverable { get { return proxy.discoverable; } set { proxy.discoverable = value; } } + + /** + * Switch an adapter to pairable or non-pairable. + */ public bool pairable { get { return proxy.pairable; } set { proxy.pairable = value; } } + + /** + * Switch an adapter on or off. + */ public bool powered { get { return proxy.powered; } set { proxy.powered = value; } } + + /** + * The Bluetooth friendly name. + * + * In case no alias is set, it will return [[email protected]:name]. + */ public string alias { owned get { return proxy.alias; } set { proxy.alias = value; } } + + /** + * The discoverable timeout in seconds. + * A value of zero means that the timeout is disabled + * and it will stay in discoverable/limited mode forever + * until [[email protected]_discovery] is invoked. + * The default value for the discoverable timeout should be `180`. + */ public uint discoverable_timeout { get { return proxy.discoverable_timeout; } set { proxy.discoverable_timeout = value; } } + + /** + * The pairable timeout in seconds. + * + * A value of zero means that the timeout is disabled and it will stay in pairable mode forever. + * The default value for pairable timeout should be disabled `0`. + */ 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); } + + /** + * This removes the remote device and the pairing information. + * + * Possible errors: `InvalidArguments`, `Failed`. + */ + public void remove_device(Device device) throws Error { + proxy.remove_device(device.object_path); } - public void start_discovery() { - try { proxy.start_discovery(); } catch (Error err) { critical(err.message); } + + /** + * This method starts the device discovery procedure. + * + * Possible errors: `NotReady`, `Failed`. + */ + public void start_discovery() throws Error { + proxy.start_discovery(); } - public void stop_discovery() { - try { proxy.stop_discovery(); } catch (Error err) { critical(err.message); } + + /** + * This method will cancel any previous [[email protected]_discovery] procedure. + * + * Possible errors: `NotReady`, `Failed`, `NotAuthorized`. + */ + public void stop_discovery() throws Error { + proxy.stop_discovery(); } } -} diff --git a/lib/bluetooth/bluetooth.vala b/lib/bluetooth/bluetooth.vala index ce086ba..6eb6b76 100644 --- a/lib/bluetooth/bluetooth.vala +++ b/lib/bluetooth/bluetooth.vala @@ -1,11 +1,21 @@ namespace AstalBluetooth { -public Bluetooth get_default() { - return Bluetooth.get_default(); + /** + * Gets the default singleton Bluetooth object. + */ + public Bluetooth get_default() { + return Bluetooth.get_default(); + } } -public class Bluetooth : Object { +/** + * Manager object for `org.bluez`. + */ +public class AstalBluetooth.Bluetooth : Object { private static Bluetooth _instance; + /** + * Gets the default singleton Bluetooth object. + */ public static Bluetooth get_default() { if (_instance == null) _instance = new Bluetooth(); @@ -21,30 +31,59 @@ public class Bluetooth : Object { private HashTable<string, Device> _devices = new HashTable<string, Device>(str_hash, str_equal); + /** + * Emitted when a new device is registered on the `org.bluez` bus. + */ public signal void device_added (Device device) { notify_property("devices"); } + /** + * Emitted when a device is unregistered on the `org.bluez` bus. + */ public signal void device_removed (Device device) { notify_property("devices"); } + /** + * Emitted when an adapter is registered on the `org.bluez` bus. + */ public signal void adapter_added (Adapter adapter) { notify_property("adapters"); } + /** + * Emitted when an adapter is unregistered on the `org.bluez` bus. + */ public signal void adapter_removed (Adapter adapter) { notify_property("adapters"); } + /** + * `true` if any of the [[email protected]:adapters] are powered. + */ public bool is_powered { get; private set; default = false; } + + /** + * `true` if any of the [[email protected]:devices] is connected. + */ public bool is_connected { get; private set; default = false; } + + /** + * The first registered adapter which is usually the only adapter. + */ public Adapter? adapter { get { return adapters.nth_data(0); } } + /** + * List of adapters available on the host device. + */ public List<weak Adapter> adapters { owned get { return _adapters.get_values(); } } + /** + * List of registered devices on the `org.bluez` bus. + */ public List<weak Device> devices { owned get { return _devices.get_values(); } } @@ -85,6 +124,10 @@ public class Bluetooth : Object { } } + /** + * Toggle the [[email protected]:powered] + * property of the [[email protected]:adapter]. + */ public void toggle() { adapter.powered = !adapter.powered; } @@ -178,4 +221,3 @@ public class Bluetooth : Object { return false; } } -} diff --git a/lib/bluetooth/device.vala b/lib/bluetooth/device.vala index 8fe086f..3f00cd9 100644 --- a/lib/bluetooth/device.vala +++ b/lib/bluetooth/device.vala @@ -1,37 +1,14 @@ -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 { +/** + * Object representing a [[https://github.com/luetzel/bluez/blob/master/doc/device-api.txt|device]]. + */ +public class AstalBluetooth.Device : Object { private IDevice proxy; - public string object_path { owned get; construct set; } + + internal ObjectPath object_path { owned get; private set; } internal Device(IDevice proxy) { this.proxy = proxy; - this.object_path = proxy.g_object_path; + this.object_path = (ObjectPath)proxy.g_object_path; proxy.g_properties_changed.connect((props) => { var map = (HashTable<string, Variant>)props; foreach (var key in map.get_keys()) { @@ -43,64 +20,164 @@ public class Device : Object { }); } + /** + * List of 128-bit UUIDs that represents the available remote services. + */ public string[] uuids { owned get { return proxy.uuids; } } + + /** + * Indicates if the remote device is currently connected. + */ public bool connected { get { return proxy.connected; } } + + /** + * `true` if the device only supports the pre-2.1 pairing mechanism. + */ public bool legacy_pairing { get { return proxy.legacy_pairing; } } + + /** + * Indicates if the remote device is paired. + */ public bool paired { get { return proxy.paired; } } + + /** + * Received Signal Strength Indicator of the remote device (inquiry or advertising). + */ public int16 rssi { get { return proxy.rssi; } } + + /** + * The object path of the adapter the device belongs to. + */ public ObjectPath adapter { owned get { return proxy.adapter; } } + + /** + * The Bluetooth device address of the remote device. + */ public string address { owned get { return proxy.address; } } + + /** + * Proposed icon name. + */ public string icon { owned get { return proxy.icon; } } + + /** + * Remote Device ID information in modalias format used by the kernel and udev. + */ public string modalias { owned get { return proxy.modalias; } } + + /** + * The Bluetooth remote name. + * + * It is always better to use [[email protected]:alias]. + */ public string name { owned get { return proxy.name; } } + + /** + * External appearance of device, as found on GAP service. + */ public uint16 appearance { get { return proxy.appearance; } } + + /** + * The Bluetooth class of device of the remote device. + */ public uint32 class { get { return proxy.class; } } + + /** + * Indicates if this device is currently trying to be connected. + */ public bool connecting { get; private set; } + /** + * If set to `true` any incoming connections from the device will be immediately rejected. + */ public bool blocked { get { return proxy.blocked; } set { proxy.blocked = value; } } + /** + * Indicates if the remote is seen as trusted. + */ public bool trusted { get { return proxy.trusted; } set { proxy.trusted = value; } } + /** + * The name alias for the remote device. + * + * In case no alias is set, it will return the remote device [[email protected]:name]. + */ 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() { + /** + * This is a generic method to connect any profiles + * the remote device supports that can be connected to. + * + * Possible errors: `NotReady`, `Failed`, `InProgress`, `AlreadyConnected`. + */ + public async void connect_device() throws Error { 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); } + /** + * This method gracefully disconnects all connected profiles. + * + * Possible errors: `NotConnected`. + */ + public async void disconnect_device() throws Error { + yield proxy.disconnect(); } - public void connect_profile(string uuid) { - try { proxy.connect_profile(uuid); } catch (Error err) { critical(err.message); } + /** + * This method connects a specific profile of this device. + * The UUID provided is the remote service UUID for the profile. + * + * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotAvailable`, `NotReady`. + * + * @param uuid the remote service UUID. + */ + public void connect_profile(string uuid) throws Error { + proxy.connect_profile(uuid); } - public void disconnect_profile(string uuid) { - try { proxy.disconnect_profile(uuid); } catch (Error err) { critical(err.message); } + /** + * This method disconnects a specific profile of this device. + * + * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotSupported`. + * + * @param uuid the remote service UUID. + */ + public void disconnect_profile(string uuid) throws Error { + proxy.disconnect_profile(uuid); } - public void pair() { - try { proxy.pair(); } catch (Error err) { critical(err.message); } + /** + * This method will connect to the remote device and initiate pairing. + * + * Possible errors: `InvalidArguments`, `Failed`, `AlreadyExists`, + * `AuthenticationCanceled`, `AuthenticationFailed`, `AuthenticationRejected`, + * `AuthenticationTimeout`, `ConnectionAttemptFailed`. + */ + public void pair() throws Error { + proxy.pair(); + } + + /** + * This method can be used to cancel a pairing operation + * initiated by [[email protected]]. + * + * Possible errors: `DoesNotExist`, `Failed`. + */ + public void cancel_pairing() throws Error { + proxy.cancel_pairing(); } -} } diff --git a/lib/bluetooth/gir.py b/lib/bluetooth/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/bluetooth/gir.py @@ -0,0 +1 @@ +../gir.py
\ No newline at end of file diff --git a/lib/bluetooth/interfaces.vala b/lib/bluetooth/interfaces.vala new file mode 100644 index 0000000..dcb1c4b --- /dev/null +++ b/lib/bluetooth/interfaces.vala @@ -0,0 +1,46 @@ +[DBus (name = "org.bluez.Adapter1")] +private interface AstalBluetooth.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; } +} + +[DBus (name = "org.bluez.Device1")] +private interface AstalBluetooth.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; } +} + diff --git a/lib/bluetooth/meson.build b/lib/bluetooth/meson.build index 934d380..347b463 100644 --- a/lib/bluetooth/meson.build +++ b/lib/bluetooth/meson.build @@ -33,34 +33,41 @@ deps = [ dependency('gio-2.0'), ] -sources = [ - config, - 'utils.vala', - 'device.vala', +sources = [config] + files( 'adapter.vala', 'bluetooth.vala', -] + 'device.vala', + 'interfaces.vala', + 'utils.vala', +) lib = library( meson.project_name(), sources, dependencies: deps, + vala_args: ['--vapi-comments'], 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], + install_dir: [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', +pkgs = [] +foreach dep : deps + pkgs += ['--pkg=' + dep.name()] +endforeach + +gir_tgt = custom_target( + gir, + command: [find_program('python3'), files('gir.py'), meson.project_name(), gir] + + pkgs + + sources, + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', ) custom_target( @@ -73,7 +80,17 @@ custom_target( ], input: lib, output: typelib, - depends: lib, + depends: [lib, gir_tgt], install: true, install_dir: get_option('libdir') / 'girepository-1.0', ) + +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', +) diff --git a/lib/cava/.gitignore b/lib/cava/.gitignore new file mode 100644 index 0000000..2c7a6aa --- /dev/null +++ b/lib/cava/.gitignore @@ -0,0 +1 @@ +/subprojects/**/ diff --git a/lib/cava/astal-cava.h b/lib/cava/astal-cava.h new file mode 100644 index 0000000..343234a --- /dev/null +++ b/lib/cava/astal-cava.h @@ -0,0 +1,70 @@ +#ifndef ASTAL_CAVA_H +#define ASTAL_CAVA_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_CAVA_TYPE_INPUT (astal_cava_input_get_type()) + +typedef enum { + ASTAL_CAVA_INPUT_FIFO, + ASTAL_CAVA_INPUT_PORTAUDIO, + ASTAL_CAVA_INPUT_PIPEWIRE, + ASTAL_CAVA_INPUT_ALSA, + ASTAL_CAVA_INPUT_PULSE, + ASTAL_CAVA_INPUT_SNDIO, + ASTAL_CAVA_INPUT_OSS, + ASTAL_CAVA_INPUT_JACK, + ASTAL_CAVA_INPUT_SHMEM, + ASTAL_CAVA_INPUT_WINSCAP, +} AstalCavaInput; + +#define ASTAL_CAVA_TYPE_CAVA (astal_cava_cava_get_type()) + +G_DECLARE_FINAL_TYPE(AstalCavaCava, astal_cava_cava, ASTAL_CAVA, CAVA, GObject) + +AstalCavaCava* astal_cava_cava_get_default(); +AstalCavaCava* astal_cava_get_default(); + +gboolean astal_cava_cava_get_active(AstalCavaCava* self); +void astal_cava_cava_set_active(AstalCavaCava* self, gboolean active); + +GArray* astal_cava_cava_get_values(AstalCavaCava* self); + +gint astal_cava_cava_get_bars(AstalCavaCava* self); +void astal_cava_cava_set_bars(AstalCavaCava* self, gint bars); + +gboolean astal_cava_cava_get_autosens(AstalCavaCava* self); +void astal_cava_cava_set_autosens(AstalCavaCava* self, gboolean autosens); + +gboolean astal_cava_cava_get_stereo(AstalCavaCava* self); +void astal_cava_cava_set_stereo(AstalCavaCava* self, gboolean stereo); + +gdouble astal_cava_cava_get_noise_reduction(AstalCavaCava* self); +void astal_cava_cava_set_noise_reduction(AstalCavaCava* self, gdouble noise); + +gint astal_cava_cava_get_framerate(AstalCavaCava* self); +void astal_cava_cava_set_framerate(AstalCavaCava* self, gint framerate); + +AstalCavaInput astal_cava_cava_get_input(AstalCavaCava* self); +void astal_cava_cava_set_input(AstalCavaCava* self, AstalCavaInput input); + +gchar* astal_cava_cava_get_source(AstalCavaCava* self); +void astal_cava_cava_set_source(AstalCavaCava* self, const gchar* source); + +gint astal_cava_cava_get_channels(AstalCavaCava* self); +void astal_cava_cava_set_channels(AstalCavaCava* self, gint channels); + +gint astal_cava_cava_get_low_cutoff(AstalCavaCava* self); +void astal_cava_cava_set_low_cutoff(AstalCavaCava* self, gint low_cutoff); + +gint astal_cava_cava_get_high_cutoff(AstalCavaCava* self); +void astal_cava_cava_set_high_cutoff(AstalCavaCava* self, gint high_cutoff); + +gint astal_cava_cava_get_samplerate(AstalCavaCava* self); +void astal_cava_cava_set_samplerate(AstalCavaCava* self, gint samplerate); + +G_END_DECLS + +#endif // !ASTAL_CAVA_H diff --git a/lib/cava/cava.c b/lib/cava/cava.c new file mode 100644 index 0000000..1c5ef66 --- /dev/null +++ b/lib/cava/cava.c @@ -0,0 +1,659 @@ +#include <cava/common.h> +#include <gio/gio.h> + +#include "astal-cava.h" +#include "cava/config.h" +#include "glib-object.h" +#include "glib.h" +#include "glibconfig.h" + +struct _AstalCavaCava { + GObject parent_instance; + + gint bars; + gboolean autosens; + gboolean stereo; + gdouble noise_reduction; + gint framerate; + AstalCavaInput input; + gchar* audio_source; + gboolean active; + gint channels; + gint low_cutoff; + gint high_cutoff; + gint samplerate; + + GArray* values; +}; + +typedef struct { + struct cava_plan plan; + struct config_params cfg; + struct audio_data audio_data; + struct audio_raw audio_raw; + ptr input_src; + + gboolean constructed; + GThread* input_thread; + guint timer_id; + +} AstalCavaCavaPrivate; + +G_DEFINE_ENUM_TYPE(AstalCavaInput, astal_cava_input, + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_FIFO, "fifo"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PORTAUDIO, "portaudio"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PIPEWIRE, "pipewire"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_ALSA, "alsa"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PULSE, "pulse"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SNDIO, "sndio"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SHMEM, "shmem"), + G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_WINSCAP, "winscap")); + +G_DEFINE_TYPE_WITH_PRIVATE(AstalCavaCava, astal_cava_cava, G_TYPE_OBJECT) + +typedef enum { + ASTAL_CAVA_CAVA_PROP_VALUES = 1, + ASTAL_CAVA_CAVA_PROP_ACTIVE, + ASTAL_CAVA_CAVA_PROP_BARS, + ASTAL_CAVA_CAVA_PROP_AUTOSENS, + ASTAL_CAVA_CAVA_PROP_STEREO, + ASTAL_CAVA_CAVA_PROP_NOISE, + ASTAL_CAVA_CAVA_PROP_FRAMERATE, + ASTAL_CAVA_CAVA_PROP_INPUT, + ASTAL_CAVA_CAVA_PROP_SOURCE, + ASTAL_CAVA_CAVA_PROP_CHANNELS, + ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF, + ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF, + ASTAL_CAVA_CAVA_PROP_SAMPLERATE, + ASTAL_CAVA_CAVA_N_PROPERTIES +} AstalCavaProperties; + +static GParamSpec* astal_cava_cava_properties[ASTAL_CAVA_CAVA_N_PROPERTIES] = { + NULL, +}; + +static gboolean exec_cava(AstalCavaCava* self) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + + pthread_mutex_lock(&priv->audio_data.lock); + cava_execute(priv->audio_data.cava_in, priv->audio_data.samples_counter, + priv->audio_raw.cava_out, &priv->plan); + if (priv->audio_data.samples_counter > 0) priv->audio_data.samples_counter = 0; + pthread_mutex_unlock(&priv->audio_data.lock); + + g_array_remove_range(self->values, 0, priv->audio_raw.number_of_bars); + g_array_insert_vals(self->values, 0, priv->audio_raw.cava_out, priv->audio_raw.number_of_bars); + + g_object_notify(G_OBJECT(self), "values"); + + return G_SOURCE_CONTINUE; +} + +static void astal_cava_cava_cleanup(AstalCavaCava* self) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + + g_source_remove(priv->timer_id); + pthread_mutex_lock(&priv->audio_data.lock); + priv->audio_data.terminate = 1; + pthread_mutex_unlock(&priv->audio_data.lock); + g_thread_join(priv->input_thread); + + cava_destroy(&priv->plan); + + g_free(priv->audio_data.cava_in); + g_free(priv->audio_data.source); + + g_free(priv->cfg.audio_source); + g_free(priv->cfg.raw_target); + g_free(priv->cfg.data_format); +} + +static void astal_cava_cava_start(AstalCavaCava* self) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + + if (self->framerate < 1) { + self->framerate = 1; + } + + if (self->low_cutoff < 1) { + self->low_cutoff = 1; + } + + if (self->high_cutoff < self->low_cutoff) { + self->high_cutoff = self->low_cutoff + 1; + } + + if (self->samplerate / 2 <= self->high_cutoff) { + self->samplerate = self->high_cutoff * 2 + 1; + } + + if (self->bars < 1) { + self->bars = 1; + } + + if (self->channels < 1 || self->channels > 2) { + self->channels = 2; + } + + priv->cfg = (struct config_params){ + .inAtty = 0, + .output = OUTPUT_RAW, + .raw_target = strdup("/dev/stdout"), + .data_format = strdup("binary"), + + .fixedbars = self->bars, + .autosens = self->autosens, + .stereo = self->stereo, + .noise_reduction = self->noise_reduction, + .framerate = self->framerate, + .input = (enum input_method)self->input, + .channels = self->channels, + .lower_cut_off = self->low_cutoff, + .upper_cut_off = self->high_cutoff, + .samplerate = self->samplerate, + + // not needed in this lib + .mono_opt = AVERAGE, + .waves = 0, + .userEQ = NULL, + .userEQ_keys = 0, + .userEQ_enabled = 0, + .samplebits = 16, + .waveform = 0, + .monstercat = 0, + .sens = 1, + .autoconnect = 2, + .reverse = 0, + .sleep_timer = 0, + .show_idle_bar_heads = 1, + .continuous_rendering = 0, + .sdl_width = 1000, + .sdl_height = 500, + .sdl_x = -1, + .sdl_y = -1, + .sdl_full_screen = 0, + .draw_and_quit = 0, + .zero_test = 0, + .non_zero_test = 0, + .sync_updates = 0, + .disable_blanking = 1, + .bar_height = 32, + .col = 0, + .bgcol = 0, + .autobars = 0, + .raw_format = FORMAT_BINARY, + .ascii_range = 1000, + .bit_format = 16, + .gradient = 0, + .gradient_count = 0, + .bar_width = 2, + .bar_spacing = 1, + .xaxis = NONE, + .orientation = ORIENT_BOTTOM, + .color = NULL, + .bcolor = NULL, + .gradient_colors = NULL, + .vertex_shader = NULL, + .fragment_shader = NULL, + .bar_delim = ';', + .frame_delim = '\n', + }; + + if (g_strcmp0(self->audio_source, "auto") == 0) { + switch (priv->cfg.input) { + case INPUT_ALSA: + priv->cfg.audio_source = g_strdup("hw:Loopback,1"); + break; + case INPUT_FIFO: + priv->cfg.audio_source = g_strdup("/tmp/mpd.fifo"); + break; + case INPUT_PULSE: + priv->cfg.audio_source = g_strdup("auto"); + break; + case INPUT_PIPEWIRE: + priv->cfg.audio_source = g_strdup("auto"); + break; + case INPUT_SNDIO: + priv->cfg.audio_source = g_strdup("default"); + break; + case INPUT_OSS: + priv->cfg.audio_source = g_strdup("/dev/dsp"); + break; + case INPUT_JACK: + priv->cfg.audio_source = g_strdup("default"); + break; + case INPUT_SHMEM: + priv->cfg.audio_source = g_strdup("/squeezelite-00:00:00:00:00:00"); + break; + case INPUT_PORTAUDIO: + priv->cfg.audio_source = g_strdup("auto"); + break; + default: + g_critical("unsupported audio source"); + } + } else { + priv->cfg.audio_source = g_strdup(self->audio_source); + } + + priv->audio_data = (struct audio_data){ + .cava_in = calloc(BUFFER_SIZE * priv->cfg.channels * 8, sizeof(gdouble)), + .input_buffer_size = BUFFER_SIZE * priv->cfg.channels, + .cava_buffer_size = BUFFER_SIZE * priv->cfg.channels * 8, + .format = -1, + .rate = 0, + .channels = priv->cfg.channels, + .source = g_strdup(priv->cfg.audio_source), + .terminate = 0, + .samples_counter = 0, + .IEEE_FLOAT = 0, + .suspendFlag = false, + }; + + priv->input_src = get_input(&priv->audio_data, &priv->cfg); + + audio_raw_init(&priv->audio_data, &priv->audio_raw, &priv->cfg, &priv->plan); + + priv->input_thread = g_thread_new("cava_input", priv->input_src, &priv->audio_data); + + priv->timer_id = g_timeout_add(1000 / priv->cfg.framerate, G_SOURCE_FUNC(exec_cava), self); +} + +static void astal_cava_cava_restart(AstalCavaCava* self) { + if (!self->active) return; + astal_cava_cava_cleanup(self); + astal_cava_cava_start(self); +} + +gboolean astal_cava_cava_get_active(AstalCavaCava* self) { return self->active; } + +void astal_cava_cava_set_active(AstalCavaCava* self, gboolean active) { + if (self->active == active) return; + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + + self->active = active; + + if (!priv->constructed) return; + if (!active) + astal_cava_cava_cleanup(self); + else + astal_cava_cava_start(self); +} + +/** + * astal_cava_cava_get_values + * @self: the AstalCavaCava object + * + * Returns: (transfer none) (element-type gdouble): a list of values + * + */ +GArray* astal_cava_cava_get_values(AstalCavaCava* self) { return self->values; } + +gint astal_cava_cava_get_bars(AstalCavaCava* self) { return self->bars; } + +void astal_cava_cava_set_bars(AstalCavaCava* self, gint bars) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->bars = bars; + if (priv->constructed) { + g_array_set_size(self->values, self->bars); + astal_cava_cava_restart(self); + } +} + +gboolean astal_cava_cava_get_autosens(AstalCavaCava* self) { return self->autosens; } + +void astal_cava_cava_set_autosens(AstalCavaCava* self, gboolean autosens) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->autosens = autosens; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gboolean astal_cava_cava_get_stereo(AstalCavaCava* self) { return self->stereo; } + +void astal_cava_cava_set_stereo(AstalCavaCava* self, gboolean stereo) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->stereo = stereo; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gdouble astal_cava_cava_get_noise_reduction(AstalCavaCava* self) { return self->noise_reduction; } + +void astal_cava_cava_set_noise_reduction(AstalCavaCava* self, gdouble noise) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->noise_reduction = noise; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gint astal_cava_cava_get_framerate(AstalCavaCava* self) { return self->framerate; } + +void astal_cava_cava_set_framerate(AstalCavaCava* self, gint framerate) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->framerate = framerate; + if (priv->constructed) astal_cava_cava_restart(self); +} + +AstalCavaInput astal_cava_cava_get_input(AstalCavaCava* self) { return self->input; } + +void astal_cava_cava_set_input(AstalCavaCava* self, AstalCavaInput input) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->input = input; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gchar* astal_cava_cava_get_source(AstalCavaCava* self) { return self->audio_source; } + +void astal_cava_cava_set_source(AstalCavaCava* self, const gchar* source) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + g_free(self->audio_source); + self->audio_source = g_strdup(source); + if (priv->constructed) astal_cava_cava_restart(self); +} + +gint astal_cava_cava_get_channels(AstalCavaCava* self) { return self->channels; } + +void astal_cava_cava_set_channels(AstalCavaCava* self, gint channels) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->channels = channels; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gint astal_cava_cava_get_low_cutoff(AstalCavaCava* self) { return self->low_cutoff; } + +void astal_cava_cava_set_low_cutoff(AstalCavaCava* self, gint low_cutoff) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->low_cutoff = low_cutoff; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gint astal_cava_cava_get_high_cutoff(AstalCavaCava* self) { return self->high_cutoff; } + +void astal_cava_cava_set_high_cutoff(AstalCavaCava* self, gint high_cutoff) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->high_cutoff = high_cutoff; + if (priv->constructed) astal_cava_cava_restart(self); +} + +gint astal_cava_cava_get_samplerate(AstalCavaCava* self) { return self->samplerate; } + +void astal_cava_cava_set_samplerate(AstalCavaCava* self, gint samplerate) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + self->samplerate = samplerate; + if (priv->constructed) astal_cava_cava_restart(self); +} + +static void astal_cava_cava_set_property(GObject* object, guint property_id, const GValue* value, + GParamSpec* pspec) { + AstalCavaCava* self = ASTAL_CAVA_CAVA(object); + + switch (property_id) { + case ASTAL_CAVA_CAVA_PROP_BARS: + astal_cava_cava_set_bars(self, g_value_get_int(value)); + break; + case ASTAL_CAVA_CAVA_PROP_ACTIVE: + astal_cava_cava_set_active(self, g_value_get_boolean(value)); + break; + case ASTAL_CAVA_CAVA_PROP_AUTOSENS: + astal_cava_cava_set_autosens(self, g_value_get_boolean(value)); + break; + case ASTAL_CAVA_CAVA_PROP_NOISE: + astal_cava_cava_set_noise_reduction(self, g_value_get_double(value)); + break; + case ASTAL_CAVA_CAVA_PROP_STEREO: + astal_cava_cava_set_stereo(self, g_value_get_boolean(value)); + break; + case ASTAL_CAVA_CAVA_PROP_FRAMERATE: + astal_cava_cava_set_framerate(self, g_value_get_int(value)); + break; + case ASTAL_CAVA_CAVA_PROP_INPUT: + astal_cava_cava_set_input(self, g_value_get_enum(value)); + break; + case ASTAL_CAVA_CAVA_PROP_SOURCE: + g_free(self->audio_source); + astal_cava_cava_set_source(self, g_value_get_string(value)); + break; + case ASTAL_CAVA_CAVA_PROP_CHANNELS: + astal_cava_cava_set_channels(self, g_value_get_int(value)); + break; + case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF: + astal_cava_cava_set_low_cutoff(self, g_value_get_int(value)); + break; + case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF: + astal_cava_cava_set_high_cutoff(self, g_value_get_int(value)); + break; + case ASTAL_CAVA_CAVA_PROP_SAMPLERATE: + astal_cava_cava_set_samplerate(self, g_value_get_int(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_cava_cava_get_property(GObject* object, guint property_id, GValue* value, + GParamSpec* pspec) { + AstalCavaCava* self = ASTAL_CAVA_CAVA(object); + + switch (property_id) { + case ASTAL_CAVA_CAVA_PROP_ACTIVE: + g_value_set_boolean(value, self->active); + break; + case ASTAL_CAVA_CAVA_PROP_BARS: + g_value_set_int(value, self->bars); + break; + case ASTAL_CAVA_CAVA_PROP_VALUES: + g_value_set_pointer(value, self->values); + break; + case ASTAL_CAVA_CAVA_PROP_AUTOSENS: + g_value_set_boolean(value, self->autosens); + break; + case ASTAL_CAVA_CAVA_PROP_NOISE: + g_value_set_double(value, self->noise_reduction); + break; + case ASTAL_CAVA_CAVA_PROP_STEREO: + g_value_set_boolean(value, self->stereo); + break; + case ASTAL_CAVA_CAVA_PROP_FRAMERATE: + g_value_set_int(value, self->framerate); + break; + case ASTAL_CAVA_CAVA_PROP_INPUT: + g_value_set_enum(value, self->input); + break; + case ASTAL_CAVA_CAVA_PROP_SOURCE: + g_value_set_string(value, self->audio_source); + break; + case ASTAL_CAVA_CAVA_PROP_CHANNELS: + g_value_set_int(value, self->channels); + break; + case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF: + g_value_set_int(value, self->low_cutoff); + break; + case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF: + g_value_set_int(value, self->high_cutoff); + break; + case ASTAL_CAVA_CAVA_PROP_SAMPLERATE: + g_value_set_int(value, self->samplerate); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_cava_cava_constructed(GObject* object) { + AstalCavaCava* self = ASTAL_CAVA_CAVA(object); + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + + gdouble* data = calloc(self->bars, sizeof(gdouble)); + memset(data, 0, self->bars * sizeof(gdouble)); + self->values = g_array_new_take(data, self->bars, TRUE, sizeof(gdouble)); + + priv->constructed = true; + + if (self->active) astal_cava_cava_start(self); +} + +static void astal_cava_cava_init(AstalCavaCava* self) { + AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self); + priv->constructed = false; + self->low_cutoff = 50; + self->high_cutoff = 10000; + self->samplerate = 44100; +} + +/** + * astal_cava_get_default + * + * gets the default Cava object. + * + * Returns: (nullable) (transfer none): + */ +AstalCavaCava* astal_cava_get_default() { return astal_cava_cava_get_default(); } + +/** + * astal_cava_cava_get_default + * + * gets the default Cava object. + * + * Returns: (nullable) (transfer none): + */ +AstalCavaCava* astal_cava_cava_get_default() { + static AstalCavaCava* self = NULL; + + if (self == NULL) self = g_object_new(ASTAL_CAVA_TYPE_CAVA, NULL); + + return self; +} + +static void astal_cava_cava_dispose(GObject* object) { + AstalCavaCava* self = ASTAL_CAVA_CAVA(object); + + if (self->active) astal_cava_cava_cleanup(self); + G_OBJECT_CLASS(astal_cava_cava_parent_class)->dispose(object); +} + +static void astal_cava_cava_finalize(GObject* object) { + AstalCavaCava* self = ASTAL_CAVA_CAVA(object); + + g_array_free(self->values, TRUE); + + G_OBJECT_CLASS(astal_cava_cava_parent_class)->finalize(object); +} + +static void astal_cava_cava_class_init(AstalCavaCavaClass* class) { + GObjectClass* object_class = G_OBJECT_CLASS(class); + object_class->get_property = astal_cava_cava_get_property; + object_class->set_property = astal_cava_cava_set_property; + object_class->constructed = astal_cava_cava_constructed; + object_class->dispose = astal_cava_cava_dispose; + object_class->finalize = astal_cava_cava_finalize; + + /** + * AstalCavaCava:values: (type GArray(gdouble)) + * + * A list of values, each represent the height of one bar. The values are generally between 0 + * and 1 but can overshoot occasionally, in which case the sensitivity will be decreased + * automatically if [[email protected]:autosens] is set. The array will have + * [[email protected]:bars] entries. If [[email protected]:stereo] is set, the first + * half of the array will represent the left channel and the second half the right channel, so + * there will be only bars/2 bars per channel. If the number of bars is odd, the last value will + * be 0. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_VALUES] = + g_param_spec_pointer("values", "values", "a list of values", G_PARAM_READABLE); + /** + * AstalCavaCava:active: + * + * whether or not the audio capture and visualization is running. if false the values array will + * not be updated. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_ACTIVE] = g_param_spec_boolean( + "active", "active", "active", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:bars: + * + * the number of bars the visualizer should create. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_BARS] = + g_param_spec_int("bars", "bars", "number of bars per channel", 1, G_MAXINT, 20, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:autosens: + * + * When set, the sensitivity will automatically be adjusted. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_AUTOSENS] = + g_param_spec_boolean("autosens", "autosens", "dynamically adjust sensitivity", TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:cava: + * + * When set the output will contain visualization data for both channels. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_STEREO] = g_param_spec_boolean( + "stereo", "stereo", "stereo", FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:noise-reduction: + * + * adjusts the noise-reduction filter. low values are fast and noisy, large values are slow and + * smooth. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_NOISE] = + g_param_spec_double("noise_reduction", "noise_reduction", "noise reduction", 0, 1, 0.77, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:framerate: + * + * how often the values should be updated + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_FRAMERATE] = + g_param_spec_int("framerate", "framerate", "framerate", 1, G_MAXINT, 60, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:channels: + * + * how many input channels to consider + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_CHANNELS] = g_param_spec_int( + "channels", "channels", "channels", 1, 2, 2, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:low-cutoff: + * + * cut off frequencies below this value + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF] = + g_param_spec_int("low-cutoff", "low-cutoff", "lower frequency cutoff", 1, G_MAXINT, 50, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:high-cutoff: + * + * cut off frequencies above this value + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF] = + g_param_spec_int("high-cutoff", "high-cutoff", "higher frequency cutoff", 1, G_MAXINT, + 10000, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:samplerate: + * + * the samplerate of the input + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SAMPLERATE] = + g_param_spec_int("samplerate", "samplerate", "samplerate", 1, G_MAXINT, 44100, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:input: (type AstalCavaInput) + * + * specifies which audio server should be used. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_INPUT] = + g_param_spec_enum("input", "input", "input", ASTAL_CAVA_TYPE_INPUT, + ASTAL_CAVA_INPUT_PIPEWIRE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + /** + * AstalCavaCava:source: + * + * specifies which audio source should be used. Refer to the cava docs on how to use this + * property. + */ + astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SOURCE] = g_param_spec_string( + "source", "source", "source", "auto", G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + g_object_class_install_properties(object_class, ASTAL_CAVA_CAVA_N_PROPERTIES, + astal_cava_cava_properties); +} diff --git a/lib/cava/meson.build b/lib/cava/meson.build new file mode 100644 index 0000000..874eaf7 --- /dev/null +++ b/lib/cava/meson.build @@ -0,0 +1,80 @@ +project( + 'astal-cava', + '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', '-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') + +srcs = files( + 'astal-cava.h', + 'cava.c', +) + +install_headers('astal-cava.h') + +cava = dependency( + 'cava', + version: '>=0.10.3', + required: true, + fallback: ['cava', 'cava_dep'], +) + +deps = [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + cava, +] + +astal_cava_lib = library( + 'astal-cava', + sources: srcs, + dependencies: deps, + version: meson.project_version(), + install: true, +) + +libastal_cava = declare_dependency(link_with: astal_cava_lib) + +pkg_config_name = 'astal-cava-' + lib_so_version + +if get_option('introspection') + gir = gnome.generate_gir( + astal_cava_lib, + sources: srcs, + nsversion: '0.1', + namespace: 'AstalCava', + symbol_prefix: 'astal_cava', + identifier_prefix: 'AstalCava', + includes: ['GObject-2.0', 'Gio-2.0'], + header: 'astal-cava.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-cava', + version: meson.project_version(), + libraries: [astal_cava_lib], + filebase: pkg_config_name, + subdirs: 'astal', + description: 'audio analyzing service using cava', + url: 'https://github.com/Aylur/astal', +) diff --git a/lib/cava/meson_options.txt b/lib/cava/meson_options.txt new file mode 100644 index 0000000..97aa4e7 --- /dev/null +++ b/lib/cava/meson_options.txt @@ -0,0 +1,13 @@ +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/lib/cava/subprojects/cava.wrap b/lib/cava/subprojects/cava.wrap new file mode 100644 index 0000000..f0309bf --- /dev/null +++ b/lib/cava/subprojects/cava.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = cava-0.10.3 +source_url = https://github.com/LukashonakV/cava/archive/0.10.3.tar.gz +source_filename = cava-0.10.3.tar.gz +source_hash = aab0a4ed3f999e8461ad9de63ef8a77f28b6b2011f7dd0c69ba81819d442f6f9 +[provide] +cava = cava_dep diff --git a/lib/cava/version b/lib/cava/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/lib/cava/version @@ -0,0 +1 @@ +0.1.0 diff --git a/lib/hyprland/client.vala b/lib/hyprland/client.vala index 3df644b..3f2d0fb 100644 --- a/lib/hyprland/client.vala +++ b/lib/hyprland/client.vala @@ -73,10 +73,11 @@ public class Client : Object { } } +[Flags] public enum Fullscreen { CURRENT = -1, NONE = 0, - FULLSCREEN = 1, - MAXIMIZED = 2, + MAXIMIZED = 1, + FULLSCREEN = 2, } } diff --git a/lib/mpris/gir.py b/lib/mpris/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/mpris/gir.py @@ -0,0 +1 @@ +../gir.py
\ No newline at end of file diff --git a/lib/mpris/ifaces.vala b/lib/mpris/ifaces.vala index 4a9d715..298a288 100644 --- a/lib/mpris/ifaces.vala +++ b/lib/mpris/ifaces.vala @@ -1,19 +1,18 @@ -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); +private interface AstalMpris.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); +private interface AstalMpris.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; +private interface AstalMpris.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; } @@ -27,18 +26,18 @@ internal interface IMpris : PropsIface { } [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; +private interface AstalMpris.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 signal void seeked(int64 position); public abstract string playback_status { owned get; } public abstract string loop_status { owned get; set; } @@ -57,4 +56,3 @@ internal interface IPlayer : IMpris { public abstract bool can_seek { get; } public abstract bool can_control { get; } } -} diff --git a/lib/mpris/meson.build b/lib/mpris/meson.build index c9a5c53..bf215c9 100644 --- a/lib/mpris/meson.build +++ b/lib/mpris/meson.build @@ -38,34 +38,40 @@ deps = [ dependency('json-glib-1.0'), ] -sources = [ - config, +sources = [config] + files( 'ifaces.vala', - 'player.vala', 'mpris.vala', -] + 'player.vala', +) if get_option('lib') lib = library( meson.project_name(), sources, dependencies: deps, + vala_args: ['--vapi-comments'], 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], + install_dir: [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', + pkgs = [] + foreach dep : deps + pkgs += ['--pkg=' + dep.name()] + endforeach + + gir_tgt = custom_target( + gir, + command: [find_program('python3'), files('gir.py'), meson.project_name(), gir] + + pkgs + + sources, + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', ) custom_target( @@ -78,10 +84,20 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + depends: [lib, gir_tgt], install: true, install_dir: get_option('libdir') / 'girepository-1.0', ) + + 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', + ) endif if get_option('cli') diff --git a/lib/mpris/mpris.vala b/lib/mpris/mpris.vala index 0e55a2e..8039d39 100644 --- a/lib/mpris/mpris.vala +++ b/lib/mpris/mpris.vala @@ -1,12 +1,24 @@ namespace AstalMpris { -public Mpris get_default() { - return Mpris.get_default(); + /** + * Gets the default singleton Mpris instance. + */ + public Mpris get_default() { + return Mpris.get_default(); + } } -public class Mpris : Object { +/** + * Object that monitors dbus for players to appear and disappear. + */ +public class AstalMpris.Mpris : Object { internal static string PREFIX = "org.mpris.MediaPlayer2."; private static Mpris instance; + private DBusImpl proxy; + + /** + * Gets the default singleton Mpris instance. + */ public static Mpris get_default() { if (instance == null) instance = new Mpris(); @@ -14,15 +26,23 @@ public class Mpris : Object { return instance; } - private DBusImpl proxy; - private HashTable<string, Player> _players = new HashTable<string, Player> (str_hash, str_equal); + /** + * List of currently available players. + */ 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); + /** + * Emitted when a new mpris Player appears. + */ + public signal void player_added(Player player); + + /** + * Emitted when a Player disappears. + */ + public signal void player_closed(Player player); construct { try { @@ -63,4 +83,3 @@ public class Mpris : Object { notify_property("players"); } } -} diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala index 6764d2b..2050f61 100644 --- a/lib/mpris/player.vala +++ b/lib/mpris/player.vala @@ -1,75 +1,184 @@ -namespace AstalMpris { -public class Player : Object { +/** + * Object which tracks players through their mpris dbus interface. + * The most simple way is to use [[email protected]] which tracks every player, + * but [[email protected]] can be constructed for a dedicated players too. + */ +public class AstalMpris.Player : Object { private static string COVER_CACHE = Environment.get_user_cache_dir() + "/astal/mpris"; private IPlayer proxy; + private uint pollid; // periodically notify position - public signal void appeared () { available = true; } - public signal void closed () { available = false; } + internal signal void appeared() { available = true; } + internal signal void closed() { available = false; } - // identifiers - public string bus_name { owned get; construct set; } - public bool available { get; private set; } + /** + * Full dbus namae of this player. + */ + public string bus_name { owned get; private set; } - // periodically notify position - private uint pollid; + /** + * Indicates if [[email protected]:bus_name] is available on dbus. + */ + public bool available { get; private set; } // mpris + + /** + * Brings the player's user interface to the front + * using any appropriate mechanism available. + * + * The media player may be unable to control how its user interface is displayed, + * or it may not have a graphical user interface at all. + * In this case, the [[email protected]:can_raise] is `false` and this method does nothing. + */ public void raise() { - try { proxy.raise(); } catch (Error error) { critical(error.message); } + try { proxy.raise(); } catch (Error err) { critical(err.message); } } + /** + * Causes the media player to stop running. + * + * The media player may refuse to allow clients to shut it down. + * In this case, the [[email protected]:can_quit] property is false and this method does nothing. + */ public void quit() { - try { proxy.quit(); } catch (Error error) { critical(error.message); } + try { proxy.quit(); } catch (Error err) { critical(err.message); } } + /** + * Indicates if [[email protected]] has any effect. + */ public bool can_quit { get; private set; } + + /** + * Indicates if the player is occupying the fullscreen. This is typically used for videos. + * Use [[email protected]_fullscreen] to toggle fullscreen state. + */ public bool fullscreen { get; private set; } + + /** + * Indicates if [[email protected]_fullscreen] has any effect. + */ public bool can_set_fullscreen { get; private set; } + + /** + * Indicates if [[email protected]] has any effect. + */ public bool can_raise { get; private set; } - public bool has_track_list { get; private set; } + + // TODO: Tracklist interface + // public bool has_track_list { get; private set; } + + /** + * A human friendly name to identify the player. + */ public string identity { owned get; private set; } + + /** + * The base name of a .desktop file + */ public string entry { owned get; private set; } + + /** + * The URI schemes supported by the media player. + * + * This can be viewed as protocols supported by the player in almost all cases. + * Almost every media player will include support for the "file" scheme. + * Other common schemes are "http" and "rtsp". + */ public string[] supported_uri_schemas { owned get; private set; } + + /** + * The mime-types supported by the player. + */ public string[] supported_mime_types { owned get; private set; } + /** + * Toggle [[email protected]:fullscreen] state. + */ public void toggle_fullscreen() { if (!can_set_fullscreen) - critical("can not set fullscreen on " + bus_name); + critical(@"can not set fullscreen on $bus_name"); proxy.fullscreen = !fullscreen; } - // player + /** + * Skips to the next track in the tracklist. + * If there is no next track (and endless playback and track repeat are both off), stop playback. + * If [[email protected]:can_go_next] is `false` this method has no effect. + */ public void next() { try { proxy.next(); } catch (Error error) { critical(error.message); } } + /** + * Skips to the previous track in the tracklist. + * If there is no previous track (and endless playback and track repeat are both off), stop playback. + * If [[email protected]:can_go_previous] is `false` this method has no effect. + */ public void previous() { try { proxy.previous(); } catch (Error error) { critical(error.message); } } + /** + * Pauses playback. + * If playback is already paused, this has no effect. + * If [[email protected]:can_pause] is `false` this method has no effect. + */ public void pause() { try { proxy.pause(); } catch (Error error) { critical(error.message); } } + /** + * Pauses playback. + * If playback is already paused, resumes playback. + * If playback is stopped, starts playback. + */ public void play_pause() { try { proxy.play_pause(); } catch (Error error) { critical(error.message); } } + /** + * Stops playback. + * If playback is already stopped, this has no effect. + * If [[email protected]:can_control] is `false` this method has no effect. + */ public void stop() { try { proxy.stop(); } catch (Error error) { critical(error.message); } } + /** + * Starts or resumes playback. + * If already playing, this has no effect. + * If paused, playback resumes from the current position. + * If [[email protected]:can_play] is `false` this method has no effect. + */ public void play() { try { proxy.play(); } catch (Error error) { critical(error.message); } } + /** + * uri scheme should be an element of [[email protected]:supported_uri_schemas] + * and the mime-type should match one of the elements of [[email protected]:supported_mime_types]. + * + * @param uri Uri of the track to load. + */ public void open_uri(string uri) { try { proxy.open_uri(uri); } catch (Error error) { critical(error.message); } } + /** + * Change [[email protected]:loop_status] from none to track, + * from track to playlist, from playlist to none. + */ public void loop() { + if (loop_status == Loop.UNSUPPORTED) { + critical(@"loop is unsupported by $bus_name"); + return; + } + switch (loop_status) { case Loop.NONE: loop_status = Loop.TRACK; @@ -85,15 +194,21 @@ public class Player : Object { } } + /** + * Toggle [[email protected]:shuffle_status]. + */ public void shuffle() { + if (shuffle_status == Shuffle.UNSUPPORTED) { + critical(@"shuffle is unsupported by $bus_name"); + return; + } + shuffle_status = shuffle_status == Shuffle.ON ? Shuffle.OFF : Shuffle.ON; } - public signal void seeked (int64 position); - - public double _get_position() { + private double _get_position() { try { var reply = proxy.call_sync( "org.freedesktop.DBus.Properties.Get", @@ -130,63 +245,175 @@ public class Player : Object { private Shuffle _shuffle_status = Shuffle.UNSUPPORTED; private double _volume = -1; + /** + * The current loop/repeat status. + */ public Loop loop_status { get { return _loop_status; } set { proxy.loop_status = value.to_string(); } } + /** + * The current playback rate. + */ public double rate { get { return _rate; } set { proxy.rate = value; } } + /** + * The current shuffle status. + */ public Shuffle shuffle_status { get { return _shuffle_status; } set { proxy.shuffle = value == Shuffle.ON; } } + /** + * The current volume level between 0 and 1. + */ public double volume { get { return _volume; } set { proxy.volume = value; } } + /** + * The current position of the track in seconds. + * To get a progress percentage simply divide this with [[email protected]:length]. + */ public double position { get { return _get_position(); } set { _set_position(value); } } + /** + * The current playback status. + */ public PlaybackStatus playback_status { get; private set; } + + /** + * The minimum value which the [[email protected]:rate] can take. + */ public double minimum_rate { get; private set; } + + /** + * The maximum value which the [[email protected]:rate] can take. + */ public double maximum_rate { get; private set; } + + /** + * Indicates if invoking [[email protected]] has effect. + */ public bool can_go_next { get; private set; } + + /** + * Indicates if invoking [[email protected]] has effect. + */ public bool can_go_previous { get; private set; } + + /** + * Indicates if invoking [[email protected]] has effect. + */ public bool can_play { get; private set; } + + /** + * Indicates if invoking [[email protected]] has effect. + */ public bool can_pause { get; private set; } + + /** + * Indicates if setting [[email protected]:position] has effect. + */ 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; } + /** + * Indicates if the player can be controlled with + * methods such as [[email protected]_pause]. + */ + public bool can_control { get; private set; } + /** + * Metadata hashtable of this player. + * In languages that cannot introspect this + * use [[email protected]_meta]. + */ + [CCode (notify = false)] // notified manually in sync + public HashTable<string, Variant> metadata { owned get; private set; } + + /** + * Currently playing track's id. + */ public string trackid { owned get; private set; } + + /** + * Length of the currently playing track in seconds. + */ public double length { get; private set; } + + /** + * The location of an image representing the track or album. + * You should always prefer to use [[email protected]:cover_art]. + */ public string art_url { owned get; private set; } + /** + * Title of the currently playing album. + */ public string album { owned get; private set; } + + /** + * Artists of the currently playing album. + */ public string album_artist { owned get; private set; } + + /** + * Artists of the currently playing track. + */ public string artist { owned get; private set; } + + /** + * Lyrics of the currently playing track. + */ public string lyrics { owned get; private set; } + + /** + * Title of the currently playing track. + */ public string title { owned get; private set; } + + /** + * Composers of the currently playing track. + */ public string composer { owned get; private set; } + + /** + * Comments of the currently playing track. + */ public string comments { owned get; private set; } - // cached cover art + /** + * Path of the cached [[email protected]:art_url]. + */ public string cover_art { owned get; private set; } + /** + * Lookup a key from [[email protected]:metadata]. + * This method is useful for languages that fail to introspect hashtables. + */ + public Variant? get_meta(string key) { + return metadata.lookup(key); + } + + /** + * Construct a Player that tracks a dbus name. For example "org.mpris.MediaPlayer2.spotify". + * The "org.mpris.MediaPlayer2." prefix can be leftout so simply "spotify" would mean the same. + * [[email protected]:available] indicates whether the player is actually running or not. + * + * @param name dbus name of the player. + */ public Player(string name) { - Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.") - ? name : "org.mpris.MediaPlayer2." + name); + bus_name = name.has_prefix("org.mpris.MediaPlayer2.") + ? name : @"org.mpris.MediaPlayer2.$name"; } private void sync() { @@ -195,7 +422,7 @@ public class Player : Object { fullscreen = proxy.fullscreen; can_set_fullscreen = proxy.can_set_fullscreen; can_raise = proxy.can_raise; - has_track_list = proxy.has_track_list; + // has_track_list = proxy.has_track_list; identity = proxy.identity; entry = proxy.desktop_entry; supported_uri_schemas = proxy.supported_uri_schemas; @@ -310,10 +537,6 @@ public class Player : Object { } } - public Variant? get_meta(string key) { - return metadata.lookup(key); - } - private string get_str(string key) { if (metadata.get(key) == null) return ""; @@ -341,15 +564,33 @@ public class Player : Object { } construct { - try { - try_proxy(); - sync(); - } catch (Error error) { - critical(error.message); - } + notify["bus-name"].connect(() => { + try { + setup_proxy(); + setup_position_poll(); + sync(); + } catch (Error error) { + critical(error.message); + } + }); } - public void try_proxy() throws Error { + private void setup_position_poll() { + var current_position = position; + + pollid = Timeout.add_seconds(1, () => { + if (!available) + return Source.CONTINUE; + + if (position >= 0 && current_position != position) { + current_position = position; + notify_property("position"); + } + return Source.CONTINUE; + }, Priority.DEFAULT); + } + + private void setup_proxy() throws Error { if (proxy != null) return; @@ -359,27 +600,19 @@ public class Player : Object { "/org/mpris/MediaPlayer2" ); - if (proxy.g_name_owner != null) + if (proxy.g_name_owner != null) { appeared(); + } proxy.notify["g-name-owner"].connect(() => { - if (proxy.g_name_owner != null) + if (proxy.g_name_owner != null) { appeared(); - else + } 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() { @@ -387,12 +620,12 @@ public class Player : Object { } } -public enum PlaybackStatus { +public enum AstalMpris.PlaybackStatus { PLAYING, PAUSED, STOPPED; - public static PlaybackStatus from_string(string? str) { + internal static PlaybackStatus from_string(string? str) { switch (str) { case "Playing": return PLAYING; @@ -403,27 +636,18 @@ public enum PlaybackStatus { return STOPPED; } } - - public string to_string() { - switch (this) { - case PLAYING: - return "Playing"; - case PAUSED: - return "Paused"; - case STOPPED: - default: - return "Stopped"; - } - } } -public enum Loop { +public enum AstalMpris.Loop { UNSUPPORTED, + /** The playback will stop when there are no more tracks to play. */ NONE, + /** The current track will start again from the begining once it has finished playing. */ TRACK, + /** The playback loops through a list of tracks. */ PLAYLIST; - public static Loop from_string(string? str) { + internal static Loop from_string(string? str) { switch (str) { case "None": return NONE; @@ -436,7 +660,7 @@ public enum Loop { } } - public string? to_string() { + internal string? to_string() { switch (this) { case NONE: return "None"; @@ -450,16 +674,18 @@ public enum Loop { } } -public enum Shuffle { +public enum AstalMpris.Shuffle { UNSUPPORTED, + /** Playback is progressing through a playlist in some other order. */ ON, + /** Playback is progressing linearly through a playlist. */ OFF; - public static Shuffle from_bool(bool b) { + internal static Shuffle from_bool(bool b) { return b ? Shuffle.ON : Shuffle.OFF; } - public string? to_string() { + internal string? to_string() { switch (this) { case OFF: return "Off"; @@ -470,4 +696,3 @@ public enum Shuffle { } } } -} diff --git a/lib/notifd/notification.vala b/lib/notifd/notification.vala index 527a352..29c6c56 100644 --- a/lib/notifd/notification.vala +++ b/lib/notifd/notification.vala @@ -158,12 +158,6 @@ public class AstalNotifd.Notification : Object { */ public signal void resolved(ClosedReason reason); - /** - * Emitted when the user dismisses this notification. - * - * @see dismiss - */ - public signal void dismissed(); /** * Emitted when an [[email protected]] of this notification is invoked. @@ -173,12 +167,10 @@ public class AstalNotifd.Notification : Object { public signal void invoked(string action_id); /** - * Dismiss this notification popup - * - * This method doesn't have any functionality on its own, but should be handled - * by frontend implementation to hide notification popups. + * Resolve this notification with [[email protected]_BY_USER]. */ public void dismiss() { dismissed(); } + internal signal void dismissed(); /** * Invoke an [[email protected]] of this notification. diff --git a/lib/notifd/proxy.vala b/lib/notifd/proxy.vala index bedb8b9..95e7105 100644 --- a/lib/notifd/proxy.vala +++ b/lib/notifd/proxy.vala @@ -32,10 +32,6 @@ internal class AstalNotifd.DaemonProxy : Object { 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); } diff --git a/lib/notifd/signals.md b/lib/notifd/signals.md index cdc6688..e13b92c 100644 --- a/lib/notifd/signals.md +++ b/lib/notifd/signals.md @@ -5,7 +5,7 @@ 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()` +* dismissed() - by user with `.dismiss()`, used to emit resolved from proxy/daemon * invoked(action) - by user with `.invoke()` ## Deamon |