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',