src/X11Command.h
author František Kučera <franta-hg@frantovo.cz>
Mon, 05 Apr 2021 18:09:25 +0200
branchv_0
changeset 8 b498160cc2ab
parent 7 039b3f8a3442
child 9 c8d681753bc2
permissions -rw-r--r--
list screens: --list-screens + support multiple screens in --list-windows and --list-input-events

/**
 * Relational pipes
 * Copyright © 2021 František Kučera (Frantovo.cz, GlobalCode.info)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
#pragma once

#include <codecvt>
#include <memory>
#include <algorithm>
#include <iostream>
#include <stdexcept>

#include <X11/extensions/XInput.h>
#include <X11/Xutil.h>

#include "Configuration.h"

namespace relpipe {
namespace in {
namespace x11 {

class X11Command {
private:

	class Display {
	public:
		::Display* display = nullptr;
		// TODO: more OOP

		virtual ~Display() {
			if (display) XCloseDisplay(display);
		}
	};

	class Device {
	public:
		XDevice* device = nullptr;
		// TODO: more OOP

		virtual ~Device() {
			if (device) XFree(device);
		}
	};

	class DeviceInfoList {
	public:
		XDeviceInfo* items = nullptr;
		int size = 0;
		// TODO: more OOP

		XDeviceInfo* operator[](int index) const {
			if (index < size) return items + index;
			else throw std::out_of_range("invalid index of XDeviceInfo");
		}

		virtual ~DeviceInfoList() {
			if (items) XFreeDeviceList(items);
		}
	};


	std::wstring_convert<codecvt_utf8<wchar_t>> convertor; // TODO: use platform encoding as default

	relpipe::common::type::StringX getDeviceType(const Display& display, const XDeviceInfo* device) {
		if (device && device->type) {
			char* raw = XGetAtomName(display.display, device->type);
			if (raw) {
				relpipe::common::type::StringX type = convertor.from_bytes(raw);
				XFree(raw);
				transform(type.begin(), type.end(), type.begin(), ::tolower);
				return type;
			}
		}
		return L"";
	}

	void listInputDevices(const Display& display, Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		writer->startRelation(L"x11_input_device",{
			{L"id", relpipe::writer::TypeId::INTEGER},
			{L"name", relpipe::writer::TypeId::STRING},
			{L"type", relpipe::writer::TypeId::STRING},
		}, true);

		DeviceInfoList devices;
		devices.items = XListInputDevices(display.display, &devices.size);

		for (int i = 0; i < devices.size; i++) {
			relpipe::common::type::Integer id = devices[i]->id;
			relpipe::common::type::StringX name = convertor.from_bytes(devices[i]->name);
			relpipe::common::type::StringX type = getDeviceType(display, devices[i]);

			writer->writeAttribute(&id, typeid (id));
			writer->writeAttribute(&name, typeid (name));
			writer->writeAttribute(&type, typeid (type));
		}

		relationalWriterFlush();
	}

	/**
	 * Fields of this structure are filled in DeviceKeyPress(), DeviceButtonPress() … macros.
	 */
	class EventType {
	private:
		const static int UNUSED = -1;
	public:
		int motion = UNUSED;
		int buttonPress = UNUSED;
		int buttonRelease = UNUSED;
		int keyPress = UNUSED;
		int keyRelease = UNUSED;
		int proximityIn = UNUSED;
		int proximityOut = UNUSED;
	} eventType;

	void registerEvents(const Display& display, int screen, const XDeviceInfo* deviceInfo) {
		bool registerProximityEvents = false; // TODO: proximity?

		Device device;
		Window window;

		int eventCount = 0;
		XEventClass events[7]; // TODO: check array size
		XInputClassInfo* classInfo;
		int classIndex;

		window = RootWindow(display.display, screen);
		// TODO: configurable window from which we capture the events or optionally open our own window (can also provide some visual feedback/info)
		// Currently we can do something like:
		//   Xephyr  :5 -screen 1920x1080
		//   DISPLAY=:5 relpipe-in-x11 --list-input-devices false --list-input-events true | …

		device.device = XOpenDevice(display.display, deviceInfo->id);

		if (device.device) {
			if (device.device->num_classes > 0) {
				for (classInfo = device.device->classes, classIndex = 0; classIndex < deviceInfo->num_classes; classInfo++, classIndex++) {
					if (classInfo->input_class == KeyClass) {
						DeviceKeyPress(device.device, eventType.keyPress, events[eventCount]);
						eventCount++;
						DeviceKeyRelease(device.device, eventType.keyRelease, events[eventCount]);
						eventCount++;
					} else if (classInfo->input_class == ButtonClass) {
						DeviceButtonPress(device.device, eventType.buttonPress, events[eventCount]);
						eventCount++;
						DeviceButtonRelease(device.device, eventType.buttonRelease, events[eventCount]);
						eventCount++;
					} else if (classInfo->input_class == ValuatorClass) {
						DeviceMotionNotify(device.device, eventType.motion, events[eventCount]);
						eventCount++;
						if (registerProximityEvents) {
							ProximityIn(device.device, eventType.proximityIn, events[eventCount]);
							eventCount++;
							ProximityOut(device.device, eventType.proximityOut, events[eventCount]);
							eventCount++;
						}
					}
				}

				int result = XSelectExtensionEvent(display.display, window, events, eventCount);
				if (result != Success) throw std::logic_error("Unable to register events from device: " + std::to_string(deviceInfo->id) + " Result: " + std::to_string(result));
			}
		} else {
			throw std::invalid_argument("Unable to open the device: " + std::to_string(deviceInfo->id));
		}
	}

	void listInputEvents(Display& display, Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		writer->startRelation(L"x11_input_event",{
			{L"device", relpipe::writer::TypeId::INTEGER},
			{L"type", relpipe::writer::TypeId::STRING},
			{L"state", relpipe::writer::TypeId::STRING},
			{L"key", relpipe::writer::TypeId::INTEGER},
			{L"button", relpipe::writer::TypeId::INTEGER},
			{L"x", relpipe::writer::TypeId::INTEGER},
			{L"y", relpipe::writer::TypeId::INTEGER},
			// {L"x_root", relpipe::writer::TypeId::INTEGER},
			// {L"y_root", relpipe::writer::TypeId::INTEGER},
		}, true);


		{
			DeviceInfoList devices;
			devices.items = XListInputDevices(display.display, &devices.size);
			for (int i = 0; i < devices.size; i++) {
				if (devices[i]->type) {
					for (int screenCount = XScreenCount(display.display), screen = 0; screen < screenCount; screen++)
						registerEvents(display, screen, devices[i]);
				}
			}
		}

		for (XEvent event; true;) {
			XNextEvent(display.display, &event);

			relpipe::common::type::Integer device = -1; // TODO: null
			relpipe::common::type::StringX type;
			relpipe::common::type::StringX state;
			relpipe::common::type::Integer button = -1; // TODO: null
			relpipe::common::type::Integer key = -1; // TODO: null
			relpipe::common::type::Integer x = -1; // TODO: null
			relpipe::common::type::Integer y = -1; // TODO: null
			// relpipe::common::type::Integer xRoot = -1; // TODO: null
			// relpipe::common::type::Integer yRoot = -1; // TODO: null

			if (event.type == eventType.motion) {
				XDeviceMotionEvent* e = (XDeviceMotionEvent*) & event;
				device = e->deviceid;
				type = L"motion";
				x = e->x;
				y = e->y;
				// xRoot = e->x_root;
				// yRoot = e->y_root;
			} else if (event.type == eventType.buttonPress || event.type == eventType.buttonRelease) {
				XDeviceButtonEvent* e = (XDeviceButtonEvent*) & event;
				device = e->deviceid;
				type = L"button";
				state = event.type == eventType.buttonPress ? L"pressed" : L"released";
				button = e->button;
				x = e->x;
				y = e->y;
				// xRoot = e->x_root;
				// yRoot = e->y_root;
			} else if (event.type == eventType.keyPress || event.type == eventType.keyRelease) {
				XDeviceKeyEvent* e = (XDeviceKeyEvent*) & event;
				device = e->deviceid;
				type = L"key";
				state = event.type == eventType.keyPress ? L"pressed" : L"released";
				key = e->keycode;
				x = e->x;
				y = e->y;
				// xRoot = e->x_root;
				// yRoot = e->y_root;
			} else if (event.type == eventType.proximityIn || event.type == eventType.proximityOut) {
				XProximityNotifyEvent* e = (XProximityNotifyEvent*) & event;
				device = e->deviceid;
				type = L"proximity";
			}

			writer->writeAttribute(&device, typeid (device));
			writer->writeAttribute(type);
			writer->writeAttribute(state);
			writer->writeAttribute(&key, typeid (key));
			writer->writeAttribute(&button, typeid (button));
			writer->writeAttribute(&x, typeid (x));
			writer->writeAttribute(&y, typeid (y));
			// writer->writeAttribute(&xRoot, typeid (xRoot));
			// writer->writeAttribute(&yRoot, typeid (yRoot));
			relationalWriterFlush();
		}
	}

	relpipe::common::type::StringX fetchWindowName(const Display& display, Window window) {
		relpipe::common::type::StringX name;
		// TODO: this does not work → use XGetWMName(), XmbTextPropertyToTextList(), XFreeStringList() instead of XFetchName()
		// char* rawName = nullptr;
		// if (XFetchName(display.display, window, &rawName) && rawName) 
		// name = convertor.from_bytes(rawName); // bad encoding, std::range_error – wstring_convert::from_bytes
		// std::cout << "\n\n\n>>> " << rawName << " <<<\n\n\n"; // bad encoding, not UTF-8
		//XFree(rawName);
		return name;
	}

	void listWindow(const Display& display, Window window, relpipe::common::type::Integer level, Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		Window rootWindow;
		Window parentWindow;
		unsigned int childrenCount;
		Window* childrenList;
		if (!XQueryTree(display.display, window, &rootWindow, &parentWindow, &childrenList, &childrenCount))
			throw std::invalid_argument("Unable to query the tree for window: " + std::to_string(window));

		XWindowAttributes windowAttributes;
		if (!XGetWindowAttributes(display.display, window, &windowAttributes))
			throw std::invalid_argument("Unable to get attributes for window: " + std::to_string(window));

		XClassHint classHint;
		relpipe::common::type::StringX resClass;
		relpipe::common::type::StringX resName;
		if (XGetClassHint(display.display, window, &classHint)) {
			if (classHint.res_class) resClass = convertor.from_bytes(classHint.res_class);
			if (classHint.res_name) resName = convertor.from_bytes(classHint.res_name);
			XFree(classHint.res_class);
			XFree(classHint.res_name);
		}

		writer->writeAttribute(std::to_wstring(window));
		writer->writeAttribute(std::to_wstring(windowAttributes.root));
		writer->writeAttribute(std::to_wstring(parentWindow));
		writer->writeAttribute(&level, typeid (level));
		writer->writeAttribute(fetchWindowName(display, window));
		writer->writeAttribute(resClass);
		writer->writeAttribute(resName);
		writer->writeAttribute(std::to_wstring(windowAttributes.x));
		writer->writeAttribute(std::to_wstring(windowAttributes.y));
		writer->writeAttribute(std::to_wstring(windowAttributes.width));
		writer->writeAttribute(std::to_wstring(windowAttributes.height));

		for (unsigned int i = 0; i < childrenCount; i++) listWindow(display, childrenList[i], level + 1, configuration, writer, relationalWriterFlush);

		XFree(childrenList);
	}

	void listWindows(const Display& display, Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		writer->startRelation(L"x11_window",{
			{L"id", relpipe::writer::TypeId::INTEGER},
			{L"root", relpipe::writer::TypeId::INTEGER},
			{L"parent", relpipe::writer::TypeId::INTEGER},
			{L"level", relpipe::writer::TypeId::INTEGER},
			{L"name", relpipe::writer::TypeId::STRING},
			{L"res_class", relpipe::writer::TypeId::STRING},
			{L"res_name", relpipe::writer::TypeId::STRING},
			{L"x", relpipe::writer::TypeId::INTEGER},
			{L"y", relpipe::writer::TypeId::INTEGER},
			{L"width", relpipe::writer::TypeId::INTEGER},
			{L"height", relpipe::writer::TypeId::INTEGER},
		}, true);

		for (int screenCount = XScreenCount(display.display), screen = 0; screen < screenCount; screen++) {
			Window root = RootWindow(display.display, screen);
			listWindow(display, root, 0, configuration, writer, relationalWriterFlush);
		}
	}

	void listScreens(const Display& display, Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		writer->startRelation(L"x11_screen",{
			{L"id", relpipe::writer::TypeId::INTEGER},
			{L"root", relpipe::writer::TypeId::INTEGER},
			{L"width", relpipe::writer::TypeId::INTEGER},
			{L"height", relpipe::writer::TypeId::INTEGER},
			// {L"width_mm", relpipe::writer::TypeId::INTEGER},
			// {L"height_mm", relpipe::writer::TypeId::INTEGER},
		}, true);

		for (int screenCount = XScreenCount(display.display), screen = 0; screen < screenCount; screen++) {
			Window root = RootWindow(display.display, screen);
			XWindowAttributes attributes;
			if (!XGetWindowAttributes(display.display, root, &attributes))
				throw std::invalid_argument("Unable to get attributes for window: " + std::to_string(root));

			writer->writeAttribute(std::to_wstring(screen));
			writer->writeAttribute(std::to_wstring(root));
			writer->writeAttribute(std::to_wstring(attributes.width));
			writer->writeAttribute(std::to_wstring(attributes.height));
			// writer->writeAttribute(std::to_wstring(attributes.screen->mwidth));
			// writer->writeAttribute(std::to_wstring(attributes.screen->mheight));
		}

	}

	static int handleXError(::Display* display, XErrorEvent* errorEvent) {
		std::wcerr << L"X11 error:"
				<< L" display=" << errorEvent->display
				<< L" error_code=" << errorEvent->error_code
				<< L" minor_code=" << errorEvent->minor_code
				<< L" request_code=" << errorEvent->request_code
				<< L" resourceid=" << errorEvent->resourceid
				<< L" serial=" << errorEvent->serial
				<< L" type=" << errorEvent->type
				<< std::endl;
		return 0;
	}


public:

	void process(Configuration& configuration, std::shared_ptr<writer::RelationalWriter> writer, std::function<void() > relationalWriterFlush) {
		XSetErrorHandler(handleXError);

		Display display;
		display.display = XOpenDisplay(nullptr);

		if (display.display) {
			if (configuration.listInputDevices) listInputDevices(display, configuration, writer, relationalWriterFlush);
			if (configuration.listInputEvents) listInputEvents(display, configuration, writer, relationalWriterFlush);
			if (configuration.listWindows) listWindows(display, configuration, writer, relationalWriterFlush);
			if (configuration.listScreens) listScreens(display, configuration, writer, relationalWriterFlush);
		} else {
			throw std::invalid_argument("Unable to open display. Please check the $DISPLAY variable.");
		}
	}
};

}
}
}