Blob Blame History Raw
/*
 * Copyright © 2018 Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "config.h"

#include <errno.h>
#include <inttypes.h>
#include <linux/input.h>
#include <libevdev/libevdev.h>
#include <libudev.h>
#include <sys/signalfd.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <string.h>
#include <dirent.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>

#include "libinput-versionsort.h"
#include "libinput-util.h"
#include "libinput-version.h"
#include "libinput-git-version.h"
#include "shared.h"
#include "builddir.h"

static const int FILE_VERSION_NUMBER = 1;

/* libinput is not designed to keep events past immediate use so we need to
 * cache our events. Simplest way to do this is to just cache the printf
 * output */
struct li_event {
	char msg[256];
};

enum event_type {
	NONE,
	EVDEV,
	LIBINPUT,
	COMMENT,
};

struct event {
	enum event_type type;
	uint64_t time;
	union {
		struct input_event evdev;
		struct li_event libinput;
		char comment[200];
	} u;
};

struct record_device {
	struct list link;
	char *devnode;		/* device node of the source device */
	struct libevdev *evdev;
	struct libinput_device *device;

	struct event *events;
	size_t nevents;
	size_t events_sz;

	struct {
		bool is_touch_device;
		uint16_t slot_state;
		uint16_t last_slot_state;
	} touch;
};

struct record_context {
	int timeout;
	bool show_keycodes;

	uint64_t offset;

	struct list devices;
	int ndevices;

	char *outfile; /* file name given on cmdline */
	char *output_file; /* full file name with suffix */

	int out_fd;
	unsigned int indent;

	struct libinput *libinput;
};

static inline bool
obfuscate_keycode(struct input_event *ev)
{
	switch (ev->type) {
	case EV_KEY:
		if (ev->code >= KEY_ESC && ev->code < KEY_ZENKAKUHANKAKU) {
			ev->code = KEY_A;
			return true;
		}
		break;
	case EV_MSC:
		if (ev->code == MSC_SCAN) {
			ev->value = 30; /* KEY_A scancode */
			return true;
		}
		break;
	}

	return false;
}

static inline void
indent_push(struct record_context *ctx)
{
	ctx->indent += 2;
}

static inline void
indent_pop(struct record_context *ctx)
{
	assert(ctx->indent >= 2);
	ctx->indent -= 2;
}

/**
 * Indented dprintf, indentation is given as second parameter.
 */
static inline void
iprintf(const struct record_context *ctx, const char *format, ...)
{
	va_list args;
	char fmt[1024];
	static const char space[] = "                                     ";
	static const size_t len = sizeof(space);
	unsigned int indent = ctx->indent;
	int rc;

	assert(indent < len);
	assert(strlen(format) > 1);

	/* Special case: if we're printing a new list item, we want less
	 * indentation because the '- ' takes up one level of indentation
	 *
	 * This is only needed because I don't want to deal with open/close
	 * lists statements.
	 */
	if (format[0] == '-')
		indent -= 2;

	snprintf(fmt, sizeof(fmt), "%s%s", &space[len - indent - 1], format);
	va_start(args, format);
	rc = vdprintf(ctx->out_fd, fmt, args);
	va_end(args);

	assert(rc != -1 && (unsigned int)rc > indent);
}

/**
 * Normal printf, just wrapped for the context
 */
static inline void
noiprintf(const struct record_context *ctx, const char *format, ...)
{
	va_list args;
	int rc;

	va_start(args, format);
	rc = vdprintf(ctx->out_fd, format, args);
	va_end(args);
	assert(rc != -1 && (unsigned int)rc > 0);
}

static inline void
print_evdev_event(struct record_context *ctx, struct input_event *ev)
{
	const char *cname;
	bool was_modified = false;
	char desc[1024];

	ev->time = us2tv(tv2us(&ev->time) - ctx->offset);

	/* Don't leak passwords unless the user wants to */
	if (!ctx->show_keycodes)
		was_modified = obfuscate_keycode(ev);

	cname = libevdev_event_code_get_name(ev->type, ev->code);

	if (ev->type == EV_SYN && ev->code == SYN_MT_REPORT) {
		snprintf(desc,
			 sizeof(desc),
			 "++++++++++++ %s (%d) ++++++++++",
			 cname,
			 ev->value);
	} else if (ev->type == EV_SYN) {
		static unsigned long last_ms = 0;
		unsigned long time, dt;

		time = us2ms(tv2us(&ev->time));
		dt = time - last_ms;
		last_ms = time;

		snprintf(desc,
			 sizeof(desc),
			"------------ %s (%d) ---------- %+ldms",
			cname,
			ev->value,
			dt);
	} else {
		const char *tname = libevdev_event_type_get_name(ev->type);

		snprintf(desc,
			 sizeof(desc),
			 "%s / %-20s %6d%s",
			 tname,
			 cname,
			 ev->value,
			 was_modified ? " (obfuscated)" : "");
	}

	iprintf(ctx,
		"- [%3lu, %6u, %3d, %3d, %6d] # %s\n",
		ev->time.tv_sec,
		(unsigned int)ev->time.tv_usec,
		ev->type,
		ev->code,
		ev->value,
		desc);
}

#define resize(array_, sz_) \
{ \
	size_t new_size = (sz_) + 1000; \
	void *tmp = realloc((array_), new_size * sizeof(*(array_))); \
	assert(tmp); \
	(array_)  = tmp; \
	(sz_) = new_size; \
}

static inline size_t
handle_evdev_frame(struct record_context *ctx, struct record_device *d)
{
	struct libevdev *evdev = d->evdev;
	struct input_event e;
	size_t count = 0;
	uint32_t last_time = 0;
	struct event *event;

	while (libevdev_next_event(evdev,
				   LIBEVDEV_READ_FLAG_NORMAL,
				   &e) == LIBEVDEV_READ_STATUS_SUCCESS) {

		if (ctx->offset == 0)
			ctx->offset = tv2us(&e.time);

		if (d->nevents == d->events_sz)
			resize(d->events, d->events_sz);

		event = &d->events[d->nevents++];
		event->type = EVDEV;
		event->time = tv2us(&e.time) - ctx->offset;
		event->u.evdev = e;
		count++;

		if (d->touch.is_touch_device &&
		    e.type == EV_ABS &&
		    e.code == ABS_MT_TRACKING_ID) {
			unsigned int slot = libevdev_get_current_slot(evdev);
			assert(slot < sizeof(d->touch.slot_state) * 8);

			if (e.value != -1)
				d->touch.slot_state |= 1 << slot;
			else
				d->touch.slot_state &= ~(1 << slot);
		}

		last_time = event->time;

		if (e.type == EV_SYN && e.code == SYN_REPORT)
			break;
	}

	if (d->touch.slot_state != d->touch.last_slot_state) {
		d->touch.last_slot_state = d->touch.slot_state;
		if (d->nevents == d->events_sz)
			resize(d->events, d->events_sz);

		if (d->touch.slot_state == 0) {
			event = &d->events[d->nevents++];
			event->type = COMMENT;
			event->time = last_time;
			snprintf(event->u.comment,
				 sizeof(event->u.comment),
				 "                               # Touch device in neutral state\n");
			count++;
		}
	}

	return count;
}

static void
buffer_device_notify(struct record_context *ctx,
		     struct libinput_event *e,
		     struct event *event)
{
	struct libinput_device *dev = libinput_event_get_device(e);
	struct libinput_seat *seat = libinput_device_get_seat(dev);
	const char *type = NULL;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_DEVICE_ADDED:
		type = "DEVICE_ADDED";
		break;
	case LIBINPUT_EVENT_DEVICE_REMOVED:
		type = "DEVICE_REMOVED";
		break;
	default:
		abort();
	}

	event->time = 0;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{type: %s, seat: %5s, logical_seat: %7s}",
		 type,
		 libinput_seat_get_physical_name(seat),
		 libinput_seat_get_logical_name(seat));
}

static void
buffer_key_event(struct record_context *ctx,
		 struct libinput_event *e,
		 struct event *event)
{
	struct libinput_event_keyboard *k = libinput_event_get_keyboard_event(e);
	enum libinput_key_state state;
	uint32_t key;
	uint64_t time;
	const char *type;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_KEYBOARD_KEY:
		type = "KEYBOARD_KEY";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_keyboard_get_time_usec(k) - ctx->offset : 0;

	state = libinput_event_keyboard_get_key_state(k);

	key = libinput_event_keyboard_get_key(k);
	if (!ctx->show_keycodes &&
	    (key >= KEY_ESC && key < KEY_ZENKAKUHANKAKU))
		key = -1;

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, key: %d, state: %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 key,
		 state == LIBINPUT_KEY_STATE_PRESSED ? "pressed" : "released");
}

static void
buffer_motion_event(struct record_context *ctx,
		    struct libinput_event *e,
		    struct event *event)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(e);
	double x = libinput_event_pointer_get_dx(p),
	       y = libinput_event_pointer_get_dy(p);
	double uax = libinput_event_pointer_get_dx_unaccelerated(p),
	       uay = libinput_event_pointer_get_dy_unaccelerated(p);
	uint64_t time;
	const char *type;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_POINTER_MOTION:
		type = "POINTER_MOTION";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_pointer_get_time_usec(p) - ctx->offset : 0;

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, delta: [%6.2f, %6.2f], unaccel: [%6.2f, %6.2f]}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 x, y,
		 uax, uay);
}

static void
buffer_absmotion_event(struct record_context *ctx,
		       struct libinput_event *e,
		       struct event *event)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(e);
	double x = libinput_event_pointer_get_absolute_x(p),
	       y = libinput_event_pointer_get_absolute_y(p);
	double tx = libinput_event_pointer_get_absolute_x_transformed(p, 100),
	       ty = libinput_event_pointer_get_absolute_y_transformed(p, 100);
	uint64_t time;
	const char *type;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
		type = "POINTER_MOTION_ABSOLUTE";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_pointer_get_time_usec(p) - ctx->offset : 0;

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, point: [%6.2f, %6.2f], transformed: [%6.2f, %6.2f]}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 x, y,
		 tx, ty);
}

static void
buffer_pointer_button_event(struct record_context *ctx,
			    struct libinput_event *e,
			    struct event *event)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(e);
	enum libinput_button_state state;
	int button;
	uint64_t time;
	const char *type;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_POINTER_BUTTON:
		type = "POINTER_BUTTON";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_pointer_get_time_usec(p) - ctx->offset : 0;
	button = libinput_event_pointer_get_button(p);
	state = libinput_event_pointer_get_button_state(p);

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, button: %d, state: %s, seat_count: %u}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 button,
		 state == LIBINPUT_BUTTON_STATE_PRESSED ? "pressed" : "released",
		 libinput_event_pointer_get_seat_button_count(p));
}

static void
buffer_pointer_axis_event(struct record_context *ctx,
			  struct libinput_event *e,
			  struct event *event)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(e);
	uint64_t time;
	const char *type, *source;
	double h = 0, v = 0;
	int hd = 0, vd = 0;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_POINTER_AXIS:
		type = "POINTER_AXIS";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_pointer_get_time_usec(p) - ctx->offset : 0;
	if (libinput_event_pointer_has_axis(p,
				LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) {
		h = libinput_event_pointer_get_axis_value(p,
				LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
		hd = libinput_event_pointer_get_axis_value_discrete(p,
				LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
	}
	if (libinput_event_pointer_has_axis(p,
				LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) {
		v = libinput_event_pointer_get_axis_value(p,
				LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
		vd = libinput_event_pointer_get_axis_value_discrete(p,
				LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
	}
	switch(libinput_event_pointer_get_axis_source(p)) {
	case LIBINPUT_POINTER_AXIS_SOURCE_WHEEL: source = "wheel"; break;
	case LIBINPUT_POINTER_AXIS_SOURCE_FINGER: source = "finger"; break;
	case LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS: source = "continuous"; break;
	case LIBINPUT_POINTER_AXIS_SOURCE_WHEEL_TILT: source = "wheel-tilt"; break;
	default:
		source = "unknown";
		break;
	}

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, axes: [%2.2f, %2.2f], discrete: [%d, %d], source: %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 h, v,
		 hd, vd,
		 source);
}

static void
buffer_touch_event(struct record_context *ctx,
		   struct libinput_event *e,
		   struct event *event)
{
	enum libinput_event_type etype = libinput_event_get_type(e);
	struct libinput_event_touch *t = libinput_event_get_touch_event(e);
	const char *type;
	double x, y;
	double tx, ty;
	uint64_t time;
	int32_t slot, seat_slot;

	switch(etype) {
	case LIBINPUT_EVENT_TOUCH_DOWN:
		type = "TOUCH_DOWN";
		break;
	case LIBINPUT_EVENT_TOUCH_UP:
		type = "TOUCH_UP";
		break;
	case LIBINPUT_EVENT_TOUCH_MOTION:
		type = "TOUCH_MOTION";
		break;
	case LIBINPUT_EVENT_TOUCH_CANCEL:
		type = "TOUCH_CANCEL";
		break;
	case LIBINPUT_EVENT_TOUCH_FRAME:
		type = "TOUCH_FRAME";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_touch_get_time_usec(t) - ctx->offset : 0;

	if (etype != LIBINPUT_EVENT_TOUCH_FRAME) {
		slot = libinput_event_touch_get_slot(t);
		seat_slot = libinput_event_touch_get_seat_slot(t);
	}
	event->time = time;

	switch (etype) {
	case LIBINPUT_EVENT_TOUCH_FRAME:
		snprintf(event->u.libinput.msg,
			 sizeof(event->u.libinput.msg),
			 "{time: %ld.%06ld, type: %s}",
			 (long)(time / (int)1e6),
			 (long)(time % (int)1e6),
			 type);
		break;
	case LIBINPUT_EVENT_TOUCH_DOWN:
	case LIBINPUT_EVENT_TOUCH_MOTION:
		x = libinput_event_touch_get_x(t);
		y = libinput_event_touch_get_y(t);
		tx = libinput_event_touch_get_x_transformed(t, 100);
		ty = libinput_event_touch_get_y_transformed(t, 100);
		snprintf(event->u.libinput.msg,
			 sizeof(event->u.libinput.msg),
			 "{time: %ld.%06ld, type: %s, slot: %d, seat_slot: %d, point: [%6.2f, %6.2f], transformed: [%6.2f, %6.2f]}",
			 (long)(time / (int)1e6),
			 (long)(time % (int)1e6),
			 type,
			 slot,
			 seat_slot,
			 x, y,
			 tx, ty);
		break;
	case LIBINPUT_EVENT_TOUCH_UP:
	case LIBINPUT_EVENT_TOUCH_CANCEL:
		snprintf(event->u.libinput.msg,
			 sizeof(event->u.libinput.msg),
			 "{time: %ld.%06ld, type: %s, slot: %d, seat_slot: %d}",
			 (long)(time / (int)1e6),
			 (long)(time % (int)1e6),
			 type,
			 slot,
			 seat_slot);
		break;
	default:
		abort();
	}
}

static void
buffer_gesture_event(struct record_context *ctx,
		     struct libinput_event *e,
		     struct event *event)
{
	enum libinput_event_type etype = libinput_event_get_type(e);
	struct libinput_event_gesture *g = libinput_event_get_gesture_event(e);
	const char *type;
	uint64_t time;

	switch(etype) {
	case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
		type = "GESTURE_PINCH_BEGIN";
		break;
	case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
		type = "GESTURE_PINCH_UPDATE";
		break;
	case LIBINPUT_EVENT_GESTURE_PINCH_END:
		type = "GESTURE_PINCH_END";
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
		type = "GESTURE_SWIPE_BEGIN";
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
		type = "GESTURE_SWIPE_UPDATE";
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_END:
		type = "GESTURE_SWIPE_END";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_gesture_get_time_usec(g) - ctx->offset : 0;
	event->time = time;

	switch (etype) {
	case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
	case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
	case LIBINPUT_EVENT_GESTURE_PINCH_END:
		snprintf(event->u.libinput.msg,
			 sizeof(event->u.libinput.msg),
			 "{time: %ld.%06ld, type: %s, nfingers: %d, "
			 "delta: [%6.2f, %6.2f], unaccel: [%6.2f, %6.2f], "
			 "angle_delta: %6.2f, scale: %6.2f}",
			 (long)(time / (int)1e6),
			 (long)(time % (int)1e6),
			 type,
			 libinput_event_gesture_get_finger_count(g),
			 libinput_event_gesture_get_dx(g),
			 libinput_event_gesture_get_dy(g),
			 libinput_event_gesture_get_dx_unaccelerated(g),
			 libinput_event_gesture_get_dy_unaccelerated(g),
			 libinput_event_gesture_get_angle_delta(g),
			 libinput_event_gesture_get_scale(g)
			 );
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
	case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
	case LIBINPUT_EVENT_GESTURE_SWIPE_END:
		snprintf(event->u.libinput.msg,
			 sizeof(event->u.libinput.msg),
			 "{time: %ld.%06ld, type: %s, nfingers: %d, "
			 "delta: [%6.2f, %6.2f], unaccel: [%6.2f, %6.2f]}",
			 (long)(time / (int)1e6),
			 (long)(time % (int)1e6),
			 type,
			 libinput_event_gesture_get_finger_count(g),
			 libinput_event_gesture_get_dx(g),
			 libinput_event_gesture_get_dy(g),
			 libinput_event_gesture_get_dx_unaccelerated(g),
			 libinput_event_gesture_get_dy_unaccelerated(g)
			 );
		break;
	default:
		abort();
	}
}

static char *
buffer_tablet_axes(struct libinput_event_tablet_tool *t)
{
	const int MAX_AXES = 10;
	struct libinput_tablet_tool *tool;
	char *s = NULL;
	int idx = 0;
	int len;
	double x, y;
	char **strv;

	tool = libinput_event_tablet_tool_get_tool(t);

	strv = zalloc(MAX_AXES * sizeof *strv);

	x = libinput_event_tablet_tool_get_x(t);
	y = libinput_event_tablet_tool_get_y(t);
	len = xasprintf(&strv[idx++], "point: [%.2f, %.2f]", x, y);
	if (len <= 0)
		goto out;

	if (libinput_tablet_tool_has_tilt(tool)) {
		x = libinput_event_tablet_tool_get_tilt_x(t);
		y = libinput_event_tablet_tool_get_tilt_y(t);
		len = xasprintf(&strv[idx++], "tilt: [%.2f, %.2f]", x, y);
		if (len <= 0)
			goto out;
	}

	if (libinput_tablet_tool_has_distance(tool) ||
	    libinput_tablet_tool_has_pressure(tool)) {
		double dist, pressure;

		dist = libinput_event_tablet_tool_get_distance(t);
		pressure = libinput_event_tablet_tool_get_pressure(t);
		if (dist)
			len = xasprintf(&strv[idx++], "distance: %.2f", dist);
		else
			len = xasprintf(&strv[idx++], "pressure: %.2f", pressure);
		if (len <= 0)
			goto out;
	}

	if (libinput_tablet_tool_has_rotation(tool)) {
		double rotation;

		rotation = libinput_event_tablet_tool_get_rotation(t);
		len = xasprintf(&strv[idx++], "rotation: %.2f", rotation);
		if (len <= 0)
			goto out;
	}

	if (libinput_tablet_tool_has_slider(tool)) {
		double slider;

		slider = libinput_event_tablet_tool_get_slider_position(t);
		len = xasprintf(&strv[idx++], "slider: %.2f", slider);
		if (len <= 0)
			goto out;

	}

	if (libinput_tablet_tool_has_wheel(tool)) {
		double wheel;
		int delta;

		wheel = libinput_event_tablet_tool_get_wheel_delta(t);
		len = xasprintf(&strv[idx++], "wheel: %.2f", wheel);
		if (len <= 0)
			goto out;

		delta = libinput_event_tablet_tool_get_wheel_delta_discrete(t);
		len = xasprintf(&strv[idx++], "wheel-discrete: %d", delta);
		if (len <= 0)
			goto out;
	}

	assert(idx < MAX_AXES);

	s = strv_join(strv, ", ");
out:
	strv_free(strv);
	return s;
}

static void
buffer_tablet_tool_proximity_event(struct record_context *ctx,
				   struct libinput_event *e,
				   struct event *event)
{
	struct libinput_event_tablet_tool *t =
		libinput_event_get_tablet_tool_event(e);
	struct libinput_tablet_tool *tool =
		libinput_event_tablet_tool_get_tool(t);
	uint64_t time;
	const char *type, *tool_type;
	char *axes;
	char caps[10] = {0};
	enum libinput_tablet_tool_proximity_state prox;
	size_t idx;

	switch (libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY:
		type = "TABLET_TOOL_PROXIMITY";
		break;
	default:
		abort();
	}

	switch (libinput_tablet_tool_get_type(tool)) {
	case LIBINPUT_TABLET_TOOL_TYPE_PEN:
		tool_type = "pen";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_ERASER:
		tool_type = "eraser";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_BRUSH:
		tool_type = "brush";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_PENCIL:
		tool_type = "brush";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_AIRBRUSH:
		tool_type = "airbrush";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_MOUSE:
		tool_type = "mouse";
		break;
	case LIBINPUT_TABLET_TOOL_TYPE_LENS:
		tool_type = "lens";
		break;
	default:
		tool_type = "unknown";
		break;
	}

	prox = libinput_event_tablet_tool_get_proximity_state(t);

	time = ctx->offset ?
		libinput_event_tablet_tool_get_time_usec(t) - ctx->offset : 0;

	axes = buffer_tablet_axes(t);

	idx = 0;
	if (libinput_tablet_tool_has_pressure(tool))
		caps[idx++] = 'p';
	if (libinput_tablet_tool_has_distance(tool))
		caps[idx++] = 'd';
	if (libinput_tablet_tool_has_tilt(tool))
		caps[idx++] = 't';
	if (libinput_tablet_tool_has_rotation(tool))
		caps[idx++] = 'r';
	if (libinput_tablet_tool_has_slider(tool))
		caps[idx++] = 's';
	if (libinput_tablet_tool_has_wheel(tool))
		caps[idx++] = 'w';
	assert(idx <= ARRAY_LENGTH(caps));

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, proximity: %s, tool-type: %s, serial: %" PRIu64 ", axes: %s, %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 prox ? "in" : "out",
		 tool_type,
		 libinput_tablet_tool_get_serial(tool),
		 caps,
		 axes);
	free(axes);
}

static void
buffer_tablet_tool_button_event(struct record_context *ctx,
				struct libinput_event *e,
				struct event *event)
{
	struct libinput_event_tablet_tool *t =
		libinput_event_get_tablet_tool_event(e);
	uint64_t time;
	const char *type;
	uint32_t button;
	enum libinput_button_state state;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_TABLET_TOOL_BUTTON:
		type = "TABLET_TOOL_BUTTON";
		break;
	default:
		abort();
	}


	button = libinput_event_tablet_tool_get_button(t);
	state = libinput_event_tablet_tool_get_button_state(t);

	time = ctx->offset ?
		libinput_event_tablet_tool_get_time_usec(t) - ctx->offset : 0;

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, button: %d, state: %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 button,
		 state ? "pressed" : "released");
}

static void
buffer_tablet_tool_event(struct record_context *ctx,
			 struct libinput_event *e,
			 struct event *event)
{
	struct libinput_event_tablet_tool *t =
		libinput_event_get_tablet_tool_event(e);
	uint64_t time;
	const char *type;
	char *axes;
	enum libinput_tablet_tool_tip_state tip;
	char btn_buffer[30] = {0};

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
		type = "TABLET_TOOL_AXIS";
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_TIP:
		type = "TABLET_TOOL_TIP";
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_BUTTON:
		type = "TABLET_TOOL_BUTTON";
		break;
	default:
		abort();
	}

	if (libinput_event_get_type(e) == LIBINPUT_EVENT_TABLET_TOOL_BUTTON) {
		uint32_t button;
		enum libinput_button_state state;

		button = libinput_event_tablet_tool_get_button(t);
		state = libinput_event_tablet_tool_get_button_state(t);
		snprintf(btn_buffer, sizeof(btn_buffer),
			 ", button: %d, state: %s\n",
			 button,
			 state ? "pressed" : "released");
	}

	tip = libinput_event_tablet_tool_get_tip_state(t);

	time = ctx->offset ?
		libinput_event_tablet_tool_get_time_usec(t) - ctx->offset : 0;

	axes = buffer_tablet_axes(t);

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s%s, tip: %s, %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 btn_buffer, /* may be empty string */
		 tip ? "down" : "up",
		 axes);
	free(axes);
}

static void
buffer_tablet_pad_button_event(struct record_context *ctx,
			       struct libinput_event *e,
			       struct event *event)
{
	struct libinput_event_tablet_pad *p =
		libinput_event_get_tablet_pad_event(e);
	struct libinput_tablet_pad_mode_group *group;
	enum libinput_button_state state;
	unsigned int button, mode;
	const char *type;
	uint64_t time;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_TABLET_PAD_BUTTON:
		type = "TABLET_PAD_BUTTON";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_tablet_pad_get_time_usec(p) - ctx->offset : 0;

	button = libinput_event_tablet_pad_get_button_number(p),
	state = libinput_event_tablet_pad_get_button_state(p);
	mode = libinput_event_tablet_pad_get_mode(p);
	group = libinput_event_tablet_pad_get_mode_group(p);

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, button: %d, state: %s, mode: %d, is-toggle: %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 button,
		 state == LIBINPUT_BUTTON_STATE_PRESSED ? "pressed" : "released",
		 mode,
		 libinput_tablet_pad_mode_group_button_is_toggle(group, button) ? "true" : "false"
		 );


}

static void
buffer_tablet_pad_ringstrip_event(struct record_context *ctx,
				  struct libinput_event *e,
				  struct event *event)
{
	struct libinput_event_tablet_pad *p =
		libinput_event_get_tablet_pad_event(e);
	const char *source = NULL;
	unsigned int mode, number;
	const char *type;
	uint64_t time;
	double pos;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_TABLET_PAD_RING:
		type = "TABLET_PAD_RING";
		number = libinput_event_tablet_pad_get_ring_number(p);
	        pos = libinput_event_tablet_pad_get_ring_position(p);

		switch (libinput_event_tablet_pad_get_ring_source(p)) {
		case LIBINPUT_TABLET_PAD_RING_SOURCE_FINGER:
			source = "finger";
			break;
		case LIBINPUT_TABLET_PAD_RING_SOURCE_UNKNOWN:
			source = "unknown";
			break;
		}
		break;
	case LIBINPUT_EVENT_TABLET_PAD_STRIP:
		type = "TABLET_PAD_STRIP";
		number = libinput_event_tablet_pad_get_strip_number(p);
	        pos = libinput_event_tablet_pad_get_strip_position(p);

		switch (libinput_event_tablet_pad_get_strip_source(p)) {
		case LIBINPUT_TABLET_PAD_STRIP_SOURCE_FINGER:
			source = "finger";
			break;
		case LIBINPUT_TABLET_PAD_STRIP_SOURCE_UNKNOWN:
			source = "unknown";
			break;
		}
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_tablet_pad_get_time_usec(p) - ctx->offset : 0;

	mode = libinput_event_tablet_pad_get_mode(p);

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, number: %d, position: %.2f, source: %s, mode: %d}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 number,
		 pos,
		 source,
		 mode);
}

static void
buffer_switch_event(struct record_context *ctx,
		    struct libinput_event *e,
		    struct event *event)
{
	struct libinput_event_switch *s = libinput_event_get_switch_event(e);
	enum libinput_switch_state state;
	uint32_t sw;
	const char *type;
	uint64_t time;

	switch(libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_SWITCH_TOGGLE:
		type = "SWITCH_TOGGLE";
		break;
	default:
		abort();
	}

	time = ctx->offset ?
		libinput_event_switch_get_time_usec(s) - ctx->offset : 0;

	sw = libinput_event_switch_get_switch(s);
	state = libinput_event_switch_get_switch_state(s);

	event->time = time;
	snprintf(event->u.libinput.msg,
		 sizeof(event->u.libinput.msg),
		 "{time: %ld.%06ld, type: %s, switch: %d, state: %s}",
		 (long)(time / (int)1e6),
		 (long)(time % (int)1e6),
		 type,
		 sw,
		 state == LIBINPUT_SWITCH_STATE_ON ? "on" : "off");
}

static void
buffer_libinput_event(struct record_context *ctx,
		      struct libinput_event *e,
		      struct event *event)
{
	switch (libinput_event_get_type(e)) {
	case LIBINPUT_EVENT_NONE:
		abort();
	case LIBINPUT_EVENT_DEVICE_ADDED:
	case LIBINPUT_EVENT_DEVICE_REMOVED:
		buffer_device_notify(ctx, e, event);
		break;
	case LIBINPUT_EVENT_KEYBOARD_KEY:
		buffer_key_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_POINTER_MOTION:
		buffer_motion_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
		buffer_absmotion_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_POINTER_BUTTON:
		buffer_pointer_button_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_POINTER_AXIS:
		buffer_pointer_axis_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TOUCH_DOWN:
	case LIBINPUT_EVENT_TOUCH_UP:
	case LIBINPUT_EVENT_TOUCH_MOTION:
	case LIBINPUT_EVENT_TOUCH_CANCEL:
	case LIBINPUT_EVENT_TOUCH_FRAME:
		buffer_touch_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
	case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
	case LIBINPUT_EVENT_GESTURE_PINCH_END:
	case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
	case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
	case LIBINPUT_EVENT_GESTURE_SWIPE_END:
		buffer_gesture_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY:
		buffer_tablet_tool_proximity_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
	case LIBINPUT_EVENT_TABLET_TOOL_TIP:
		buffer_tablet_tool_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_BUTTON:
		buffer_tablet_tool_button_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TABLET_PAD_BUTTON:
		buffer_tablet_pad_button_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_TABLET_PAD_RING:
	case LIBINPUT_EVENT_TABLET_PAD_STRIP:
		buffer_tablet_pad_ringstrip_event(ctx, e, event);
		break;
	case LIBINPUT_EVENT_SWITCH_TOGGLE:
		buffer_switch_event(ctx, e, event);
		break;
	default:
		break;
	}
}

static void
print_cached_events(struct record_context *ctx,
		    struct record_device *d,
		    unsigned int offset,
		    int len)
{
	unsigned int idx;
	enum event_type last_type;
	uint64_t last_time;

	if (len == -1)
		len = d->nevents - offset;
	assert(offset + len <= d->nevents);

	if (offset == 0) {
		last_type = NONE;
		last_time = 0;
	} else {
		last_type = d->events[offset - 1].type;
		last_time = d->events[offset - 1].time;
	}

	idx = offset;
	indent_push(ctx);
	while (idx < offset + len) {
		struct event *e;

		e = &d->events[idx++];
		if (e->type != last_type || e->time != last_time) {
			bool new_frame = false;

			if (last_time == 0 || e->time != last_time)
				new_frame = true;

			indent_pop(ctx);

			switch(e->type) {
			case EVDEV:
				if (new_frame)
					iprintf(ctx, "- evdev:\n");
				else
					iprintf(ctx, "evdev:\n");
				break;
			case LIBINPUT:
				if (new_frame)
					iprintf(ctx, "- libinput:\n");
				else
					iprintf(ctx, "libinput:\n");
				break;
			case COMMENT:
				break;
			default:
				abort();
			}
			indent_push(ctx);

			last_type = e->type;
		}

		switch (e->type) {
		case EVDEV:
			print_evdev_event(ctx, &e->u.evdev);
			break;
		case LIBINPUT:
			iprintf(ctx, "- %s\n", e->u.libinput.msg);
			break;
		case COMMENT:
			iprintf(ctx, "%s", e->u.comment);
			break;
		default:
			abort();
		}

		last_time = e->time;
	}
	indent_pop(ctx);
}

static inline size_t
handle_libinput_events(struct record_context *ctx,
		       struct record_device *d)
{
	struct libinput_event *e;
	size_t count = 0;
	struct record_device *current = d;

	libinput_dispatch(ctx->libinput);

	while ((e = libinput_get_event(ctx->libinput)) != NULL) {
		struct libinput_device *device = libinput_event_get_device(e);
		struct event *event;

		if (device != current->device) {
			struct record_device *tmp;
			bool found = false;
			list_for_each(tmp, &ctx->devices, link) {
				if (device == tmp->device) {
					current = tmp;
					found = true;
					break;
				}
			}
			assert(found);
		}

		if (current->nevents == current->events_sz)
			resize(current->events, current->events_sz);

		event = &current->events[current->nevents++];
		event->type = LIBINPUT;
		buffer_libinput_event(ctx, e, event);

		if (current == d)
			count++;
		libinput_event_destroy(e);
	}
	return count;
}

static inline void
handle_events(struct record_context *ctx, struct record_device *d, bool print)
{
	while(true) {
		size_t first_idx = d->nevents;
		size_t evcount = 0,
		       licount = 0;

		evcount = handle_evdev_frame(ctx, d);

		if (ctx->libinput)
			licount = handle_libinput_events(ctx, d);

		if (evcount == 0 && licount == 0)
			break;

		if (!print)
			continue;

		print_cached_events(ctx, d, first_idx, evcount + licount);
	}
}

static inline void
print_libinput_header(struct record_context *ctx)
{
	iprintf(ctx, "libinput:\n");
	indent_push(ctx);
	iprintf(ctx, "version: \"%s\"\n", LIBINPUT_VERSION);
	iprintf(ctx, "git: \"%s\"\n", LIBINPUT_GIT_VERSION);
	if (ctx->timeout > 0)
		iprintf(ctx, "autorestart: %d\n", ctx->timeout);
	indent_pop(ctx);
}

static inline void
print_system_header(struct record_context *ctx)
{
	struct utsname u;
	const char *kernel = "unknown";
	FILE *dmi;
	char modalias[2048] = "unknown";

	if (uname(&u) != -1)
		kernel = u.release;

	dmi = fopen("/sys/class/dmi/id/modalias", "r");
	if (dmi) {
		if (fgets(modalias, sizeof(modalias), dmi)) {
			modalias[strlen(modalias) - 1] = '\0'; /* linebreak */
		} else {
			sprintf(modalias, "unknown");
		}
		fclose(dmi);
	}

	iprintf(ctx, "system:\n");
	indent_push(ctx);
	iprintf(ctx, "kernel: \"%s\"\n", kernel);
	iprintf(ctx, "dmi: \"%s\"\n", modalias);
	indent_pop(ctx);
}

static inline void
print_header(struct record_context *ctx)
{
	iprintf(ctx, "version: %d\n", FILE_VERSION_NUMBER);
	iprintf(ctx, "ndevices: %d\n", ctx->ndevices);
	print_libinput_header(ctx);
	print_system_header(ctx);
}

static inline void
print_description_abs(struct record_context *ctx,
		      struct libevdev *dev,
		      unsigned int code)
{
	const struct input_absinfo *abs;

	abs = libevdev_get_abs_info(dev, code);
	assert(abs);

	iprintf(ctx, "#       Value      %6d\n", abs->value);
	iprintf(ctx, "#       Min        %6d\n", abs->minimum);
	iprintf(ctx, "#       Max        %6d\n", abs->maximum);
	iprintf(ctx, "#       Fuzz       %6d\n", abs->fuzz);
	iprintf(ctx, "#       Flat       %6d\n", abs->flat);
	iprintf(ctx, "#       Resolution %6d\n", abs->resolution);
}

static inline void
print_description_state(struct record_context *ctx,
			struct libevdev *dev,
			unsigned int type,
			unsigned int code)
{
	int state = libevdev_get_event_value(dev, type, code);
	iprintf(ctx, "#       State %d\n", state);
}

static inline void
print_description_codes(struct record_context *ctx,
			struct libevdev *dev,
			unsigned int type)
{
	int max;

	max = libevdev_event_type_get_max(type);
	if (max == -1)
		return;

	iprintf(ctx,
		"# Event type %d (%s)\n",
		type,
		libevdev_event_type_get_name(type));

	if (type == EV_SYN)
		return;

	for (unsigned int code = 0; code <= (unsigned int)max; code++) {
		if (!libevdev_has_event_code(dev, type, code))
			continue;

		iprintf(ctx,
			"#   Event code %d (%s)\n",
			code,
			libevdev_event_code_get_name(type,
						     code));

		switch (type) {
		case EV_ABS:
			print_description_abs(ctx, dev, code);
			break;
		case EV_LED:
		case EV_SW:
			print_description_state(ctx, dev, type, code);
			break;
		}
	}
}

static inline void
print_description(struct record_context *ctx, struct libevdev *dev)
{
	const struct input_absinfo *x, *y;

	iprintf(ctx, "# Name: %s\n", libevdev_get_name(dev));
	iprintf(ctx,
		"# ID: bus %#02x vendor %#02x product %#02x version %#02x\n",
		libevdev_get_id_bustype(dev),
		libevdev_get_id_vendor(dev),
		libevdev_get_id_product(dev),
		libevdev_get_id_version(dev));

	x = libevdev_get_abs_info(dev, ABS_X);
	y = libevdev_get_abs_info(dev, ABS_Y);
	if (x && y) {
		if (x->resolution && y->resolution) {
			int w, h;

			w = (x->maximum - x->minimum)/x->resolution;
			h = (y->maximum - y->minimum)/y->resolution;
			iprintf(ctx, "# Size in mm: %dx%d\n", w, h);
		} else {
			iprintf(ctx,
				"# Size in mm: unknown, missing resolution\n");
		}
	}

	iprintf(ctx, "# Supported Events:\n");

	for (unsigned int type = 0; type < EV_CNT; type++) {
		if (!libevdev_has_event_type(dev, type))
			continue;

		print_description_codes(ctx, dev, type);
	}

	iprintf(ctx, "# Properties:\n");

	for (unsigned int prop = 0; prop < INPUT_PROP_CNT; prop++) {
		if (libevdev_has_property(dev, prop)) {
			iprintf(ctx,
				"#    Property %d (%s)\n",
				prop,
				libevdev_property_get_name(prop));
		}
	}
}

static inline void
print_bits_info(struct record_context *ctx, struct libevdev *dev)
{
	iprintf(ctx, "name: \"%s\"\n", libevdev_get_name(dev));
	iprintf(ctx,
		"id: [%d, %d, %d, %d]\n",
		libevdev_get_id_bustype(dev),
		libevdev_get_id_vendor(dev),
		libevdev_get_id_product(dev),
		libevdev_get_id_version(dev));
}

static inline void
print_bits_absinfo(struct record_context *ctx, struct libevdev *dev)
{
	const struct input_absinfo *abs;

	if (!libevdev_has_event_type(dev, EV_ABS))
		return;

	iprintf(ctx, "absinfo:\n");
	indent_push(ctx);

	for (unsigned int code = 0; code < ABS_CNT; code++) {
		abs = libevdev_get_abs_info(dev, code);
		if (!abs)
			continue;

		iprintf(ctx,
			"%d: [%d, %d, %d, %d, %d]\n",
			code,
			abs->minimum,
			abs->maximum,
			abs->fuzz,
			abs->flat,
			abs->resolution);
	}
	indent_pop(ctx);
}

static inline void
print_bits_codes(struct record_context *ctx,
		 struct libevdev *dev,
		 unsigned int type)
{
	int max;
	bool first = true;

	max = libevdev_event_type_get_max(type);
	if (max == -1)
		return;

	iprintf(ctx, "%d: [", type);

	for (unsigned int code = 0; code <= (unsigned int)max; code++) {
		if (!libevdev_has_event_code(dev, type, code))
			continue;

		noiprintf(ctx, "%s%d", first ? "" : ", ", code);
		first = false;
	}

	noiprintf(ctx, "] # %s\n", libevdev_event_type_get_name(type));
}

static inline void
print_bits_types(struct record_context *ctx, struct libevdev *dev)
{
	iprintf(ctx, "codes:\n");
	indent_push(ctx);
	for (unsigned int type = 0; type < EV_CNT; type++) {
		if (!libevdev_has_event_type(dev, type))
			continue;
		print_bits_codes(ctx, dev, type);
	}
	indent_pop(ctx);
}

static inline void
print_bits_props(struct record_context *ctx, struct libevdev *dev)
{
	bool first = true;

	iprintf(ctx, "properties: [");
	for (unsigned int prop = 0; prop < INPUT_PROP_CNT; prop++) {
		if (libevdev_has_property(dev, prop)) {
			noiprintf(ctx, "%s%d", first ? "" : ", ", prop);
			first = false;
		}
	}
	noiprintf(ctx, "]\n"); /* last entry, no comma */
}

static inline void
print_evdev_description(struct record_context *ctx, struct record_device *dev)
{
	struct libevdev *evdev = dev->evdev;

	iprintf(ctx, "evdev:\n");
	indent_push(ctx);

	print_description(ctx, evdev);
	print_bits_info(ctx, evdev);
	print_bits_types(ctx, evdev);
	print_bits_absinfo(ctx, evdev);
	print_bits_props(ctx, evdev);

	indent_pop(ctx);
}

static inline void
print_hid_report_descriptor(struct record_context *ctx,
			    struct record_device *dev)
{
	const char *prefix = "/dev/input/event";
	const char *node;
	char syspath[PATH_MAX];
	unsigned char buf[1024];
	int len;
	int fd;
	bool first = true;

	/* we take the shortcut rather than the proper udev approach, the
	   report_descriptor is available in sysfs and two devices up from
	   our device. 2 digits for the event number should be enough.
	   This approach won't work for /dev/input/by-id devices. */
	if (!strneq(dev->devnode, prefix, strlen(prefix)) ||
	    strlen(dev->devnode) > strlen(prefix) + 2)
		return;

	node = &dev->devnode[strlen(prefix)];
	len = snprintf(syspath,
		       sizeof(syspath),
		       "/sys/class/input/event%s/device/device/report_descriptor",
		       node);
	if (len < 55 || len > 56)
		return;

	fd = open(syspath, O_RDONLY);
	if (fd == -1)
		return;

	iprintf(ctx, "hid: [");

	while ((len = read(fd, buf, sizeof(buf))) > 0) {
		for (int i = 0; i < len; i++) {
			/* YAML requires decimal */
			noiprintf(ctx, "%s%u",first ? "" : ", ", buf[i]);
			first = false;
		}
	}
	noiprintf(ctx, " ]\n");

	close(fd);
}

static inline void
print_udev_properties(struct record_context *ctx, struct record_device *dev)
{
	struct udev *udev = NULL;
	struct udev_device *udev_device = NULL;
	struct udev_list_entry *entry;
	struct stat st;

	if (stat(dev->devnode, &st) < 0)
		return;

	udev = udev_new();
	if (!udev)
		goto out;

	udev_device = udev_device_new_from_devnum(udev, 'c', st.st_rdev);
	if (!udev_device)
		goto out;

	iprintf(ctx, "udev:\n");
	indent_push(ctx);

	iprintf(ctx, "properties:\n");
	indent_push(ctx);

	entry = udev_device_get_properties_list_entry(udev_device);
	while (entry) {
		const char *key, *value;

		key = udev_list_entry_get_name(entry);

		if (strneq(key, "ID_INPUT", 8) ||
		    strneq(key, "LIBINPUT", 8) ||
		    strneq(key, "EV_ABS", 6) ||
		    strneq(key, "MOUSE_DPI", 9) ||
		    strneq(key, "POINTINGSTICK_", 14)) {
			value = udev_list_entry_get_value(entry);
			iprintf(ctx, "- %s=%s\n", key, value);
		}

		entry = udev_list_entry_get_next(entry);
	}

	indent_pop(ctx);
	indent_pop(ctx);
out:
	udev_device_unref(udev_device);
	udev_unref(udev);
}

static void
quirks_log_handler(struct libinput *this_is_null,
		   enum libinput_log_priority priority,
		   const char *format,
		   va_list args)
{
}

static void
list_print(void *userdata, const char *val)
{
	struct record_context *ctx = userdata;

	iprintf(ctx, "- %s\n", val);
}

static inline void
print_device_quirks(struct record_context *ctx, struct record_device *dev)
{
	struct udev *udev = NULL;
	struct udev_device *udev_device = NULL;
	struct stat st;
	struct quirks_context *quirks;
	const char *data_path = LIBINPUT_QUIRKS_DIR;
	const char *override_file = LIBINPUT_QUIRKS_OVERRIDE_FILE;
	char *builddir = NULL;

	if (stat(dev->devnode, &st) < 0)
		return;

	if ((builddir = builddir_lookup())) {
		setenv("LIBINPUT_QUIRKS_DIR", LIBINPUT_QUIRKS_SRCDIR, 0);
		data_path = LIBINPUT_QUIRKS_SRCDIR;
		override_file = NULL;
	}

	free(builddir);

	quirks = quirks_init_subsystem(data_path,
				       override_file,
				       quirks_log_handler,
				       NULL,
				       QLOG_CUSTOM_LOG_PRIORITIES);
	if (!quirks) {
		fprintf(stderr,
			"Failed to initialize the device quirks. "
			"Please see the above errors "
			"and/or re-run with --verbose for more details\n");
		return;
	}

	udev = udev_new();
	if (!udev)
		goto out;

	udev_device = udev_device_new_from_devnum(udev, 'c', st.st_rdev);
	if (!udev_device)
		goto out;

	iprintf(ctx, "quirks:\n");
	indent_push(ctx);

	tools_list_device_quirks(quirks, udev_device, list_print, ctx);

	indent_pop(ctx);
out:
	udev_device_unref(udev_device);
	udev_unref(udev);
	quirks_context_unref(quirks);
}
static inline void
print_libinput_description(struct record_context *ctx,
			   struct record_device *dev)
{
	struct libinput_device *device = dev->device;
	double w, h;
	struct cap {
		enum libinput_device_capability cap;
		const char *name;
	} caps[] =  {
		{LIBINPUT_DEVICE_CAP_KEYBOARD, "keyboard"},
		{LIBINPUT_DEVICE_CAP_POINTER, "pointer"},
		{LIBINPUT_DEVICE_CAP_TOUCH, "touch"},
		{LIBINPUT_DEVICE_CAP_TABLET_TOOL, "tablet"},
		{LIBINPUT_DEVICE_CAP_TABLET_PAD, "pad"},
		{LIBINPUT_DEVICE_CAP_GESTURE, "gesture"},
		{LIBINPUT_DEVICE_CAP_SWITCH, "switch"},
	};
	struct cap *cap;
	bool is_first;

	if (!device)
		return;

	iprintf(ctx, "libinput:\n");
	indent_push(ctx);

	if (libinput_device_get_size(device, &w, &h) == 0)
		iprintf(ctx, "size: [%.f, %.f]\n", w, h);

	iprintf(ctx, "capabilities: [");
	is_first = true;
	ARRAY_FOR_EACH(caps, cap) {
		if (!libinput_device_has_capability(device, cap->cap))
			continue;
		noiprintf(ctx, "%s%s", is_first ? "" : ", ", cap->name);
		is_first = false;
	}
	noiprintf(ctx, "]\n");

	/* Configuration options should be printed here, but since they
	 * don't reflect the user-configured ones their usefulness is
	 * questionable. We need the ability to specify the options like in
	 * debug-events.
	 */
	indent_pop(ctx);
}

static inline void
print_device_description(struct record_context *ctx, struct record_device *dev)
{
	iprintf(ctx, "- node: %s\n", dev->devnode);

	print_evdev_description(ctx, dev);
	print_hid_report_descriptor(ctx, dev);
	print_udev_properties(ctx, dev);
	print_device_quirks(ctx, dev);
	print_libinput_description(ctx, dev);
}

static int is_event_node(const struct dirent *dir) {
	return strneq(dir->d_name, "event", 5);
}

static inline char *
select_device(void)
{
	struct dirent **namelist;
	int ndev, selected_device;
	int rc;
	char *device_path;
	bool has_eaccess = false;
	int available_devices = 0;

	ndev = scandir("/dev/input", &namelist, is_event_node, versionsort);
	if (ndev <= 0)
		return NULL;

	fprintf(stderr, "Available devices:\n");
	for (int i = 0; i < ndev; i++) {
		struct libevdev *device;
		char path[PATH_MAX];
		int fd = -1;

		snprintf(path,
			 sizeof(path),
			 "/dev/input/%s",
			 namelist[i]->d_name);
		fd = open(path, O_RDONLY);
		if (fd < 0) {
			if (errno == EACCES)
				has_eaccess = true;
			continue;
		}

		rc = libevdev_new_from_fd(fd, &device);
		close(fd);
		if (rc != 0)
			continue;

		fprintf(stderr, "%s:	%s\n", path, libevdev_get_name(device));
		libevdev_free(device);
		available_devices++;
	}

	for (int i = 0; i < ndev; i++)
		free(namelist[i]);
	free(namelist);

	if (available_devices == 0) {
		fprintf(stderr, "No devices available. ");
		if (has_eaccess)
				fprintf(stderr, "Please re-run as root.");
		fprintf(stderr, "\n");
		return NULL;
	}

	fprintf(stderr, "Select the device event number: ");
	rc = scanf("%d", &selected_device);

	if (rc != 1 || selected_device < 0)
		return NULL;

	rc = xasprintf(&device_path, "/dev/input/event%d", selected_device);
	if (rc == -1)
		return NULL;

	return device_path;
}

static inline char **
all_devices(void)
{
	struct dirent **namelist;
	int ndev;
	int rc;
	char **devices = NULL;

	ndev = scandir("/dev/input", &namelist, is_event_node, versionsort);
	if (ndev <= 0)
		return NULL;

	devices = zalloc((ndev + 1)* sizeof *devices); /* NULL-terminated */
	for (int i = 0; i < ndev; i++) {
		char *device_path;

		rc = xasprintf(&device_path,
			       "/dev/input/%s",
			       namelist[i]->d_name);
		if (rc == -1)
			goto error;

		devices[i] = device_path;
	}

	return devices;

error:
	if (devices)
		strv_free(devices);
	return NULL;
}

static char *
init_output_file(const char *file, bool is_prefix)
{
	char name[PATH_MAX];

	assert(file != NULL);

	if (is_prefix) {
		struct tm *tm;
		time_t t;
		char suffix[64];

		t = time(NULL);
		tm = localtime(&t);
		strftime(suffix, sizeof(suffix), "%F-%T", tm);
		snprintf(name,
			 sizeof(name),
			 "%s.%s",
			 file,
			 suffix);
	} else {
		snprintf(name, sizeof(name), "%s", file);
	}

	return strdup(name);
}

static bool
open_output_file(struct record_context *ctx, bool is_prefix)
{
	int out_fd;

	if (ctx->outfile) {
		char *fname = init_output_file(ctx->outfile, is_prefix);
		ctx->output_file = fname;
		out_fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC, 0666);
		if (out_fd < 0)
			return false;
	} else {
		ctx->output_file = safe_strdup("stdout");
		out_fd = STDOUT_FILENO;
	}

	ctx->out_fd = out_fd;

	return true;
}

static inline void
print_progress_bar(void)
{
	static uint8_t foo = 0;

	if (!isatty(STDERR_FILENO))
		return;

	if (++foo > 20)
		foo = 1;
	fprintf(stderr, "\rReceiving events: [%*s%*s]", foo, "*", 21 - foo, " ");
}

static int
mainloop(struct record_context *ctx)
{
	bool autorestart = (ctx->timeout > 0);
	struct pollfd fds[ctx->ndevices + 2];
	unsigned int nfds = 0;
	struct record_device *d = NULL;
	struct record_device *first_device = NULL;
	struct timespec ts;
	sigset_t mask;

	assert(ctx->timeout != 0);
	assert(!list_empty(&ctx->devices));

	sigemptyset(&mask);
	sigaddset(&mask, SIGINT);
	sigaddset(&mask, SIGQUIT);
	sigprocmask(SIG_BLOCK, &mask, NULL);

	fds[0].fd = signalfd(-1, &mask, SFD_NONBLOCK);
	fds[0].events = POLLIN;
	fds[0].revents = 0;
	assert(fds[0].fd != -1);
	nfds++;

	if (ctx->libinput) {
		fds[1].fd = libinput_get_fd(ctx->libinput);
		fds[1].events = POLLIN;
		fds[1].revents = 0;
		nfds++;
		assert(nfds == 2);
	}

	list_for_each(d, &ctx->devices, link) {
		fds[nfds].fd = libevdev_get_fd(d->evdev);
		fds[nfds].events = POLLIN;
		fds[nfds].revents = 0;
		assert(fds[nfds].fd != -1);
		nfds++;
	}

	/* If we have more than one device, the time starts at recording
	 * start time. Otherwise, the first event starts the recording time.
	 */
	if (ctx->ndevices > 1) {
		clock_gettime(CLOCK_MONOTONIC, &ts);
		ctx->offset = s2us(ts.tv_sec) + ns2us(ts.tv_nsec);
	}

	do {
		int rc;
		bool had_events = false; /* we delete files without events */

		if (!open_output_file(ctx, autorestart)) {
			fprintf(stderr,
				"Failed to open '%s'\n",
				ctx->output_file);
			break;
		}
		fprintf(stderr, "Recording to '%s'.\n", ctx->output_file);

		print_header(ctx);
		if (autorestart)
			iprintf(ctx,
				"# Autorestart timeout: %d\n",
				ctx->timeout);

		iprintf(ctx, "devices:\n");
		indent_push(ctx);

		/* we only print the first device's description, the
		 * rest is assembled after CTRL+C */
		first_device = list_first_entry(&ctx->devices,
						first_device,
						link);
		print_device_description(ctx, first_device);

		iprintf(ctx, "events:\n");
		indent_push(ctx);

		if (ctx->libinput) {
			size_t count;
			libinput_dispatch(ctx->libinput);
			count = handle_libinput_events(ctx, first_device);
			print_cached_events(ctx, first_device, 0, count);
		}

		while (true) {
			rc = poll(fds, nfds, ctx->timeout);
			if (rc == -1) { /* error */
				fprintf(stderr, "Error: %m\n");
				autorestart = false;
				break;
			} else if (rc == 0) {
				fprintf(stderr,
					" ... timeout%s\n",
					had_events ? "" : " (file is empty)");
				break;
			} else if (fds[0].revents != 0) { /* signal */
				autorestart = false;
				break;
			}

			/* Pull off the evdev events first since they cause
			 * libinput events.
			 * handle_events de-queues libinput events so by the
			 * time we finish that, we hopefully have all evdev
			 * events and libinput events roughly in sync.
			 */
			had_events = true;
			list_for_each(d, &ctx->devices, link)
				handle_events(ctx, d, d == first_device);

			/* This shouldn't pull any events off unless caused
			 * by libinput-internal timeouts (e.g. tapping) */
			if (ctx->libinput && fds[1].revents) {
				size_t count, offset;

				libinput_dispatch(ctx->libinput);
				offset = first_device->nevents;
				count = handle_libinput_events(ctx,
							       first_device);
				if (count) {
					print_cached_events(ctx,
							    first_device,
							    offset,
							    count);
				}
				rc--;
			}

			if (ctx->out_fd != STDOUT_FILENO)
				print_progress_bar();

		}
		indent_pop(ctx); /* events: */

		if (autorestart) {
			noiprintf(ctx,
				  "# Closing after %ds inactivity",
				  ctx->timeout/1000);
		}

		/* First device is printed, now append all the data from the
		 * other devices, if any */
		list_for_each(d, &ctx->devices, link) {
			if (d == list_first_entry(&ctx->devices, d, link))
				continue;

			print_device_description(ctx, d);
			iprintf(ctx, "events:\n");
			indent_push(ctx);
			print_cached_events(ctx, d, 0, -1);
			indent_pop(ctx);
		}

		indent_pop(ctx); /* devices: */
		assert(ctx->indent == 0);

		fsync(ctx->out_fd);

		/* If we didn't have events, delete the file. */
		if (!isatty(ctx->out_fd)) {
			if (!had_events && ctx->output_file) {
				fprintf(stderr, "No events recorded, deleting '%s'\n", ctx->output_file);
				unlink(ctx->output_file);
			}

			close(ctx->out_fd);
			ctx->out_fd = -1;
		}
		free(ctx->output_file);
		ctx->output_file = NULL;
	} while (autorestart);

	close(fds[0].fd);

	sigprocmask(SIG_UNBLOCK, &mask, NULL);

	return 0;
}

static inline bool
init_device(struct record_context *ctx, char *path)
{
	struct record_device *d;
	int fd, rc;

	d = zalloc(sizeof(*d));
	d->devnode = path;
	d->nevents = 0;
	d->events_sz = 5000;
	d->events = zalloc(d->events_sz * sizeof(*d->events));

	fd = open(d->devnode, O_RDONLY|O_NONBLOCK);
	if (fd < 0) {
		fprintf(stderr,
			"Failed to open device %s (%m)\n",
			d->devnode);
		goto error;
	}

	rc = libevdev_new_from_fd(fd, &d->evdev);
	if (rc != 0) {
		fprintf(stderr,
			"Failed to create context for %s (%s)\n",
			d->devnode,
			strerror(-rc));
		goto error;
	}

	libevdev_set_clock_id(d->evdev, CLOCK_MONOTONIC);

	if (libevdev_get_num_slots(d->evdev) > 0)
		d->touch.is_touch_device = true;

	list_insert(&ctx->devices, &d->link);
	ctx->ndevices++;

	return true;
error:
	close(fd);
	free(d);
	return false;

}
static int
open_restricted(const char *path, int flags, void *user_data)
{
	int fd = open(path, flags);
	return fd == -1 ? -errno : fd;
}

static void close_restricted(int fd, void *user_data)
{
	close(fd);
}

const struct libinput_interface interface = {
	.open_restricted = open_restricted,
	.close_restricted = close_restricted,
};

static inline bool
init_libinput(struct record_context *ctx)
{
	struct record_device *dev;
	struct libinput *li;

	li = libinput_path_create_context(&interface, NULL);
	if (li == NULL) {
		fprintf(stderr,
			"Failed to create libinput context\n");
		return false;
	}

	ctx->libinput = li;

	list_for_each(dev, &ctx->devices, link) {
		struct libinput_device *d;

		d = libinput_path_add_device(li, dev->devnode);
		if (!d) {
			fprintf(stderr,
				"Failed to add device %s\n",
				dev->devnode);
			continue;
		}
		dev->device = libinput_device_ref(d);
		/* FIXME: this needs to be a commandline option */
		libinput_device_config_tap_set_enabled(d,
					       LIBINPUT_CONFIG_TAP_ENABLED);
	}

	return true;
}

static inline void
usage(void)
{
	printf("Usage: %s [--help] [--multiple|--all] [--autorestart] [--output-file filename] [/dev/input/event0] [...]\n"
	       "Common use-cases:\n"
	       "\n"
	       " sudo %s -o recording.yml\n"
	       "    Then select the device to record and it Ctrl+C to stop.\n"
	       "    The recorded data is in recording.yml and can be attached to a bug report.\n"
	       "\n"
	       " sudo %s -o recording.yml --autorestart 2\n"
	       "    As above, but restarts after 2s of inactivity on the device.\n"
	       "    Note, the output file is only the prefix.\n"
	       "\n"
	       " sudo %s --multiple -o recording.yml /dev/input/event3 /dev/input/event4\n"
	       "    Records the two devices into the same recordings file.\n"
	       "\n"
	       "For more information, see the %s(1) man page\n",
	       program_invocation_short_name,
	       program_invocation_short_name,
	       program_invocation_short_name,
	       program_invocation_short_name,
	       program_invocation_short_name);
}

enum options {
	OPT_AUTORESTART,
	OPT_HELP,
	OPT_OUTFILE,
	OPT_KEYCODES,
	OPT_MULTIPLE,
	OPT_ALL,
	OPT_LIBINPUT,
};

int
main(int argc, char **argv)
{
	struct record_context ctx = {
		.timeout = -1,
		.show_keycodes = false,
	};
	struct option opts[] = {
		{ "autorestart", required_argument, 0, OPT_AUTORESTART },
		{ "output-file", required_argument, 0, OPT_OUTFILE },
		{ "show-keycodes", no_argument, 0, OPT_KEYCODES },
		{ "multiple", no_argument, 0, OPT_MULTIPLE },
		{ "all", no_argument, 0, OPT_ALL },
		{ "help", no_argument, 0, OPT_HELP },
		{ "with-libinput", no_argument, 0, OPT_LIBINPUT },
		{ 0, 0, 0, 0 },
	};
	struct record_device *d, *tmp;
	const char *output_arg = NULL;
	bool multiple = false, all = false, with_libinput = false;
	int ndevices;
	int rc = 1;

	list_init(&ctx.devices);

	while (1) {
		int c;
		int option_index = 0;

		c = getopt_long(argc, argv, "ho:", opts, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'h':
		case OPT_HELP:
			usage();
			rc = 0;
			goto out;
		case OPT_AUTORESTART:
			if (!safe_atoi(optarg, &ctx.timeout) ||
			    ctx.timeout <= 0) {
				usage();
				goto out;
			}
			ctx.timeout = ctx.timeout * 1000;
			break;
		case 'o':
		case OPT_OUTFILE:
			output_arg = optarg;
			break;
		case OPT_KEYCODES:
			ctx.show_keycodes = true;
			break;
		case OPT_MULTIPLE:
			multiple = true;
			break;
		case OPT_ALL:
			all = true;
			break;
		case OPT_LIBINPUT:
			with_libinput = true;
			break;
		}
	}

	if (all && multiple) {
		fprintf(stderr,
			"Only one of --multiple and --all allowed.\n");
		goto out;
	}

	if (ctx.timeout > 0 && output_arg == NULL) {
		fprintf(stderr,
			"Option --autorestart requires --output-file\n");
		goto out;
	}

	ctx.outfile = safe_strdup(output_arg);

	ndevices = argc - optind;

	if (multiple) {
		if (output_arg == NULL) {
			fprintf(stderr,
				"Option --multiple requires --output-file\n");
			goto out;
		}

		if (ndevices <= 1) {
			fprintf(stderr,
				"Option --multiple requires all device nodes on the commandline\n");
			goto out;
		}

		for (int i = ndevices; i > 0; i -= 1) {
			char *devnode = safe_strdup(argv[optind + i - 1]);

			if (!init_device(&ctx, devnode))
				goto out;
		}
	} else if (all) {
		char **devices; /* NULL-terminated */
		char **d;

		if (output_arg == NULL) {
			fprintf(stderr,
				"Option --all requires --output-file\n");
			goto out;
		}

		devices = all_devices();
		d = devices;

		while (*d) {
			if (!init_device(&ctx, safe_strdup(*d))) {
				strv_free(devices);
				goto out;
			}
			d++;
		}

		strv_free(devices);
	} else {
		char *path;

		if (ndevices > 1) {
			fprintf(stderr, "More than one device, do you want --multiple?\n");
			goto out;
		}

		path = ndevices <= 0 ? select_device() : safe_strdup(argv[optind++]);
		if (path == NULL) {
			goto out;
		}

		if (!init_device(&ctx, path))
			goto out;
	}

	if (with_libinput && !init_libinput(&ctx))
		goto out;

	rc = mainloop(&ctx);
out:
	list_for_each_safe(d, tmp, &ctx.devices, link) {
		if (d->device)
			libinput_device_unref(d->device);
		free(d->events);
		free(d->devnode);
		libevdev_free(d->evdev);
	}

	libinput_unref(ctx.libinput);

	return rc;
}