feat(xwindow): New module "xwindow"

Add module to display title of active window.
Requires WM with support for the _NET_ACTIVE_WINDOW hint.

Ref #84
This commit is contained in:
Michael Carlberg 2016-11-19 04:03:18 +01:00
parent b921225487
commit 51d8f289fa
21 changed files with 445 additions and 16 deletions

View File

@ -34,6 +34,7 @@ problems by [creating an issue ticket](https://github.com/jaagr/lemonbuddy/issue
The main purpose of **Lemonbuddy** is to help users create awesome status bars.
It has built-in functionality to generate content for the most commonly used widgets, such as:
- Window title
- Playback controls and status display for [MPD](https://www.musicpd.org/) using [libmpdclient](https://www.musicpd.org/libs/libmpdclient/)
- [ALSA](http://www.alsa-project.org/main/index.php/Main_Page) volume controls
- Workspace and desktop panel for [bspwm](https://github.com/baskerville/bspwm) and [i3](https://github.com/i3/i3)

View File

@ -3,7 +3,7 @@
#
set(MODULES_LEFT "bspwm i3 mpd")
set(MODULES_CENTER "")
set(MODULES_CENTER "xwindow")
set(MODULES_RIGHT "backlight volume memory cpu wlan eth battery temperature date powermenu")
# Strip disabled modules {{{

View File

@ -41,7 +41,7 @@ font-1 = unifont:size=6:heavy;-2
font-2 = siji:pixelsize=10;0
modules-left = bspwm i3 mpd
modules-center =
modules-center = xwindow
modules-right = backlight volume memory cpu wlan eth battery temperature date powermenu
tray-position = right
@ -52,6 +52,12 @@ tray-padding = 4
;wm-restack = bspwm
[module/xwindow]
type = internal/xwindow
label = %title%
label-maxlen = 30
[module/filesystem]
type = internal/fs
interval = 25

View File

@ -52,6 +52,12 @@ tray-padding = 4
;wm-restack = bspwm
[module/xwindow]
type = internal/xwindow
label = %title%
label-maxlen = 30
[module/filesystem]
type = internal/fs
interval = 25

View File

@ -52,6 +52,7 @@ namespace drawtypes {
operator bool();
label_t clone();
void reset_tokens();
bool has_token(string token);
void replace_token(string token, string replacement);
void replace_defined_values(const label_t& label);
void copy_undefined(const label_t& label);

View File

@ -32,6 +32,7 @@ namespace modules {
using static_module::static_module;
void setup();
void teardown();
void handle(const evt::randr_notify& evt);
void update();
string get_output();

View File

@ -0,0 +1,92 @@
#pragma once
#include <bitset>
#include "components/config.hpp"
#include "drawtypes/label.hpp"
#include "modules/meta.hpp"
#include "x11/connection.hpp"
#include "x11/ewmh.hpp"
#include "x11/icccm.hpp"
#include "x11/window.hpp"
LEMONBUDDY_NS
namespace modules {
/**
* Wrapper used to update the event mask of the
* currently active to enable title tracking
*/
class active_window {
public:
explicit active_window(xcb_window_t win)
: m_connection(configure_connection().create<decltype(m_connection)>()), m_window(m_connection, win) {
try {
m_window.change_event_mask(XCB_EVENT_MASK_PROPERTY_CHANGE);
} catch (const xpp::x::error::window& err) {
}
}
~active_window() {
try {
m_window.change_event_mask(XCB_EVENT_MASK_NO_EVENT);
} catch (const xpp::x::error::window& err) {
}
}
/**
* Check if current window matches passed value
*/
bool match(const xcb_window_t win) const {
return m_window == win;
}
/**
* Get the title by returning the first non-empty value of:
* _NET_WM_VISIBLE_NAME
* _NET_WM_NAME
*/
string title(xcb_ewmh_connection_t* ewmh) {
string title;
if (!(title = ewmh_util::get_visible_name(ewmh, m_window)).empty()) {
return title;
} else if (!(title = icccm_util::get_wm_name(m_connection, m_window)).empty()) {
return title;
} else {
return "";
}
}
private:
connection& m_connection;
window m_window{m_connection};
};
/**
* Module used to display information about the
* currently active X window.
*/
class xwindow_module : public static_module<xwindow_module>, public xpp::event::sink<evt::property_notify> {
public:
using static_module::static_module;
void setup();
void teardown();
void handle(const evt::property_notify& evt);
void update();
string get_output();
bool build(builder* builder, string tag) const;
private:
static constexpr auto TAG_LABEL = "<label>";
xcb_ewmh_connection_t m_ewmh;
xcb_timestamp_t m_timestamp;
unique_ptr<active_window> m_active;
label_t m_label;
};
}
LEMONBUDDY_NS_END

View File

@ -20,6 +20,9 @@ namespace memory_util {
inline auto countof(T& p) {
return sizeof(p) / sizeof(p[0]);
}
template <typename T>
using malloc_ptr_t = shared_ptr<T>;
}
LEMONBUDDY_NS_END

View File

@ -8,10 +8,14 @@ struct cached_atom {
xcb_atom_t* atom;
};
extern cached_atom ATOMS[29];
extern cached_atom ATOMS[33];
extern xcb_atom_t _NET_SUPPORTED;
extern xcb_atom_t _NET_CURRENT_DESKTOP;
extern xcb_atom_t _NET_ACTIVE_WINDOW;
extern xcb_atom_t _NET_WM_NAME;
extern xcb_atom_t _NET_WM_DESKTOP;
extern xcb_atom_t _NET_WM_VISIBLE_NAME;
extern xcb_atom_t _NET_WM_WINDOW_TYPE;
extern xcb_atom_t _NET_WM_WINDOW_TYPE_DOCK;
extern xcb_atom_t _NET_WM_WINDOW_TYPE_NORMAL;

21
include/x11/ewmh.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <xcb/xcb_ewmh.h>
#include "common.hpp"
#include "x11/connection.hpp"
LEMONBUDDY_NS
namespace ewmh_util {
bool setup(connection& conn, xcb_ewmh_connection_t* dst);
bool supports(xcb_ewmh_connection_t* ewmh, xcb_atom_t atom);
xcb_window_t get_active_window(xcb_ewmh_connection_t* conn);
string get_visible_name(xcb_ewmh_connection_t* conn, xcb_window_t win);
string get_icon_name(xcb_ewmh_connection_t* conn, xcb_window_t win);
string get_reply_string(xcb_ewmh_get_utf8_strings_reply_t* reply);
}
LEMONBUDDY_NS_END

15
include/x11/icccm.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <xcb/xcb_icccm.h>
#include "common.hpp"
#include "x11/connection.hpp"
LEMONBUDDY_NS
namespace icccm_util {
string get_wm_name(xcb_connection_t* conn, xcb_window_t win);
string get_reply_string(xcb_icccm_get_text_property_reply_t* reply);
}
LEMONBUDDY_NS_END

View File

@ -12,11 +12,19 @@ class window : public xpp::window<connection_t&> {
public:
using xpp::window<connection_t&>::window;
explicit window(connection_t& conn) : xpp::window<connection_t&>(conn, conn.generate_id()) {}
explicit window(connection_t& conn) : xpp::window<connection_t&>(conn, XCB_NONE) {}
window& operator=(const xcb_window_t win) {
*this = window{connection(), win};
return *this;
}
window create_checked(
int16_t x, int16_t y, uint16_t w, uint16_t h, uint32_t mask = 0, const xcb_params_cw_t* p = nullptr);
window change_event_mask(uint32_t mask);
window ensure_event_mask(uint32_t event);
window reconfigure_geom(uint16_t w, uint16_t h, int16_t x = 0, int16_t y = 0);
window reconfigure_pos(int16_t x, int16_t y);
window reconfigure_struts(uint16_t w, uint16_t h, int16_t x, bool bottom = false);

View File

@ -722,7 +722,7 @@ void bar::set_wmhints() {
wm_util::set_wmstate(m_connection, m_window, {_NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE});
m_log.trace("bar: Set _NET_WM_DESKTOP");
wm_util::set_wmdesktop(m_connection, m_window, -1u);
wm_util::set_wmdesktop(m_connection, m_window, 0xFFFFFFFF);
m_log.trace("bar: Set _NET_WM_PID");
wm_util::set_wmpid(m_connection, m_window, getpid());

View File

@ -18,6 +18,7 @@
#include "modules/text.hpp"
#include "modules/unsupported.hpp"
#include "modules/xbacklight.hpp"
#include "modules/xwindow.hpp"
#include "components/bar.hpp"
#include "components/config.hpp"
@ -358,8 +359,6 @@ void controller::bootstrap_modules() {
module.reset(new counter_module(bar, m_log, m_conf, module_name));
else if (type == "internal/backlight")
module.reset(new backlight_module(bar, m_log, m_conf, module_name));
else if (type == "internal/xbacklight")
module.reset(new xbacklight_module(bar, m_log, m_conf, module_name));
else if (type == "internal/battery")
module.reset(new battery_module(bar, m_log, m_conf, module_name));
else if (type == "internal/bspwm")
@ -382,6 +381,10 @@ void controller::bootstrap_modules() {
module.reset(new network_module(bar, m_log, m_conf, module_name));
else if (type == "internal/temperature")
module.reset(new temperature_module(bar, m_log, m_conf, module_name));
else if (type == "internal/xbacklight")
module.reset(new xbacklight_module(bar, m_log, m_conf, module_name));
else if (type == "internal/xwindow")
module.reset(new xwindow_module(bar, m_log, m_conf, module_name));
else if (type == "custom/text")
module.reset(new text_module(bar, m_log, m_conf, module_name));
else if (type == "custom/script")

View File

@ -20,6 +20,10 @@ namespace drawtypes {
m_tokenized = m_text;
}
bool label::has_token(string token) {
return m_text.find(token) != string::npos;
}
void label::replace_token(string token, string replacement) {
m_tokenized = string_util::replace_all(m_tokenized, token, replacement);
}

View File

@ -61,6 +61,13 @@ namespace modules {
update();
}
/**
* Disconnect from the event registry
*/
void xbacklight_module::teardown() {
m_connection.detach_sink(this, 1);
}
/**
* Handler for XCB_RANDR_NOTIFY events
*/

123
src/modules/xwindow.cpp Normal file
View File

@ -0,0 +1,123 @@
#include "modules/xwindow.hpp"
#include "x11/atoms.hpp"
#include "x11/graphics.hpp"
LEMONBUDDY_NS
namespace modules {
/**
* Bootstrap the module
*/
void xwindow_module::setup() {
connection& conn{configure_connection().create<decltype(conn)>()};
// Initialize ewmh atoms
if (!ewmh_util::setup(conn, &m_ewmh)) {
throw module_error("Failed to initialize ewmh atoms");
}
// Check if the WM supports _NET_ACTIVE_WINDOW
if (!ewmh_util::supports(&m_ewmh, _NET_ACTIVE_WINDOW)) {
throw module_error("The WM does not list _NET_ACTIVE_WINDOW as a supported hint");
}
// Add formats and elements
m_formatter->add(DEFAULT_FORMAT, TAG_LABEL, {TAG_LABEL});
if (m_formatter->has(TAG_LABEL)) {
m_label = load_optional_label(m_conf, name(), TAG_LABEL, "%title%");
}
// No need to setup X components if we can't show the title
if (!m_label || !m_label->has_token("%title%")) {
return;
}
// Make sure we get notified when root properties change
window root{conn, conn.root()};
root.ensure_event_mask(XCB_EVENT_MASK_PROPERTY_CHANGE);
// Connect with the event registry
conn.attach_sink(this, 1);
// Trigger the initial draw event
update();
}
/**
* Disconnect from the event registry
*/
void xwindow_module::teardown() {
connection& conn{configure_connection().create<decltype(conn)>()};
conn.detach_sink(this, 1);
}
/**
* Handler for XCB_PROPERTY_NOTIFY events
*/
void xwindow_module::handle(const evt::property_notify& evt) {
if (evt->time <= m_timestamp) {
return;
} else if (evt->atom == _NET_ACTIVE_WINDOW) {
update();
} else if (evt->atom == _NET_CURRENT_DESKTOP) {
update();
} else if (evt->atom == _NET_WM_VISIBLE_NAME) {
update();
} else if (evt->atom == _NET_WM_NAME) {
update();
} else {
return;
}
m_timestamp = evt->time;
}
/**
* Update the currently active window and query its title
*/
void xwindow_module::update() {
xcb_window_t win{ewmh_util::get_active_window(&m_ewmh)};
string title;
if (m_active && m_active->match(win)) {
title = m_active->title(&m_ewmh);
} else if (win != XCB_NONE) {
m_active = make_unique<active_window>(win);
title = m_active->title(&m_ewmh);
} else {
m_active.reset();
}
if (m_label) {
m_label->reset_tokens();
m_label->replace_token("%title%", title);
}
// Emit notification to trigger redraw
broadcast();
}
/**
* Generate the module output
*/
string xwindow_module::get_output() {
m_builder->node(static_module::get_output());
return m_builder->flush();
}
/**
* Output content as defined in the config
*/
bool xwindow_module::build(builder* builder, string tag) const {
if (tag == TAG_LABEL) {
builder->node(m_label);
} else {
return false;
}
return true;
}
}
LEMONBUDDY_NS_END

View File

@ -3,8 +3,12 @@
#include <xcb/xcb.h>
#include <xcb/xcb_atom.h>
xcb_atom_t _NET_SUPPORTED;
xcb_atom_t _NET_CURRENT_DESKTOP;
xcb_atom_t _NET_ACTIVE_WINDOW;
xcb_atom_t _NET_WM_NAME;
xcb_atom_t _NET_WM_DESKTOP;
xcb_atom_t _NET_WM_VISIBLE_NAME;
xcb_atom_t _NET_WM_WINDOW_TYPE;
xcb_atom_t _NET_WM_WINDOW_TYPE_DOCK;
xcb_atom_t _NET_WM_WINDOW_TYPE_NORMAL;
@ -34,9 +38,13 @@ xcb_atom_t _XSETROOT_ID;
xcb_atom_t ESETROOT_PMAP_ID;
// clang-format off
cached_atom ATOMS[29] = {
cached_atom ATOMS[33] = {
{"_NET_SUPPORTED", sizeof("_NET_SUPPORTED") - 1, &_NET_SUPPORTED},
{"_NET_CURRENT_DESKTOP", sizeof("_NET_CURRENT_DESKTOP") - 1, &_NET_CURRENT_DESKTOP},
{"_NET_ACTIVE_WINDOW", sizeof("_NET_ACTIVE_WINDOW") - 1, &_NET_ACTIVE_WINDOW},
{"_NET_WM_NAME", sizeof("_NET_WM_NAME") - 1, &_NET_WM_NAME},
{"_NET_WM_DESKTOP", sizeof("_NET_WM_DESKTOP") - 1, &_NET_WM_DESKTOP},
{"_NET_WM_VISIBLE_NAME", sizeof("_NET_WM_VISIBLE_NAME") - 1, &_NET_WM_VISIBLE_NAME},
{"_NET_WM_WINDOW_TYPE", sizeof("_NET_WM_WINDOW_TYPE") - 1, &_NET_WM_WINDOW_TYPE},
{"_NET_WM_WINDOW_TYPE_DOCK", sizeof("_NET_WM_WINDOW_TYPE_DOCK") - 1, &_NET_WM_WINDOW_TYPE_DOCK},
{"_NET_WM_WINDOW_TYPE_NORMAL", sizeof("_NET_WM_WINDOW_TYPE_NORMAL") - 1, &_NET_WM_WINDOW_TYPE_NORMAL},

64
src/x11/ewmh.cpp Normal file
View File

@ -0,0 +1,64 @@
#include "x11/ewmh.hpp"
LEMONBUDDY_NS
namespace ewmh_util {
bool setup(connection& conn, xcb_ewmh_connection_t* dst) {
return xcb_ewmh_init_atoms_replies(dst, xcb_ewmh_init_atoms(conn, dst), nullptr);
}
bool supports(xcb_ewmh_connection_t* ewmh, xcb_atom_t atom) {
bool supports{false};
xcb_ewmh_get_atoms_reply_t reply;
reply.atoms = nullptr;
if (!xcb_ewmh_get_supported_reply(ewmh, xcb_ewmh_get_supported(ewmh, 0), &reply, nullptr)) {
return false;
}
for (size_t n = 0; n < reply.atoms_len; ++n) {
if (reply.atoms[n] == atom) {
supports = true;
break;
}
}
if (reply.atoms != nullptr) {
xcb_ewmh_get_atoms_reply_wipe(&reply);
}
return supports;
}
xcb_window_t get_active_window(xcb_ewmh_connection_t* conn) {
xcb_window_t win{XCB_NONE};
xcb_ewmh_get_active_window_reply(conn, xcb_ewmh_get_active_window(conn, 0), &win, nullptr);
return win;
}
string get_visible_name(xcb_ewmh_connection_t* conn, xcb_window_t win) {
xcb_ewmh_get_utf8_strings_reply_t utf8_reply;
if (!xcb_ewmh_get_wm_visible_name_reply(conn, xcb_ewmh_get_wm_visible_name(conn, win), &utf8_reply, nullptr))
return "";
return get_reply_string(&utf8_reply);
}
string get_icon_name(xcb_ewmh_connection_t* conn, xcb_window_t win) {
xcb_ewmh_get_utf8_strings_reply_t utf8_reply;
if (!xcb_ewmh_get_wm_icon_name_reply(conn, xcb_ewmh_get_wm_icon_name(conn, win), &utf8_reply, nullptr))
return "";
return get_reply_string(&utf8_reply);
}
string get_reply_string(xcb_ewmh_get_utf8_strings_reply_t* reply) {
if (reply == nullptr || !reply->strings_len)
return "";
char buffer[BUFSIZ]{'\0'};
strncpy(buffer, reply->strings, reply->strings_len);
xcb_ewmh_get_utf8_strings_reply_wipe(reply);
return buffer;
}
}
LEMONBUDDY_NS_END

23
src/x11/icccm.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "x11/icccm.hpp"
LEMONBUDDY_NS
namespace icccm_util {
string get_wm_name(xcb_connection_t* conn, xcb_window_t win) {
xcb_icccm_get_text_property_reply_t reply;
if (!xcb_icccm_get_wm_name_reply(conn, xcb_icccm_get_wm_name(conn, win), &reply, nullptr))
return "";
return get_reply_string(&reply);
}
string get_reply_string(xcb_icccm_get_text_property_reply_t* reply) {
if (reply->name == nullptr || !reply->name_len)
return "";
char buffer[BUFSIZ]{'\0'};
strncpy(buffer, reply->name, reply->name_len);
xcb_icccm_get_text_property_reply_wipe(reply);
return buffer;
}
}
LEMONBUDDY_NS_END

View File

@ -9,20 +9,49 @@
LEMONBUDDY_NS
/**
* Create window and check for errors
*/
window window::create_checked(int16_t x, int16_t y, uint16_t w, uint16_t h, uint32_t mask, const xcb_params_cw_t* p) {
auto conn = connection();
if (*this == XCB_NONE) {
resource(connection(), connection().generate_id());
*this = conn.generate_id();
}
auto root{connection().screen()->root};
auto root{conn.screen()->root};
auto copy{XCB_COPY_FROM_PARENT};
uint32_t values[16]{0};
xutils::pack_values(mask, p, values);
connection().create_window_checked(copy, *this, root, x, y, w, h, 0, copy, copy, mask, values);
conn.create_window_checked(copy, *this, root, x, y, w, h, 0, copy, copy, mask, values);
return *this;
}
/**
* Change the window event mask
*/
window window::change_event_mask(uint32_t mask) {
change_attributes_checked(XCB_CW_EVENT_MASK, &mask);
return *this;
}
/**
* Add given event to the event mask unless already added
*/
window window::ensure_event_mask(uint32_t event) {
auto attributes = get_attributes();
if ((attributes->your_event_mask & event) != event) {
change_event_mask(attributes->your_event_mask | event);
}
return *this;
}
/**
* Reconfigure the window geometry
*/
window window::reconfigure_geom(uint16_t w, uint16_t h, int16_t x, int16_t y) {
uint32_t mask{0};
uint32_t values[7]{0};
@ -34,11 +63,14 @@ window window::reconfigure_geom(uint16_t w, uint16_t h, int16_t x, int16_t y) {
XCB_AUX_ADD_PARAM(&mask, &params, y, y);
xutils::pack_values(mask, &params, values);
connection().configure_window_checked(*this, mask, values);
configure_checked(mask, values);
return *this;
}
/**
* Reconfigure the window position
*/
window window::reconfigure_pos(int16_t x, int16_t y) {
uint32_t mask{0};
uint32_t values[2]{0};
@ -48,11 +80,14 @@ window window::reconfigure_pos(int16_t x, int16_t y) {
XCB_AUX_ADD_PARAM(&mask, &params, y, y);
xutils::pack_values(mask, &params, values);
connection().configure_window_checked(*this, mask, values);
configure_checked(mask, values);
return *this;
}
/**
* Reconfigure the windows ewmh strut
*/
window window::reconfigure_struts(uint16_t w, uint16_t h, int16_t x, bool bottom) {
auto& conn = connection();
@ -75,10 +110,14 @@ window window::reconfigure_struts(uint16_t w, uint16_t h, int16_t x, bool bottom
return *this;
}
/**
* Trigger redraw by toggling visibility state
*/
void window::redraw() {
xutils::visibility_notify(connection(), *this, XCB_VISIBILITY_FULLY_OBSCURED);
xutils::visibility_notify(connection(), *this, XCB_VISIBILITY_UNOBSCURED);
connection().flush();
auto conn = connection();
xutils::visibility_notify(conn, *this, XCB_VISIBILITY_FULLY_OBSCURED);
xutils::visibility_notify(conn, *this, XCB_VISIBILITY_UNOBSCURED);
conn.flush();
}
LEMONBUDDY_NS_END