commit
						f5370fcff5
					
				|  | @ -17,6 +17,7 @@ class AModule : public IModule { | |||
|   operator Gtk::Widget &() override; | ||||
|   auto doAction(const std::string &name) -> void override; | ||||
| 
 | ||||
|   /// Emitting on this dispatcher triggers a update() call
 | ||||
|   Glib::Dispatcher dp; | ||||
| 
 | ||||
|  protected: | ||||
|  |  | |||
|  | @ -94,6 +94,7 @@ | |||
| #include "modules/cava.hpp" | ||||
| #endif | ||||
| #include "bar.hpp" | ||||
| #include "modules/cffi.hpp" | ||||
| #include "modules/custom.hpp" | ||||
| #include "modules/image.hpp" | ||||
| #include "modules/temperature.hpp" | ||||
|  |  | |||
|  | @ -0,0 +1,60 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <string> | ||||
| 
 | ||||
| #include "AModule.hpp" | ||||
| #include "util/command.hpp" | ||||
| #include "util/json.hpp" | ||||
| #include "util/sleeper_thread.hpp" | ||||
| 
 | ||||
| namespace waybar::modules { | ||||
| 
 | ||||
| namespace ffi { | ||||
| extern "C" { | ||||
| typedef struct wbcffi_module wbcffi_module; | ||||
| 
 | ||||
| typedef struct { | ||||
|   wbcffi_module* obj; | ||||
|   const char* waybar_version; | ||||
|   GtkContainer* (*get_root_widget)(wbcffi_module*); | ||||
|   void (*queue_update)(wbcffi_module*); | ||||
| } wbcffi_init_info; | ||||
| 
 | ||||
| struct wbcffi_config_entry { | ||||
|   const char* key; | ||||
|   const char* value; | ||||
| }; | ||||
| } | ||||
| }  // namespace ffi
 | ||||
| 
 | ||||
| class CFFI : public AModule { | ||||
|  public: | ||||
|   CFFI(const std::string&, const std::string&, const Json::Value&); | ||||
|   virtual ~CFFI(); | ||||
| 
 | ||||
|   virtual auto refresh(int signal) -> void override; | ||||
|   virtual auto doAction(const std::string& name) -> void override; | ||||
|   virtual auto update() -> void override; | ||||
| 
 | ||||
|  private: | ||||
|   ///
 | ||||
|   void* cffi_instance_ = nullptr; | ||||
| 
 | ||||
|   typedef void*(InitFn)(const ffi::wbcffi_init_info* init_info, | ||||
|                         const ffi::wbcffi_config_entry* config_entries, size_t config_entries_len); | ||||
|   typedef void(DenitFn)(void* instance); | ||||
|   typedef void(RefreshFn)(void* instance, int signal); | ||||
|   typedef void(DoActionFn)(void* instance, const char* name); | ||||
|   typedef void(UpdateFn)(void* instance); | ||||
| 
 | ||||
|   // FFI hooks
 | ||||
|   struct { | ||||
|     std::function<InitFn> init = nullptr; | ||||
|     std::function<DenitFn> deinit = nullptr; | ||||
|     std::function<RefreshFn> refresh = [](void*, int) {}; | ||||
|     std::function<DoActionFn> doAction = [](void*, const char*) {}; | ||||
|     std::function<UpdateFn> update = [](void*) {}; | ||||
|   } hooks_; | ||||
| }; | ||||
| 
 | ||||
| }  // namespace waybar::modules
 | ||||
|  | @ -0,0 +1,37 @@ | |||
| waybar-cffi(5) | ||||
| # NAME | ||||
| 
 | ||||
| waybar - cffi module | ||||
| 
 | ||||
| # DESCRIPTION | ||||
| 
 | ||||
| The *cffi* module gives full control of a GTK widget to a third-party dynamic library, to create more complex modules using different programming languages. | ||||
| 
 | ||||
| # CONFIGURATION | ||||
| 
 | ||||
| Addressed by *cffi/<name>* | ||||
| 
 | ||||
| *module_path*: ++ | ||||
| 	typeof: string ++ | ||||
| 	The path to the dynamic library to load to control the widget. | ||||
| 
 | ||||
| Some additional configuration may be required depending on the cffi dynamic library being used. | ||||
| 
 | ||||
| 
 | ||||
| # EXAMPLES | ||||
| 
 | ||||
| ## C example: | ||||
| 
 | ||||
| An example module written in C can be found at https://github.com/Alexays/Waybar/resources/custom_modules/cffi_example/ | ||||
| 
 | ||||
| Waybar config to enable the module: | ||||
| ``` | ||||
| "cffi/c_example": { | ||||
| 	"module_path": ".config/waybar/cffi/wb_cffi_example.so" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| # STYLE | ||||
| 
 | ||||
| The classes and IDs are managed by the cffi dynamic library. | ||||
|  | @ -206,6 +206,7 @@ if is_linux | |||
|     add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') | ||||
|     src_files += files( | ||||
|         'src/modules/battery.cpp', | ||||
|         'src/modules/cffi.cpp', | ||||
|         'src/modules/cpu.cpp', | ||||
|         'src/modules/cpu_frequency/common.cpp', | ||||
|         'src/modules/cpu_frequency/linux.cpp', | ||||
|  | @ -218,6 +219,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd | |||
|     add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') | ||||
|     add_project_arguments('-DHAVE_MEMORY_BSD', language: 'cpp') | ||||
|     src_files += files( | ||||
|         'src/modules/cffi.cpp', | ||||
|         'src/modules/cpu.cpp', | ||||
|         'src/modules/cpu_frequency/bsd.cpp', | ||||
|         'src/modules/cpu_frequency/common.cpp', | ||||
|  | @ -468,6 +470,7 @@ if scdoc.found() | |||
|         'waybar-backlight-slider.5.scd', | ||||
|         'waybar-battery.5.scd', | ||||
|         'waybar-cava.5.scd', | ||||
|         'waybar-cffi.5.scd', | ||||
|         'waybar-clock.5.scd', | ||||
|         'waybar-cpu.5.scd', | ||||
|         'waybar-custom.5.scd', | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| .cache/ | ||||
|  | @ -0,0 +1,38 @@ | |||
| # C FFI module | ||||
| 
 | ||||
| A C FFI module is a dynamic library that exposes standard C functions and | ||||
| constants, that Waybar can load and execute to create custom advanced widgets. | ||||
| 
 | ||||
| Most language can implement the required functions and constants (C, C++, Rust, | ||||
| Go, Python, ...), meaning you can develop custom modules using your language of | ||||
| choice, as long as there's GTK bindings. | ||||
| 
 | ||||
| Symbols to implement are documented in the | ||||
| [waybar_cffi_module.h](waybar_cffi_module.h) file. | ||||
| 
 | ||||
| # Usage | ||||
| 
 | ||||
| ## Building this module | ||||
| 
 | ||||
| ```bash | ||||
| meson setup build | ||||
| meson compile -C build | ||||
| ``` | ||||
| 
 | ||||
| ## Load the module | ||||
| 
 | ||||
| Edit your waybar config: | ||||
| ```json | ||||
| { | ||||
| 	// ... | ||||
| 	"modules-center": [ | ||||
| 		// ... | ||||
| 		"cffi/c_example" | ||||
| 	], | ||||
| 	// ... | ||||
| 	"cffi/c_example": { | ||||
| 		// Path to the compiled dynamic library file | ||||
| 		"module_path": "resources/custom_modules/cffi_example/build/wb_cffi_example.so" | ||||
| 	} | ||||
| } | ||||
| ``` | ||||
|  | @ -0,0 +1,70 @@ | |||
| 
 | ||||
| #include "waybar_cffi_module.h" | ||||
| 
 | ||||
| typedef struct { | ||||
|   wbcffi_module* waybar_module; | ||||
|   GtkBox* container; | ||||
|   GtkButton* button; | ||||
| } ExampleMod; | ||||
| 
 | ||||
| // This static variable is shared between all instances of this module
 | ||||
| static int instance_count = 0; | ||||
| 
 | ||||
| void onclicked(GtkButton* button) { | ||||
|   char text[256]; | ||||
|   snprintf(text, 256, "Dice throw result: %d", rand() % 6 + 1); | ||||
|   gtk_button_set_label(button, text); | ||||
| } | ||||
| 
 | ||||
| // You must
 | ||||
| const size_t wbcffi_version = 1; | ||||
| 
 | ||||
| void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, | ||||
|                   size_t config_entries_len) { | ||||
|   printf("cffi_example: init config:\n"); | ||||
|   for (size_t i = 0; i < config_entries_len; i++) { | ||||
|     printf("  %s = %s\n", config_entries[i].key, config_entries[i].value); | ||||
|   } | ||||
| 
 | ||||
|   // Allocate the instance object
 | ||||
|   ExampleMod* inst = malloc(sizeof(ExampleMod)); | ||||
|   inst->waybar_module = init_info->obj; | ||||
| 
 | ||||
|   GtkContainer* root = init_info->get_root_widget(init_info->obj); | ||||
| 
 | ||||
|   // Add a container for displaying the next widgets
 | ||||
|   inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); | ||||
|   gtk_container_add(GTK_CONTAINER(root), GTK_WIDGET(inst->container)); | ||||
| 
 | ||||
|   // Add a label
 | ||||
|   GtkLabel* label = GTK_LABEL(gtk_label_new("[Example C FFI Module:")); | ||||
|   gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); | ||||
| 
 | ||||
|   // Add a button
 | ||||
|   inst->button = GTK_BUTTON(gtk_button_new_with_label("click me !")); | ||||
|   g_signal_connect(inst->button, "clicked", G_CALLBACK(onclicked), NULL); | ||||
|   gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->button)); | ||||
| 
 | ||||
|   // Add a label
 | ||||
|   label = GTK_LABEL(gtk_label_new("]")); | ||||
|   gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); | ||||
| 
 | ||||
|   // Return instance object
 | ||||
|   printf("cffi_example inst=%p: init success ! (%d total instances)\n", inst, ++instance_count); | ||||
|   return inst; | ||||
| } | ||||
| 
 | ||||
| void wbcffi_deinit(void* instance) { | ||||
|   printf("cffi_example inst=%p: free memory\n", instance); | ||||
|   free(instance); | ||||
| } | ||||
| 
 | ||||
| void wbcffi_update(void* instance) { printf("cffi_example inst=%p: Update request\n", instance); } | ||||
| 
 | ||||
| void wbcffi_refresh(void* instance, int signal) { | ||||
|   printf("cffi_example inst=%p: Received refresh signal %d\n", instance, signal); | ||||
| } | ||||
| 
 | ||||
| void wbcffi_doaction(void* instance, const char* name) { | ||||
|   printf("cffi_example inst=%p: doAction(%s)\n", instance, name); | ||||
| } | ||||
|  | @ -0,0 +1,13 @@ | |||
| project( | ||||
|     'waybar_cffi_example', 'c', | ||||
|     version: '0.1.0', | ||||
|     license: 'MIT', | ||||
| ) | ||||
| 
 | ||||
| shared_library('wb_cffi_example', | ||||
|     ['main.c'], | ||||
|     dependencies: [ | ||||
|         dependency('gtk+-3.0', version : ['>=3.22.0']) | ||||
|     ], | ||||
|     name_prefix: '' | ||||
| ) | ||||
|  | @ -0,0 +1,89 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <gtk/gtk.h> | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| /// Waybar ABI version. 1 is the latest version
 | ||||
| extern const size_t wbcffi_version; | ||||
| 
 | ||||
| /// Private Waybar CFFI module
 | ||||
| typedef struct wbcffi_module wbcffi_module; | ||||
| 
 | ||||
| /// Waybar module information
 | ||||
| typedef struct { | ||||
|   /// Waybar CFFI object pointer
 | ||||
|   wbcffi_module* obj; | ||||
| 
 | ||||
|   /// Waybar version string
 | ||||
|   const char* waybar_version; | ||||
| 
 | ||||
|   /// Returns the waybar widget allocated for this module
 | ||||
|   /// @param obj Waybar CFFI object pointer
 | ||||
|   GtkContainer* (*get_root_widget)(wbcffi_module* obj); | ||||
| 
 | ||||
|   /// Queues a request for calling wbcffi_update() on the next GTK main event
 | ||||
|   /// loop iteration
 | ||||
|   /// @param obj Waybar CFFI object pointer
 | ||||
|   void (*queue_update)(wbcffi_module*); | ||||
| } wbcffi_init_info; | ||||
| 
 | ||||
| /// Config key-value pair
 | ||||
| typedef struct { | ||||
|   /// Entry key
 | ||||
|   const char* key; | ||||
|   /// Entry value as string. JSON object and arrays are serialized.
 | ||||
|   const char* value; | ||||
| } wbcffi_config_entry; | ||||
| 
 | ||||
| /// Module init/new function, called on module instantiation
 | ||||
| ///
 | ||||
| /// MANDATORY CFFI function
 | ||||
| ///
 | ||||
| /// @param init_info          Waybar module information
 | ||||
| /// @param config_entries     Flat representation of the module JSON config. The data only available
 | ||||
| ///                           during wbcffi_init call.
 | ||||
| /// @param config_entries_len Number of entries in `config_entries`
 | ||||
| ///
 | ||||
| /// @return A untyped pointer to module data, NULL if the module failed to load.
 | ||||
| void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, | ||||
|                   size_t config_entries_len); | ||||
| 
 | ||||
| /// Module deinit/delete function, called when Waybar is closed or when the module is removed
 | ||||
| ///
 | ||||
| /// MANDATORY CFFI function
 | ||||
| ///
 | ||||
| /// @param instance Module instance data (as returned by `wbcffi_init`)
 | ||||
| void wbcffi_deinit(void* instance); | ||||
| 
 | ||||
| /// Called from the GTK main event loop, to update the UI
 | ||||
| ///
 | ||||
| /// Optional CFFI function
 | ||||
| ///
 | ||||
| /// @param instance Module instance data (as returned by `wbcffi_init`)
 | ||||
| /// @param action_name Action name
 | ||||
| void wbcffi_update(void* instance); | ||||
| 
 | ||||
| /// Called when Waybar receives a POSIX signal and forwards it to each module
 | ||||
| ///
 | ||||
| /// Optional CFFI function
 | ||||
| ///
 | ||||
| /// @param instance Module instance data (as returned by `wbcffi_init`)
 | ||||
| /// @param signal Signal ID
 | ||||
| void wbcffi_refresh(void* instance, int signal); | ||||
| 
 | ||||
| /// Called on module action (see
 | ||||
| /// https://github.com/Alexays/Waybar/wiki/Configuration#module-actions-config)
 | ||||
| ///
 | ||||
| /// Optional CFFI function
 | ||||
| ///
 | ||||
| /// @param instance Module instance data (as returned by `wbcffi_init`)
 | ||||
| /// @param action_name Action name
 | ||||
| void wbcffi_doaction(void* instance, const char* action_name); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
|  | @ -207,6 +207,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, | |||
|     if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { | ||||
|       return new waybar::modules::Custom(ref.substr(7), id, config_[name]); | ||||
|     } | ||||
|     if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { | ||||
|       return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); | ||||
|     } | ||||
|   } catch (const std::exception& e) { | ||||
|     auto err = fmt::format("Disabling module \"{}\", {}", name, e.what()); | ||||
|     throw std::runtime_error(err); | ||||
|  |  | |||
|  | @ -0,0 +1,119 @@ | |||
| #include "modules/cffi.hpp" | ||||
| 
 | ||||
| #include <dlfcn.h> | ||||
| #include <json/value.h> | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <iostream> | ||||
| #include <type_traits> | ||||
| 
 | ||||
| namespace waybar::modules { | ||||
| 
 | ||||
| CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& config) | ||||
|     : AModule(config, name, id, true, true) { | ||||
|   const auto dynlib_path = config_["module_path"].asString(); | ||||
|   if (dynlib_path.empty()) { | ||||
|     throw std::runtime_error{"Missing or empty 'module_path' in module config"}; | ||||
|   } | ||||
| 
 | ||||
|   void* handle = dlopen(dynlib_path.c_str(), RTLD_LAZY); | ||||
|   if (handle == nullptr) { | ||||
|     throw std::runtime_error{std::string{"Failed to load CFFI module: "} + dlerror()}; | ||||
|   } | ||||
| 
 | ||||
|   // Fetch ABI version
 | ||||
|   auto wbcffi_version = reinterpret_cast<size_t*>(dlsym(handle, "wbcffi_version")); | ||||
|   if (wbcffi_version == nullptr) { | ||||
|     throw std::runtime_error{std::string{"Missing wbcffi_version function: "} + dlerror()}; | ||||
|   } | ||||
| 
 | ||||
|   // Fetch functions
 | ||||
|   if (*wbcffi_version == 1) { | ||||
|     // Mandatory functions
 | ||||
|     hooks_.init = reinterpret_cast<InitFn*>(dlsym(handle, "wbcffi_init")); | ||||
|     if (!hooks_.init) { | ||||
|       throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()}; | ||||
|     } | ||||
|     hooks_.deinit = reinterpret_cast<DenitFn*>(dlsym(handle, "wbcffi_deinit")); | ||||
|     if (!hooks_.init) { | ||||
|       throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; | ||||
|     } | ||||
|     // Optional functions
 | ||||
|     if (auto fn = reinterpret_cast<UpdateFn*>(dlsym(handle, "wbcffi_update"))) { | ||||
|       hooks_.update = fn; | ||||
|     } | ||||
|     if (auto fn = reinterpret_cast<RefreshFn*>(dlsym(handle, "wbcffi_refresh"))) { | ||||
|       hooks_.refresh = fn; | ||||
|     } | ||||
|     if (auto fn = reinterpret_cast<DoActionFn*>(dlsym(handle, "wbcffi_doaction"))) { | ||||
|       hooks_.doAction = fn; | ||||
|     } | ||||
|   } else { | ||||
|     throw std::runtime_error{"Unknown wbcffi_version " + std::to_string(*wbcffi_version)}; | ||||
|   } | ||||
| 
 | ||||
|   // Prepare init() arguments
 | ||||
|   // Convert JSON values to string
 | ||||
|   std::vector<std::string> config_entries_stringstor; | ||||
|   const auto& keys = config.getMemberNames(); | ||||
|   for (size_t i = 0; i < keys.size(); i++) { | ||||
|     const auto& value = config[keys[i]]; | ||||
|     if (value.isConvertibleTo(Json::ValueType::stringValue)) { | ||||
|       config_entries_stringstor.push_back(config[keys[i]].asString()); | ||||
|     } else { | ||||
|       config_entries_stringstor.push_back(config[keys[i]].toStyledString()); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Prepare config_entries array
 | ||||
|   std::vector<ffi::wbcffi_config_entry> config_entries; | ||||
|   for (size_t i = 0; i < keys.size(); i++) { | ||||
|     config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()}); | ||||
|   } | ||||
| 
 | ||||
|   ffi::wbcffi_init_info init_info = { | ||||
|       .obj = (ffi::wbcffi_module*)this, | ||||
|       .waybar_version = VERSION, | ||||
|       .get_root_widget = | ||||
|           [](ffi::wbcffi_module* obj) { | ||||
|             return dynamic_cast<Gtk::Container*>(&((CFFI*)obj)->event_box_)->gobj(); | ||||
|           }, | ||||
|       .queue_update = [](ffi::wbcffi_module* obj) { ((CFFI*)obj)->dp.emit(); }, | ||||
|   }; | ||||
| 
 | ||||
|   // Call init
 | ||||
|   cffi_instance_ = hooks_.init(&init_info, config_entries.data(), config_entries.size()); | ||||
| 
 | ||||
|   // Handle init failures
 | ||||
|   if (cffi_instance_ == nullptr) { | ||||
|     throw std::runtime_error{"Failed to initialize C ABI module"}; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| CFFI::~CFFI() { | ||||
|   if (cffi_instance_ != nullptr) { | ||||
|     hooks_.deinit(cffi_instance_); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| auto CFFI::update() -> void { | ||||
|   assert(cffi_instance_ != nullptr); | ||||
|   hooks_.update(cffi_instance_); | ||||
| 
 | ||||
|   // Execute the on-update command set in config
 | ||||
|   AModule::update(); | ||||
| } | ||||
| 
 | ||||
| auto CFFI::refresh(int signal) -> void { | ||||
|   assert(cffi_instance_ != nullptr); | ||||
|   hooks_.refresh(cffi_instance_, signal); | ||||
| } | ||||
| 
 | ||||
| auto CFFI::doAction(const std::string& name) -> void { | ||||
|   assert(cffi_instance_ != nullptr); | ||||
|   if (!name.empty()) { | ||||
|     hooks_.doAction(cffi_instance_, name.c_str()); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| }  // namespace waybar::modules
 | ||||
		Loading…
	
		Reference in New Issue