view dep/animia/src/win/x11.cc @ 235:593108b3d555

dep/animia: x11: finalize xcb conversion
author Paper <paper@paper.us.eu.org>
date Tue, 16 Jan 2024 15:22:29 -0500
parents 8ccf0302afb1
children 4d461ef7d424
line wrap: on
line source

/*
 * win/x11.cc: provides support for X11 clients via XCB
 *
 * This code is fairly fast (I think...). As a result,
 * a lot of it is hard to read if you're unfamiliar with
 * asynchronous programming, but it works much better than
 * Xlib (which can take much longer).
*/
#include "animia/win/x11.h"
#include "animia/win.h"
#include "animia.h"

#include <xcb/xcb.h>
#include <xcb/res.h>

#include <cstdint>
#include <climits>
#include <cstring>
#include <string>
#include <set>

static size_t str_nlen(const char* s, size_t len) {
    size_t i = 0;
    for (; i < len && s[i]; i++);
    return i;
}

namespace animia::internal::x11 {

static bool GetAllTopLevelWindowsEWMH(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots, std::set<xcb_window_t>& result) {
	const xcb_atom_t Atom__NET_CLIENT_LIST = [connection]{
		static constexpr std::string_view name = "_NET_CLIENT_LIST";
		xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
		xcb_intern_atom_reply_t* reply = ::xcb_intern_atom_reply(connection, cookie, NULL);

		xcb_atom_t atom = reply->atom;
		free(reply);
		return atom;
	}();
	if (Atom__NET_CLIENT_LIST == XCB_ATOM_NONE)
		return false; // BTFO

	bool success = false;

	std::vector<xcb_get_property_cookie_t> cookies;
	cookies.reserve(roots.size());

	for (const auto& root : roots)
		cookies.push_back(::xcb_get_property(connection, 0, root, Atom__NET_CLIENT_LIST, XCB_ATOM_ANY, 0L, UINT_MAX));

	for (const auto& cookie : cookies) {
		xcb_get_property_reply_t* reply = ::xcb_get_property_reply(connection, cookie, NULL);

		if (reply && reply->length == 32) {
			xcb_window_t* value = reinterpret_cast<xcb_window_t*>(::xcb_get_property_value(reply));
			int len = ::xcb_get_property_value_length(reply);

			for (size_t i = 0; i < len; i++)
				result.insert(value[i]);

			success = true;
		}

		free(reply);
	}

	return success;
}

static bool WalkWindows(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots, std::set<xcb_window_t>& result) {
	/* move this somewhere else pl0x */
	xcb_atom_t Atom_WM_STATE = [connection]{
		static constexpr std::string_view name = "WM_STATE";
		xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
		xcb_intern_atom_reply_t* reply = ::xcb_intern_atom_reply(connection, cookie, NULL);

		xcb_atom_t atom = reply->atom;
		free(reply);
		return atom;
	}();
	if (Atom_WM_STATE == XCB_ATOM_NONE)
		return false;

	/* for each toplevel: search recursively for */
	std::function<bool(const xcb_window_t*, const int, xcb_window_t&)> find_wm_state_window = [&](const xcb_window_t* wins, const int wins_len, xcb_window_t& out) {
		/* Check for wm_state */
		{
			std::vector<xcb_get_property_cookie_t> property_cookies;
			property_cookies.reserve(wins_len);

			for (size_t j = 0; j < wins_len; j++)
				property_cookies.push_back(::xcb_get_property(connection, 0, wins[j], Atom_WM_STATE, Atom_WM_STATE, 0, 0));

			for (size_t j = 0; j < property_cookies.size(); j++) {
				xcb_generic_error_t* err = NULL;
				xcb_get_property_reply_t* reply = ::xcb_get_property_reply(connection, property_cookies.at(j), &err);

				if (reply->format || reply->type || reply->length) {
					out = wins[j];
					free(reply);
					return true;
				}

				free(reply);
			}
		}

		/* Query tree for recursion */
		{
			std::vector<xcb_query_tree_cookie_t> cookies;
			cookies.reserve(wins_len);

			for (size_t j = 0; j < wins_len; j++)
				cookies.push_back(::xcb_query_tree(connection, wins[j]));

			for (const auto& cookie : cookies) {
				xcb_query_tree_reply_t* reply = ::xcb_query_tree_reply(connection, cookie, NULL);

				xcb_window_t* windows = ::xcb_query_tree_children(reply);
				int len = ::xcb_query_tree_children_length(reply);

				if (find_wm_state_window(windows, len, out)) {
					free(reply);
					return true;
				}

				free(reply);
			}
		}

		return false;
	};

	/* Get the tree for each root */
	std::vector<xcb_query_tree_cookie_t> cookies;
	cookies.reserve(roots.size());

	for (const auto& root : roots)
		cookies.push_back(::xcb_query_tree(connection, root));

	for (size_t i = 0; i < cookies.size(); i++) {
		xcb_query_tree_reply_t* reply = ::xcb_query_tree_reply(connection, cookies.at(i), NULL);

		xcb_window_t* windows = ::xcb_query_tree_children(reply);
		int len = ::xcb_query_tree_children_length(reply);
		if (len < 1)
			continue;

		/* Then get the tree of each child window. */
		std::vector<xcb_query_tree_cookie_t> cookies;
		cookies.reserve(len);

		for (size_t j = 0; j < len; j++)
			cookies.push_back(::xcb_query_tree(connection, windows[j]));

		/* For each child window... */
		for (size_t j = 0; j < cookies.size(); j++) {
			xcb_query_tree_reply_t* reply = ::xcb_query_tree_reply(connection, cookies.at(j), NULL);

			xcb_window_t* children = ::xcb_query_tree_children(reply);
			int children_len = ::xcb_query_tree_children_length(reply);
			if (children_len < 1) {
				result.insert(windows[j]);
				continue;
			}

			xcb_window_t out = windows[j];
			/* Search recursively for a window with WM_STATE. If we don't,
			 * just add the toplevel.
			*/
			find_wm_state_window(children, children_len, out);
			result.insert(out);
		}

		free(reply);
	}

	return false;
}

bool EnumerateWindows(window_proc_t window_proc) {
	if (!window_proc)
		return false;

	xcb_connection_t* connection = ::xcb_connect(NULL, NULL);
	if (!connection)
		return false;

	std::vector<xcb_screen_t> screens;

	{
		xcb_screen_iterator_t iter = ::xcb_setup_roots_iterator(xcb_get_setup(connection));
		for (; iter.rem; ::xcb_screen_next(&iter))
			screens.push_back(*iter.data);
	}

	std::vector<xcb_window_t> roots;
	roots.reserve(screens.size());

	for (const auto& screen : screens)
		roots.push_back(screen.root);

	std::set<xcb_window_t> windows;
	if (!GetAllTopLevelWindowsEWMH(connection, roots, windows))
		WalkWindows(connection, roots, windows);

	std::vector<xcb_get_property_cookie_t> class_property_cookies;
	std::vector<xcb_get_property_cookie_t> name_property_cookies;
	std::vector<xcb_res_query_client_ids_cookie_t> pid_property_cookies;
	class_property_cookies.reserve(windows.size());
	name_property_cookies.reserve(windows.size());
	pid_property_cookies.reserve(windows.size());

	for (const auto& window : windows) {
		class_property_cookies.push_back(::xcb_get_property(connection, 0, window, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0L, 2048L));
		name_property_cookies.push_back(::xcb_get_property(connection, 0, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0L, UINT_MAX));

		xcb_res_client_id_spec_t spec = {
			.client = window,
			.mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID
		};

		pid_property_cookies.push_back(::xcb_res_query_client_ids(connection, 1, &spec));
	}

	size_t i = 0;
	for (const auto& window : windows) {
		Window win = {0};
		win.id = window;
		{
			xcb_get_property_reply_t* reply = ::xcb_get_property_reply(connection, class_property_cookies.at(i), NULL);

			if (reply && reply->format == 8) {
				const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply));
				const int data_len = ::xcb_get_property_value_length(reply);

				int instance_len = str_nlen(data, data_len);
				const char* class_name = data + instance_len + 1;

				win.class_name = std::string(class_name, str_nlen(class_name, data_len - (instance_len + 1)));
			}

			free(reply);
		}
		{
			xcb_get_property_reply_t* reply = ::xcb_get_property_reply(connection, name_property_cookies.at(i), NULL);

			if (reply && reply->format == 8) {
				const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply));
				int len = ::xcb_get_property_value_length(reply);

				win.text = std::string(data, len);
			}

			free(reply);
		}
		Process proc = {0};
		{
			xcb_res_query_client_ids_reply_t* reply = ::xcb_res_query_client_ids_reply(connection, pid_property_cookies.at(i), NULL);

			if (reply->length) {
				xcb_res_client_id_value_iterator_t it = ::xcb_res_query_client_ids_ids_iterator(reply);
				for (; it.rem; ::xcb_res_client_id_value_next(&it)) {
					if (it.data->spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) {
						proc.pid = *::xcb_res_client_id_value_value(it.data);
						break;
					}
				}
			}

			free(reply);
		}

		if (!window_proc(proc, win)) {
			::xcb_disconnect(connection);
			return false;
		}
		i++;
	}

	::xcb_disconnect(connection);

	return true;
}

}