diff options
-rw-r--r-- | flake.nix | 1 | ||||
-rw-r--r-- | lib/sway/cli.vala | 50 | ||||
-rw-r--r-- | lib/sway/config.vala.in | 7 | ||||
-rw-r--r-- | lib/sway/default.nix | 15 | ||||
-rw-r--r-- | lib/sway/device.vala | 458 | ||||
l--------- | lib/sway/gir.py | 1 | ||||
-rw-r--r-- | lib/sway/ifaces.vala | 63 | ||||
-rw-r--r-- | lib/sway/meson.build | 114 | ||||
-rw-r--r-- | lib/sway/meson_options.txt | 11 | ||||
-rw-r--r-- | lib/sway/upower.vala | 80 | ||||
-rw-r--r-- | lib/sway/version | 1 |
11 files changed, 801 insertions, 0 deletions
@@ -37,6 +37,7 @@ river = mkPkg ./lib/river; tray = mkPkg ./lib/tray; wireplumber = mkPkg ./lib/wireplumber; + sway = mkPkg ./lib/sway; gjs = import ./lang/gjs {inherit self pkgs;}; }); diff --git a/lib/sway/cli.vala b/lib/sway/cli.vala new file mode 100644 index 0000000..c51264b --- /dev/null +++ b/lib/sway/cli.vala @@ -0,0 +1,50 @@ +static bool help; +static bool version; +static bool monitor; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "monitor", 'm', OptionFlags.NONE, OptionArg.NONE, ref monitor, 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(" -m, --monitor Monitor property changes\n"); + return 0; + } + + if (version) { + print(AstalBattery.VERSION); + return 0; + } + + var battery = AstalBattery.get_default(); + print("%s\n", Json.gobject_to_data(battery, null)); + + if (monitor) { + battery.notify.connect(() => { + print("%s\n", Json.gobject_to_data(battery, null)); + }); + new GLib.MainLoop(null, false).run(); + } + + return 0; +} diff --git a/lib/sway/config.vala.in b/lib/sway/config.vala.in new file mode 100644 index 0000000..404e60a --- /dev/null +++ b/lib/sway/config.vala.in @@ -0,0 +1,7 @@ +[CCode (gir_namespace = "AstalBattery", gir_version = "@API_VERSION@")] +namespace AstalBattery { + 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/lib/sway/default.nix b/lib/sway/default.nix new file mode 100644 index 0000000..17bf67a --- /dev/null +++ b/lib/sway/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-battery"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "battery"; + authors = "Aylur"; + gir-suffix = "Battery"; + description = "DBus proxy for upowerd devices"; +} diff --git a/lib/sway/device.vala b/lib/sway/device.vala new file mode 100644 index 0000000..db69574 --- /dev/null +++ b/lib/sway/device.vala @@ -0,0 +1,458 @@ +namespace AstalBattery { + /** Get the DisplayDevice. */ + public Device get_default() { + return Device.get_default(); + } +} + +/** + * Client for a UPower [[https://upower.freedesktop.org/docs/Device.html|device]]. + */ +public class AstalBattery.Device : Object { + private static Device display_device; + + /** Get the DisplayDevice. */ + public static Device? get_default() { + if (display_device != null) { + return display_device; + } + + try { + display_device = new Device((ObjectPath)"/org/freedesktop/UPower/devices/DisplayDevice"); + return display_device; + } catch (Error error) { + critical(error.message); + } + + return null; + } + + private IUPowerDevice proxy; + + public Device(ObjectPath path) throws Error { + proxy = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.UPower", path); + proxy.g_properties_changed.connect(sync); + sync(); + } + + /** + * If it is [[email protected]], you will need to verify that the + * property power-supply has the value `true` before considering it as a laptop battery. + * Otherwise it will likely be the battery for a device of an unknown type. + */ + public Type device_type { get; private set; } + + /** + * Native path of the power source. This is the sysfs path, + * for example /sys/devices/LNXSYSTM:00/device:00/PNP0C0A:00/power_supply/BAT0. + * It is blank if the device is being driven by a user space driver. + */ + public string native_path { owned get; private set; } + + /** Name of the vendor of the battery. */ + public string vendor { owned get; private set; } + + /** Name of the model of this battery. */ + public string model { owned get; private set; } + + /** Unique serial number of the battery. */ + public string serial { owned get; private set; } + + /** + * The point in time (seconds since the Epoch) + * that data was read from the power source. + */ + public uint64 update_time { get; private set; } + + /** + * If the power device is used to supply the system. + * This would be set `true` for laptop batteries and UPS devices, + * but set to `false` for wireless mice or PDAs. + */ + public bool power_supply { get; private set; } + + /** If the power device has history. */ + // TODO: public bool has_history { get; private set; } + + /** If the power device has statistics. */ + // TODO: public bool has_statistics { get; private set; } + + /** + * Whether power is currently being provided through line power. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]_POWER]. + */ + public bool online { get; private set; } + + /** + * Amount of energy (measured in Wh) currently available in the power source. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double energy { get; private set; } + + /** + * Amount of energy (measured in Wh) in the power source when it's considered to be empty. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double energy_empty { get; private set; } + + /** + * Amount of energy (measured in Wh) in the power source when it's considered full. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double energy_full { get; private set; } + + /** + * Amount of energy (measured in Wh) the power source is designed to hold when it's considered full. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double energy_full_design { get; private set; } + + /** + * Amount of energy being drained from the source, measured in W. + * If positive, the source is being discharged, if negative it's being charged. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double energy_rate { get; private set; } + + /** Voltage in the Cell or being recorded by the meter. */ + public double voltage { get; private set; } + + /** + * The number of charge cycles as defined by the TCO certification, + * or -1 if that value is unknown or not applicable. + */ + public int charge_cycles { get; private set; } + + /** Luminosity being recorded by the meter. */ + public double luminosity { get; private set; } + + /** + * Number of seconds until the power source is considered empty. Is set to 0 if unknown. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public int64 time_to_empty { get; private set; } + + /** + * Number of seconds until the power source is considered full. Is set to 0 if unknown. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public int64 time_to_full { get; private set;} + + /** + * The amount of energy left in the power source expressed as a percentage between 0 and 1. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + * The percentage will be an approximation if [[email protected]:battery_level] + * is set to something other than None. + */ + public double percentage { get; private set; } + + /** + * The temperature of the device in degrees Celsius. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double temperature { get; private set; } + + /** + * If the power source is present in the bay. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public bool is_present { get; private set; } + + /** + * The battery power state. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public State state { get; private set; } + + /** + * If the power source is rechargeable. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public bool is_rechargable { get; private set; } + + /** + * The capacity of the power source expressed as a percentage between 0 and 1. + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public double capacity { get; private set; } + + /** + * Technology used in the battery: + * + * This property is only valid if [[email protected]:device_type] is [[email protected]]. + */ + public Technology technology { get; private set; } + + /** Warning level of the battery. */ + public WarningLevel warning_level { get; private set; } + + /** + * The level of the battery for devices which do not report a percentage + * but rather a coarse battery level. If the value is None. + * then the device does not support coarse battery reporting, + * and the [[email protected]:percentage] should be used instead. + */ + public BatteryLevel battery_level { get; private set; } + + /** + * An icon name representing this Device. + * + * NOTE: [[email protected]:battery_icon_name] might be a better fit + * as it is calculated from percentage. + */ + public string icon_name { owned get; private set; } + + /** + * Indicates if [[email protected]:state] is charging or fully charged. + */ + public bool charging { get; private set; } + + /** + * Indicates if [[email protected]:device_type] is not line power or unknown. + */ + public bool is_battery { get; private set; } + + /** + * An icon name in the form of "battery-level-$percentage-$state-symbolic". + */ + public string battery_icon_name { get; private set; } + + /** + * A string representation of this device's [[email protected]:device_type]. + */ + public string device_type_name { get; private set; } + + /** + * An icon name that can be used to represent this device's [[email protected]:device_type]. + */ + public string device_type_icon { get; private set; } + + // TODO: get_history + // TODO: get_statistics + + private void sync() { + device_type = (Type)proxy.Type; + native_path = proxy.native_path; + vendor = proxy.vendor; + model = proxy.model; + serial = proxy.serial; + update_time = proxy.update_time; + power_supply = proxy.power_supply; + // TODO: has_history = proxy.has_history; + // TODO: has_statistics = proxy.has_statistics; + online = proxy.online; + energy = proxy.energy; + energy_empty = proxy.energy_empty; + energy_full = proxy.energy_full; + energy_full_design = proxy.energy_full_design; + energy_rate = proxy.energy_rate; + voltage = proxy.voltage; + charge_cycles = proxy.charge_cycles; + luminosity = proxy.luminosity; + time_to_empty = proxy.time_to_empty; + time_to_full = proxy.time_to_full; + percentage = proxy.percentage / 100; + temperature = proxy.temperature; + is_present = proxy.is_present; + state = (State)proxy.state; + is_rechargable = proxy.is_rechargable; + capacity = proxy.capacity / 100; + technology = (Technology)proxy.technology; + warning_level = (WarningLevel)proxy.warning_level; + battery_level = (BatteryLevel)proxy.battery_level; + icon_name = proxy.icon_name; + + charging = state == State.FULLY_CHARGED || state == State.CHARGING; + is_battery = device_type != Type.UNKNOWN && device_type != Type.LINE_POWER; + + if (!is_battery) { + battery_icon_name = "battery-missing-symbolic"; + } else if (percentage >= 0.95 && charging) { + battery_icon_name = "battery-level-100-charged-symbolic"; + } else { + var state = charging ? "-charging" : ""; + var level = (int)Math.round(percentage * 10)*10; + battery_icon_name = @"battery-level-$level$state-symbolic"; + } + + device_type_name = device_type.get_name(); + device_type_icon = device_type.get_icon_name(); + } +} + +[CCode (type_signature = "u")] +public enum AstalBattery.State { + UNKNOWN, + CHARGING, + DISCHARGING, + EMPTY, + FULLY_CHARGED, + PENDING_CHARGE, + PENDING_DISCHARGE, +} + +[CCode (type_signature = "u")] +public enum AstalBattery.Technology { + UNKNOWN, + LITHIUM_ION, + LITHIUM_POLYMER, + LITHIUM_IRON_PHOSPHATE, + LEAD_ACID, + NICKEL_CADMIUM, + NICKEL_METAL_HYDRIDE, +} + +[CCode (type_signature = "u")] +public enum AstalBattery.WarningLevel { + UNKNOWN, + NONE, + DISCHARGING, + LOW, + CRITICIAL, + ACTION, +} + +[CCode (type_signature = "u")] +public enum AstalBattery.BatteryLevel { + UNKNOWN, + NONE, + LOW, + CRITICIAL, + NORMAL, + HIGH, + FULL, +} + +[CCode (type_signature = "u")] +public enum AstalBattery.Type { + UNKNOWN, + LINE_POWER, + BATTERY, + UPS, + MONITOR, + MOUSE, + KEYBOARD, + PDA, + PHONE, + MEDIA_PLAYER, + TABLET, + COMPUTER, + GAMING_INPUT, + PEN, + TOUCHPAD, + MODEM, + NETWORK, + HEADSET, + SPEAKERS, + HEADPHONES, + VIDEO, + OTHER_AUDIO, + REMOVE_CONTROL, + PRINTER, + SCANNER, + CAMERA, + WEARABLE, + TOY, + BLUETOOTH_GENERIC; + + // TODO: add more icon names + internal string? get_icon_name () { + switch (this) { + case UPS: + return "uninterruptible-power-supply"; + case MOUSE: + return "input-mouse"; + case KEYBOARD: + return "input-keyboard"; + case PDA: + case PHONE: + return "phone"; + case MEDIA_PLAYER: + return "multimedia-player"; + case TABLET: + case PEN: + return "input-tablet"; + case GAMING_INPUT: + return "input-gaming"; + default: + return null; + } + } + + internal unowned string? get_name () { + switch (this) { + case LINE_POWER: + return "Plugged In"; + case BATTERY: + return "Battery"; + case UPS: + return "UPS"; + case MONITOR: + return "Display"; + case MOUSE: + return "Mouse"; + case KEYBOARD: + return "Keyboard"; + case PDA: + return "PDA"; + case PHONE: + return "Phone"; + case MEDIA_PLAYER: + return "Media Player"; + case TABLET: + return "Tablet"; + case COMPUTER: + return "Computer"; + case GAMING_INPUT: + return "Controller"; + case PEN: + return "Pen"; + case TOUCHPAD: + return "Touchpad"; + case MODEM: + return "Modem"; + case NETWORK: + return "Network"; + case HEADSET: + return "Headset"; + case SPEAKERS: + return "Speakers"; + case HEADPHONES: + return "Headphones"; + case VIDEO: + return "Video"; + case OTHER_AUDIO: + return "Other Audio"; + case REMOVE_CONTROL: + return "Remove Control"; + case PRINTER: + return "Printer"; + case SCANNER: + return "Scanner"; + case CAMERA: + return "Camera"; + case WEARABLE: + return "Wearable"; + case TOY: + return "Toy"; + case BLUETOOTH_GENERIC: + return "Bluetooth Generic"; + default: + return "Unknown"; + } + } +} diff --git a/lib/sway/gir.py b/lib/sway/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/sway/gir.py @@ -0,0 +1 @@ +../gir.py
\ No newline at end of file diff --git a/lib/sway/ifaces.vala b/lib/sway/ifaces.vala new file mode 100644 index 0000000..36d35fe --- /dev/null +++ b/lib/sway/ifaces.vala @@ -0,0 +1,63 @@ +[DBus (name = "org.freedesktop.UPower")] +private interface AstalBattery.IUPower : DBusProxy { + public abstract ObjectPath[] enumerate_devices() throws Error; + public abstract ObjectPath get_display_device() throws Error; + public abstract string get_critical_action() throws Error; + + public signal void device_added(ObjectPath object_path); + public signal void device_removed(ObjectPath object_path); + + public abstract string daemon_version { owned get; } + public abstract bool on_battery { get; } + public abstract bool lid_is_closed { get; } + public abstract bool lid_is_present { get; } +} + +[DBus (name = "org.freedesktop.UPower.Device")] +private interface AstalBattery.IUPowerDevice : DBusProxy { + // public abstract HistoryDataPoint[] get_history (string type, uint32 timespan, uint32 resolution) throws GLib.Error; + // public abstract StatisticsDataPoint[] get_statistics (string type) throws GLib.Error; + // public abstract void refresh () throws GLib.Error; + + public abstract uint Type { get; } + public abstract string native_path { owned get; } + public abstract string vendor { owned get; } + public abstract string model { owned get; } + public abstract string serial { owned get; } + public abstract uint64 update_time { get; } + public abstract bool power_supply { get; } + public abstract bool has_history { get; } + public abstract bool has_statistics { get; } + public abstract bool online { get; } + public abstract double energy { get; } + public abstract double energy_empty { get; } + public abstract double energy_full { get; } + public abstract double energy_full_design { get; } + public abstract double energy_rate { get; } + public abstract double voltage { get; } + public abstract int32 charge_cycles { get; } + public abstract double luminosity { get; } + public abstract int64 time_to_empty { get; } + public abstract int64 time_to_full { get; } + public abstract double percentage { get; } + public abstract double temperature { get; } + public abstract bool is_present { get; } + public abstract uint state { get; } + public abstract bool is_rechargable { get; } + public abstract double capacity { get; } + public abstract uint technology { get; } + public abstract uint32 warning_level { get; } + public abstract uint32 battery_level { get; } + public abstract string icon_name { owned get; } +} + +// private struct AstalBattery.HistoryDataPoint { +// uint32 time; +// double value; +// uint32 state; +// } +// +// private struct AstalBattery.StatisticsDataPoint { +// double value; +// double accuracy; +// } diff --git a/lib/sway/meson.build b/lib/sway/meson.build new file mode 100644 index 0000000..ec8e3d0 --- /dev/null +++ b/lib/sway/meson.build @@ -0,0 +1,114 @@ +project( + 'astal-battery', + '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.', +) + +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalBattery-' + api_version + '.gir' +typelib = 'AstalBattery-' + api_version + '.typelib' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'API_VERSION': api_version, + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +pkgconfig_deps = [ + dependency('glib-2.0'), + dependency('gio-2.0'), + dependency('gobject-2.0'), + dependency('json-glib-1.0'), +] + +deps = pkgconfig_deps + meson.get_compiler('c').find_library('m') + +sources = [config] + files( + 'device.vala', + 'ifaces.vala', + 'upower.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', + version: meson.project_version(), + install: true, + install_dir: [true, true, true], + ) + + pkgs = [] + foreach dep : pkgconfig_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( + 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, 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: pkgconfig_deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) +endif + +if get_option('cli') + executable( + meson.project_name(), + ['cli.vala', sources], + dependencies: deps, + install: true, + ) +endif diff --git a/lib/sway/meson_options.txt b/lib/sway/meson_options.txt new file mode 100644 index 0000000..f110242 --- /dev/null +++ b/lib/sway/meson_options.txt @@ -0,0 +1,11 @@ +option( + 'lib', + type: 'boolean', + value: true, +) + +option( + 'cli', + type: 'boolean', + value: true, +) diff --git a/lib/sway/upower.vala b/lib/sway/upower.vala new file mode 100644 index 0000000..ea5946a --- /dev/null +++ b/lib/sway/upower.vala @@ -0,0 +1,80 @@ +/** + * Client for the UPower [[https://upower.freedesktop.org/docs/UPower.html|dbus interface]]. + */ +public class AstalBattery.UPower : Object { + private IUPower proxy; + private HashTable<string, Device> _devices = + new HashTable<string, Device>(str_hash, str_equal); + + /** List of UPower devices. */ + public List<weak Device> devices { + owned get { return _devices.get_values(); } + } + + /** Emitted when a new device is connected. */ + public signal void device_added(Device device); + + /** Emitted a new device is disconnected. */ + public signal void device_removed(Device device); + + /** A composite device that represents the battery status. */ + public Device display_device { owned get { return Device.get_default(); }} + + public string daemon_version { owned get { return proxy.daemon_version; } } + + /** Indicates whether the system is running on battery power. */ + public bool on_battery { get { return proxy.on_battery; } } + + /** Indicates if the laptop lid is closed where the display cannot be seen. */ + public bool lid_is_closed { get { return proxy.lid_is_closed; } } + + /** Indicates if the system has a lid device. */ + public bool lid_is_present { get { return proxy.lid_is_closed; } } + + /** + * When the system's power supply is critical (critically low batteries or UPS), + * the system will take this action. + */ + public string critical_action { + owned get { + try { + return proxy.get_critical_action(); + } catch (Error error) { + critical(error.message); + return ""; + } + } + } + + construct { + try { + proxy = Bus.get_proxy_sync( + BusType.SYSTEM, + "org.freedesktop.UPower", + "/org/freedesktop/UPower" + ); + + foreach (var path in proxy.enumerate_devices()) + _devices.set(path, new Device(path)); + + proxy.device_added.connect((path) => { + try { + var d = new Device(path); + _devices.set(path, d); + device_added(d); + notify_property("devices"); + } catch (Error err) { + critical(err.message); + } + }); + + proxy.device_removed.connect((path) => { + device_removed(_devices.get(path)); + _devices.remove(path); + notify_property("devices"); + }); + } catch (Error error) { + critical(error.message); + } + } +} diff --git a/lib/sway/version b/lib/sway/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/lib/sway/version @@ -0,0 +1 @@ +0.1.0 |