diff --git a/README.md b/README.md index f7bc4c89e..a158784f9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ > *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* **Current features** -- Sway (Workspaces, Binding mode, Focused window name) +- Sway (Workspaces, Binding mode, Focused window name, Keyboard Layout) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery @@ -44,6 +44,8 @@ $ waybar ``` gtkmm3 jsoncpp +pugixml +libinput libsigc++ fmt wayland @@ -81,6 +83,7 @@ sudo apt install \ libgtkmm-3.0-dev \ libinput-dev \ libjsoncpp-dev \ + libpugixml-dev \ libmpdclient-dev \ libnl-3-dev \ libnl-genl-3-dev \ diff --git a/include/factory.hpp b/include/factory.hpp index c698aa32d..ed27c4c98 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -6,6 +6,7 @@ #include "modules/sway/mode.hpp" #include "modules/sway/window.hpp" #include "modules/sway/workspaces.hpp" +#include "modules/sway/layout.hpp" #endif #if defined(__linux__) && !defined(NO_FILESYSTEM) #include "modules/battery.hpp" diff --git a/include/modules/sway/ipc/ipc.hpp b/include/modules/sway/ipc/ipc.hpp index 2c5a7a6e4..d06276499 100644 --- a/include/modules/sway/ipc/ipc.hpp +++ b/include/modules/sway/ipc/ipc.hpp @@ -29,4 +29,6 @@ enum ipc_command_type { IPC_EVENT_BINDING = ((1 << 31) | 5), IPC_EVENT_SHUTDOWN = ((1 << 31) | 6), IPC_EVENT_TICK = ((1 << 31) | 7), + IPC_EVENT_BAR_STATE_UPDATE = ((1 << 31) | 0x14), + IPC_EVENT_INPUT = ((1 << 31) | 0x15), }; diff --git a/include/modules/sway/layout.hpp b/include/modules/sway/layout.hpp new file mode 100644 index 000000000..638027865 --- /dev/null +++ b/include/modules/sway/layout.hpp @@ -0,0 +1,61 @@ +#pragma once + +#ifdef FILESYSTEM_EXPERIMENTAL +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include "ALabel.hpp" +#include "bar.hpp" +#include "client.hpp" +#include "modules/sway/ipc/client.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" +#include + +namespace waybar::modules::sway { + +#ifdef FILESYSTEM_EXPERIMENTAL +namespace fs = std::experimental::filesystem; +#else +namespace fs = std::filesystem; +#endif + +class Layout : public ALabel, public sigc::trackable { + public: + Layout(const std::string&, const Json::Value&); + ~Layout() = default; + auto update() -> void; + + private: + + using ShortNames = std::tuple; + using MemoizedShortNames = std::unordered_map< + std::string, + std::tuple + >; + + static inline const fs::path xbk_file_ = "/usr/share/X11/xkb/rules/evdev.xml"; + + void onCmd(const struct Ipc::ipc_response&); + void onEvent(const struct Ipc::ipc_response&); + void worker(); + ShortNames getShortNames(); + ShortNames fromFileGetShortNames(); + + std::string layout_; + util::JsonParser parser_; + MemoizedShortNames memoizedShortNames_; + + std::string keyboard_id_; + + util::SleeperThread thread_; + Ipc ipc_; + }; + +} // namespace waybar::modules::sway diff --git a/man/waybar-sway-layout-5.scd b/man/waybar-sway-layout-5.scd new file mode 100644 index 000000000..3ad3a798f --- /dev/null +++ b/man/waybar-sway-layout-5.scd @@ -0,0 +1,82 @@ +waybar-sway-layout(5) + +# NAME + +waybar - sway layout module + +# DESCRIPTION + +The *layout* module displays the current keyboard layout of Sway + +# CONFIGURATION + +Addressed by *sway/layout* + +*format*: ++ + typeof: string ++ + default: "{}" ++ + The format, how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: "{long}" ++ + The format of the information displayed in the tooltip. + +# FORMAT REPLACEMENTS + +*{long}*: The complete layout and variant indication (e.g "English (US)") + +*{short}*: The layout short name (e.g "en") + +*{variant}*: The variant short name (e.g "us") + +# EXAMPLES + +``` +"sway/layout": { + "tooltip-format": "{long}", + "format": "{short} ({variant})", + "keyboard_id": "1:1:AT_Translated_Set_2_keyboard" +} +``` + +# STYLE + +- *#layout* diff --git a/man/waybar.5.scd b/man/waybar.5.scd index 1e8004f27..ba6fcfe03 100644 --- a/man/waybar.5.scd +++ b/man/waybar.5.scd @@ -196,5 +196,6 @@ Valid options for the "rotate" property are: 0, 90, 180 and 270. - *waybar-sway-mode(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* +- *waybar-sway-layout(5)* - *waybar-temperature(5)* - *waybar-tray(5)* diff --git a/meson.build b/meson.build index 86e7ba4d1..ca745e056 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,8 @@ gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) giounix = dependency('gio-unix-2.0', required: get_option('dbusmenu-gtk')) jsoncpp = dependency('jsoncpp') +pugixml = dependency('pugixml') +libinput = dependency('libinput') sigcpp = dependency('sigc++-2.0') libepoll = dependency('epoll-shim', required: false) libnl = dependency('libnl-3.0', required: get_option('libnl')) @@ -156,7 +158,8 @@ if true # find_program('sway', required : false).found() 'src/modules/sway/ipc/client.cpp', 'src/modules/sway/mode.cpp', 'src/modules/sway/window.cpp', - 'src/modules/sway/workspaces.cpp' + 'src/modules/sway/workspaces.cpp', + 'src/modules/sway/layout.cpp' ] endif @@ -207,6 +210,8 @@ executable( spdlog, sigcpp, jsoncpp, + pugixml, + libinput, wayland_cursor, gtkmm, dbusmenu_gtk, diff --git a/resources/config b/resources/config index 832f76c8b..7226393f4 100644 --- a/resources/config +++ b/resources/config @@ -6,7 +6,7 @@ // Choose the order of the modules "modules-left": ["sway/workspaces", "sway/mode", "custom/media"], "modules-center": ["sway/window"], - "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "battery", "battery#bat2", "clock", "tray"], + "modules-right": ["sway/layout", "mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "battery", "battery#bat2", "clock", "tray"], // Modules configuration // "sway/workspaces": { // "disable-scroll": true, @@ -26,6 +26,11 @@ "sway/mode": { "format": "{}" }, + "sway/layout": { + "tooltip-format": "{long}", + "format": "{short} ", + "keyboard_id": "1:1:AT_Translated_Set_2_keyboard" + }, "mpd": { "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ", "format-disconnected": "Disconnected ", diff --git a/resources/style.css b/resources/style.css index e21ae00ea..0e988c907 100644 --- a/resources/style.css +++ b/resources/style.css @@ -77,6 +77,7 @@ window#waybar.chromium { #tray, #mode, #idle_inhibitor, +#layout, #mpd { padding: 0 10px; margin: 0 4px; diff --git a/src/factory.cpp b/src/factory.cpp index 6005cad56..d46662ed5 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -22,6 +22,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "sway/window") { return new waybar::modules::sway::Window(id, bar_, config_[name]); } + if (ref == "sway/layout") { + return new waybar::modules::sway::Layout(id, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); diff --git a/src/modules/sway/layout.cpp b/src/modules/sway/layout.cpp new file mode 100644 index 000000000..844303d72 --- /dev/null +++ b/src/modules/sway/layout.cpp @@ -0,0 +1,132 @@ +#include "modules/sway/layout.hpp" +#include + +namespace waybar::modules::sway { + +Layout::Layout(const std::string& id, const Json::Value& config) + : ALabel(config, "layout", id, "{}", 0) { + + keyboard_id_ = config_["keyboard_id"].isString() + ? config_["keyboard_id"].asString() + : "1:1:AT_Translated_Set_2_keyboard"; + + ipc_.subscribe(R"(["input"])"); + ipc_.signal_event.connect(sigc::mem_fun(*this, &Layout::onEvent)); + ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Layout::onCmd)); + ipc_.sendCmd(IPC_GET_INPUTS); + // Launch worker + worker(); +} + +void Layout::onEvent(const struct Ipc::ipc_response& res) { + if (res.type != static_cast(IPC_EVENT_INPUT)) { + return; + } + + auto payload = parser_.parse(res.payload); + + if ((payload["change"] != "xkb_layout" || payload["change"] != "xkb_keymap" ) && + payload["input"]["identifier"] == keyboard_id_) { + layout_ = payload["input"]["xkb_active_layout_name"].asString(); + dp.emit(); + } +} + +void Layout::onCmd(const struct Ipc::ipc_response &res) { + if (res.type != IPC_GET_INPUTS) { + return; + } + + auto payload = parser_.parse(res.payload); + + for (auto keyboard : payload) { + if (keyboard["identifier"] == keyboard_id_) { + layout_ = keyboard["xkb_active_layout_name"].asString(); + dp.emit(); + break; + } + } +} + +void Layout::worker() { + thread_ = [this] { + try { + ipc_.handleEvent(); + } catch (const std::exception& e) { + spdlog::error("Layout: {}", e.what()); + } + }; +} + +auto Layout::update() -> void { + auto short_names = getShortNames(); + auto short_description = std::get<0>(short_names); + auto short_variant = std::get<1>(short_names); + + if (layout_.empty()) { + event_box_.hide(); + } else { + label_.set_markup(fmt::format(format_, + fmt::arg("long", layout_), + fmt::arg("short", short_description), + fmt::arg("variant", short_variant))); + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + auto tooltip_format = config_["tooltip-format"].asString(); + label_.set_tooltip_text(fmt::format(tooltip_format, + fmt::arg("long", layout_), + fmt::arg("short", short_description), + fmt::arg("variant", short_variant))); + } else { + label_.set_tooltip_text(layout_); + } + } + event_box_.show(); + } +} + +Layout::ShortNames Layout::getShortNames() { + try { + return memoizedShortNames_.at(layout_); + } catch (std::out_of_range &e) { + spdlog::debug("Layout: Getting short names from XKB file for {}.", layout_); + auto shortNames = fromFileGetShortNames(); + memoizedShortNames_[layout_] = shortNames; + return shortNames; + } +} + +Layout::ShortNames Layout::fromFileGetShortNames() { + std::string short_description; + std::string short_variant; + + pugi::xml_document doc; + doc.load_file(xbk_file_.c_str()); + + for (auto xkb_layout_xpath : doc.select_nodes("/xkbConfigRegistry/layoutList/layout")) { + auto xkb_layout_config = xkb_layout_xpath.node().child("configItem"); + + if (xkb_layout_config.child_value("description") == layout_) { + return std::make_tuple( + xkb_layout_config.child_value("shortDescription"), + xkb_layout_config.child_value("name") + ); + } + + for (auto xkb_variant_xpath : xkb_layout_xpath.node().select_nodes("variantList/variant")) { + auto xkb_variant_config = xkb_variant_xpath.node().child("configItem"); + + if (xkb_variant_config.child_value("description") == layout_) { + return std::make_tuple( + xkb_layout_config.child_value("shortDescription"), + xkb_variant_config.child_value("name") + ); + } + } + } + + spdlog::warn("Layout: Could not find layout \"{}\" in XKB file.", layout_); + return std::make_tuple("N/A", "N/A"); +} + +} // namespace waybar::modules::sway