diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index cc1c4b4..a6da7ef 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -21,7 +21,8 @@ jobs: pkg install -y git # subprojects/date pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ - pkgconf pulseaudio scdoc sndio spdlog wayland-protocols upower + pkgconf pulseaudio scdoc sndio spdlog wayland-protocols upower \ + libinotify run: | meson build -Dman-pages=enabled ninja -C build diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index 05fbec1..ce9faba 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -3,12 +3,15 @@ #include #include +#include + #include "AModule.hpp" #include "bar.hpp" #include "util/sleeper_thread.hpp" extern "C" { #include +#include } namespace waybar::modules { @@ -20,6 +23,8 @@ class KeyboardState : public AModule { auto update() -> void; private: + auto tryAddDevice(const std::string&) -> void; + Gtk::Box box_; Gtk::Label numlock_label_; Gtk::Label capslock_label_; @@ -31,11 +36,12 @@ class KeyboardState : public AModule { const std::chrono::seconds interval_; std::string icon_locked_; std::string icon_unlocked_; + std::string devices_path_; - int fd_; - libevdev* dev_; + struct libinput* libinput_; + std::unordered_map libinput_devices_; - util::SleeperThread thread_; + util::SleeperThread libinput_thread_, hotplug_thread_; }; } // namespace waybar::modules diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index 905a321..fef4670 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -13,6 +13,7 @@ You must be a member of the input group to use this module. # CONFIGURATION *interval*: ++ + Deprecated, this module use event loop now, the interval has no effect. typeof: integer ++ default: 1 ++ The interval, in seconds, to poll the keyboard state. diff --git a/meson.build b/meson.build index e72d99f..05a32f4 100644 --- a/meson.build +++ b/meson.build @@ -89,7 +89,9 @@ dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gt giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled() or get_option('upower_glib').enabled())) jsoncpp = dependency('jsoncpp') sigcpp = dependency('sigc++-2.0') +libinotify = dependency('libinotify', required: false) libepoll = dependency('epoll-shim', required: false) +libinput = dependency('libinput', required: get_option('libinput')) libnl = dependency('libnl-3.0', required: get_option('libnl')) libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) upower_glib = dependency('upower-glib', required: get_option('upower_glib')) @@ -260,8 +262,9 @@ if libudev.found() and (is_linux or libepoll.found()) src_files += 'src/modules/backlight.cpp' endif -if libevdev.found() and (is_linux or libepoll.found()) +if libevdev.found() and (is_linux or libepoll.found()) and libinput.found() and (is_linux or libinotify.found()) add_project_arguments('-DHAVE_LIBEVDEV', language: 'cpp') + add_project_arguments('-DHAVE_LIBINPUT', language: 'cpp') src_files += 'src/modules/keyboard_state.cpp' endif @@ -321,12 +324,14 @@ executable( gtkmm, dbusmenu_gtk, giounix, + libinput, libnl, libnlgen, upower_glib, libpulse, libjack, libudev, + libinotify, libepoll, libmpdclient, libevdev, diff --git a/meson_options.txt b/meson_options.txt index b8eb921..bd5eb81 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,5 @@ option('libcxx', type : 'boolean', value : false, description : 'Build with Clang\'s libc++ instead of libstdc++ on Linux.') +option('libinput', type: 'feature', value: 'auto', description: 'Enable libinput support for libinput related features') option('libnl', type: 'feature', value: 'auto', description: 'Enable libnl support for network related features') option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev support for udev related features') option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features') diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 38faacd..b2750b6 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -8,8 +8,13 @@ extern "C" { #include +#include +#include +#include +#include #include #include +#include } class errno_error : public std::runtime_error { @@ -99,8 +104,18 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& icon_unlocked_(config_["format-icons"]["unlocked"].isString() ? config_["format-icons"]["unlocked"].asString() : "unlocked"), - fd_(0), - dev_(nullptr) { + devices_path_("/dev/input/"), + libinput_(nullptr), + libinput_devices_({}) { + static struct libinput_interface interface = { + [](const char* path, int flags, void* user_data) { return open(path, flags); }, + [](int fd, void* user_data) { close(fd); }}; + if (config_["interval"].isUInt()) { + spdlog::warn("keyboard-state: interval is deprecated"); + } + + libinput_ = libinput_path_create_context(&interface, NULL); + box_.set_name("keyboard-state"); if (config_["numlock"].asBool()) { numlock_label_.get_style_context()->add_class("numlock"); @@ -121,70 +136,135 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& if (config_["device-path"].isString()) { std::string dev_path = config_["device-path"].asString(); - fd_ = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); - dev_ = openDevice(fd_); - } else { - DIR* dev_dir = opendir("/dev/input"); - if (dev_dir == nullptr) { - throw errno_error(errno, "Failed to open /dev/input"); - } - dirent* ep; - while ((ep = readdir(dev_dir))) { - if (ep->d_type != DT_CHR) continue; - std::string dev_path = std::string("/dev/input/") + ep->d_name; - int fd = openFile(dev_path.c_str(), O_NONBLOCK | O_CLOEXEC | O_RDONLY); - try { - auto dev = openDevice(fd); - if (supportsLockStates(dev)) { - spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path); - fd_ = fd; - dev_ = dev; - break; - } - } catch (const errno_error& e) { - // ENOTTY just means the device isn't an evdev device, skip it - if (e.code != ENOTTY) { - spdlog::warn(e.what()); - } - } - closeFile(fd); - } - if (dev_ == nullptr) { - throw errno_error(errno, "Failed to find keyboard device"); + tryAddDevice(dev_path); + if (libinput_devices_.empty()) { + spdlog::error("keyboard-state: Cannot find device {}", dev_path); } } - thread_ = [this] { + DIR* dev_dir = opendir(devices_path_.c_str()); + if (dev_dir == nullptr) { + throw errno_error(errno, "Failed to open " + devices_path_); + } + dirent* ep; + while ((ep = readdir(dev_dir))) { + if (ep->d_type == DT_DIR) continue; + std::string dev_path = devices_path_ + ep->d_name; + tryAddDevice(dev_path); + } + + if (libinput_devices_.empty()) { + throw errno_error(errno, "Failed to find keyboard device"); + } + + libinput_thread_ = [this] { dp.emit(); - thread_.sleep_for(interval_); + while (1) { + struct pollfd fd = {libinput_get_fd(libinput_), POLLIN, 0}; + poll(&fd, 1, -1); + libinput_dispatch(libinput_); + struct libinput_event* event; + while ((event = libinput_get_event(libinput_))) { + auto type = libinput_event_get_type(event); + if (type == LIBINPUT_EVENT_KEYBOARD_KEY) { + auto keyboard_event = libinput_event_get_keyboard_event(event); + auto state = libinput_event_keyboard_get_key_state(keyboard_event); + if (state == LIBINPUT_KEY_STATE_RELEASED) { + uint32_t key = libinput_event_keyboard_get_key(keyboard_event); + switch (key) { + case KEY_CAPSLOCK: + case KEY_NUMLOCK: + case KEY_SCROLLLOCK: + dp.emit(); + break; + default: + break; + } + } + } + libinput_event_destroy(event); + } + } + }; + + hotplug_thread_ = [this] { + int fd; + fd = inotify_init(); + if (fd < 0) { + spdlog::error("Failed to initialize inotify: {}", strerror(errno)); + return; + } + inotify_add_watch(fd, devices_path_.c_str(), IN_CREATE | IN_DELETE); + while (1) { + int BUF_LEN = 1024 * (sizeof(struct inotify_event) + 16); + char buf[BUF_LEN]; + int length = read(fd, buf, 1024); + if (length < 0) { + spdlog::error("Failed to read inotify: {}", strerror(errno)); + return; + } + for (int i = 0; i < length;) { + struct inotify_event* event = (struct inotify_event*)&buf[i]; + std::string dev_path = devices_path_ + event->name; + if (event->mask & IN_CREATE) { + // Wait for device setup + int timeout = 10; + while (timeout--) { + try { + int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); + closeFile(fd); + break; + } catch (const errno_error& e) { + if (e.code == EACCES) { + sleep(1); + } + } + } + tryAddDevice(dev_path); + } else if (event->mask & IN_DELETE) { + auto it = libinput_devices_.find(dev_path); + if (it != libinput_devices_.end()) { + spdlog::info("Keyboard {} has been removed.", dev_path); + libinput_devices_.erase(it); + } + } + i += sizeof(struct inotify_event) + event->len; + } + } }; } waybar::modules::KeyboardState::~KeyboardState() { - libevdev_free(dev_); - try { - closeFile(fd_); - } catch (const std::runtime_error& e) { - spdlog::warn(e.what()); + for (const auto& [_, dev_ptr] : libinput_devices_) { + libinput_path_remove_device(dev_ptr); } } auto waybar::modules::KeyboardState::update() -> void { - int err = LIBEVDEV_READ_STATUS_SUCCESS; - while (err == LIBEVDEV_READ_STATUS_SUCCESS) { - input_event ev; - err = libevdev_next_event(dev_, LIBEVDEV_READ_FLAG_NORMAL, &ev); - while (err == LIBEVDEV_READ_STATUS_SYNC) { - err = libevdev_next_event(dev_, LIBEVDEV_READ_FLAG_SYNC, &ev); + sleep(0); // Wait for keyboard status change + int numl = 0, capsl = 0, scrolll = 0; + + try { + std::string dev_path; + if (config_["device-path"].isString() && + libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) { + dev_path = config_["device-path"].asString(); + } else { + dev_path = libinput_devices_.begin()->first; + } + int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); + auto dev = openDevice(fd); + numl = libevdev_get_event_value(dev, EV_LED, LED_NUML); + capsl = libevdev_get_event_value(dev, EV_LED, LED_CAPSL); + scrolll = libevdev_get_event_value(dev, EV_LED, LED_SCROLLL); + libevdev_free(dev); + closeFile(fd); + } catch (const errno_error& e) { + // ENOTTY just means the device isn't an evdev device, skip it + if (e.code != ENOTTY) { + spdlog::warn(e.what()); } } - if (-err != EAGAIN) { - throw errno_error(-err, "Failed to sync evdev device"); - } - - int numl = libevdev_get_event_value(dev_, EV_LED, LED_NUML); - int capsl = libevdev_get_event_value(dev_, EV_LED, LED_CAPSL); - int scrolll = libevdev_get_event_value(dev_, EV_LED, LED_SCROLLL); struct { bool state; @@ -211,3 +291,25 @@ auto waybar::modules::KeyboardState::update() -> void { AModule::update(); } + +auto waybar::modules ::KeyboardState::tryAddDevice(const std::string& dev_path) -> void { + try { + int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); + auto dev = openDevice(fd); + if (supportsLockStates(dev)) { + spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path); + if (libinput_devices_.find(dev_path) == libinput_devices_.end()) { + auto device = libinput_path_add_device(libinput_, dev_path.c_str()); + libinput_device_ref(device); + libinput_devices_[dev_path] = device; + } + } + libevdev_free(dev); + closeFile(fd); + } catch (const errno_error& e) { + // ENOTTY just means the device isn't an evdev device, skip it + if (e.code != ENOTTY) { + spdlog::warn(e.what()); + } + } +}