polybar-dwm/src/modules/xkeyboard.cpp
Patrick Ziegler 26be83f893
module: Implement action router (#2336)
* module: Implement proof of concept action router

Action implementation inside module becomes much cleaner because each
module just registers action names together with a callback (pointer to
member function) and the action router does the rest.

* Make input function final

This forces all modules to use the action router

* modules: Catch exceptions in action handlers

* Use action router for all modules

* Use action_ prefix for function names

The mpd module's 'stop' action overwrote the base module's stop function
which caused difficult to debug behavior.

To prevent this in the future we now prefix each function that is
responsible for an action with 'action_'

* Cleanup

* actions: Throw exception when re-registering action

Action names are unique inside modules. Unfortunately there is no way to
ensure this statically, the next best thing is to crash the module and
let the user know that this is a bug.

* Formatting

* actions: Ignore data for actions without data

This is the same behavior as before.

* action_router: Write tests
2021-01-04 10:25:52 +01:00

288 lines
9.7 KiB
C++

#include "modules/xkeyboard.hpp"
#include "drawtypes/iconset.hpp"
#include "drawtypes/label.hpp"
#include "modules/meta/base.inl"
#include "utils/factory.hpp"
#include "x11/atoms.hpp"
#include "x11/connection.hpp"
POLYBAR_NS
namespace modules {
template class module<xkeyboard_module>;
// clang-format off
static const keyboard::indicator::type INDICATOR_TYPES[] {
keyboard::indicator::type::CAPS_LOCK,
keyboard::indicator::type::NUM_LOCK,
keyboard::indicator::type::SCROLL_LOCK
};
// clang-format on
/**
* Construct module
*/
xkeyboard_module::xkeyboard_module(const bar_settings& bar, string name_)
: static_module<xkeyboard_module>(bar, move(name_)), m_connection(connection::make()) {
m_router->register_action(EVENT_SWITCH, &xkeyboard_module::action_switch);
// Setup extension
// clang-format off
m_connection.xkb().select_events_checked(XCB_XKB_ID_USE_CORE_KBD,
XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
XCB_XKB_EVENT_TYPE_STATE_NOTIFY |
XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY, 0,
XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
XCB_XKB_EVENT_TYPE_STATE_NOTIFY |
XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY, 0, 0, nullptr);
// clang-format on
// Create keyboard object
query_keyboard();
// Load config values
m_blacklist = m_conf.get_list(name(), "blacklist", {});
// load layout icons
m_layout_icons = factory_util::shared<iconset>();
m_layout_icons->add(DEFAULT_LAYOUT_ICON, load_optional_label(m_conf, name(), DEFAULT_LAYOUT_ICON, ""s));
for (const auto& it : m_conf.get_list<string>(name(), "layout-icon", {})) {
auto vec = string_util::tokenize(it, ';');
if (vec.size() == 2) {
m_layout_icons->add(vec[0], factory_util::shared<label>(vec[1]));
}
}
// Add formats and elements
m_formatter->add(DEFAULT_FORMAT, FORMAT_DEFAULT, {TAG_LABEL_LAYOUT, TAG_LABEL_INDICATOR});
if (m_formatter->has(TAG_LABEL_LAYOUT)) {
m_layout = load_optional_label(m_conf, name(), TAG_LABEL_LAYOUT, "%layout%");
}
if (m_formatter->has(TAG_LABEL_INDICATOR)) {
m_conf.warn_deprecated(name(), "label-indicator", "label-indicator-on");
// load an empty label if 'label-indicator-off' is not explicitly specified so
// no existing user configs are broken (who expect nothing to be shown when indicator is off)
m_indicator_state_off = load_optional_label(m_conf, name(), "label-indicator-off"s, ""s);
if (m_conf.has(name(), "label-indicator-on"s)) {
m_indicator_state_on = load_optional_label(m_conf, name(), "label-indicator-on"s, "%name%"s);
} else {
// if 'label-indicator-on' is not explicitly specified, use 'label-indicator'
// as to not break existing user configs
m_indicator_state_on = load_optional_label(m_conf, name(), TAG_LABEL_INDICATOR, "%name%"s);
}
// load indicator icons
m_indicator_icons_off = factory_util::shared<iconset>();
m_indicator_icons_on = factory_util::shared<iconset>();
auto icon_pair = string_util::tokenize(m_conf.get(name(), DEFAULT_INDICATOR_ICON, ""s), ';');
if (icon_pair.size() == 2) {
m_indicator_icons_off->add(DEFAULT_INDICATOR_ICON, factory_util::shared<label>(icon_pair[0]));
m_indicator_icons_on->add(DEFAULT_INDICATOR_ICON, factory_util::shared<label>(icon_pair[1]));
} else {
m_indicator_icons_off->add(DEFAULT_INDICATOR_ICON, factory_util::shared<label>(""s));
m_indicator_icons_on->add(DEFAULT_INDICATOR_ICON, factory_util::shared<label>(""s));
}
for (const auto& it : m_conf.get_list<string>(name(), "indicator-icon", {})) {
auto icon_triple = string_util::tokenize(it, ';');
if (icon_triple.size() == 3) {
auto const indicator_str = string_util::lower(icon_triple[0]);
m_indicator_icons_off->add(indicator_str, factory_util::shared<label>(icon_triple[1]));
m_indicator_icons_on->add(indicator_str, factory_util::shared<label>(icon_triple[2]));
}
}
for (auto it : INDICATOR_TYPES) {
const auto& indicator_str = m_keyboard->indicator_name(it);
auto key_name = string_util::replace(string_util::lower(indicator_str), " "s, ""s);
const auto indicator_key_on = "label-indicator-on-"s + key_name;
const auto indicator_key_off = "label-indicator-off-"s + key_name;
if (m_conf.has(name(), indicator_key_on)) {
m_indicator_on_labels.emplace(it, load_label(m_conf, name(), indicator_key_on));
}
if (m_conf.has(name(), indicator_key_off)) {
m_indicator_off_labels.emplace(it, load_label(m_conf, name(), indicator_key_off));
}
}
}
}
/**
* Update labels with extension data
*/
void xkeyboard_module::update() {
if (m_layout) {
m_layout->reset_tokens();
m_layout->replace_token("%name%", m_keyboard->group_name(m_keyboard->current()));
m_layout->replace_token("%variant%", m_keyboard->variant_name(m_keyboard->current()));
auto const current_layout = m_keyboard->layout_name(m_keyboard->current());
auto icon = m_layout_icons->get(current_layout, DEFAULT_LAYOUT_ICON);
m_layout->replace_token("%icon%", icon->get());
m_layout->replace_token("%layout%", current_layout);
m_layout->replace_token("%number%", to_string(m_keyboard->current()));
}
if (m_formatter->has(TAG_LABEL_INDICATOR)) {
m_indicators.clear();
for (auto it : INDICATOR_TYPES) {
const auto& indicator_str = m_keyboard->indicator_name(it);
if (blacklisted(indicator_str)) {
continue;
}
auto indicator_on = m_keyboard->on(it);
auto& indicator_labels = indicator_on ? m_indicator_on_labels : m_indicator_off_labels;
auto& indicator_icons = indicator_on ? m_indicator_icons_on : m_indicator_icons_off;
auto& indicator_state = indicator_on ? m_indicator_state_on : m_indicator_state_off;
label_t indicator;
if (indicator_labels.find(it) != indicator_labels.end()) {
indicator = indicator_labels[it]->clone();
} else {
indicator = indicator_state->clone();
}
auto icon = indicator_icons->get(string_util::lower(indicator_str), DEFAULT_INDICATOR_ICON);
indicator->replace_token("%name%", indicator_str);
indicator->replace_token("%icon%", icon->get());
m_indicators.emplace(it, move(indicator));
}
}
// Trigger redraw
broadcast();
}
/**
* Build module output and wrap it in a click handler use
* to cycle between configured layout groups
*/
string xkeyboard_module::get_output() {
string output{module::get_output()};
if (m_keyboard && m_keyboard->size() > 1) {
m_builder->action(mousebtn::LEFT, *this, EVENT_SWITCH, "");
m_builder->append(output);
m_builder->action_close();
} else {
m_builder->append(output);
}
return m_builder->flush();
}
/**
* Map format tags to content
*/
bool xkeyboard_module::build(builder* builder, const string& tag) const {
if (tag == TAG_LABEL_LAYOUT) {
builder->node(m_layout);
} else if (tag == TAG_LABEL_INDICATOR && !m_indicators.empty()) {
size_t n{0};
for (auto&& indicator : m_indicators) {
if (*indicator.second) {
if (n++) {
builder->space(m_formatter->get(DEFAULT_FORMAT)->spacing);
}
builder->node(indicator.second);
}
}
return n > 0;
} else {
return false;
}
return true;
}
/**
* Handle input command
*/
void xkeyboard_module::action_switch() {
size_t current_group = m_keyboard->current() + 1;
if (current_group >= m_keyboard->size()) {
current_group = 0;
}
xkb_util::switch_layout(m_connection, XCB_XKB_ID_USE_CORE_KBD, current_group);
m_keyboard->current(current_group);
m_connection.flush();
update();
}
/**
* Create keyboard object by querying current extension data
*/
bool xkeyboard_module::query_keyboard() {
try {
auto layouts = xkb_util::get_layouts(m_connection, XCB_XKB_ID_USE_CORE_KBD);
auto indicators = xkb_util::get_indicators(m_connection, XCB_XKB_ID_USE_CORE_KBD);
auto current_group = xkb_util::get_current_group(m_connection, XCB_XKB_ID_USE_CORE_KBD);
m_keyboard = factory_util::unique<keyboard>(move(layouts), move(indicators), current_group);
return true;
} catch (const exception& err) {
throw module_error("Failed to query keyboard, err: " + string{err.what()});
}
return false;
}
/**
* Check if the indicator has been blacklisted by the user
*/
bool xkeyboard_module::blacklisted(const string& indicator_name) {
for (auto&& i : m_blacklist) {
if (string_util::compare(i, indicator_name)) {
return true;
}
}
return false;
}
/**
* Handler for XCB_XKB_NEW_KEYBOARD_NOTIFY events
*/
void xkeyboard_module::handle(const evt::xkb_new_keyboard_notify& evt) {
if (evt->changed & XCB_XKB_NKN_DETAIL_KEYCODES && m_xkb_newkb_notify.allow(evt->time)) {
query_keyboard();
update();
}
}
/**
* Handler for XCB_XKB_STATE_NOTIFY events
*/
void xkeyboard_module::handle(const evt::xkb_state_notify& evt) {
if (m_keyboard && evt->changed & XCB_XKB_STATE_PART_GROUP_STATE && m_xkb_state_notify.allow(evt->time)) {
m_keyboard->current(evt->group);
update();
}
}
/**
* Handler for XCB_XKB_INDICATOR_STATE_NOTIFY events
*/
void xkeyboard_module::handle(const evt::xkb_indicator_state_notify& evt) {
if (m_keyboard && m_xkb_indicator_notify.allow(evt->time)) {
m_keyboard->set(m_connection.xkb().get_state(XCB_XKB_ID_USE_CORE_KBD)->lockedMods);
update();
}
}
} // namespace modules
POLYBAR_NS_END