diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp new file mode 100644 index 00000000..0aa7112c --- /dev/null +++ b/include/modules/wwan.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include + +#include "ALabel.hpp" +#include "util/format.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Wwan : public ALabel { + public: + Wwan(const std::string&, const Json::Value&); + virtual ~Wwan(); + auto update() -> void override; + + private: + void updateCurrentModem(); + + util::SleeperThread thread_; + std::string state_; + GDBusConnection* connection = nullptr; + MMManager* manager = nullptr; + MMModem* current_modem = nullptr; + + bool hideDisconnected = true; + + const std::string dbus_name = "org.freedesktop.ModemManager1"; + const std::string dbus_obj_path = "/org/freedesktop/ModemManager1/"; + const std::string dbus_modems_path = "/org/freedesktop/ModemManager1/Modems/"; +}; + +} // namespace waybar::modules diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd new file mode 100644 index 00000000..0169de07 --- /dev/null +++ b/man/waybar-wwan.5.scd @@ -0,0 +1,156 @@ +waybar-wwan(5) "waybar-wwan" "User Manual" + +# NAME + +waybar - wwan module + +# DESCRIPTION + +*wwan* module for ModemManager. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libmm-glib lives in: + +. /usr/lib/libmm-glib.so or /usr/lib64/libmm-glib.so +. /usr/lib/pkgconfig/libmm-glib.pc or /usr/lib64/pkgconfig/libmm-glib.pc +. /usr/include/libmm-glib + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: *{state}* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + +*format-failed*: ++ + typeof: string ++ + This format is used when the modem cannot start. + +*format-locked*: ++ + typeof: string ++ + This format is used when the SIM on the modem is locked. + +*format-disabled*: ++ + typeof: string ++ + This format is used when the modem is disabled. + +*format-enabled*: ++ + typeof: string ++ + This format is used when the modem is enabled, but isn't registered to any carrier network. + +*format-searching*: ++ + typeof: string ++ + This format is used when the modem is looking for a carrier network to register to. + +*format-registered*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, but no data connection is active. + +*format-connected*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, and there is an active data connection. + +*format-icons*: ++ + typeof: array/object ++ + Based on the current signal strength, the corresponding icon gets selected. ++ + The order is *low* to *high*. Or by the state if it is an object. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: strirng ++ + default: {state} ++ + The format, how information should be displayed in the tooltip. This format is used when other formats aren't specified. + +*tooltip-format-failed*: ++ + typeof: string ++ + This format is used when the modem cannot start. + +*tooltip-format-locked*: ++ + typeof: string ++ + This format is used when the SIM on the modem is locked. + +*tooltip-format-disabled*: ++ + typeof: string ++ + This format is used when the modem is disabled. + +*tooltip-format-enabled*: ++ + typeof: string ++ + This format is used when the modem is enabled, but isn't registered to any carrier network. + +*tooltip-format-searching*: ++ + typeof: string ++ + This format is used when the modem is looking for a carrier network to register to. + +*tooltip-format-registered*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, but no data connection is active. + +*tooltip-format-connected*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, and there is an active data connection. + +*interval*: ++ + typeof: integer ++ + default: 5 ++ + The interval in which the WWAN information gets polled (e.g. carrier name). + +*imei*: ++ + typeof: string ++ + Select only the modem with the corresponding imei. + +*path*: ++ + typeof: string ++ + Select only the modem with the corresponding hardware path. + +*hide-disconnected*: ++ + typeof: bool ++ + default: true ++ + Defines if the module should be hidden while the modem does not have an active data connection. + + +# FORMAT REPLACEMENTS + +*{state}*: Modem state. + +*{current_modes}*: List of all the currently active connection modes. + +*{preferred_mode}*: The preferred connection mode in use. + +*{signal_quality}*: Signal quality (0-100). + +*{operator_name}*: Name of the network operator. + +# EXAMPLES + +``` +"wwan": { + "format": "{state}", + "format-disabled": "", // an empty format will hide the module + "format-locked": "SIM Locked", + "format-connected": "{operator_name} {preferred_mode} | {signal_quality}%" + "tooltip-format": "{mode}", + "tooltip-format-connected": "{current_modes}", + "hide-disconnected": false +} +``` +# STYLE + +- *#wwan* +- *#wwan.failed* Applied when the modem cannot start. +- *#wwan.locked* Applied when the SIM on the modem is locked. +- *#wwan.disabled* Applied when the modem is disabled. +- *#wwan.enabled* Applied when the modem is enabled, but isn't registered to any carrier network. +- *#wwan.searching* Applied when the modem is looking for a carrier network to register to. +- *#wwan.registered* Applied when the modem is registered to a network, but no data connection is active. +- *#wwan.connected* Applied when the modem is registered to a network, and there is an active data connection. diff --git a/meson.build b/meson.build index 5563d610..7bbad3fe 100644 --- a/meson.build +++ b/meson.build @@ -94,6 +94,7 @@ xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) libgps = dependency('libgps', required: get_option('gps')) +libmm_glib = dependency('mm-glib', required: get_option('wwan')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -557,6 +558,12 @@ if libgps.found() man_files += files('man/waybar-gps.5.scd') endif +if libmm_glib.found() + add_project_arguments('-DHAVE_LIBMM_GLIB', language: 'cpp') + src_files += files('src/modules/wwan.cpp') + man_files += files('man/waybar-wwan.5.scd') +endif + subdir('protocol') app_resources = [] @@ -598,7 +605,8 @@ executable( xkbregistry, cava, eproxy, - libgps + libgps, + libmm_glib ], include_directories: inc_dirs, install: true, diff --git a/meson_options.txt b/meson_options.txt index 278fdd02..139ed6cb 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,3 +23,4 @@ option('niri', type: 'boolean', description: 'Enable support for niri') option('mango', type: 'boolean', description: 'Enable support for mango') option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') +option('wwan', type: 'feature', value: 'auto', description: 'Enable support for WWAN') diff --git a/nix/default.nix b/nix/default.nix index 1850a414..8102363a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,6 +36,7 @@ waybar.overrideAttrs (oldAttrs: { ++ [ pkgs.wireplumber pkgs.gpsd + pkgs.modemmanager ]; postUnpack = '' diff --git a/src/factory.cpp b/src/factory.cpp index 6969da6b..c0d018b7 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -123,6 +123,9 @@ #include "modules/gps.hpp" #endif #include "modules/cava/cava_frontend.hpp" +#ifdef HAVE_LIBMM_GLIB +#include "modules/wwan.hpp" +#endif #include "modules/cffi.hpp" #include "modules/custom.hpp" #include "modules/custom_graph.hpp" @@ -380,6 +383,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "gps") { return new waybar::modules::Gps(id, config_[name]); } +#endif +#ifdef HAVE_LIBMM_GLIB + if (ref == "wwan") { + return new waybar::modules::Wwan(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp new file mode 100644 index 00000000..86e68624 --- /dev/null +++ b/src/modules/wwan.cpp @@ -0,0 +1,308 @@ +#include "modules/wwan.hpp" + +#include + +#include +#include + +#include "ModemManager.h" +#include "libmm-glib.h" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +namespace { +using namespace waybar::util; +constexpr const char* DEFAULT_FORMAT = "{state}"; +} // namespace + +waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) + : ALabel(config, "wwan", id, "{}", 5) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; + + GError* error = nullptr; + connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error); + if (error) { + g_error_free(error); + return; + } + + manager = mm_manager_new_sync(connection, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + nullptr, // cancellable + &error); + + if (error) { + spdlog::error("Failed to create ModemManager proxy: " + std::string(error->message)); + g_error_free(error); + g_object_unref(connection); + return; + } + + updateCurrentModem(); + + if (config_["hide-disconnected"].isBool()) { + hideDisconnected = config_["hide-disconnected"].asBool(); + } +} + +void waybar::modules::Wwan::updateCurrentModem() { + // get 1st modem (or modem with the specified hardware path) + + GList* modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(manager)); + if (!modems) { + return; + } + + for (GList* m = modems; m; m = g_list_next(m)) { + MMObject* modem_obj = MM_OBJECT(m->data); + + MMModem* modem = mm_object_get_modem(modem_obj); + + if (config_["path"].isString()) { + std::string path = config_["path"].asString(); + gchar* physdev = mm_modem_dup_physdev(modem); + std::string other = physdev ? physdev : ""; + g_free(physdev); + + if (path != other) { + g_object_unref(modem); + modem = nullptr; + continue; + } + } + + if (config_["imei"].isString()) { + std::string imei = config_["imei"].asString(); + gchar* equipment_id = mm_modem_dup_equipment_identifier(modem); + std::string other = equipment_id ? equipment_id : ""; + g_free(equipment_id); + + if (imei != other) { + g_object_unref(modem); + modem = nullptr; + continue; + } + } + + current_modem = modem; + g_list_free_full(modems, g_object_unref); + return; + } + // no modems found + current_modem = nullptr; + g_list_free_full(modems, g_object_unref); +} + +std::string getModemStateString(MMModem* modem) { + switch (mm_modem_get_state(modem)) { + case MMModemState::MM_MODEM_STATE_FAILED: + return "Failed"; + case MMModemState::MM_MODEM_STATE_INITIALIZING: + return "Initializing"; + case MMModemState::MM_MODEM_STATE_LOCKED: + return "Locked"; + case MMModemState::MM_MODEM_STATE_DISABLED: + return "Disabled"; + case MMModemState::MM_MODEM_STATE_DISABLING: + return "Disabling"; + case MMModemState::MM_MODEM_STATE_ENABLED: + return "Enabled"; + case MMModemState::MM_MODEM_STATE_ENABLING: + return "Enabling"; + case MMModemState::MM_MODEM_STATE_SEARCHING: + return "Searching"; + case MMModemState::MM_MODEM_STATE_REGISTERED: + return "Registered"; + case MMModemState::MM_MODEM_STATE_DISCONNECTING: + return "Disconnecting"; + case MMModemState::MM_MODEM_STATE_CONNECTING: + return "Connecting"; + case MMModemState::MM_MODEM_STATE_CONNECTED: + return "Connected"; + default: + return "Unknown"; + } +} + +std::string getModemStateFormatString(MMModem* modem) { + switch (mm_modem_get_state(modem)) { + case MMModemState::MM_MODEM_STATE_FAILED: + return "failed"; + case MMModemState::MM_MODEM_STATE_LOCKED: + return "locked"; + case MMModemState::MM_MODEM_STATE_DISABLED: + return "disabled"; + case MMModemState::MM_MODEM_STATE_ENABLED: + return "enabled"; + case MMModemState::MM_MODEM_STATE_SEARCHING: + return "searching"; + case MMModemState::MM_MODEM_STATE_REGISTERED: + return "registered"; + case MMModemState::MM_MODEM_STATE_CONNECTED: + return "connected"; + default: + return ""; + } +} + +std::string getPreferredModeString(MMModem* modem) { + MMModemModeCombination combination; + mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); + + switch (combination.preferred) { + case MMModemMode::MM_MODEM_MODE_CS: + return "CS"; + case MMModemMode::MM_MODEM_MODE_2G: + return "2G"; + case MMModemMode::MM_MODEM_MODE_3G: + return "3G"; + case MMModemMode::MM_MODEM_MODE_4G: + return "4G"; + case MMModemMode::MM_MODEM_MODE_5G: + return "5G"; + default: + return "No Service"; + } +} + +std::string getCurrentModesString(MMModem* modem) { + MMModemModeCombination combination; + mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); + + std::string buffer; + + if (combination.allowed & MMModemMode::MM_MODEM_MODE_CS) buffer += "CS | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_2G) buffer += "2G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_3G) buffer += "3G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_4G) buffer += "4G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_5G) buffer += "5G | "; + + if (buffer.length() > 2) { + buffer.erase(buffer.length() - 2); + } + return buffer; +} + +std::string getPowerStateString(MMModem* modem) { + MMModemPowerState state = mm_modem_get_power_state(modem); + + switch (state) { + case MM_MODEM_POWER_STATE_ON: + return "On"; + case MM_MODEM_POWER_STATE_OFF: + return "Off"; + case MM_MODEM_POWER_STATE_LOW: + return "Low"; + default: + return "Unknown"; + } +} + +std::string getOperatorNameString(MMModem* modem) { + MMSim* sim = mm_modem_get_sim_sync(modem, nullptr, nullptr); + + if (sim == nullptr) { + return "No SIM"; + } + + gchar* operator_name = mm_sim_dup_operator_name(sim); + std::string name = operator_name ? operator_name : ""; + g_free(operator_name); + g_object_unref(sim); + return name; +} + +auto waybar::modules::Wwan::update() -> void { + if (current_modem == nullptr) { + event_box_.set_visible(false); + updateCurrentModem(); + return; + } + + if (hideDisconnected && mm_modem_get_state(current_modem) != MM_MODEM_STATE_CONNECTED) { + event_box_.set_visible(false); + + return; + } + + // Show the module + if (!event_box_.get_visible()) event_box_.set_visible(true); + + std::string tooltip_format; + + if (!alt_) { + std::string state = getModemStateFormatString(current_modem); + if (!state_.empty() && label_.get_style_context()->has_class(state_)) { + label_.get_style_context()->remove_class(state_); + } + if (config_["format-" + state].isString()) { + default_format_ = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + default_format_ = config_["format"].asString(); + } else { + default_format_ = DEFAULT_FORMAT; + } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } + if (!label_.get_style_context()->has_class(state)) { + label_.get_style_context()->add_class(state); + } + format_ = default_format_; + state_ = state; + } + + auto format = format_; + + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("state", getModemStateString(current_modem))); + + store.push_back(fmt::arg("current_modes", getCurrentModesString(current_modem))); + store.push_back(fmt::arg("preferred_mode", getPreferredModeString(current_modem))); + + int percentage = mm_modem_get_signal_quality(current_modem, nullptr); + store.push_back(fmt::arg("signal_quality", percentage)); + store.push_back(fmt::arg("icon", getIcon(percentage))); + + store.push_back(fmt::arg("power_state", getPowerStateString(current_modem))); + gchar* imei_raw = mm_modem_dup_equipment_identifier(current_modem); + std::string imei = imei_raw ? imei_raw : ""; + g_free(imei_raw); + store.push_back(fmt::arg("imei", imei)); + + store.push_back(fmt::arg("operator_name", getOperatorNameString(current_modem))); + + auto text = fmt::vformat(format, store); + + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + auto tooltip_text = fmt::vformat(tooltip_format, store); + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else if (label_.get_tooltip_text() != text) { + label_.set_tooltip_markup(text); + } + } + label_.set_markup(text); + // Call parent update + ALabel::update(); +} + +waybar::modules::Wwan::~Wwan() { + g_object_unref(current_modem); + g_object_unref(manager); + g_object_unref(connection); +} diff --git a/subprojects/.wraplock b/subprojects/.wraplock deleted file mode 100644 index e69de29b..00000000