Index: uspace/lib/c/include/ipc/services.h
===================================================================
--- uspace/lib/c/include/ipc/services.h	(revision ffed09d79c4f737ed0a49581161c8c053ce34980)
+++ uspace/lib/c/include/ipc/services.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2023 Jiri Svoboda
  * Copyright (c) 2006 Jakub Jermar
  * All rights reserved.
@@ -54,4 +55,5 @@
 #define SERVICE_NAME_CLIPBOARD "clipboard"
 #define SERVICE_NAME_CORECFG  "corecfg"
+#define SERVICE_NAME_DISPCFG  "hid/display"
 #define SERVICE_NAME_DISPLAY  "hid/display"
 #define SERVICE_NAME_WNDMGT   "hid/display"
Index: uspace/lib/dispcfg/doc/doxygroups.h
===================================================================
--- uspace/lib/dispcfg/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,3 @@
+/** @addtogroup libdispcfg libdispcfg
+ * @ingroup libs
+ */
Index: uspace/lib/dispcfg/include/dispcfg.h
===================================================================
--- uspace/lib/dispcfg/include/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/include/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libwndmgt
+ * @{
+ */
+/** @file
+ */
+
+#ifndef _LIBDISPCFG_DISPCFG_H_
+#define _LIBDISPCFG_DISPCFG_H_
+
+#include <errno.h>
+#include <types/common.h>
+#include "types/dispcfg.h"
+
+extern errno_t dispcfg_open(const char *, dispcfg_cb_t *, void *, dispcfg_t **);
+extern void dispcfg_close(dispcfg_t *);
+extern errno_t dispcfg_get_seat_list(dispcfg_t *, dispcfg_seat_list_t **);
+extern void dispcfg_free_seat_list(dispcfg_seat_list_t *);
+extern errno_t dispcfg_get_seat_info(dispcfg_t *, sysarg_t,
+    dispcfg_seat_info_t **);
+extern void dispcfg_free_seat_info(dispcfg_seat_info_t *);
+extern errno_t dispcfg_seat_create(dispcfg_t *, const char *, sysarg_t *);
+extern errno_t dispcfg_seat_delete(dispcfg_t *, sysarg_t);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/dispcfg/include/dispcfg_srv.h
===================================================================
--- uspace/lib/dispcfg/include/dispcfg_srv.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/include/dispcfg_srv.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/** @file
+ */
+
+#ifndef _LIBDISPCFG_DISPCFG_SRV_H_
+#define _LIBDISPCFG_DISPCFG_SRV_H_
+
+#include <async.h>
+#include <errno.h>
+#include "types/dispcfg.h"
+
+typedef struct dispcfg_ops dispcfg_ops_t;
+
+/** Display configuration server structure (per client session) */
+typedef struct {
+	async_sess_t *client_sess;
+	dispcfg_ops_t *ops;
+	void *arg;
+} dispcfg_srv_t;
+
+struct dispcfg_ops {
+	errno_t (*get_seat_list)(void *, dispcfg_seat_list_t **);
+	errno_t (*get_seat_info)(void *, sysarg_t, dispcfg_seat_info_t **);
+	errno_t (*seat_create)(void *, const char *, sysarg_t *);
+	errno_t (*seat_delete)(void *, sysarg_t);
+	errno_t (*get_event)(void *, dispcfg_ev_t *);
+};
+
+extern void dispcfg_conn(ipc_call_t *, dispcfg_srv_t *);
+extern void dispcfg_srv_initialize(dispcfg_srv_t *);
+extern void dispcfg_srv_ev_pending(dispcfg_srv_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/dispcfg/include/ipc/dispcfg.h
===================================================================
--- uspace/lib/dispcfg/include/ipc/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/include/ipc/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/** @file
+ */
+
+#ifndef LIBDISPCFG_IPC_DISPCFG_H
+#define LIBDISPCFG_IPC_DISPCFG_H
+
+#include <ipc/common.h>
+
+typedef enum {
+	DISPCFG_CALLBACK_CREATE = IPC_FIRST_USER_METHOD,
+	DISPCFG_GET_SEAT_LIST,
+	DISPCFG_GET_SEAT_INFO,
+	DISPCFG_SEAT_CREATE,
+	DISPCFG_SEAT_DELETE,
+	DISPCFG_GET_EVENT,
+} dispcfg_request_t;
+
+typedef enum {
+	DISPCFG_EV_PENDING = IPC_FIRST_USER_METHOD
+} dispcfg_event_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/dispcfg/include/types/dispcfg.h
===================================================================
--- uspace/lib/dispcfg/include/types/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/include/types/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/** @file
+ */
+
+#ifndef _LIBDISPCFG_TYPES_DISPCFG_H_
+#define _LIBDISPCFG_TYPES_DISPCFG_H_
+
+#include <ipc/services.h>
+#include <types/common.h>
+
+/** Use the default display configuration service (argument to dispcfg_open() */
+#define DISPCFG_DEFAULT SERVICE_NAME_DISPCFG
+
+struct dispcfg;
+
+/** Display configuration session */
+typedef struct dispcfg dispcfg_t;
+
+/** Display configuration callbacks */
+typedef struct {
+	/** Seat added */
+	void (*seat_added)(void *, sysarg_t);
+	/** Seat removed */
+	void (*seat_removed)(void *, sysarg_t);
+} dispcfg_cb_t;
+
+/** Display configuration event type */
+typedef enum {
+	/** Seat added */
+	dcev_seat_added,
+	/** Seat removed */
+	dcev_seat_removed,
+} dispcfg_ev_type_t;
+
+/** Display configuration event */
+typedef struct {
+	/** Event type */
+	dispcfg_ev_type_t etype;
+	/** Seat ID */
+	sysarg_t seat_id;
+} dispcfg_ev_t;
+
+/** Seat list */
+typedef struct {
+	/** Number of seats */
+	size_t nseats;
+	/** ID for each seat */
+	sysarg_t *seats;
+} dispcfg_seat_list_t;
+
+/** Seat information */
+typedef struct {
+	/** Seat name */
+	char *name;
+} dispcfg_seat_info_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/dispcfg/meson.build
===================================================================
--- uspace/lib/dispcfg/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2023 Jiri Svoboda
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# - Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+# - The name of the author may not be used to endorse or promote products
+#   derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+src = files(
+	'src/dispcfg.c',
+	'src/dispcfg_srv.c',
+)
+
+test_src = files(
+	'test/main.c',
+	'test/dispcfg.c',
+)
Index: uspace/lib/dispcfg/private/dispcfg.h
===================================================================
--- uspace/lib/dispcfg/private/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/private/dispcfg.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/** @file
+ */
+
+#ifndef _LIBDISPCFG_PRIVATE_DISPCFG_H_
+#define _LIBDISPCFG_PRIVATE_DISPCFG_H_
+
+#include <async.h>
+#include <fibril_synch.h>
+#include <stdbool.h>
+
+/** Display configuration session structure */
+struct dispcfg {
+	/** Session with display configuration service */
+	async_sess_t *sess;
+	/** Callbacks */
+	dispcfg_cb_t *cb;
+	/** Calback argument */
+	void *cb_arg;
+	/** Synchronize access to display configuration object */
+	fibril_mutex_t lock;
+	/** @c true if callback handler terminated */
+	bool cb_done;
+	/** Signalled when cb_done or ev_pending is changed */
+	fibril_condvar_t cv;
+};
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/dispcfg/src/dispcfg.c
===================================================================
--- uspace/lib/dispcfg/src/dispcfg.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/src/dispcfg.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,452 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/**
+ * @file
+ * @brief Display configuration protocol client
+ */
+
+#include <async.h>
+#include <dispcfg.h>
+#include <errno.h>
+#include <fibril_synch.h>
+#include <ipc/dispcfg.h>
+#include <ipc/services.h>
+#include <loc.h>
+#include <mem.h>
+#include <stdlib.h>
+#include <str.h>
+#include "../private/dispcfg.h"
+
+static errno_t dispcfg_callback_create(dispcfg_t *);
+static void dispcfg_cb_conn(ipc_call_t *, void *);
+
+/** Open display configuration service.
+ *
+ * @param wmname Display configuration service name or @c NULL to use default
+ * @param cb Display configuration callbacks
+ * @param cb_arg Callback argument
+ * @param rdispcfg Place to store pointer to display configuration session
+ * @return EOK on success or an error code
+ */
+errno_t dispcfg_open(const char *wmname, dispcfg_cb_t *cb, void *cb_arg,
+    dispcfg_t **rdispcfg)
+{
+	service_id_t dispcfg_svc;
+	dispcfg_t *dispcfg;
+	errno_t rc;
+
+	dispcfg = calloc(1, sizeof(dispcfg_t));
+	if (dispcfg == NULL)
+		return ENOMEM;
+
+	dispcfg->cb = cb;
+	dispcfg->cb_arg = cb_arg;
+
+	fibril_mutex_initialize(&dispcfg->lock);
+	fibril_condvar_initialize(&dispcfg->cv);
+
+	if (wmname == NULL)
+		wmname = SERVICE_NAME_DISPCFG;
+
+	rc = loc_service_get_id(wmname, &dispcfg_svc, 0);
+	if (rc != EOK) {
+		free(dispcfg);
+		return ENOENT;
+	}
+
+	dispcfg->sess = loc_service_connect(dispcfg_svc, INTERFACE_DISPCFG,
+	    0);
+	if (dispcfg->sess == NULL) {
+		free(dispcfg);
+		return ENOENT;
+	}
+
+	rc = dispcfg_callback_create(dispcfg);
+	if (rc != EOK) {
+		async_hangup(dispcfg->sess);
+		free(dispcfg);
+		return EIO;
+	}
+
+	*rdispcfg = dispcfg;
+	return EOK;
+}
+
+/** Create callback connection from display configuration service.
+ *
+ * @param dispcfg Display configuration
+ * @return EOK on success or an error code
+ */
+static errno_t dispcfg_callback_create(dispcfg_t *dispcfg)
+{
+	async_exch_t *exch = async_exchange_begin(dispcfg->sess);
+
+	aid_t req = async_send_0(exch, DISPCFG_CALLBACK_CREATE, NULL);
+
+	port_id_t port;
+	errno_t rc = async_create_callback_port(exch, INTERFACE_DISPCFG_CB, 0, 0,
+	    dispcfg_cb_conn, dispcfg, &port);
+
+	async_exchange_end(exch);
+
+	if (rc != EOK)
+		return rc;
+
+	errno_t retval;
+	async_wait_for(req, &retval);
+
+	return retval;
+}
+
+/** Close display configuration service.
+ *
+ * @param dispcfg Display configuration
+ */
+void dispcfg_close(dispcfg_t *dispcfg)
+{
+	fibril_mutex_lock(&dispcfg->lock);
+	async_hangup(dispcfg->sess);
+	dispcfg->sess = NULL;
+
+	/* Wait for callback handler to terminate */
+
+	while (!dispcfg->cb_done)
+		fibril_condvar_wait(&dispcfg->cv, &dispcfg->lock);
+	fibril_mutex_unlock(&dispcfg->lock);
+
+	free(dispcfg);
+}
+
+/** Get seat list.
+ *
+ * @param dispcfg Display configuration
+ * @param rlist Place to store pointer to new seat list structure
+ * @return EOK on success or an error code
+ */
+errno_t dispcfg_get_seat_list(dispcfg_t *dispcfg, dispcfg_seat_list_t **rlist)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	dispcfg_seat_list_t *list;
+	sysarg_t nseats;
+	sysarg_t *seats;
+	errno_t rc;
+
+	exch = async_exchange_begin(dispcfg->sess);
+	req = async_send_0(exch, DISPCFG_GET_SEAT_LIST, &answer);
+
+	/* Receive seat list length */
+	rc = async_data_read_start(exch, &nseats, sizeof (nseats));
+	if (rc != EOK) {
+		async_exchange_end(exch);
+		async_wait_for(req, &rc);
+		return rc;
+	}
+
+	seats = calloc(nseats, sizeof(sysarg_t));
+	if (seats == NULL) {
+		async_exchange_end(exch);
+		async_forget(req);
+		return ENOMEM;
+	}
+
+	/* Receive seat list */
+	rc = async_data_read_start(exch, seats, nseats * sizeof (sysarg_t));
+	async_exchange_end(exch);
+
+	if (rc != EOK) {
+		async_forget(req);
+		return rc;
+	}
+
+	async_wait_for(req, &rc);
+	if (rc != EOK)
+		return rc;
+
+	list = calloc(1, sizeof(dispcfg_seat_list_t));
+	if (list == NULL)
+		return ENOMEM;
+
+	list->nseats = nseats;
+	list->seats = seats;
+	*rlist = list;
+	return EOK;
+}
+
+/** Free seat list.
+ *
+ * @param list Display configuration list
+ */
+void dispcfg_free_seat_list(dispcfg_seat_list_t *list)
+{
+	free(list->seats);
+	free(list);
+}
+
+/** Get seat information.
+ *
+ * @param dispcfg Display configuration
+ * @param seat_id Seat ID
+ * @param rinfo Place to store pointer to new seat information structure
+ * @return EOK on success or an error code
+ */
+errno_t dispcfg_get_seat_info(dispcfg_t *dispcfg, sysarg_t seat_id,
+    dispcfg_seat_info_t **rinfo)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	dispcfg_seat_info_t *info;
+	sysarg_t namesize;
+	char *name;
+	errno_t rc;
+
+	exch = async_exchange_begin(dispcfg->sess);
+	req = async_send_1(exch, DISPCFG_GET_SEAT_INFO, seat_id, &answer);
+
+	/* Receive name size */
+	rc = async_data_read_start(exch, &namesize, sizeof (namesize));
+	if (rc != EOK) {
+		async_exchange_end(exch);
+		async_wait_for(req, &rc);
+		return rc;
+	}
+
+	name = calloc(namesize + 1, sizeof(char));
+	if (name == NULL) {
+		async_exchange_end(exch);
+		async_forget(req);
+		return ENOMEM;
+	}
+
+	/* Receive name */
+	rc = async_data_read_start(exch, name, namesize);
+	async_exchange_end(exch);
+
+	if (rc != EOK) {
+		async_forget(req);
+		return rc;
+	}
+
+	async_wait_for(req, &rc);
+	if (rc != EOK)
+		return rc;
+
+	/* Null-terminate the string */
+	name[namesize] = '\0';
+
+	info = calloc(1, sizeof(dispcfg_seat_info_t));
+	if (info == NULL)
+		return ENOMEM;
+
+	info->name = name;
+	*rinfo = info;
+	return EOK;
+}
+
+/** Free seat information.
+ *
+ * @param info Display configuration information
+ */
+void dispcfg_free_seat_info(dispcfg_seat_info_t *info)
+{
+	free(info->name);
+	free(info);
+}
+
+/** Create seat.
+ *
+ * @param dispcfg Display configuration session
+ * @param name Seat name
+ * @param rseat_id Place to store ID of the new seat
+ * @return EOK on success or an error code
+ */
+errno_t dispcfg_seat_create(dispcfg_t *dispcfg, const char *name,
+    sysarg_t *rseat_id)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	size_t name_size;
+	errno_t rc;
+
+	name_size = str_size(name);
+
+	exch = async_exchange_begin(dispcfg->sess);
+	req = async_send_0(exch, DISPCFG_SEAT_CREATE, &answer);
+
+	/* Write name */
+	rc = async_data_write_start(exch, name, name_size);
+	async_exchange_end(exch);
+	if (rc != EOK) {
+		async_forget(req);
+		return rc;
+	}
+
+	async_wait_for(req, &rc);
+	if (rc != EOK)
+		return rc;
+
+	*rseat_id = ipc_get_arg1(&answer);
+	return EOK;
+}
+
+/** Delete seat.
+ *
+ * @param dispcfg Display configuration
+ * @param seat_id Seat ID
+ * @return EOK on success or an error code
+ */
+errno_t dispcfg_seat_delete(dispcfg_t *dispcfg, sysarg_t seat_id)
+{
+	async_exch_t *exch;
+	errno_t rc;
+
+	exch = async_exchange_begin(dispcfg->sess);
+	rc = async_req_1_0(exch, DISPCFG_SEAT_DELETE, seat_id);
+
+	async_exchange_end(exch);
+	return rc;
+}
+
+/** Get display configuration event.
+ *
+ * @param dispcfg Display configuration
+ * @param event Place to store event
+ * @return EOK on success or an error code
+ */
+static errno_t dispcfg_get_event(dispcfg_t *dispcfg, dispcfg_ev_t *event)
+{
+	async_exch_t *exch;
+	ipc_call_t answer;
+	aid_t req;
+	errno_t rc;
+
+	exch = async_exchange_begin(dispcfg->sess);
+	req = async_send_0(exch, DISPCFG_GET_EVENT, &answer);
+	rc = async_data_read_start(exch, event, sizeof(*event));
+	async_exchange_end(exch);
+	if (rc != EOK) {
+		async_forget(req);
+		return rc;
+	}
+
+	async_wait_for(req, &rc);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Display configuration events are pending.
+ *
+ * @param dispcfg Display configuration
+ * @param icall Call data
+ */
+static void dispcfg_ev_pending(dispcfg_t *dispcfg, ipc_call_t *icall)
+{
+	errno_t rc;
+	dispcfg_ev_t event;
+
+	while (true) {
+		fibril_mutex_lock(&dispcfg->lock);
+
+		if (dispcfg->sess != NULL)
+			rc = dispcfg_get_event(dispcfg, &event);
+		else
+			rc = ENOENT;
+
+		fibril_mutex_unlock(&dispcfg->lock);
+
+		if (rc != EOK)
+			break;
+
+		switch (event.etype) {
+		case dcev_seat_added:
+			if (dispcfg->cb != NULL &&
+			    dispcfg->cb->seat_added != NULL) {
+				dispcfg->cb->seat_added(dispcfg->cb_arg,
+				    event.seat_id);
+			}
+			break;
+		case dcev_seat_removed:
+			if (dispcfg->cb != NULL &&
+			    dispcfg->cb->seat_removed != NULL) {
+				dispcfg->cb->seat_removed(dispcfg->cb_arg,
+				    event.seat_id);
+			}
+			break;
+		}
+	}
+
+	async_answer_0(icall, EOK);
+}
+
+/** Callback connection handler.
+ *
+ * @param icall Connect call data
+ * @param arg   Argument, dispcfg_t *
+ */
+static void dispcfg_cb_conn(ipc_call_t *icall, void *arg)
+{
+	dispcfg_t *dispcfg = (dispcfg_t *) arg;
+
+	while (true) {
+		ipc_call_t call;
+		async_get_call(&call);
+
+		if (!ipc_get_imethod(&call)) {
+			/* Hangup */
+			async_answer_0(&call, EOK);
+			goto out;
+		}
+
+		switch (ipc_get_imethod(&call)) {
+		case DISPCFG_EV_PENDING:
+			dispcfg_ev_pending(dispcfg, &call);
+			break;
+		default:
+			async_answer_0(&call, ENOTSUP);
+			break;
+		}
+	}
+
+out:
+	fibril_mutex_lock(&dispcfg->lock);
+	dispcfg->cb_done = true;
+	fibril_mutex_unlock(&dispcfg->lock);
+	fibril_condvar_broadcast(&dispcfg->cv);
+}
+
+/** @}
+ */
Index: uspace/lib/dispcfg/src/dispcfg_srv.c
===================================================================
--- uspace/lib/dispcfg/src/dispcfg_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/src/dispcfg_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,373 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libdispcfg
+ * @{
+ */
+/**
+ * @file
+ * @brief Display configuration protocol server stub
+ */
+
+#include <dispcfg.h>
+#include <dispcfg_srv.h>
+#include <errno.h>
+#include <ipc/dispcfg.h>
+#include <mem.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <str.h>
+#include "../private/dispcfg.h"
+
+static void dispcfg_callback_create_srv(dispcfg_srv_t *srv, ipc_call_t *call)
+{
+	async_sess_t *sess = async_callback_receive(EXCHANGE_SERIALIZE);
+	if (sess == NULL) {
+		async_answer_0(call, ENOMEM);
+		return;
+	}
+
+	srv->client_sess = sess;
+	async_answer_0(call, EOK);
+}
+
+static void dispcfg_get_seat_list_srv(dispcfg_srv_t *srv, ipc_call_t *icall)
+{
+	ipc_call_t call;
+	dispcfg_seat_list_t *list = NULL;
+	size_t size;
+	errno_t rc;
+
+	if (srv->ops->get_seat_list == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->get_seat_list(srv->arg, &list);
+	if (rc != EOK) {
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send list size */
+
+	if (!async_data_read_receive(&call, &size)) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != sizeof(list->nseats)) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, &list->nseats, size);
+	if (rc != EOK) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send seat list */
+
+	if (!async_data_read_receive(&call, &size)) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != list->nseats * sizeof(sysarg_t)) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, list->seats, size);
+	if (rc != EOK) {
+		dispcfg_free_seat_list(list);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	async_answer_0(icall, EOK);
+	dispcfg_free_seat_list(list);
+}
+
+static void dispcfg_get_seat_info_srv(dispcfg_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t seat_id;
+	ipc_call_t call;
+	dispcfg_seat_info_t *info = NULL;
+	size_t namesize;
+	size_t size;
+	errno_t rc;
+
+	seat_id = ipc_get_arg1(icall);
+
+	if (srv->ops->get_seat_info == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->get_seat_info(srv->arg, seat_id, &info);
+	if (rc != EOK) {
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send name size */
+
+	if (!async_data_read_receive(&call, &size)) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != sizeof(size_t)) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	namesize = str_size(info->name);
+
+	rc = async_data_read_finalize(&call, &namesize, size);
+	if (rc != EOK) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send name */
+
+	if (!async_data_read_receive(&call, &size)) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != namesize) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, info->name, size);
+	if (rc != EOK) {
+		dispcfg_free_seat_info(info);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	async_answer_0(icall, EOK);
+	dispcfg_free_seat_info(info);
+}
+
+static void dispcfg_seat_create_srv(dispcfg_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t seat_id;
+	ipc_call_t call;
+	char *name;
+	size_t size;
+	errno_t rc;
+
+	if (!async_data_write_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	name = calloc(size + 1, 1);
+	if (name == NULL) {
+		async_answer_0(&call, ENOMEM);
+		async_answer_0(icall, ENOMEM);
+		return;
+	}
+
+	rc = async_data_write_finalize(&call, name, size);
+	if (rc != EOK) {
+		free(name);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	if (srv->ops->seat_create == NULL) {
+		free(name);
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->seat_create(srv->arg, name, &seat_id);
+	async_answer_1(icall, rc, seat_id);
+	free(name);
+}
+
+static void dispcfg_seat_delete_srv(dispcfg_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t seat_id;
+	errno_t rc;
+
+	seat_id = ipc_get_arg1(icall);
+
+	if (srv->ops->seat_delete == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->seat_delete(srv->arg, seat_id);
+	async_answer_0(icall, rc);
+}
+
+static void dispcfg_get_event_srv(dispcfg_srv_t *srv, ipc_call_t *icall)
+{
+	dispcfg_ev_t event;
+	ipc_call_t call;
+	size_t size;
+	errno_t rc;
+
+	if (srv->ops->get_event == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->get_event(srv->arg, &event);
+	if (rc != EOK) {
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Transfer event data */
+	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != sizeof(event)) {
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, &event, sizeof(event));
+	if (rc != EOK) {
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	async_answer_0(icall, EOK);
+}
+
+void dispcfg_conn(ipc_call_t *icall, dispcfg_srv_t *srv)
+{
+	/* Accept the connection */
+	async_accept_0(icall);
+
+	while (true) {
+		ipc_call_t call;
+
+		async_get_call(&call);
+		sysarg_t method = ipc_get_imethod(&call);
+
+		if (!method) {
+			/* The other side has hung up */
+			async_answer_0(&call, EOK);
+			break;
+		}
+
+		switch (method) {
+		case DISPCFG_CALLBACK_CREATE:
+			dispcfg_callback_create_srv(srv, &call);
+			break;
+		case DISPCFG_GET_SEAT_LIST:
+			dispcfg_get_seat_list_srv(srv, &call);
+			break;
+		case DISPCFG_GET_SEAT_INFO:
+			dispcfg_get_seat_info_srv(srv, &call);
+			break;
+		case DISPCFG_SEAT_CREATE:
+			dispcfg_seat_create_srv(srv, &call);
+			break;
+		case DISPCFG_SEAT_DELETE:
+			dispcfg_seat_delete_srv(srv, &call);
+			break;
+		case DISPCFG_GET_EVENT:
+			dispcfg_get_event_srv(srv, &call);
+			break;
+		default:
+			async_answer_0(&call, ENOTSUP);
+		}
+	}
+
+	/* Hang up callback session */
+	if (srv->client_sess != NULL) {
+		async_hangup(srv->client_sess);
+		srv->client_sess = NULL;
+	}
+}
+
+/** Initialize display configuration server structure
+ *
+ * @param srv Display configuration server structure to initialize
+ */
+void dispcfg_srv_initialize(dispcfg_srv_t *srv)
+{
+	memset(srv, 0, sizeof(*srv));
+}
+
+/** Send 'pending' event to client.
+ *
+ * @param srv Display configuration server structure
+ */
+void dispcfg_srv_ev_pending(dispcfg_srv_t *srv)
+{
+	async_exch_t *exch;
+
+	exch = async_exchange_begin(srv->client_sess);
+	async_msg_0(exch, DISPCFG_EV_PENDING);
+	async_exchange_end(exch);
+}
+
+/** @}
+ */
Index: uspace/lib/dispcfg/test/dispcfg.c
===================================================================
--- uspace/lib/dispcfg/test/dispcfg.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/test/dispcfg.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,644 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <async.h>
+#include <errno.h>
+#include <dispcfg.h>
+#include <dispcfg_srv.h>
+#include <fibril_synch.h>
+#include <loc.h>
+#include <pcut/pcut.h>
+#include <str.h>
+#include "../private/dispcfg.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(dispcfg);
+
+static const char *test_dispcfg_server = "test-dispcfg";
+static const char *test_dispcfg_svc = "test/dispcfg";
+
+static void test_dispcfg_conn(ipc_call_t *, void *);
+
+static errno_t test_get_seat_list(void *, dispcfg_seat_list_t **);
+static errno_t test_get_seat_info(void *, sysarg_t, dispcfg_seat_info_t **);
+static errno_t test_seat_create(void *, const char *, sysarg_t *);
+static errno_t test_seat_delete(void *, sysarg_t);
+static errno_t test_get_event(void *, dispcfg_ev_t *);
+
+static void test_seat_added(void *, sysarg_t);
+static void test_seat_removed(void *, sysarg_t);
+
+static dispcfg_ops_t test_dispcfg_srv_ops = {
+	.get_seat_list = test_get_seat_list,
+	.get_seat_info = test_get_seat_info,
+	.seat_create = test_seat_create,
+	.seat_delete = test_seat_delete,
+	.get_event = test_get_event
+};
+
+static dispcfg_cb_t test_dispcfg_cb = {
+	.seat_added = test_seat_added,
+	.seat_removed = test_seat_removed
+};
+
+/** Describes to the server how to respond to our request and pass tracking
+ * data back to the client.
+ */
+typedef struct {
+	errno_t rc;
+	sysarg_t seat_id;
+	dispcfg_ev_t event;
+	dispcfg_ev_t revent;
+	int event_cnt;
+
+	bool get_seat_list_called;
+	dispcfg_seat_list_t *get_seat_list_rlist;
+
+	bool get_seat_info_called;
+	sysarg_t get_seat_info_seat_id;
+	dispcfg_seat_info_t *get_seat_info_rinfo;
+
+	bool seat_create_called;
+	char *seat_create_name;
+	sysarg_t seat_create_seat_id;
+
+	bool seat_delete_called;
+	sysarg_t seat_delete_seat_id;
+
+	bool get_event_called;
+
+	bool seat_added_called;
+	sysarg_t seat_added_seat_id;
+
+	bool seat_removed_called;
+	sysarg_t seat_removed_seat_id;
+
+	bool seat_changed_called;
+	sysarg_t seat_changed_seat_id;
+
+	fibril_condvar_t event_cv;
+	fibril_mutex_t event_lock;
+	dispcfg_srv_t *srv;
+} test_response_t;
+
+/** dispcfg_open(), dispcfg_close() work for valid seat management service */
+PCUT_TEST(open_close)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_get_seat_list() with server returning error response works */
+PCUT_TEST(get_seat_list_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	dispcfg_seat_list_t *list;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	resp.rc = ENOMEM;
+	resp.get_seat_list_called = false;
+
+	rc = dispcfg_get_seat_list(dispcfg, &list);
+	PCUT_ASSERT_TRUE(resp.get_seat_list_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_get_seat_list() with server returning success response works */
+PCUT_TEST(get_seat_list_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	dispcfg_seat_list_t *list;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	resp.rc = EOK;
+	resp.get_seat_list_called = false;
+	resp.get_seat_list_rlist = calloc(1, sizeof(dispcfg_seat_list_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_seat_list_rlist);
+	resp.get_seat_list_rlist->nseats = 2;
+	resp.get_seat_list_rlist->seats = calloc(2, sizeof(sysarg_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_seat_list_rlist->seats);
+	resp.get_seat_list_rlist->seats[0] = 42;
+	resp.get_seat_list_rlist->seats[1] = 43;
+
+	rc = dispcfg_get_seat_list(dispcfg, &list);
+	PCUT_ASSERT_TRUE(resp.get_seat_list_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	PCUT_ASSERT_INT_EQUALS(2, list->nseats);
+	PCUT_ASSERT_INT_EQUALS(42, list->seats[0]);
+	PCUT_ASSERT_INT_EQUALS(43, list->seats[1]);
+
+	dispcfg_free_seat_list(list);
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_get_seat_infp() with server returning error response works */
+PCUT_TEST(get_seat_info_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	dispcfg_seat_info_t *info;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	resp.rc = ENOMEM;
+	resp.get_seat_info_called = false;
+	seat_id = 1;
+
+	rc = dispcfg_get_seat_info(dispcfg, seat_id, &info);
+	PCUT_ASSERT_TRUE(resp.get_seat_info_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.get_seat_info_seat_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_get_seat_info() with server returning success response works */
+PCUT_TEST(get_seat_info_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	dispcfg_seat_info_t *info;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	resp.rc = EOK;
+	resp.get_seat_info_called = false;
+	resp.get_seat_info_rinfo = calloc(1, sizeof(dispcfg_seat_info_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_seat_info_rinfo);
+	resp.get_seat_info_rinfo->name = str_dup("Hello");
+	PCUT_ASSERT_NOT_NULL(resp.get_seat_info_rinfo->name);
+	seat_id = 1;
+
+	rc = dispcfg_get_seat_info(dispcfg, seat_id, &info);
+	PCUT_ASSERT_TRUE(resp.get_seat_info_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.get_seat_info_seat_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	PCUT_ASSERT_STR_EQUALS("Hello", info->name);
+
+	dispcfg_free_seat_info(info);
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_seat_create() with server returning error response works */
+PCUT_TEST(seat_create_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	seat_id = 13;
+	seat_id = 42;
+	resp.rc = ENOMEM;
+	resp.seat_create_called = false;
+
+	rc = dispcfg_seat_create(dispcfg, "Alice", &seat_id);
+	PCUT_ASSERT_TRUE(resp.seat_create_called);
+	PCUT_ASSERT_STR_EQUALS("Alice", resp.seat_create_name);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+	free(resp.seat_create_name);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_seat_create() with server returning success response works */
+PCUT_TEST(seat_create_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	resp.rc = EOK;
+	resp.seat_create_called = false;
+	resp.seat_create_seat_id = 42;
+
+	rc = dispcfg_seat_create(dispcfg, "Alice", &seat_id);
+	PCUT_ASSERT_TRUE(resp.seat_create_called);
+	PCUT_ASSERT_STR_EQUALS("Alice", resp.seat_create_name);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.seat_create_seat_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+	free(resp.seat_create_name);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_seat_delete() with server returning error response works */
+PCUT_TEST(seat_delete_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	seat_id = 42;
+	resp.rc = ENOMEM;
+	resp.seat_delete_called = false;
+
+	rc = dispcfg_seat_delete(dispcfg, seat_id);
+	PCUT_ASSERT_TRUE(resp.seat_delete_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.seat_delete_seat_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** dispcfg_seat_delete() with server returning success response works */
+PCUT_TEST(seat_delete_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	sysarg_t seat_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, NULL, NULL, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+
+	seat_id = 42;
+	resp.rc = EOK;
+	resp.seat_delete_called = false;
+
+	rc = dispcfg_seat_delete(dispcfg, seat_id);
+	PCUT_ASSERT_TRUE(resp.seat_delete_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.seat_delete_seat_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	dispcfg_close(dispcfg);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Window added event can be delivered from server to client callback function */
+PCUT_TEST(seat_added_deliver)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, &test_dispcfg_cb, &resp, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+	PCUT_ASSERT_NOT_NULL(resp.srv);
+
+	resp.event_cnt = 1;
+	resp.event.etype = dcev_seat_added;
+	resp.event.seat_id = 42;
+	resp.seat_added_called = false;
+	fibril_mutex_initialize(&resp.event_lock);
+	fibril_condvar_initialize(&resp.event_cv);
+	dispcfg_srv_ev_pending(resp.srv);
+
+	/* Wait for the event handler to be called. */
+	fibril_mutex_lock(&resp.event_lock);
+	while (!resp.seat_added_called) {
+		fibril_condvar_wait(&resp.event_cv, &resp.event_lock);
+	}
+	fibril_mutex_unlock(&resp.event_lock);
+
+	/* Verify that the event was delivered correctly */
+	PCUT_ASSERT_INT_EQUALS(resp.event.etype,
+	    resp.revent.etype);
+
+	dispcfg_close(dispcfg);
+
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Window removed event can be delivered from server to client callback function */
+PCUT_TEST(seat_removed_deliver)
+{
+	errno_t rc;
+	service_id_t sid;
+	dispcfg_t *dispcfg = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_dispcfg_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_dispcfg_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_dispcfg_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = dispcfg_open(test_dispcfg_svc, &test_dispcfg_cb, &resp, &dispcfg);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(dispcfg);
+	PCUT_ASSERT_NOT_NULL(resp.srv);
+
+	resp.event_cnt = 1;
+	resp.event.etype = dcev_seat_removed;
+	resp.event.seat_id = 42;
+	resp.seat_removed_called = false;
+	fibril_mutex_initialize(&resp.event_lock);
+	fibril_condvar_initialize(&resp.event_cv);
+	dispcfg_srv_ev_pending(resp.srv);
+
+	/* Wait for the event handler to be called. */
+	fibril_mutex_lock(&resp.event_lock);
+	while (!resp.seat_removed_called) {
+		fibril_condvar_wait(&resp.event_cv, &resp.event_lock);
+	}
+	fibril_mutex_unlock(&resp.event_lock);
+
+	/* Verify that the event was delivered correctly */
+	PCUT_ASSERT_INT_EQUALS(resp.event.etype,
+	    resp.revent.etype);
+
+	dispcfg_close(dispcfg);
+
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Test seat management service connection. */
+static void test_dispcfg_conn(ipc_call_t *icall, void *arg)
+{
+	test_response_t *resp = (test_response_t *) arg;
+	dispcfg_srv_t srv;
+
+	/* Set up protocol structure */
+	dispcfg_srv_initialize(&srv);
+	srv.ops = &test_dispcfg_srv_ops;
+	srv.arg = arg;
+	resp->srv = &srv;
+
+	/* Handle connection */
+	dispcfg_conn(icall, &srv);
+
+	resp->srv = NULL;
+}
+
+static void test_seat_added(void *arg, sysarg_t seat_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->revent.etype = dcev_seat_added;
+
+	fibril_mutex_lock(&resp->event_lock);
+	resp->seat_added_called = true;
+	resp->seat_added_seat_id = seat_id;
+	fibril_condvar_broadcast(&resp->event_cv);
+	fibril_mutex_unlock(&resp->event_lock);
+}
+
+static void test_seat_removed(void *arg, sysarg_t seat_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->revent.etype = dcev_seat_removed;
+
+	fibril_mutex_lock(&resp->event_lock);
+	resp->seat_removed_called = true;
+	resp->seat_removed_seat_id = seat_id;
+	fibril_condvar_broadcast(&resp->event_cv);
+	fibril_mutex_unlock(&resp->event_lock);
+}
+
+static errno_t test_get_seat_list(void *arg, dispcfg_seat_list_t **rlist)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->get_seat_list_called = true;
+
+	if (resp->rc != EOK)
+		return resp->rc;
+
+	*rlist = resp->get_seat_list_rlist;
+	return EOK;
+}
+
+static errno_t test_get_seat_info(void *arg, sysarg_t seat_id,
+    dispcfg_seat_info_t **rinfo)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->get_seat_info_called = true;
+	resp->get_seat_info_seat_id = seat_id;
+
+	if (resp->rc != EOK)
+		return resp->rc;
+
+	*rinfo = resp->get_seat_info_rinfo;
+	return EOK;
+}
+
+static errno_t test_seat_create(void *arg, const char *name, sysarg_t *rseat_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->seat_create_called = true;
+	resp->seat_create_name = str_dup(name);
+	*rseat_id = resp->seat_create_seat_id;
+	return resp->rc;
+}
+
+static errno_t test_seat_delete(void *arg, sysarg_t seat_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->seat_delete_called = true;
+	resp->seat_delete_seat_id = seat_id;
+	return resp->rc;
+}
+
+static errno_t test_get_event(void *arg, dispcfg_ev_t *event)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->get_event_called = true;
+	if (resp->event_cnt > 0) {
+		--resp->event_cnt;
+		*event = resp->event;
+		return EOK;
+	}
+
+	return ENOENT;
+}
+
+PCUT_EXPORT(dispcfg);
Index: uspace/lib/dispcfg/test/main.c
===================================================================
--- uspace/lib/dispcfg/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/dispcfg/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <pcut/pcut.h>
+
+PCUT_INIT;
+
+PCUT_IMPORT(dispcfg);
+
+PCUT_MAIN();
Index: uspace/lib/meson.build
===================================================================
--- uspace/lib/meson.build	(revision ffed09d79c4f737ed0a49581161c8c053ce34980)
+++ uspace/lib/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -102,4 +102,5 @@
 	'ieee80211',
 	'ddev',
+	'dispcfg',
 	'display',
 	'wndmgt',
