diff --git a/meson.build b/meson.build
index 50d6dc66..a21f19b0 100644
--- a/meson.build
+++ b/meson.build
@@ -22,6 +22,7 @@ epoxy = dependency('epoxy')
gtklayershell = dependency('gtk4-layer-shell-0', fallback: ['gtk4-layer-shell'])
libpulse = dependency('libpulse', required: get_option('pulse'))
wireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber'))
+ddcutil = dependency('ddcutil', required: get_option('ddcutil'))
dbusmenu_gtk = dependency('dbusmenu-glib-0.4')
libgvc = subproject('gvc', default_options: ['static=true'], required: get_option('pulse'))
xkbregistry = dependency('xkbregistry')
@@ -40,6 +41,10 @@ if wireplumber.found()
add_project_arguments('-DHAVE_WIREPLUMBER=1', language: 'cpp')
endif
+if ddcutil.found()
+ add_project_arguments('-DHAVE_DDCUTIL=1', language: 'cpp')
+endif
+
needs_libinotify = ['freebsd', 'dragonfly'].contains(host_machine.system())
libinotify = dependency('libinotify', required: needs_libinotify)
diff --git a/meson_options.txt b/meson_options.txt
index 8ecd76c0..bb8c07fa 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -10,6 +10,12 @@ option(
value: 'auto',
description: 'Build wireplumber and mixer widget',
)
+option(
+ 'ddcutil',
+ type: 'feature',
+ value: 'auto',
+ description: 'Build external monitor support for backlight widget'
+)
option(
'wayland-logout',
type: 'boolean',
diff --git a/metadata/panel.xml b/metadata/panel.xml
index b59900e7..0e24a3d9 100644
--- a/metadata/panel.xml
+++ b/metadata/panel.xml
@@ -467,5 +467,38 @@ Set to -1 to only run it by clicking the button.
1
+
+ <_short>Light
+
+
+
+
+
+
+
diff --git a/src/panel/meson.build b/src/panel/meson.build
index bff4e6b3..10cbbdc0 100644
--- a/src/panel/meson.build
+++ b/src/panel/meson.build
@@ -20,6 +20,8 @@ widget_sources = [
'widgets/tray/item.cpp',
'widgets/tray/host.cpp',
'widgets/tray/dbusmenu.cpp',
+ 'widgets/light/light.cpp',
+ 'widgets/light/sysfs.cpp',
]
deps = [
@@ -56,6 +58,13 @@ else
message('Wireplumber not found, mixer widget will not be available.')
endif
+if ddcutil.found()
+ widget_sources += 'widgets/light/ddcutil.cpp'
+ deps += ddcutil
+else
+ message('Libddcutil not found, light widget will not support external monitors.')
+endif
+
executable(
'wf-panel',
['panel.cpp'] + widget_sources,
diff --git a/src/panel/panel.cpp b/src/panel/panel.cpp
index 80673018..05e023b3 100644
--- a/src/panel/panel.cpp
+++ b/src/panel/panel.cpp
@@ -29,6 +29,7 @@
#ifdef HAVE_WIREPLUMBER
#include "widgets/wp-mixer/wp-mixer.hpp"
#endif
+#include "widgets/light/light.hpp"
#include "widgets/window-list/window-list.hpp"
#include "widgets/notifications/notification-center.hpp"
#include "widgets/tray/tray.hpp"
@@ -178,6 +179,15 @@ class WayfirePanel::impl
#endif
}
+ if (name == "light")
+ {
+ return Widget(new WayfireLight(output));
+#ifndef HAVE_DDCUTIL
+ std::cout << "Built without DDC/CI support, light widget "
+ " doesn’t support external monitors." << std::endl;
+#endif
+ }
+
if (name == "window-list")
{
return Widget(new WayfireWindowList(output));
@@ -381,7 +391,8 @@ void WayfirePanelApp::on_activate()
{"panel/volume_icon_size", ".volume"},
{"panel/wp_icon_size", ".wireplumber"},
{"panel/notifications_icon_size", ".notification-center "},
- {"panel/tray_icon_size", ".tray-button"}
+ {"panel/tray_icon_size", ".tray-button"},
+ {"panel/light_icon_size", ".light"}
};
for (auto pair : icon_sizes_args)
{
diff --git a/src/panel/widgets/light/ddcutil.cpp b/src/panel/widgets/light/ddcutil.cpp
new file mode 100644
index 00000000..bd367bb1
--- /dev/null
+++ b/src/panel/widgets/light/ddcutil.cpp
@@ -0,0 +1,118 @@
+#include
+#include
+#include
+#include
+
+#include "light.hpp"
+
+#define VCP_BRIGHTNESS_CODE 0x10
+
+void show_err(std::string location, DDCA_Status status){
+ // if (!status)
+ std::cerr << location << " :" << ddca_rc_name(status) << " : " << ddca_rc_desc(status) << "\n";
+}
+
+
+class WfLightDdcaControl : public WfLightControl
+{
+ private:
+ DDCA_Display_Ref ref;
+ int max;
+
+ int get_max(){
+ return max;
+ }
+
+ public:
+ WfLightDdcaControl(WayfireLight *parent, DDCA_Display_Ref _ref) : WfLightControl(parent){
+ ref = _ref;
+
+ DDCA_Display_Handle handle;
+ DDCA_Status status = ddca_open_display2(ref, false, &handle);
+ show_err("open display", status);
+
+ DDCA_Non_Table_Vcp_Value value;
+ status = ddca_get_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, &value);
+ max = value.mh << 8 | value.ml;
+
+ ddca_close_display(handle);
+ }
+
+ std::string get_name(){
+ std::string name;
+ name = "display";
+ return name;
+ }
+
+ void set_brightness(double brightness){
+ DDCA_Display_Handle handle;
+ DDCA_Status status = ddca_open_display2(ref, false, &handle);
+ show_err("open display", status);
+
+ uint16_t value = (uint16_t)(get_max() * brightness);
+ uint8_t sh = value >> 8;
+ uint8_t sl = value & 0xFF;
+ status = ddca_set_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, sh, sl);
+ show_err("set brigthness", status);
+ ddca_close_display(handle);
+ }
+
+ double get_brightness(){
+ DDCA_Display_Handle handle;
+ DDCA_Status status = ddca_open_display2(ref, false, &handle);
+ show_err("open display", status);
+
+ DDCA_Non_Table_Vcp_Value value;
+ status = ddca_get_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, &value);
+ show_err("get brightness", status);
+ ddca_close_display(handle);
+ return value.sh << 8 | value.sl;
+ }
+};
+
+DdcaSurveillor::DdcaSurveillor(){
+ // watch for new valid monitors
+
+ auto status = ddca_start_watch_displays(DDCA_EVENT_CLASS_DISPLAY_ALL);
+
+ status = ddca_register_display_status_callback(on_new_display);
+
+ ddca_enable_verify(true);
+ DDCA_Display_Info_List *display_list = NULL;
+ ddca_get_display_info_list2(false, &display_list);
+
+ for (int i = 0 ; i < display_list->ct ; i++){
+ displays_info.push_back(&display_list->info[i]);
+ }
+}
+
+DdcaSurveillor::~DdcaSurveillor(){
+ ddca_stop_watch_displays(true);
+}
+
+void DdcaSurveillor::on_new_display(DDCA_Display_Status_Event event){
+ std::cout << "new display\n";
+ // ddca_redetect_displays();
+ // ddca_get_display_refs(false, );
+}
+
+void DdcaSurveillor::catch_up_widget(WayfireLight *widget){
+ for (auto info : displays_info){
+ auto control = std::make_shared(widget, info->dref);
+ // it.second.second.push_back(std::shared_ptr(control));
+ widget->add_control((std::shared_ptr)control);
+ }
+
+}
+
+void DdcaSurveillor::strip_widget(WayfireLight *widget){
+
+}
+
+DdcaSurveillor& DdcaSurveillor::get(){
+ if (!instance)
+ {
+ instance = std::unique_ptr(new DdcaSurveillor());
+ }
+ return *instance;
+}
diff --git a/src/panel/widgets/light/light.cpp b/src/panel/widgets/light/light.cpp
new file mode 100644
index 00000000..e8217917
--- /dev/null
+++ b/src/panel/widgets/light/light.cpp
@@ -0,0 +1,237 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "light.hpp"
+#include "wf-popover.hpp"
+#include "wf-shell-app.hpp"
+
+static BrightnessLevel light_icon_for(double value)
+{
+ double max = 1.0;
+ auto third = max / 3;
+ if (value <= third)
+ {
+ return BRIGHTNESS_LEVEL_LOW;
+ } else if ((value > third) && (value <= (third * 2)))
+ {
+ return BRIGHTNESS_LEVEL_MEDIUM;
+ } else if ((value > (third * 2)) && (value <= max))
+ {
+ return BRIGHTNESS_LEVEL_HIGH;
+ }
+
+ return BRIGHTNESS_LEVEL_OOR;
+}
+
+WfLightControl::WfLightControl(WayfireLight *_parent){
+ parent = _parent;
+
+ // preparation
+ scale.set_range(0.0, 1.0);
+ scale.set_size_request(slider_length.value());
+
+ scale.set_user_changed_callback([this](){
+ this->set_brightness(scale.get_target_value());
+ });
+
+ // layout
+ set_orientation(Gtk::Orientation::VERTICAL);
+ append(label);
+ append(scale);
+
+ // scroll
+ auto scroll_gesture = Gtk::EventControllerScroll::create();
+ scroll_gesture->signal_scroll().connect([=] (double dx, double dy)
+ {
+ double change = 0;
+
+ if (scroll_gesture->get_unit() == Gdk::ScrollUnit::WHEEL)
+ {
+ // +- number of clicks.
+ change = (dy * parent->scroll_sensitivity) / 10;
+ } else
+ {
+ // Number of pixels expected to have scrolled. usually in 100s
+ change = (dy * parent->scroll_sensitivity) / 100;
+ }
+ if (!(parent->invert_scroll))
+ change *= -1;
+
+ // correct for a "good feeling" change at sensitivity 1
+ change *= 0.2;
+
+ set_brightness(get_scale_target_value() + change);
+ return true;
+ }, true);
+ scroll_gesture->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
+ add_controller(scroll_gesture);
+}
+
+WayfireLight *WfLightControl::get_parent(){
+ return parent;
+}
+
+void WfLightControl::set_scale_target_value(double brightness)
+{
+ scale.set_target_value(brightness);
+ update_parent_icon();
+}
+
+double WfLightControl::get_scale_target_value()
+{
+ return scale.get_target_value();
+}
+
+void WfLightControl::update_parent_icon(){
+ if (parent->ctrl_this_display.get() == this){
+ parent->update_icon();
+ if (parent->popup_on_change){
+ parent->popover->popup();
+ parent->check_set_popover_timeout();
+ }
+ }
+}
+
+void LightManager::add_widget(WayfireLight *widget){
+ widgets.push_back(widget);
+ catch_up_widget(widget);
+}
+
+void LightManager::rem_widget(WayfireLight *widget){
+ strip_widget(widget);
+ widgets.erase(find(widgets.begin(), widgets.end(), widget));
+}
+
+WayfireLight::WayfireLight(WayfireOutput *_output)
+{
+ output = _output;
+}
+
+WayfireLight::~WayfireLight()
+{
+ SysfsSurveillor::get().rem_widget(this);
+ #ifdef HAVE_DDCUTIL
+ DdcaSurveillor::get().rem_widget(this);
+ #endif
+}
+
+void WayfireLight::init(Gtk::Box *container){
+ button = std::make_unique("panel");
+ button->get_style_context()->add_class("widget-icon");
+ button->get_style_context()->add_class("light");
+ button->get_style_context()->add_class("flat");
+ button->set_child(icon);
+ button->show();
+ popover = button->get_popover();
+ popover->set_autohide(false);
+
+ // layout
+ box.append(display_box);
+ box.set_orientation(Gtk::Orientation::VERTICAL);
+
+ disp_othr_sep.set_orientation(Gtk::Orientation::HORIZONTAL);
+ box.append(disp_othr_sep);
+ box.append(other_box);
+
+ display_label.set_text("This monitor");
+ display_box.append(display_label);
+ display_box.set_orientation(Gtk::Orientation::VERTICAL);
+
+ other_label.set_text("Other monitors");
+ other_box.append(other_label);
+ other_box.set_orientation(Gtk::Orientation::VERTICAL);
+
+ // scroll to brighten and dim the monitor the panel is on
+ auto scroll_gesture = Gtk::EventControllerScroll::create();
+ scroll_gesture->signal_scroll().connect([=] (double dx, double dy)
+ {
+ double change = 0;
+
+ if (scroll_gesture->get_unit() == Gdk::ScrollUnit::WHEEL)
+ {
+ // +- number of clicks.
+ change = (dy * scroll_sensitivity) / 10;
+ } else
+ {
+ // Number of pixels expected to have scrolled. usually in 100s
+ change = (dy * scroll_sensitivity) / 100;
+ }
+ if (!invert_scroll)
+ change *= -1;
+
+ // correct for a "good feeling" change at sensitivity 1
+ change *= 0.2;
+
+ ctrl_this_display->set_brightness(ctrl_this_display->get_scale_target_value() + change);
+ return true;
+ }, true);
+ scroll_gesture->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
+ button->add_controller(scroll_gesture);
+
+ popover->set_child(box);
+ popover->get_style_context()->add_class("light-popover");
+
+ container->append(*button);
+
+ SysfsSurveillor::get().add_widget(this);
+ #ifdef HAVE_DDCUTIL
+ DdcaSurveillor::get().add_widget(this);
+ #endif
+
+ update_icon();
+}
+
+void WayfireLight::add_control(std::shared_ptr control){
+ if (!ctrl_this_display){
+ auto connector = output->monitor->get_connector();
+ if (control->get_name() == connector)
+ {
+ ctrl_this_display = std::shared_ptr(control);
+ display_box.append(*control);
+ } else
+ {
+ box.append(*control);
+ }
+ }
+
+ controls.push_back(control);
+}
+
+void WayfireLight::update_icon(){
+ std::cout << "updating icon : ";
+ // if none, show unavailable
+ if (!ctrl_this_display){
+ std::cout << "no face\n";
+ icon.set_from_icon_name(brightness_display_icons.at(BRIGHTNESS_LEVEL_OOR));
+ return;
+ }
+
+ std::cout << "normal\n";
+ icon.set_from_icon_name(brightness_display_icons.at(
+ light_icon_for(ctrl_this_display->get_scale_target_value()))
+ );
+}
+
+bool WayfireLight::on_popover_timeout(int timer)
+{
+ popover_timeout.disconnect();
+ popover->popdown();
+ return false;
+}
+
+void WayfireLight::check_set_popover_timeout()
+{
+ popover_timeout.disconnect();
+
+ popover_timeout = Glib::signal_timeout().connect(sigc::bind(sigc::mem_fun(*this,
+ &WayfireLight::on_popover_timeout), 0), popup_timeout * 1000);
+}
+
+void WayfireLight::cancel_popover_timeout()
+{
+ popover_timeout.disconnect();
+}
diff --git a/src/panel/widgets/light/light.hpp b/src/panel/widgets/light/light.hpp
new file mode 100644
index 00000000..fad67536
--- /dev/null
+++ b/src/panel/widgets/light/light.hpp
@@ -0,0 +1,173 @@
+#include
+#include
+#include
+#include
+#ifdef HAVE_DDCUTIL
+extern "C"{
+ #include
+ #include
+}
+#endif
+
+#include "wf-shell-app.hpp"
+#include "widget.hpp"
+#include "animated-scale.hpp"
+
+enum BrightnessLevel
+{
+ BRIGHTNESS_LEVEL_LOW,
+ BRIGHTNESS_LEVEL_MEDIUM,
+ BRIGHTNESS_LEVEL_HIGH,
+ BRIGHTNESS_LEVEL_OOR, /* Out of range */
+};
+
+const std::map brightness_display_icons = {
+ {BRIGHTNESS_LEVEL_LOW, "display-brightness-low"},
+ {BRIGHTNESS_LEVEL_MEDIUM, "display-brightness-medium"},
+ {BRIGHTNESS_LEVEL_HIGH, "display-brightness-high"},
+ // this icon seems rare, so probably best to have a generic failure
+ {BRIGHTNESS_LEVEL_OOR, "display-brightness-invalid"},
+};
+
+class WayfireLight;
+
+class WfLightControl : public Gtk::Box
+{
+ protected:
+ WayfireAnimatedScale scale;
+ Gtk::Label label;
+ std::map icons;
+ WayfireLight *parent;
+ std::vector signals;
+
+ void update_parent_icon();
+
+ WfOption slider_length{"panel/light_slider_length"};
+
+ public:
+ WfLightControl(WayfireLight *parent);
+
+ virtual std::string get_name() = 0;
+ WayfireLight *get_parent();
+
+ void set_scale_target_value(double value);
+ double get_scale_target_value();
+ // a double from 0 to 1 for min to max
+ virtual void set_brightness(double brightness) = 0;
+ virtual double get_brightness() = 0;
+
+};
+
+class LightManager {
+ protected:
+ LightManager(){}
+ // managed widgets
+ std::vector widgets;
+
+ virtual void catch_up_widget(WayfireLight *widget) = 0;
+ virtual void strip_widget(WayfireLight *widget) = 0;
+
+ public:
+ void add_widget(WayfireLight *widget);
+ void rem_widget(WayfireLight *widget);
+};
+
+// singleton that monitors sysfs and calls the necessary functions
+// monitors appearance and deletion of backlight devices
+// and the brightness of each of them
+class SysfsSurveillor : public LightManager {
+ private:
+ SysfsSurveillor();
+ void handle_inotify_events();
+ bool check_perms(std::filesystem::path);
+ void add_dev(std::filesystem::path);
+ void rem_dev(std::filesystem::path);
+ void catch_up_widget(WayfireLight *widget);
+ void strip_widget(WayfireLight *widget);
+
+ static inline std::unique_ptr instance;
+
+ int fd; // inotify file descriptor
+
+ // stores the data that goes with the inotify watch descriptor (the int)
+ // the controls are all the controls which represent this device, to be updated
+ std::map<
+ int,
+ std::pair<
+ std::filesystem::path,
+ std::vector>
+ >
+ > wd_to_path_controls;
+
+ // watch descriptors for files (so, a device) being added or removed
+ int wd_additions, wd_removal;
+
+ // managed widgets
+ std::vector widgets;
+
+ // thread on which to run handle_inotify_event on loop
+ std::thread inotify_thread;
+
+ public:
+ ~SysfsSurveillor();
+
+ static SysfsSurveillor& get();
+};
+
+#ifdef HAVE_DDCUTIL
+class DdcaSurveillor : public LightManager {
+ private:
+ DdcaSurveillor();
+ void catch_up_widget(WayfireLight *widget);
+ void strip_widget(WayfireLight *widget);
+ static void on_new_display(DDCA_Display_Status_Event event);
+
+ static inline std::unique_ptr instance;
+
+ std::vector displays_info;
+
+ public:
+ ~DdcaSurveillor();
+
+ static DdcaSurveillor& get();
+};
+#endif
+
+class WayfireLight : public WayfireWidget {
+ private:
+ void init(Gtk::Box *container) override;
+
+ WayfireOutput *output;
+
+ Gtk::Image icon;
+ std::unique_ptr button;
+ Gtk::Box box, display_box, other_box;
+ Gtk::Label display_label, other_label;
+ Gtk::Separator disp_othr_sep;
+ sigc::connection popover_timeout;
+
+ WfOption popup_timeout{"panel/light_popup_timeout"};
+
+ bool on_popover_timeout(int timer);
+
+ public:
+ WayfireLight(WayfireOutput *output);
+ ~WayfireLight();
+
+ WfOption scroll_sensitivity{"panel/light_scroll_sensitivity"};
+ WfOption invert_scroll{"panel/light_invert_scroll"};
+ WfOption popup_on_change{"panel/light_popup_on_change"};
+
+ Gtk::Popover *popover;
+
+ std::shared_ptr ctrl_this_display;
+
+ std::vector> controls;
+
+ void check_set_popover_timeout();
+ void cancel_popover_timeout();
+
+ void add_control(std::shared_ptr control);
+
+ void update_icon();
+};
diff --git a/src/panel/widgets/light/sysfs.cpp b/src/panel/widgets/light/sysfs.cpp
new file mode 100644
index 00000000..c7c202a4
--- /dev/null
+++ b/src/panel/widgets/light/sysfs.cpp
@@ -0,0 +1,353 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+extern "C"{
+ #include
+}
+#include
+
+#include "light.hpp"
+
+class WfLightSysfsControl: public WfLightControl
+{
+ // if we exist, it means we can just read/write, as the files and permissions
+ // have already been checked and are being monitored with inotify
+
+ private:
+ std::string path, connector_name;
+
+ int get_max(){
+ std::ifstream max_file(path + "/max_brightness");
+ if (!max_file.is_open()){
+ std::cerr << "Failed to get max brightness for device at " << path << '\n';
+ return 0;
+ }
+
+ int max;
+ max_file >> max;
+ max_file.close();
+ return max;
+ }
+
+ public:
+ WfLightSysfsControl(WayfireLight *parent, std::string _path) : WfLightControl(parent){
+ path = _path;
+
+ // this resolves to something of the sort :
+ // /sys/devices/pciXXXX:XX/XXXX:XX:XX.X/XXXX:XX:XX.X/drm/cardX-/
+ // what we are intersted in here is the connector name
+ std::string realpath = std::filesystem::canonical(path);
+ // the offset is constant until cardX, after which we look for the -.
+ connector_name = realpath.substr(60, realpath.size());
+ connector_name = connector_name.substr(connector_name.find("-") + 1, connector_name.size());
+ // then, the connector is what remains until /
+ connector_name = connector_name.substr(0, connector_name.find("/"));
+
+ scale.set_target_value(get_brightness());
+ label.set_text(get_name());
+
+ icons = brightness_display_icons;
+ }
+
+ std::string get_name(){
+ return connector_name;
+ }
+
+ void set_brightness(double brightness){
+ std::ofstream b_file(path + "/brightness");
+ if (!b_file.is_open()){
+ std::cerr << "Failed to open brightness for device at " << path << '\n';
+ return;
+ }
+ // something of the sort avoids formatting issues with locales
+ b_file << std::to_string((int)(brightness * (double)get_max()));
+ if (b_file.fail()){
+ std::cerr << "Failed to write brightness for device at " << path << '\n';
+ return;
+ }
+
+ update_parent_icon();
+ }
+
+ double get_brightness(){
+ std::ifstream b_file(path + "/brightness");
+ if (!b_file.is_open()){
+ std::cerr << "Failed to get brightness for device at " << path << '\n';
+ return 0;
+ }
+
+ int brightness, max;
+ b_file >> brightness;
+ b_file.close();
+ max = get_max();
+ return (((double)brightness + (double)max) / (double)max) - 1;
+ }
+};
+
+// utilities to check permissions
+bool is_group_member(gid_t file_group_id)
+{
+ gid_t current_group = getgid();
+ gid_t supplementary_groups[NGROUPS_MAX];
+ int n_groups = getgroups(NGROUPS_MAX, supplementary_groups);
+
+ if (current_group == file_group_id)
+ return true;
+
+ for (int i = 0; i < n_groups; ++i)
+ {
+ if (supplementary_groups[i] == file_group_id)
+ return true;
+ }
+
+ return false;
+}
+
+// let’s assume the file is not owned by the user and only bother with groups
+bool is_in_file_group(const std::filesystem::path& file_path)
+{
+ struct stat file_info;
+ if (stat(file_path.c_str(), &file_info) != 0)
+ {
+ std::cerr << "Failed to stat " << file_path << ".\n";
+ return false;
+ }
+
+ struct group *file_group = getgrgid(file_info.st_gid);
+
+ if (!file_group)
+ {
+ std::cerr << "Failed to fetch owner/group info for " << file_path << ".\n";
+ return false;
+ }
+
+ if (is_group_member(file_info.st_gid))
+ {
+ return true;
+ } else
+ {
+ return false;
+ }
+}
+
+SysfsSurveillor::SysfsSurveillor(){
+ fd = inotify_init();
+ if (fd == -1){
+ std::cerr << "Light widget: initialisation of inotify on sysfs failed.\n";
+ return;
+ }
+
+ const auto path = "/sys/class/backlight";
+
+ wd_additions = inotify_add_watch(fd, path, IN_CREATE);
+ wd_additions = inotify_add_watch(fd, path, IN_DELETE);
+
+ // look for present integrated backlights
+ for (const auto& entry : std::filesystem::directory_iterator(path)){
+ add_dev(entry);
+ }
+
+ inotify_thread = std::thread(&SysfsSurveillor::handle_inotify_events, this);
+}
+
+SysfsSurveillor::~SysfsSurveillor(){
+ // clean up inotify
+ close(fd);
+ fd = -1;
+
+ // remove controls from every widget
+ for (auto& widget : widgets)
+ strip_widget(widget);
+}
+
+void SysfsSurveillor::handle_inotify_events(){
+ // according to the inotify man page, aligning as such ensures
+ // proper function and avoids performance loss for "some systems"
+ char buf[2048] __attribute__((aligned(__alignof__(struct inotify_event))));
+ const struct inotify_event *event;
+ ssize_t size;
+
+ for (;;){
+ // read, which will block until the next inotify event
+ size = read(fd, buf, sizeof(buf));
+ if (size == -1){
+ if (errno != EAGAIN)
+ std::cerr << "Light widget: error reading inotify event.\n";
+ else
+ break;
+ }
+
+ for (char *ptr = buf ; ptr < buf + size ; ptr += sizeof(struct inotify_event) + event->len){
+ event = (const struct inotify_event*) ptr;
+
+ // a registered brightness file was changed
+ if (event->mask & IN_CLOSE_WRITE){
+ // look for the watch descriptor
+ if (wd_to_path_controls.find(event->wd) != wd_to_path_controls.end()){
+ // update every control
+ for (auto control : wd_to_path_controls[event->wd].second){
+ control->set_scale_target_value(control->get_brightness());
+ }
+ }
+ }
+
+ // metadata changed, so maybe permissions
+ if (event->mask & IN_ATTRIB){
+ if (wd_to_path_controls.find(event->wd) != wd_to_path_controls.end())
+ {
+ // get the path without which file, just the directory
+ auto path = wd_to_path_controls[event->wd].first;
+
+ // only recheck if the permissions to brightness or max_brightenss changed
+ if (event->name == (path.string() + "/brightness") ||
+ event->name == (path.string() + "/max_brightness"))
+ {
+ // if we cannot do what’s needed on the device, remove it
+ if (!check_perms(path)){
+ rem_dev(path);
+ }
+
+ }
+ }
+ }
+
+ // a backlight device appeared
+ if (event->mask & IN_CREATE){
+ if (wd_additions == event->wd){
+ if (event->len)
+ {
+ add_dev(event->name);
+ }
+ }
+ }
+
+ // a backlight device was removed
+ if (event->mask & IN_DELETE){
+ if (wd_removal == event->wd){
+ if (event->len)
+ {
+ rem_dev(event->name);
+ }
+ }
+ }
+ }
+ }
+}
+
+bool SysfsSurveillor::check_perms(std::filesystem::path path){
+ // those are the two files we are interested in,
+ // brightness for reading / setting the value (0 to max),
+ // and max_brightness for getting the maximum value for this device.
+ // we need to be able to read max_brightness and write to brightness.
+ const std::filesystem::path b_path = path.string() + "/brightness";
+ const std::filesystem::path max_b_path = path.string() + "/max_brightness";
+
+ // verity they exist
+ if (!std::filesystem::exists(b_path)){
+ std::cout << "No brightness found for " << path.string() << ", ignoring.\n";
+ return false;
+ }
+ if (!std::filesystem::exists(b_path)){
+ std::cout << "No max_brightness found for " << path.string() << ", ignoring.\n";
+ return false;
+ }
+
+ auto max_perms = std::filesystem::status(max_b_path).permissions();
+ // can the file be read?
+ if (!((max_perms & std::filesystem::perms::others_read) != std::filesystem::perms::none
+ || (is_in_file_group(max_b_path) && (max_perms & std::filesystem::perms::group_read) != std::filesystem::perms::none)
+ )){
+ std::cout << "Cannot read max_brightness file.\n";
+ return false;
+ }
+
+ auto perms = std::filesystem::status(b_path).permissions();
+ // can the file be read?
+ if (!((perms & std::filesystem::perms::others_read) != std::filesystem::perms::none
+ || (is_in_file_group(b_path) && (perms & std::filesystem::perms::group_read) != std::filesystem::perms::none)
+ )){
+ std::cout << "Cannot read brightness file.\n";
+ return false;
+ }
+ // and written?
+ if (!((perms & std::filesystem::perms::others_write) != std::filesystem::perms::none
+ || (is_in_file_group(b_path) && (perms & std::filesystem::perms::group_write) != std::filesystem::perms::none)
+ ))
+ {
+ std::cout << "Can read backlight, but cannot write. Ignoring.\n";
+ return false;
+ }
+
+ return true;
+}
+
+void SysfsSurveillor::add_dev(std::filesystem::path path){
+ if (!check_perms(path))
+ return;
+
+ // create a watch descriptor on the brightness file
+ int wd = inotify_add_watch(fd, path.string().c_str(), IN_CLOSE_WRITE | IN_ATTRIB);
+ if (wd == -1){
+ std::cerr << "Light widget: failed to register inotify watch descriptor.\n";
+ return;
+ }
+
+ wd_to_path_controls.insert({wd, {path, {}}});
+
+ // create a control for each widget, and insert it in the vector we just created
+ for (auto widget : widgets)
+ {
+ auto control = std::make_shared(widget, path);
+ wd_to_path_controls[wd].second.push_back(std::shared_ptr(control));
+
+ widget->add_control(control);
+ }
+}
+
+void SysfsSurveillor::rem_dev(std::filesystem::path path){
+ // device was removed, we can remove the entire entry (wd path, controls)
+ for (auto it : wd_to_path_controls){
+ if (it.second.first == path){
+ auto& controls = it.second.second;
+ for (auto control : controls){
+ controls.erase(find(controls.begin(), controls.end(), control));
+ }
+ wd_to_path_controls.erase(it.first);
+ }
+ }
+}
+
+void SysfsSurveillor::catch_up_widget(WayfireLight* widget){
+ // for each managed device, create a control and add it to the widget and keep track of it
+ for (auto& it : wd_to_path_controls){
+ auto control = std::make_shared(widget, it.second.first.string());
+ it.second.second.push_back(std::shared_ptr(control));
+ widget->add_control((std::shared_ptr)control);
+ }
+}
+
+void SysfsSurveillor::strip_widget(WayfireLight *widget){
+ for (auto& it : wd_to_path_controls)
+ {
+ auto& controls = it.second.second;
+ for (auto& control : controls)
+ {
+ if (control->get_parent() == widget)
+ {
+ controls.erase(find(controls.begin(), controls.end(), control));
+ }
+ }
+ }
+}
+
+SysfsSurveillor& SysfsSurveillor::get(){
+ if (!instance)
+ {
+ instance = std::unique_ptr(new SysfsSurveillor());
+ }
+ return *instance;
+}
diff --git a/src/util/meson.build b/src/util/meson.build
index 5621206e..117a7174 100644
--- a/src/util/meson.build
+++ b/src/util/meson.build
@@ -1,6 +1,7 @@
util = static_library(
'util',
[
+ 'animated-scale.cpp',
'gtk-utils.cpp',
'wf-shell-app.cpp',
'wf-autohide-window.cpp',