Index: lang-format
===================================================================
--- .clang-format	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ 	(revision )
@@ -1,122 +1,0 @@
-# This format definition attempts to be as close to the style of existing
-# HelenOS code as possible. However, there are several significant issues:
-#
-#    "AlignEscapedNewlines: DontAlign" currently hangs
-#        the formatter on some files.
-#
-#    The formatter currently isn't able to align macro definitions,
-#        and removes existing alignment in files.
-#        (There is a patch for this, but it hasn't been integrated yet.)
-#
-#    SpacesBeforeTrailingComments don't apply to C-style comments.
-#
-#    enum definition is always crammed into a single line if it fits,
-#        and there is no option to put members on separate lines.
-#
-#    Possibly others.
-#
-#
-# Provisions are made to reformat the entire source tree via 'make format',
-# but due to the above, this shouldn't be done except to evaluate the
-# impact. Reformatting a single source file can be done using
-#
-#     clang-format -i source/file.c
-#
----
-Language:        Cpp
-BasedOnStyle:  LLVM
-AccessModifierOffset: 0
-AlignAfterOpenBracket: DontAlign
-AlignConsecutiveAssignments: false
-AlignConsecutiveDeclarations: false
-AlignEscapedNewlines: Left
-AlignOperands:   false
-AlignTrailingComments: true
-AllowAllParametersOfDeclarationOnNextLine: true
-AllowShortBlocksOnASingleLine: false
-AllowShortCaseLabelsOnASingleLine: false
-AllowShortFunctionsOnASingleLine: None
-AllowShortIfStatementsOnASingleLine: false
-AllowShortLoopsOnASingleLine: false
-AlwaysBreakAfterDefinitionReturnType: None
-AlwaysBreakAfterReturnType: None
-AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: true
-BinPackArguments: true
-BinPackParameters: true
-BraceWrapping:
-  AfterClass:      false
-  AfterControlStatement: false
-  AfterEnum:       false
-  AfterFunction:   true
-  AfterNamespace:  false
-  AfterObjCDeclaration: false
-  AfterStruct:     false
-  AfterUnion:      false
-  BeforeCatch:     false
-  BeforeElse:      false
-  IndentBraces:    false
-  SplitEmptyFunction: true
-  SplitEmptyRecord: true
-  SplitEmptyNamespace: true
-BreakBeforeBinaryOperators: NonAssignment
-BreakBeforeBraces: Custom
-BreakBeforeInheritanceComma: true
-BreakBeforeTernaryOperators: true
-BreakConstructorInitializers: BeforeColon
-BreakStringLiterals: true
-ColumnLimit:     80
-CompactNamespaces: false
-ConstructorInitializerAllOnOneLineOrOnePerLine: false
-ConstructorInitializerIndentWidth: 4
-ContinuationIndentWidth: 4
-Cpp11BracedListStyle: true
-DerivePointerAlignment: false
-FixNamespaceComments: true
-ForEachMacros:
-  - foreach
-  - Q_FOREACH
-  - BOOST_FOREACH
-IncludeCategories:
-  - Regex:           '^"'
-    Priority:        3
-  - Regex:           '^<'
-    Priority:        2
-  - Regex:           '.*'
-    Priority:        1
-IncludeIsMainRegex: '(-test)?$'
-IndentCaseLabels: false
-IndentWidth:     8
-IndentWrappedFunctionNames: false
-KeepEmptyLinesAtTheStartOfBlocks: true
-MacroBlockBegin: ''
-MacroBlockEnd:   ''
-MaxEmptyLinesToKeep: 1
-NamespaceIndentation: None
-PenaltyBreakAssignment: 2
-PenaltyBreakBeforeFirstCallParameter: 19
-PenaltyBreakComment: 300
-PenaltyBreakFirstLessLess: 120
-PenaltyBreakString: 1000
-PenaltyExcessCharacter: 1000000
-PenaltyReturnTypeOnItsOwnLine: 60
-PointerAlignment: Right
-ReflowComments:  true
-SortIncludes:    true
-SortUsingDeclarations: true
-SpaceAfterCStyleCast: true
-SpaceAfterTemplateKeyword: false
-SpaceBeforeAssignmentOperators: true
-SpaceBeforeParens: ControlStatements
-SpaceInEmptyParentheses: false
-SpacesBeforeTrailingComments: 2
-SpacesInAngles:  false
-SpacesInContainerLiterals: false
-SpacesInCStyleCastParentheses: false
-SpacesInParentheses: false
-SpacesInSquareBrackets: false
-Standard:        Cpp11
-TabWidth:        8
-UseTab:          ForIndentation
-...
-
Index: .gitignore
===================================================================
--- .gitignore	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ .gitignore	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -4,4 +4,5 @@
 /doxygen/latex
 /tools/*.pyc
+/tools/__pycache__
 tags
 tools/sycek/
Index: abi/include/abi/ipc/interfaces.h
===================================================================
--- abi/include/abi/ipc/interfaces.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ abi/include/abi/ipc/interfaces.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2023 Jiri Svoboda
  * Copyright (c) 2014 Martin Decky
  * All rights reserved.
@@ -186,4 +187,8 @@
 	INTERFACE_DDEV =
 	    FOURCC_COMPACT('d', 'd', 'e', 'v') | IFACE_EXCHANGE_SERIALIZE,
+	INTERFACE_DISPCFG =
+	    FOURCC_COMPACT('d', 'c', 'f', 'g') | IFACE_EXCHANGE_SERIALIZE,
+	INTERFACE_DISPCFG_CB =
+	    FOURCC_COMPACT('d', 'c', 'f', 'g') | IFACE_EXCHANGE_SERIALIZE | IFACE_MOD_CALLBACK,
 	INTERFACE_DISPLAY =
 	    FOURCC_COMPACT('d', 's', 'p', 'l') | IFACE_EXCHANGE_SERIALIZE,
@@ -192,4 +197,8 @@
 	INTERFACE_GC =
 	    FOURCC_COMPACT('g', 'f', 'x', 'c') | IFACE_EXCHANGE_SERIALIZE,
+	INTERFACE_WNDMGT =
+	    FOURCC_COMPACT('w', 'm', 'g', 't') | IFACE_EXCHANGE_SERIALIZE,
+	INTERFACE_WNDMGT_CB =
+	    FOURCC_COMPACT('w', 'm', 'g', 't') | IFACE_EXCHANGE_SERIALIZE | IFACE_MOD_CALLBACK,
 } iface_t;
 
Index: uspace/app/barber/barber.c
===================================================================
--- uspace/app/barber/barber.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/app/barber/barber.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -361,5 +361,5 @@
 
 	ui_wnd_params_init(&params);
-	params.caption = "";
+	params.caption = "Barber Pole";
 	params.placement = ui_wnd_place_bottom_right;
 	/*
Index: uspace/app/gfxdemo/gfxdemo.c
===================================================================
--- uspace/app/gfxdemo/gfxdemo.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/app/gfxdemo/gfxdemo.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -244,5 +244,5 @@
 
 		pos.x = w / 2;
-		pos.y = h - 1;
+		pos.y = h;
 		rc = gfx_puttext(&pos, &fmt, text);
 		if (rc != EOK) {
@@ -801,4 +801,85 @@
 }
 
+/** Run text abbreviation demo on a graphic context.
+ *
+ * @param gc Graphic context
+ * @param w Width
+ * @param h Height
+ */
+static errno_t demo_text_abbr(gfx_context_t *gc, gfx_coord_t w, gfx_coord_t h)
+{
+	gfx_color_t *color = NULL;
+	gfx_rect_t rect;
+	gfx_coord2_t pos;
+	gfx_text_fmt_t fmt;
+	int i;
+	errno_t rc;
+
+	if (quit)
+		return EOK;
+
+	rc = demo_begin(gc, w, h, "Text abbreviation");
+	if (rc != EOK)
+		goto error;
+
+	for (i = 0; i < 11; i++) {
+
+		rc = gfx_color_new_rgb_i16(0, 0, 0x8000, &color);
+		if (rc != EOK)
+			goto error;
+
+		rc = gfx_set_color(gc, color);
+		if (rc != EOK)
+			goto error;
+
+		rect.p0.x = w / 20;
+		rect.p0.y = (2 + 2 * i) * h / 25;
+		rect.p1.x = w - w / 20 - w * i / 12;
+		rect.p1.y = (3 + 2 * i) * h / 25;
+
+		rc = gfx_fill_rect(gc, &rect);
+		if (rc != EOK)
+			goto error;
+
+		gfx_color_delete(color);
+
+		if (demo_is_text(w, h)) {
+			rc = gfx_color_new_ega(0x1f, &color);
+			if (rc != EOK)
+				goto error;
+		} else {
+			rc = gfx_color_new_rgb_i16(0xffff, 0xffff, 0xffff,
+			    &color);
+			if (rc != EOK)
+				goto error;
+		}
+
+		gfx_text_fmt_init(&fmt);
+		fmt.font = font;
+		fmt.color = color;
+		fmt.abbreviate = true;
+		fmt.width = rect.p1.x - rect.p0.x;
+
+		pos.x = rect.p0.x;
+		pos.y = rect.p0.y;
+		rc = gfx_puttext(&pos, &fmt,
+		    "The quick brow fox jumps over the lazy dog!");
+		if (rc != EOK) {
+			printf("Error rendering text.\n");
+			goto error;
+		}
+	}
+
+	for (i = 0; i < 10; i++) {
+		fibril_usleep(500 * 1000);
+		if (quit)
+			break;
+	}
+
+	return EOK;
+error:
+	return rc;
+}
+
 /** Run clipping demo on a graphic context.
  *
@@ -933,4 +1014,8 @@
 
 		rc = demo_text(gc, w, h);
+		if (rc != EOK)
+			goto error;
+
+		rc = demo_text_abbr(gc, w, h);
 		if (rc != EOK)
 			goto error;
@@ -1069,4 +1154,5 @@
 	params.rect.p1.x = 400;
 	params.rect.p1.y = 300;
+	params.caption = "GFX Demo";
 
 	rc = display_window_create(display, &params, &wnd_cb, NULL, &window);
Index: uspace/app/init/init.c
===================================================================
--- uspace/app/init/init.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/app/init/init.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -477,5 +477,5 @@
 		if (rc == EOK) {
 			app_start("/app/launcher", NULL);
-			app_start("/app/barber", NULL);
+			app_start("/app/taskbar", NULL);
 			app_start("/app/terminal", "-topleft");
 		}
Index: uspace/app/meson.build
===================================================================
--- uspace/app/meson.build	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/app/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -79,4 +79,5 @@
 	'sysinfo',
 	'sysinst',
+	'taskbar',
 	'taskdump',
 	'terminal',
Index: uspace/app/taskbar/clock.c
===================================================================
--- uspace/app/taskbar/clock.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/clock.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,341 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/** @file Task bar clock.
+ *
+ * Displays the current time in an inset frame.
+ */
+
+#include <errno.h>
+#include <gfx/render.h>
+#include <gfx/text.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <task.h>
+#include <ui/control.h>
+#include <ui/paint.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include "clock.h"
+
+static void taskbar_clock_ctl_destroy(void *);
+static errno_t taskbar_clock_ctl_paint(void *);
+static ui_evclaim_t taskbar_clock_ctl_kbd_event(void *, kbd_event_t *);
+static ui_evclaim_t taskbar_clock_ctl_pos_event(void *, pos_event_t *);
+static void taskbar_clock_timer(void *);
+
+/** Task bar clock control ops */
+static ui_control_ops_t taskbar_clock_ctl_ops = {
+	.destroy = taskbar_clock_ctl_destroy,
+	.paint = taskbar_clock_ctl_paint,
+	.kbd_event = taskbar_clock_ctl_kbd_event,
+	.pos_event = taskbar_clock_ctl_pos_event
+};
+
+/** Create task bar clock.
+ *
+ * @param window Containing window
+ * @param rclock Place to store pointer to new clock
+ * @return EOK on success or an error code
+ */
+errno_t taskbar_clock_create(ui_window_t *window, taskbar_clock_t **rclock)
+{
+	taskbar_clock_t *clock;
+	errno_t rc;
+
+	clock = calloc(1, sizeof(taskbar_clock_t));
+	if (clock == NULL)
+		return ENOMEM;
+
+	rc = ui_control_new(&taskbar_clock_ctl_ops, (void *)clock,
+	    &clock->control);
+	if (rc != EOK) {
+		free(clock);
+		return rc;
+	}
+
+	clock->timer = fibril_timer_create(NULL);
+	if (clock->timer == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	fibril_mutex_initialize(&clock->lock);
+	fibril_condvar_initialize(&clock->timer_done_cv);
+	fibril_timer_set(clock->timer, 1000000, taskbar_clock_timer, clock);
+
+	clock->window = window;
+	*rclock = clock;
+	return EOK;
+error:
+	ui_control_delete(clock->control);
+	free(clock);
+	return rc;
+}
+
+/** Destroy task bar clock.
+ *
+ * @param clock Task bar clock
+ */
+void taskbar_clock_destroy(taskbar_clock_t *clock)
+{
+	/*
+	 * Signal to the timer that we are cleaning up. If the timer handler
+	 * misses it and sets the timer again, we will clear that active
+	 * timer and be done (and if we were even slower and the timer
+	 * fired again, it's the same situation as before.
+	 */
+	fibril_mutex_lock(&clock->lock);
+	clock->timer_cleanup = true;
+	fibril_mutex_unlock(&clock->lock);
+
+	/* If we catch the timer while it's active, there's nothing to do. */
+	if (fibril_timer_clear(clock->timer) != fts_active) {
+		/* Need to wait for timer handler to finish */
+		fibril_mutex_lock(&clock->lock);
+		while (clock->timer_done == false)
+			fibril_condvar_wait(&clock->timer_done_cv, &clock->lock);
+		fibril_mutex_unlock(&clock->lock);
+	}
+
+	fibril_timer_destroy(clock->timer);
+	ui_control_delete(clock->control);
+	free(clock);
+}
+
+static errno_t taskbar_clock_get_text(taskbar_clock_t *clock, char *buf,
+    size_t bsize)
+{
+	struct timespec ts;
+	struct tm tm;
+	errno_t rc;
+
+	getrealtime(&ts);
+	rc = time_utc2tm(ts.tv_sec, &tm);
+	if (rc != EOK)
+		return rc;
+
+	snprintf(buf, bsize, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min,
+	    tm.tm_sec);
+	return EOK;
+}
+
+/** Paint task bar clock.
+ *
+ * @param clock Task bar clock
+ */
+errno_t taskbar_clock_paint(taskbar_clock_t *clock)
+{
+	gfx_context_t *gc = ui_window_get_gc(clock->window);
+	ui_resource_t *res = ui_window_get_res(clock->window);
+	ui_t *ui = ui_window_get_ui(clock->window);
+	char buf[10];
+	gfx_text_fmt_t fmt;
+	gfx_coord2_t pos;
+	gfx_rect_t irect;
+	errno_t rc;
+
+	if (!ui_is_textmode(ui)) {
+		/* Paint frame */
+		rc = ui_paint_inset_frame(res, &clock->rect, &irect);
+		if (rc != EOK)
+			goto error;
+	} else {
+		irect = clock->rect;
+	}
+
+	rc = gfx_set_color(gc, ui_resource_get_wnd_face_color(res));
+	if (rc != EOK)
+		goto error;
+
+	/* Fill background */
+	rc = gfx_fill_rect(gc, &irect);
+	if (rc != EOK)
+		goto error;
+
+	pos.x = (irect.p0.x + irect.p1.x) / 2;
+	pos.y = (irect.p0.y + irect.p1.y) / 2;
+
+	gfx_text_fmt_init(&fmt);
+	fmt.font = ui_resource_get_font(res);
+	fmt.color = ui_resource_get_wnd_text_color(res);
+	fmt.halign = gfx_halign_center;
+	fmt.valign = gfx_valign_center;
+
+	rc = taskbar_clock_get_text(clock, buf, sizeof(buf));
+	if (rc != EOK)
+		goto error;
+
+	rc = gfx_puttext(&pos, &fmt, buf);
+	if (rc != EOK)
+		goto error;
+
+	rc = gfx_update(gc);
+	if (rc != EOK)
+		goto error;
+
+	return EOK;
+error:
+	return rc;
+}
+
+/** Handle task bar clock keyboard event.
+ *
+ * @param clock Task bar clock
+ * @param event Keyboard event
+ * @return ui_claimed iff event was claimed
+ */
+ui_evclaim_t taskbar_clock_kbd_event(taskbar_clock_t *clock, kbd_event_t *event)
+{
+	return ui_unclaimed;
+}
+
+/** Handle task bar clock position event.
+ *
+ * @param clock Task bar clock
+ * @param event Position event
+ * @return ui_claimed iff event was claimed
+ */
+ui_evclaim_t taskbar_clock_pos_event(taskbar_clock_t *clock, pos_event_t *event)
+{
+	gfx_coord2_t pos;
+
+	pos.x = event->hpos;
+	pos.y = event->vpos;
+	if (!gfx_pix_inside_rect(&pos, &clock->rect))
+		return ui_unclaimed;
+
+	return ui_claimed;
+}
+
+/** Get base control for task bar clock.
+ *
+ * @param clock Task bar clock
+ * @return Base UI control
+ */
+ui_control_t *taskbar_clock_ctl(taskbar_clock_t *clock)
+{
+	return clock->control;
+}
+
+/** Set task bar clock rectangle.
+ *
+ * @param clock Task bar clock
+ * @param rect Rectangle
+ */
+void taskbar_clock_set_rect(taskbar_clock_t *clock, gfx_rect_t *rect)
+{
+	gfx_rect_t irect;
+
+	clock->rect = *rect;
+
+	irect.p0.x = clock->rect.p0.x + 1;
+	irect.p0.y = clock->rect.p0.y + 1;
+	irect.p1.x = clock->rect.p1.x;
+	irect.p1.y = clock->rect.p1.y - 1;
+
+	(void)irect;
+}
+
+/** Destroy clock control.
+ *
+ * @param arg Argument (taskbar_clock_t *)
+ */
+void taskbar_clock_ctl_destroy(void *arg)
+{
+	taskbar_clock_t *clock = (taskbar_clock_t *) arg;
+
+	taskbar_clock_destroy(clock);
+}
+
+/** Paint task bar clock control.
+ *
+ * @param arg Argument (taskbar_clock_t *)
+ * @return EOK on success or an error code
+ */
+errno_t taskbar_clock_ctl_paint(void *arg)
+{
+	taskbar_clock_t *clock = (taskbar_clock_t *) arg;
+
+	return taskbar_clock_paint(clock);
+}
+
+/** Handle task bar clock control keyboard event.
+ *
+ * @param arg Argument (taskbar_clock_t *)
+ * @param kbd_event Keyboard event
+ * @return @c ui_claimed iff the event is claimed
+ */
+ui_evclaim_t taskbar_clock_ctl_kbd_event(void *arg, kbd_event_t *event)
+{
+	taskbar_clock_t *clock = (taskbar_clock_t *) arg;
+
+	return taskbar_clock_kbd_event(clock, event);
+}
+
+/** Handle task bar clock control position event.
+ *
+ * @param arg Argument (taskbar_clock_t *)
+ * @param pos_event Position event
+ * @return @c ui_claimed iff the event is claimed
+ */
+ui_evclaim_t taskbar_clock_ctl_pos_event(void *arg, pos_event_t *event)
+{
+	taskbar_clock_t *clock = (taskbar_clock_t *) arg;
+
+	return taskbar_clock_pos_event(clock, event);
+}
+
+/** Taskbar clock timer handler.
+ *
+ * @param arg Argument (taskbar_clock_t *)
+ */
+static void taskbar_clock_timer(void *arg)
+{
+	taskbar_clock_t *clock = (taskbar_clock_t *) arg;
+
+	fibril_mutex_lock(&clock->lock);
+	(void) taskbar_clock_paint(clock);
+
+	if (!clock->timer_cleanup) {
+		fibril_timer_set(clock->timer, 1000000, taskbar_clock_timer,
+		    clock);
+	} else {
+		/* Acknowledge timer cleanup */
+		clock->timer_done = true;
+		fibril_condvar_signal(&clock->timer_done_cv);
+	}
+
+	fibril_mutex_unlock(&clock->lock);
+}
+
+/** @}
+ */
Index: uspace/app/taskbar/clock.h
===================================================================
--- uspace/app/taskbar/clock.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/clock.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/**
+ * @file Task bar clock
+ */
+
+#ifndef CLOCK_H
+#define CLOCK_H
+
+#include <errno.h>
+#include <gfx/coord.h>
+#include <io/kbd_event.h>
+#include <io/pos_event.h>
+#include <ui/control.h>
+#include <ui/window.h>
+#include <stdbool.h>
+#include "types/clock.h"
+
+extern errno_t taskbar_clock_create(ui_window_t *, taskbar_clock_t **);
+extern void taskbar_clock_destroy(taskbar_clock_t *);
+extern errno_t taskbar_clock_paint(taskbar_clock_t *);
+extern ui_evclaim_t taskbar_clock_kbd_event(taskbar_clock_t *, kbd_event_t *);
+extern ui_evclaim_t taskbar_clock_pos_event(taskbar_clock_t *, pos_event_t *);
+extern ui_control_t *taskbar_clock_ctl(taskbar_clock_t *);
+extern void taskbar_clock_set_rect(taskbar_clock_t *, gfx_rect_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/app/taskbar/doc/doxygroups.h
===================================================================
--- uspace/app/taskbar/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,4 @@
+/** @addtogroup taskbar taskbar
+ * @brief Task bar
+ * @ingroup apps
+ */
Index: uspace/app/taskbar/main.c
===================================================================
--- uspace/app/taskbar/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/** @file Task Bar
+ */
+
+#include <stdio.h>
+#include <str.h>
+#include <ui/ui.h>
+#include "taskbar.h"
+
+static void print_syntax(void)
+{
+	printf("Syntax: taskbar [-d <display-spec>]\n");
+}
+
+int main(int argc, char *argv[])
+{
+	const char *display_spec = UI_ANY_DEFAULT;
+	const char *wndmgt_svc = WNDMGT_DEFAULT;
+	taskbar_t *taskbar;
+	errno_t rc;
+	int i;
+
+	i = 1;
+	while (i < argc && argv[i][0] == '-') {
+		if (str_cmp(argv[i], "-d") == 0) {
+			++i;
+			if (i >= argc) {
+				printf("Argument missing.\n");
+				print_syntax();
+				return 1;
+			}
+
+			display_spec = argv[i++];
+		} else if (str_cmp(argv[i], "-w") == 0) {
+			++i;
+			if (i >= argc) {
+				printf("Argument missing.\n");
+				print_syntax();
+				return 1;
+			}
+
+			wndmgt_svc = argv[i++];
+		} else {
+			printf("Invalid option '%s'.\n", argv[i]);
+			print_syntax();
+			return 1;
+		}
+	}
+
+	if (i < argc) {
+		print_syntax();
+		return 1;
+	}
+
+	rc = taskbar_create(display_spec, wndmgt_svc, &taskbar);
+	if (rc != EOK)
+		return 1;
+
+	ui_run(taskbar->ui);
+	taskbar_destroy(taskbar);
+
+	return 0;
+}
+
+/** @}
+ */
Index: uspace/app/taskbar/meson.build
===================================================================
--- uspace/app/taskbar/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2022 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.
+#
+
+deps = [ 'ui', 'wndmgt' ]
+src = files(
+	'clock.c',
+	'main.c',
+	'taskbar.c',
+	'wndlist.c',
+)
+
+test_src = files(
+	'clock.c',
+	'taskbar.c',
+	'wndlist.c',
+	'test/clock.c',
+	'test/main.c',
+	'test/taskbar.c',
+	'test/wndlist.c',
+)
Index: uspace/app/taskbar/taskbar.c
===================================================================
--- uspace/app/taskbar/taskbar.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/taskbar.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/** @file Task Bar
+ */
+
+#include <gfx/coord.h>
+#include <io/pos_event.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <str.h>
+#include <ui/fixed.h>
+#include <ui/label.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include <ui/window.h>
+#include <wndmgt.h>
+#include "clock.h"
+#include "taskbar.h"
+#include "wndlist.h"
+
+static void taskbar_wnd_close(ui_window_t *, void *);
+static void taskbar_wnd_pos(ui_window_t *, void *, pos_event_t *);
+
+static ui_window_cb_t window_cb = {
+	.close = taskbar_wnd_close,
+	.pos = taskbar_wnd_pos
+};
+
+/** Window close button was clicked.
+ *
+ * @param window Window
+ * @param arg Argument (taskbar)
+ */
+static void taskbar_wnd_close(ui_window_t *window, void *arg)
+{
+	taskbar_t *taskbar = (taskbar_t *) arg;
+
+	ui_quit(taskbar->ui);
+}
+
+/** Window received position event.
+ *
+ * @param window Window
+ * @param arg Argument (taskbar)
+ * @param event Position event
+ */
+static void taskbar_wnd_pos(ui_window_t *window, void *arg, pos_event_t *event)
+{
+	taskbar_t *taskbar = (taskbar_t *) arg;
+
+	/* Remember ID of device that sent the last event */
+	taskbar->wndlist->ev_pos_id = event->pos_id;
+
+	ui_window_def_pos(window, event);
+}
+
+/** Create task bar.
+ *
+ * @param display_spec Display specification
+ * @param wndmgt_svc Window management service (or WNDMGT_DEFAULT)
+ * @param rtaskbar Place to store pointer to new task bar
+ * @return @c EOK on success or an error coe
+ */
+errno_t taskbar_create(const char *display_spec, const char *wndmgt_svc,
+    taskbar_t **rtaskbar)
+{
+	ui_wnd_params_t params;
+	taskbar_t *taskbar = NULL;
+	gfx_rect_t scr_rect;
+	gfx_rect_t rect;
+	ui_resource_t *ui_res;
+	errno_t rc;
+
+	taskbar = calloc(1, sizeof(taskbar_t));
+	if (taskbar == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	rc = ui_create(display_spec, &taskbar->ui);
+	if (rc != EOK) {
+		printf("Error creating UI on display %s.\n", display_spec);
+		goto error;
+	}
+
+	rc = ui_get_rect(taskbar->ui, &scr_rect);
+	if (rc != EOK) {
+		if (str_cmp(display_spec, UI_DISPLAY_NULL) != 0) {
+			printf("Error getting screen dimensions.\n");
+			goto error;
+		}
+
+		/* For the sake of unit tests */
+		scr_rect.p0.x = 0;
+		scr_rect.p0.y = 0;
+		scr_rect.p1.x = 100;
+		scr_rect.p1.y = 100;
+	}
+
+	ui_wnd_params_init(&params);
+	params.caption = "Task Bar";
+	params.placement = ui_wnd_place_bottom_left;
+
+	/* Window has no titlebar */
+	params.style &= ~ui_wds_titlebar;
+
+	/* Window is not obscured by other windows */
+	params.flags |= ui_wndf_topmost;
+
+	/* Prevent taskbar window from being listed in taskbar */
+	params.flags |= ui_wndf_system;
+
+	/* Make maximized windows avoid taskbar */
+	params.flags |= ui_wndf_avoid;
+
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p1.x = scr_rect.p1.x - scr_rect.p0.x;
+
+	if (ui_is_textmode(taskbar->ui)) {
+		params.rect.p1.y = 1;
+		params.style &= ~ui_wds_frame;
+	} else {
+		params.rect.p1.y = 32;
+	}
+
+	rc = ui_window_create(taskbar->ui, &params, &taskbar->window);
+	if (rc != EOK) {
+		printf("Error creating window.\n");
+		goto error;
+	}
+
+	ui_window_set_cb(taskbar->window, &window_cb, (void *)taskbar);
+	ui_res = ui_window_get_res(taskbar->window);
+
+	rc = ui_fixed_create(&taskbar->fixed);
+	if (rc != EOK) {
+		printf("Error creating fixed layout.\n");
+		goto error;
+	}
+
+	rc = ui_label_create(ui_res, "HelenOS", &taskbar->label);
+	if (rc != EOK) {
+		printf("Error creating label.\n");
+		goto error;
+	}
+
+	ui_window_get_app_rect(taskbar->window, &rect);
+	if (ui_is_textmode(taskbar->ui)) {
+		rect.p0.x += 1;
+	} else {
+		rect.p0.x += 10;
+	}
+	ui_label_set_rect(taskbar->label, &rect);
+	ui_label_set_halign(taskbar->label, gfx_halign_left);
+	ui_label_set_valign(taskbar->label, gfx_valign_center);
+
+	rc = ui_fixed_add(taskbar->fixed, ui_label_ctl(taskbar->label));
+	if (rc != EOK) {
+		printf("Error adding control to layout.\n");
+		ui_label_destroy(taskbar->label);
+		goto error;
+	}
+
+	rc = wndlist_create(taskbar->window, taskbar->fixed, &taskbar->wndlist);
+	if (rc != EOK) {
+		printf("Error creating window list.\n");
+		goto error;
+	}
+
+	if (ui_is_textmode(taskbar->ui)) {
+		rect.p0.x = params.rect.p0.x + 9;
+		rect.p0.y = 0;
+		rect.p1.x = params.rect.p1.x - 10;
+		rect.p1.y = 1;
+	} else {
+		rect.p0.x = params.rect.p0.x + 90;
+		rect.p0.y = 4;
+		rect.p1.x = params.rect.p1.x - 84;
+		rect.p1.y = 32 - 4;
+	}
+	wndlist_set_rect(taskbar->wndlist, &rect);
+
+	rc = wndlist_open_wm(taskbar->wndlist, wndmgt_svc);
+	if (rc != EOK) {
+		printf("Error attaching window management service.\n");
+		goto error;
+	}
+
+	rc = taskbar_clock_create(taskbar->window, &taskbar->clock);
+	if (rc != EOK)
+		goto error;
+
+	if (ui_is_textmode(taskbar->ui)) {
+		rect.p0.x = params.rect.p1.x - 10;
+		rect.p0.y = 0;
+		rect.p1.x = params.rect.p1.x;
+		rect.p1.y = 1;
+	} else {
+		rect.p0.x = params.rect.p1.x - 80;
+		rect.p0.y = 4;
+		rect.p1.x = params.rect.p1.x - 4;
+		rect.p1.y = 32 - 4;
+	}
+
+	taskbar_clock_set_rect(taskbar->clock, &rect);
+
+	rc = ui_fixed_add(taskbar->fixed, taskbar_clock_ctl(taskbar->clock));
+	if (rc != EOK) {
+		printf("Error adding control to layout.\n");
+		taskbar_clock_destroy(taskbar->clock);
+		goto error;
+	}
+
+	ui_window_add(taskbar->window, ui_fixed_ctl(taskbar->fixed));
+
+	rc = ui_window_paint(taskbar->window);
+	if (rc != EOK) {
+		printf("Error painting window.\n");
+		goto error;
+	}
+
+	*rtaskbar = taskbar;
+	return EOK;
+error:
+	if (taskbar->clock != NULL)
+		taskbar_clock_destroy(taskbar->clock);
+	if (taskbar->wndlist != NULL)
+		wndlist_destroy(taskbar->wndlist);
+	if (taskbar->window != NULL)
+		ui_window_destroy(taskbar->window);
+	if (taskbar->ui != NULL)
+		ui_destroy(taskbar->ui);
+	return rc;
+
+}
+
+/** Destroy task bar. */
+void taskbar_destroy(taskbar_t *taskbar)
+{
+	ui_fixed_remove(taskbar->fixed, taskbar_clock_ctl(taskbar->clock));
+	taskbar_clock_destroy(taskbar->clock);
+	ui_window_destroy(taskbar->window);
+	ui_destroy(taskbar->ui);
+}
+
+/** @}
+ */
Index: uspace/app/taskbar/taskbar.h
===================================================================
--- uspace/app/taskbar/taskbar.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/taskbar.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/**
+ * @file Task Bar
+ */
+
+#ifndef TASKBAR_H
+#define TASKBAR_H
+
+#include <errno.h>
+#include <wndmgt.h>
+#include "types/taskbar.h"
+
+extern errno_t taskbar_create(const char *, const char *, taskbar_t **);
+extern void taskbar_destroy(taskbar_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/app/taskbar/test/clock.c
===================================================================
--- uspace/app/taskbar/test/clock.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/test/clock.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2022 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 <errno.h>
+#include <gfx/coord.h>
+#include <pcut/pcut.h>
+#include <ui/ui.h>
+#include <ui/window.h>
+#include "../clock.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(clock);
+
+/** Creating and destroying taskbar clock */
+PCUT_TEST(create_destroy)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Painting taskbar clock */
+PCUT_TEST(paint)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = taskbar_clock_paint(clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Keyboard event */
+PCUT_TEST(kbd_event)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+	kbd_event_t event;
+	ui_evclaim_t claim;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	event.type = KEY_PRESS;
+	event.key = KC_ENTER;
+	event.mods = 0;
+	claim = taskbar_clock_kbd_event(clock, &event);
+	PCUT_ASSERT_EQUALS(ui_unclaimed, claim);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Position event */
+PCUT_TEST(pos_event)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+	pos_event_t event;
+	ui_evclaim_t claim;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	event.type = POS_PRESS;
+	event.hpos = 0;
+	event.vpos = 0;
+	claim = taskbar_clock_pos_event(clock, &event);
+	PCUT_ASSERT_EQUALS(ui_unclaimed, claim);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** taskbar_clock_ctl returns a usable UI control */
+PCUT_TEST(ctl)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+	ui_control_t *ctl;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ctl = taskbar_clock_ctl(clock);
+	PCUT_ASSERT_NOT_NULL(ctl);
+
+	rc = ui_control_paint(ctl);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** taskbar_clock_set_rect() sets internal field */
+PCUT_TEST(set_rect)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	taskbar_clock_t *clock;
+	gfx_rect_t rect;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = taskbar_clock_create(window, &clock);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rect.p0.x = 1;
+	rect.p0.y = 2;
+	rect.p1.x = 3;
+	rect.p1.y = 4;
+	taskbar_clock_set_rect(clock, &rect);
+
+	PCUT_ASSERT_INT_EQUALS(rect.p0.x, clock->rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(rect.p0.y, clock->rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(rect.p1.x, clock->rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(rect.p1.y, clock->rect.p1.y);
+
+	taskbar_clock_destroy(clock);
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+PCUT_EXPORT(clock);
Index: uspace/app/taskbar/test/main.c
===================================================================
--- uspace/app/taskbar/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 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(clock);
+PCUT_IMPORT(taskbar);
+PCUT_IMPORT(wndlist);
+
+PCUT_MAIN();
Index: uspace/app/taskbar/test/taskbar.c
===================================================================
--- uspace/app/taskbar/test/taskbar.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/test/taskbar.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 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 <errno.h>
+#include <pcut/pcut.h>
+#include "../taskbar.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(taskbar);
+
+/* Test creating and destroying taskbar */
+PCUT_TEST(create_destroy)
+{
+	errno_t rc;
+	taskbar_t *taskbar;
+
+	rc = taskbar_create(UI_DISPLAY_NULL, NULL, &taskbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	taskbar_destroy(taskbar);
+}
+
+PCUT_EXPORT(taskbar);
Index: uspace/app/taskbar/test/wndlist.c
===================================================================
--- uspace/app/taskbar/test/wndlist.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/test/wndlist.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,647 @@
+/*
+ * 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 <errno.h>
+#include <loc.h>
+#include <pcut/pcut.h>
+#include <str.h>
+#include <ui/fixed.h>
+#include <ui/ui.h>
+#include <ui/window.h>
+#include <wndmgt_srv.h>
+#include "../wndlist.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(taskbar);
+
+static void test_wndmgt_conn(ipc_call_t *, void *);
+
+static errno_t test_get_window_list(void *, wndmgt_window_list_t **);
+static errno_t test_get_window_info(void *, sysarg_t, wndmgt_window_info_t **);
+
+static wndmgt_ops_t test_wndmgt_srv_ops = {
+	.get_window_list = test_get_window_list,
+	.get_window_info = test_get_window_info
+};
+
+static const char *test_wndmgt_server = "test-wndlist-wm";
+static const char *test_wndmgt_svc = "test/wndlist-wm";
+
+/* Test creating and destroying window list */
+PCUT_TEST(create_destroy)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/* Test setting window list rectangle */
+PCUT_TEST(set_rect)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	gfx_rect_t rect;
+	wndlist_t *wndlist;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rect.p0.x = 1;
+	rect.p0.y = 2;
+	rect.p1.x = 3;
+	rect.p1.y = 4;
+	wndlist_set_rect(wndlist, &rect);
+	PCUT_ASSERT_INT_EQUALS(1, wndlist->rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(2, wndlist->rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(3, wndlist->rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(4, wndlist->rect.p1.y);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test opening WM service */
+PCUT_TEST(open_wm)
+{
+	errno_t rc;
+	service_id_t sid;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+
+	/* Set up a test WM service */
+
+	async_set_fallback_port_handler(test_wndmgt_conn, NULL);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Create window list and connect it to our test service */
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_open_wm(wndlist, test_wndmgt_svc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Test appending new entry */
+PCUT_TEST(append)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 123, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+	PCUT_ASSERT_INT_EQUALS(123, entry->wnd_id);
+	PCUT_ASSERT_NOT_NULL(entry->button);
+	PCUT_ASSERT_TRUE(ui_pbutton_get_light(entry->button));
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test removing entry */
+PCUT_TEST(remove)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+	PCUT_ASSERT_INT_EQUALS(1, entry->wnd_id);
+
+	rc = wndlist_remove(wndlist, entry, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+	PCUT_ASSERT_INT_EQUALS(2, entry->wnd_id);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test updating entry */
+PCUT_TEST(update)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+	PCUT_ASSERT_INT_EQUALS(1, entry->wnd_id);
+	PCUT_ASSERT_NOT_NULL(entry->button);
+	PCUT_ASSERT_TRUE(ui_pbutton_get_light(entry->button));
+
+	rc = wndlist_update(wndlist, entry, "Bar", false);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_INT_EQUALS(1, entry->wnd_id);
+	PCUT_ASSERT_NOT_NULL(entry->button);
+	PCUT_ASSERT_FALSE(ui_pbutton_get_light(entry->button));
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test setting entry rectangle */
+PCUT_TEST(set_entry_rect)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 123, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+
+	/* Set entry rectangle */
+	wndlist_set_entry_rect(wndlist, entry);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test finding entry by window ID */
+PCUT_TEST(entry_by_id)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_entry_by_id(wndlist, 1);
+	PCUT_ASSERT_NOT_NULL(entry);
+	PCUT_ASSERT_INT_EQUALS(1, entry->wnd_id);
+
+	entry = wndlist_entry_by_id(wndlist, 2);
+	PCUT_ASSERT_NOT_NULL(entry);
+	PCUT_ASSERT_INT_EQUALS(2, entry->wnd_id);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test wndlist_first() / wndlist_next() */
+PCUT_TEST(first_next)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_first(wndlist);
+	PCUT_ASSERT_NOT_NULL(entry);
+	PCUT_ASSERT_INT_EQUALS(1, entry->wnd_id);
+
+	entry = wndlist_next(entry);
+	PCUT_ASSERT_NOT_NULL(entry);
+	PCUT_ASSERT_INT_EQUALS(2, entry->wnd_id);
+
+	entry = wndlist_next(entry);
+	PCUT_ASSERT_NULL(entry);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test wndlist_last() */
+PCUT_TEST(last)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	wndlist_entry_t *entry;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	entry = wndlist_last(wndlist);
+	PCUT_ASSERT_NOT_NULL(entry);
+	PCUT_ASSERT_INT_EQUALS(2, entry->wnd_id);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test wndlist_count() */
+PCUT_TEST(count)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+	size_t count;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	count = wndlist_count(wndlist);
+	PCUT_ASSERT_INT_EQUALS(0, count);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	count = wndlist_count(wndlist);
+	PCUT_ASSERT_INT_EQUALS(1, count);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	count = wndlist_count(wndlist);
+	PCUT_ASSERT_INT_EQUALS(2, count);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test repainting window list */
+PCUT_TEST(repaint)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	ui_fixed_t *fixed = NULL;
+	wndlist_t *wndlist;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	rc = ui_fixed_create(&fixed);
+	ui_window_add(window, ui_fixed_ctl(fixed));
+
+	rc = wndlist_create(window, fixed, &wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 1, "Foo", true, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_append(wndlist, 2, "Bar", false, true);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndlist_repaint(wndlist);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	wndlist_destroy(wndlist);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
+/** Test window management service connection. */
+static void test_wndmgt_conn(ipc_call_t *icall, void *arg)
+{
+	wndmgt_srv_t srv;
+
+	/* Set up protocol structure */
+	wndmgt_srv_initialize(&srv);
+	srv.ops = &test_wndmgt_srv_ops;
+	srv.arg = arg;
+
+	/* Handle connection */
+	wndmgt_conn(icall, &srv);
+}
+
+static errno_t test_get_window_list(void *arg, wndmgt_window_list_t **rlist)
+{
+	wndmgt_window_list_t *wlist;
+
+	(void)arg;
+
+	wlist = calloc(1, sizeof(wndmgt_window_list_t));
+	if (wlist == NULL)
+		return ENOMEM;
+
+	wlist->nwindows = 1;
+	wlist->windows = calloc(1, sizeof(sysarg_t));
+	if (wlist->windows == NULL) {
+		free(wlist);
+		return ENOMEM;
+	}
+
+	wlist->windows[0] = 42;
+	*rlist = wlist;
+	return EOK;
+}
+
+static errno_t test_get_window_info(void *arg, sysarg_t wnd_id,
+    wndmgt_window_info_t **rinfo)
+{
+	wndmgt_window_info_t *winfo;
+
+	(void)arg;
+
+	winfo = calloc(1, sizeof(wndmgt_window_info_t));
+	if (winfo == NULL)
+		return ENOMEM;
+
+	winfo->caption = str_dup("Hello");
+	if (winfo->caption == NULL) {
+		free(winfo);
+		return ENOMEM;
+	}
+
+	*rinfo = winfo;
+	return EOK;
+}
+
+PCUT_EXPORT(wndlist);
Index: uspace/app/taskbar/types/clock.h
===================================================================
--- uspace/app/taskbar/types/clock.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/types/clock.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/**
+ * @file Task bar clock
+ */
+
+#ifndef TYPES_CLOCK_H
+#define TYPES_CLOCK_H
+
+#include <fibril_synch.h>
+#include <gfx/coord.h>
+#include <ui/window.h>
+
+/** Task bar clock
+ *
+ * This is a custom UI control.
+ */
+typedef struct taskbar_clock {
+	/** Base control object */
+	struct ui_control *control;
+
+	/** Containing window */
+	ui_window_t *window;
+
+	/** Clock rectangle */
+	gfx_rect_t rect;
+
+	/** Clock lock */
+	fibril_mutex_t lock;
+
+	/** Clock update timer */
+	fibril_timer_t *timer;
+
+	/** Signal to timer that we are cleaning up */
+	bool timer_cleanup;
+
+	/** Signal to maih thread that timer is done */
+	bool timer_done;
+
+	/** Timer done condition variable */
+	fibril_condvar_t timer_done_cv;
+} taskbar_clock_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/app/taskbar/types/taskbar.h
===================================================================
--- uspace/app/taskbar/types/taskbar.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/types/taskbar.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/**
+ * @file Task Bar types
+ */
+
+#ifndef TYPES_TASKBAR_H
+#define TYPES_TASKBAR_H
+
+#include <types/common.h>
+#include <ui/fixed.h>
+#include <ui/label.h>
+#include <ui/ui.h>
+#include <ui/window.h>
+#include "clock.h"
+#include "wndlist.h"
+
+/** Task bar */
+typedef struct taskbar {
+	/** User interface */
+	ui_t *ui;
+	/** Taskbar window */
+	ui_window_t *window;
+	/** Fixed layout */
+	ui_fixed_t *fixed;
+	ui_label_t *label;
+	/** Window list */
+	wndlist_t *wndlist;
+	/** Clock */
+	taskbar_clock_t *clock;
+} taskbar_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/app/taskbar/types/wndlist.h
===================================================================
--- uspace/app/taskbar/types/wndlist.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/types/wndlist.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2022 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 taskbar
+ * @{
+ */
+/**
+ * @file Task bar window list
+ */
+
+#ifndef TYPES_WNDLIST_H
+#define TYPES_WNDLIST_H
+
+#include <adt/list.h>
+#include <gfx/coord.h>
+#include <stdbool.h>
+#include <ui/pbutton.h>
+#include <ui/fixed.h>
+#include <ui/window.h>
+#include <wndmgt.h>
+
+/** Taskbar window list entry */
+typedef struct {
+	/** Containing window list */
+	struct wndlist *wndlist;
+	/** Link to wndlist->entries */
+	link_t lentries;
+	/** Window ID */
+	sysarg_t wnd_id;
+	/** Entry is visible */
+	bool visible;
+	/** Window button */
+	ui_pbutton_t *button;
+	/** Window button rectangle */
+	gfx_rect_t rect;
+} wndlist_entry_t;
+
+/** Task bar window list */
+typedef struct wndlist {
+	/** Base control object */
+	struct ui_control *control;
+
+	/** Containing window */
+	ui_window_t *window;
+
+	/** Layout to which we add window buttoons */
+	ui_fixed_t *fixed;
+
+	/** Window list rectangle */
+	gfx_rect_t rect;
+
+	/** Window list entries (of wndlist_entry_t) */
+	list_t entries;
+
+	/** Current button pitch */
+	gfx_coord_t pitch;
+
+	/** Window management service */
+	wndmgt_t *wndmgt;
+
+	/** Position ID of last position event */
+	sysarg_t ev_pos_id;
+} wndlist_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/app/taskbar/wndlist.c
===================================================================
--- uspace/app/taskbar/wndlist.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/wndlist.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,684 @@
+/*
+ * 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 taskbar
+ * @{
+ */
+/** @file Task bar window list
+ */
+
+#include <gfx/coord.h>
+#include <gfx/render.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <str.h>
+#include <ui/fixed.h>
+#include <ui/label.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include <ui/window.h>
+#include "clock.h"
+#include "wndlist.h"
+
+static void wndlist_wm_window_added(void *, sysarg_t);
+static void wndlist_wm_window_removed(void *, sysarg_t);
+static void wndlist_wm_window_changed(void *, sysarg_t);
+
+/** Window list WM callbacks */
+static wndmgt_cb_t wndlist_wndmgt_cb = {
+	.window_added = wndlist_wm_window_added,
+	.window_removed = wndlist_wm_window_removed,
+	.window_changed = wndlist_wm_window_changed
+};
+
+static void wndlist_button_clicked(ui_pbutton_t *, void *);
+
+/** Window list button callbacks */
+static ui_pbutton_cb_t wndlist_button_cb = {
+	.clicked = wndlist_button_clicked
+};
+
+enum {
+	/** Min. X distance between left edges of two consecutive buttons */
+	wndlist_button_pitch_min = 85,
+	/** Max. X distance between left edges of two consecutive buttons (text) */
+	wndlist_button_pitch_min_text = 10,
+	/** Min. X distance between left edges of two consecutive buttons */
+	wndlist_button_pitch_max = 165,
+	/** Max. X distance between left edges of two consecutive buttons (text) */
+	wndlist_button_pitch_max_text = 17,
+	/** Padding between buttons */
+	wndlist_button_pad = 5,
+	/** Padding between buttons (text) */
+	wndlist_button_pad_text = 1
+};
+
+/** Create task bar window list.
+ *
+ * @param window Containing window
+ * @param fixed Fixed layout to which buttons will be added
+ * @param wndmgt Window management service
+ * @param rwndlist Place to store pointer to new window list
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_create(ui_window_t *window, ui_fixed_t *fixed,
+    wndlist_t **rwndlist)
+{
+	ui_resource_t *res = ui_window_get_res(window);
+	wndlist_t *wndlist = NULL;
+	errno_t rc;
+
+	wndlist = calloc(1, sizeof(wndlist_t));
+	if (wndlist == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	wndlist->window = window;
+	wndlist->fixed = fixed;
+	list_initialize(&wndlist->entries);
+
+	if (ui_resource_is_textmode(res))
+		wndlist->pitch = wndlist_button_pitch_max_text;
+	else
+		wndlist->pitch = wndlist_button_pitch_max;
+
+	*rwndlist = wndlist;
+	return EOK;
+error:
+	return rc;
+}
+
+/** Set window list rectangle.
+ *
+ * @param wndlist Window list
+ * @param rect Rectangle
+ */
+void wndlist_set_rect(wndlist_t *wndlist, gfx_rect_t *rect)
+{
+	wndlist->rect = *rect;
+}
+
+/** Attach window management service to window list.
+ *
+ * @param wndlist Window list
+ * @param wndmgt_svc Window management service name
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_open_wm(wndlist_t *wndlist, const char *wndmgt_svc)
+{
+	errno_t rc;
+	wndmgt_window_list_t *wlist = NULL;
+	wndmgt_window_info_t *winfo = NULL;
+	sysarg_t i;
+
+	rc = wndmgt_open(wndmgt_svc, &wndlist_wndmgt_cb, (void *)wndlist,
+	    &wndlist->wndmgt);
+	if (rc != EOK)
+		goto error;
+
+	rc = wndmgt_get_window_list(wndlist->wndmgt, &wlist);
+	if (rc != EOK)
+		goto error;
+
+	for (i = 0; i < wlist->nwindows; i++) {
+		rc = wndmgt_get_window_info(wndlist->wndmgt, wlist->windows[i],
+		    &winfo);
+		if (rc != EOK)
+			goto error;
+
+		if ((winfo->flags & (wndf_popup | wndf_system)) == 0) {
+			rc = wndlist_append(wndlist, wlist->windows[i],
+			    winfo->caption, winfo->nfocus != 0, false);
+			if (rc != EOK) {
+				wndmgt_free_window_info(winfo);
+				goto error;
+			}
+		}
+
+		wndmgt_free_window_info(winfo);
+	}
+
+	return EOK;
+error:
+	if (wlist != NULL)
+		wndmgt_free_window_list(wlist);
+	if (wndlist->wndmgt != NULL) {
+		wndmgt_close(wndlist->wndmgt);
+		wndlist->wndmgt = NULL;
+	}
+	return rc;
+}
+
+/** Destroy task bar window list. */
+void wndlist_destroy(wndlist_t *wndlist)
+{
+	wndlist_entry_t *entry;
+
+	/* Close window management service */
+	if (wndlist->wndmgt)
+		wndmgt_close(wndlist->wndmgt);
+
+	/* Destroy entries */
+	entry = wndlist_first(wndlist);
+	while (entry != NULL) {
+		(void)wndlist_remove(wndlist, entry, false);
+		entry = wndlist_first(wndlist);
+	}
+
+	free(wndlist);
+}
+
+/** Append new entry to window list.
+ *
+ * @param wndlist Window list
+ * @param wnd_id Window ID
+ * @param caption Entry caption
+ * @param active @c true iff window is active
+ * @param paint @c true to paint immediately
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_append(wndlist_t *wndlist, sysarg_t wnd_id,
+    const char *caption, bool active, bool paint)
+{
+	wndlist_entry_t *entry = NULL;
+	ui_resource_t *res;
+	wndlist_entry_t *e;
+	errno_t rc;
+
+	entry = calloc(1, sizeof(wndlist_entry_t));
+	if (entry == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	entry->wnd_id = wnd_id;
+	res = ui_window_get_res(wndlist->window);
+
+	rc = ui_pbutton_create(res, caption, &entry->button);
+	if (rc != EOK)
+		goto error;
+
+	entry->wndlist = wndlist;
+	list_append(&entry->lentries, &wndlist->entries);
+
+	entry->visible = false;
+
+	ui_pbutton_set_light(entry->button, active);
+
+	/* Set button callbacks */
+	ui_pbutton_set_cb(entry->button, &wndlist_button_cb, (void *)entry);
+
+	if (wndlist_update_pitch(wndlist)) {
+		/*
+		 * Update rectangles for all entries, including @a entry, adding
+		 * it to the layout, if applicable.
+		 */
+		e = wndlist_first(wndlist);
+		while (e != NULL) {
+			wndlist_set_entry_rect(wndlist, e);
+			e = wndlist_next(e);
+		}
+
+		if (paint)
+			return wndlist_repaint(wndlist);
+	} else {
+		wndlist_set_entry_rect(wndlist, entry);
+		if (paint)
+			return ui_pbutton_paint(entry->button);
+	}
+
+	return EOK;
+error:
+	if (entry != NULL && entry->button != NULL)
+		ui_pbutton_destroy(entry->button);
+	if (entry != NULL)
+		free(entry);
+	return rc;
+
+}
+
+/** Remove entry from window list.
+ *
+ * @param wndlist Window list
+ * @param entry Window list entry
+ * @param paint @c true to repaint window list
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_remove(wndlist_t *wndlist, wndlist_entry_t *entry,
+    bool paint)
+{
+	wndlist_entry_t *e;
+	wndlist_entry_t *next;
+	wndlist_entry_t *last;
+	errno_t rc = EOK;
+
+	assert(entry->wndlist == wndlist);
+	next = wndlist_next(entry);
+
+	/* Remember last entry */
+	last = wndlist_last(wndlist);
+
+	if (entry->visible)
+		ui_fixed_remove(wndlist->fixed, ui_pbutton_ctl(entry->button));
+
+	list_remove(&entry->lentries);
+
+	if (wndlist_update_pitch(wndlist)) {
+		/*
+		 * Update rectangles for all entries.
+		 */
+		e = wndlist_first(wndlist);
+		while (e != NULL) {
+			wndlist_set_entry_rect(wndlist, e);
+			e = wndlist_next(e);
+		}
+
+		if (paint)
+			rc = wndlist_repaint(wndlist);
+	} else {
+		/* Unpaint the last entry */
+		if (paint)
+			rc = wndlist_unpaint_entry(last);
+
+		/*
+		 * Update rectangles for entries to the right
+		 */
+
+		e = NULL;
+		while (next != NULL) {
+			e = next;
+
+			wndlist_set_entry_rect(wndlist, e);
+			if (paint) {
+				rc = ui_pbutton_paint(e->button);
+				if (rc != EOK)
+					return rc;
+			}
+
+			next = wndlist_next(e);
+		}
+	}
+
+	ui_pbutton_destroy(entry->button);
+	free(entry);
+	return rc;
+}
+
+/** Update button pitch.
+ *
+ * Recalculatebutton pitch @c wndlist->pitch based on current number
+ * of buttons.
+ *
+ * @param wndlist Window list
+ * @return @c true iff pitch changed
+ */
+bool wndlist_update_pitch(wndlist_t *wndlist)
+{
+	ui_resource_t *res;
+	size_t nbuttons;
+	gfx_coord_t pitch;
+	gfx_coord_t pitch_max;
+	gfx_coord_t pitch_min;
+	gfx_coord_t pad;
+
+	res = ui_window_get_res(wndlist->window);
+
+	if (ui_resource_is_textmode(res)) {
+		pitch_max = wndlist_button_pitch_max_text;
+		pitch_min = wndlist_button_pitch_min_text;
+		pad = wndlist_button_pad_text;
+	} else {
+		pitch_max = wndlist_button_pitch_max;
+		pitch_min = wndlist_button_pitch_min;
+		pad = wndlist_button_pad;
+	}
+
+	/* Compute pitch that fits all buttons perfectly */
+	nbuttons = wndlist_count(wndlist);
+	if (nbuttons > 0)
+		pitch = (wndlist->rect.p1.x - wndlist->rect.p0.x + pad) / nbuttons;
+	else
+		pitch = pitch_min;
+
+	if (pitch < pitch_min)
+		pitch = pitch_min;
+	if (pitch > pitch_max)
+		pitch = pitch_max;
+
+	/* Did the pitch change? */
+	if (pitch == wndlist->pitch)
+		return false;
+
+	wndlist->pitch = pitch;
+	return true;
+}
+
+/** Update window list entry.
+ *
+ * @param wndlist Window list
+ * @param entry Window list entry
+ * @param active @c true iff the window is active
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_update(wndlist_t *wndlist, wndlist_entry_t *entry,
+    const char *caption, bool active)
+{
+	errno_t rc;
+	assert(entry->wndlist == wndlist);
+
+	rc = ui_pbutton_set_caption(entry->button, caption);
+	if (rc != EOK)
+		return rc;
+
+	ui_pbutton_set_light(entry->button, active);
+
+	rc = ui_pbutton_paint(entry->button);
+	if (rc != EOK)
+		return rc;
+
+	return wndlist_repaint(wndlist);
+}
+
+/** Compute and set window list entry rectangle.
+ *
+ * Compute rectangle for window list entry and set it.
+ *
+ * @param wndlist Window list
+ * @param entry Window list entry
+ */
+void wndlist_set_entry_rect(wndlist_t *wndlist, wndlist_entry_t *entry)
+{
+	wndlist_entry_t *e;
+	gfx_rect_t rect;
+	ui_resource_t *res;
+	gfx_coord_t pitch;
+	gfx_coord_t pad;
+	size_t idx;
+
+	/* Determine entry index */
+	idx = 0;
+	e = wndlist_first(wndlist);
+	while (e != entry) {
+		assert(e != NULL);
+		e = wndlist_next(e);
+		++idx;
+	}
+
+	res = ui_window_get_res(wndlist->window);
+
+	if (ui_resource_is_textmode(res)) {
+		pad = wndlist_button_pad_text;
+	} else {
+		pad = wndlist_button_pad;
+	}
+
+	pitch = wndlist->pitch;
+
+	rect.p0.x = wndlist->rect.p0.x + pitch * idx;
+	rect.p0.y = wndlist->rect.p0.y;
+	rect.p1.x = wndlist->rect.p0.x + pitch * (idx + 1) - pad;
+	rect.p1.y = wndlist->rect.p1.y;
+
+	/* Entry does not fit? */
+	if (rect.p1.x > wndlist->rect.p1.x) {
+		/* Make entry invisible */
+		if (entry->visible) {
+			ui_fixed_remove(wndlist->fixed,
+			    ui_pbutton_ctl(entry->button));
+			entry->visible = false;
+		}
+	} else {
+		/* Make entry visible */
+		if (!entry->visible) {
+			ui_fixed_add(wndlist->fixed,
+			    ui_pbutton_ctl(entry->button));
+			entry->visible = true;
+		}
+	}
+
+	ui_pbutton_set_rect(entry->button, &rect);
+	entry->rect = rect;
+}
+
+/** Compute and set window list entry rectangle.
+ *
+ * Compute rectangle for window list entry and set it.
+ *
+ * @param entry Window list entry
+ * @return EOK on success or an error code
+ */
+errno_t wndlist_unpaint_entry(wndlist_entry_t *entry)
+{
+	errno_t rc;
+	gfx_context_t *gc;
+	ui_resource_t *res;
+	gfx_color_t *color;
+
+	gc = ui_window_get_gc(entry->wndlist->window);
+	res = ui_window_get_res(entry->wndlist->window);
+	color = ui_resource_get_wnd_face_color(res);
+
+	rc = gfx_set_color(gc, color);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_fill_rect(gc, &entry->rect);
+	if (rc != EOK)
+		return rc;
+
+	rc = gfx_update(gc);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
+/** Handle WM window added event.
+ *
+ * @param arg Argument (wndlist_t *)
+ * @param wnd_id Window ID
+ */
+static void wndlist_wm_window_added(void *arg, sysarg_t wnd_id)
+{
+	wndlist_t *wndlist = (wndlist_t *)arg;
+	wndmgt_window_info_t *winfo = NULL;
+	errno_t rc;
+
+	rc = wndmgt_get_window_info(wndlist->wndmgt, wnd_id, &winfo);
+	if (rc != EOK)
+		goto error;
+
+	if ((winfo->flags & (wndf_popup | wndf_system)) == 0) {
+		rc = wndlist_append(wndlist, wnd_id, winfo->caption,
+		    winfo->nfocus != 0, true);
+		if (rc != EOK) {
+			wndmgt_free_window_info(winfo);
+			goto error;
+		}
+	}
+
+	wndmgt_free_window_info(winfo);
+	return;
+error:
+	if (winfo != NULL)
+		wndmgt_free_window_info(winfo);
+}
+
+/** Handle WM window removed event.
+ *
+ * @param arg Argument (wndlist_t *)
+ * @param wnd_id Window ID
+ */
+static void wndlist_wm_window_removed(void *arg, sysarg_t wnd_id)
+{
+	wndlist_t *wndlist = (wndlist_t *)arg;
+	wndlist_entry_t *entry;
+
+	entry = wndlist_entry_by_id(wndlist, wnd_id);
+	if (entry == NULL)
+		return;
+
+	(void) wndlist_remove(wndlist, entry, true);
+}
+
+/** Handle WM window changed event.
+ *
+ * @param arg Argument (wndlist_t *)
+ * @param wnd_id Window ID
+ */
+static void wndlist_wm_window_changed(void *arg, sysarg_t wnd_id)
+{
+	wndlist_t *wndlist = (wndlist_t *)arg;
+	wndmgt_window_info_t *winfo = NULL;
+	wndlist_entry_t *entry;
+	errno_t rc;
+
+	entry = wndlist_entry_by_id(wndlist, wnd_id);
+	if (entry == NULL)
+		return;
+
+	rc = wndmgt_get_window_info(wndlist->wndmgt, wnd_id, &winfo);
+	if (rc != EOK)
+		return;
+
+	(void) wndlist_update(wndlist, entry, winfo->caption,
+	    winfo->nfocus != 0);
+	wndmgt_free_window_info(winfo);
+}
+
+/** Find window list entry by ID.
+ *
+ * @param wndlist Window list
+ * @param wnd_id Window ID
+ * @return Window list entry on success or @c NULL if not found
+ */
+wndlist_entry_t *wndlist_entry_by_id(wndlist_t *wndlist, sysarg_t wnd_id)
+{
+	wndlist_entry_t *entry;
+
+	entry = wndlist_first(wndlist);
+	while (entry != NULL) {
+		if (entry->wnd_id == wnd_id)
+			return entry;
+
+		entry = wndlist_next(entry);
+	}
+
+	return NULL;
+}
+
+/** Get first window list entry.
+ *
+ * @param wndlist Window list
+ * @return First entry or @c NULL if the list is empty
+ */
+wndlist_entry_t *wndlist_first(wndlist_t *wndlist)
+{
+	link_t *link;
+
+	link = list_first(&wndlist->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, wndlist_entry_t, lentries);
+}
+
+/** Get last window list entry.
+ *
+ * @param wndlist Window list
+ * @return Last entry or @c NULL if the list is empty
+ */
+wndlist_entry_t *wndlist_last(wndlist_t *wndlist)
+{
+	link_t *link;
+
+	link = list_last(&wndlist->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, wndlist_entry_t, lentries);
+}
+
+/** Get next window list entry.
+ *
+ * @param cur Current entry
+ * @return Next entry or @c NULL if @a cur is the last entry
+ */
+wndlist_entry_t *wndlist_next(wndlist_entry_t *cur)
+{
+	link_t *link;
+
+	link = list_next(&cur->lentries, &cur->wndlist->entries);
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, wndlist_entry_t, lentries);
+}
+
+/** Get number of window list entries.
+ *
+ * @param wndlist Window list
+ * @return Number of entries
+ */
+size_t wndlist_count(wndlist_t *wndlist)
+{
+	return list_count(&wndlist->entries);
+}
+
+/** Repaint window list.
+ *
+ * @param wndlist Window list
+ * @return @c EOK on success or an error code
+ */
+errno_t wndlist_repaint(wndlist_t *wndlist)
+{
+	return ui_window_paint(wndlist->window);
+}
+
+/** Window button was clicked.
+ *
+ * @param pbutton Push button
+ * @param arg Argument (wndlist_entry_t *)
+ */
+static void wndlist_button_clicked(ui_pbutton_t *pbutton, void *arg)
+{
+	wndlist_entry_t *entry = (wndlist_entry_t *)arg;
+	sysarg_t dev_id;
+
+	/* ID of device that clicked the button */
+	dev_id = entry->wndlist->ev_pos_id;
+
+	(void) wndmgt_activate_window(entry->wndlist->wndmgt,
+	    dev_id, entry->wnd_id);
+}
+
+/** @}
+ */
Index: uspace/app/taskbar/wndlist.h
===================================================================
--- uspace/app/taskbar/wndlist.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/app/taskbar/wndlist.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,70 @@
+/*
+ * 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 taskbar
+ * @{
+ */
+/**
+ * @file Task bar window list
+ */
+
+#ifndef WNDLIST_H
+#define WNDLIST_H
+
+#include <errno.h>
+#include <gfx/coord.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <ui/fixed.h>
+#include <ui/window.h>
+#include <wndmgt.h>
+#include "types/wndlist.h"
+
+extern errno_t wndlist_create(ui_window_t *, ui_fixed_t *, wndlist_t **);
+extern void wndlist_set_rect(wndlist_t *, gfx_rect_t *);
+extern errno_t wndlist_open_wm(wndlist_t *, const char *);
+extern void wndlist_destroy(wndlist_t *);
+extern errno_t wndlist_append(wndlist_t *, sysarg_t, const char *, bool,
+    bool);
+extern errno_t wndlist_remove(wndlist_t *, wndlist_entry_t *, bool);
+extern bool wndlist_update_pitch(wndlist_t *);
+extern errno_t wndlist_update(wndlist_t *, wndlist_entry_t *, const char *,
+    bool);
+extern void wndlist_set_entry_rect(wndlist_t *, wndlist_entry_t *);
+extern wndlist_entry_t *wndlist_entry_by_id(wndlist_t *, sysarg_t);
+extern wndlist_entry_t *wndlist_first(wndlist_t *);
+extern wndlist_entry_t *wndlist_last(wndlist_t *);
+extern wndlist_entry_t *wndlist_next(wndlist_entry_t *);
+extern size_t wndlist_count(wndlist_t *);
+extern errno_t wndlist_repaint(wndlist_t *);
+extern errno_t wndlist_unpaint_entry(wndlist_entry_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/c/generic/io/input.c
===================================================================
--- uspace/lib/c/generic/io/input.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/c/generic/io/input.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -106,4 +106,5 @@
 static void input_ev_key(input_t *input, ipc_call_t *call)
 {
+	unsigned kbd_id;
 	kbd_event_type_t type;
 	keycode_t key;
@@ -112,10 +113,11 @@
 	errno_t rc;
 
-	type = ipc_get_arg1(call);
-	key = ipc_get_arg2(call);
-	mods = ipc_get_arg3(call);
-	c = ipc_get_arg4(call);
-
-	rc = input->ev_ops->key(input, type, key, mods, c);
+	kbd_id = ipc_get_arg1(call);
+	type = ipc_get_arg2(call);
+	key = ipc_get_arg3(call);
+	mods = ipc_get_arg4(call);
+	c = ipc_get_arg5(call);
+
+	rc = input->ev_ops->key(input, kbd_id, type, key, mods, c);
 	async_answer_0(call, rc);
 }
@@ -123,12 +125,14 @@
 static void input_ev_move(input_t *input, ipc_call_t *call)
 {
+	unsigned pos_id;
 	int dx;
 	int dy;
 	errno_t rc;
 
-	dx = ipc_get_arg1(call);
-	dy = ipc_get_arg2(call);
-
-	rc = input->ev_ops->move(input, dx, dy);
+	pos_id = ipc_get_arg1(call);
+	dx = ipc_get_arg2(call);
+	dy = ipc_get_arg3(call);
+
+	rc = input->ev_ops->move(input, pos_id, dx, dy);
 	async_answer_0(call, rc);
 }
@@ -136,4 +140,5 @@
 static void input_ev_abs_move(input_t *input, ipc_call_t *call)
 {
+	unsigned pos_id;
 	unsigned x;
 	unsigned y;
@@ -142,10 +147,11 @@
 	errno_t rc;
 
-	x = ipc_get_arg1(call);
-	y = ipc_get_arg2(call);
-	max_x = ipc_get_arg3(call);
-	max_y = ipc_get_arg4(call);
-
-	rc = input->ev_ops->abs_move(input, x, y, max_x, max_y);
+	pos_id = ipc_get_arg1(call);
+	x = ipc_get_arg2(call);
+	y = ipc_get_arg3(call);
+	max_x = ipc_get_arg4(call);
+	max_y = ipc_get_arg5(call);
+
+	rc = input->ev_ops->abs_move(input, pos_id, x, y, max_x, max_y);
 	async_answer_0(call, rc);
 }
@@ -153,12 +159,14 @@
 static void input_ev_button(input_t *input, ipc_call_t *call)
 {
+	unsigned pos_id;
 	int bnum;
 	int press;
 	errno_t rc;
 
-	bnum = ipc_get_arg1(call);
-	press = ipc_get_arg2(call);
-
-	rc = input->ev_ops->button(input, bnum, press);
+	pos_id = ipc_get_arg1(call);
+	bnum = ipc_get_arg2(call);
+	press = ipc_get_arg3(call);
+
+	rc = input->ev_ops->button(input, pos_id, bnum, press);
 	async_answer_0(call, rc);
 }
@@ -166,10 +174,12 @@
 static void input_ev_dclick(input_t *input, ipc_call_t *call)
 {
+	unsigned pos_id;
 	int bnum;
 	errno_t rc;
 
-	bnum = ipc_get_arg1(call);
-
-	rc = input->ev_ops->dclick(input, bnum);
+	pos_id = ipc_get_arg1(call);
+	bnum = ipc_get_arg2(call);
+
+	rc = input->ev_ops->dclick(input, pos_id, bnum);
 	async_answer_0(call, rc);
 }
Index: uspace/lib/c/include/io/input.h
===================================================================
--- uspace/lib/c/include/io/input.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/c/include/io/input.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -50,9 +50,11 @@
 	errno_t (*active)(input_t *);
 	errno_t (*deactive)(input_t *);
-	errno_t (*key)(input_t *, kbd_event_type_t, keycode_t, keymod_t, char32_t);
-	errno_t (*move)(input_t *, int, int);
-	errno_t (*abs_move)(input_t *, unsigned, unsigned, unsigned, unsigned);
-	errno_t (*button)(input_t *, int, int);
-	errno_t (*dclick)(input_t *, int);
+	errno_t (*key)(input_t *, unsigned, kbd_event_type_t, keycode_t,
+	    keymod_t, char32_t);
+	errno_t (*move)(input_t *, unsigned, int, int);
+	errno_t (*abs_move)(input_t *, unsigned, unsigned, unsigned, unsigned,
+	    unsigned);
+	errno_t (*button)(input_t *, unsigned, int, int);
+	errno_t (*dclick)(input_t *, unsigned, int);
 } input_ev_ops_t;
 
Index: uspace/lib/c/include/io/kbd_event.h
===================================================================
--- uspace/lib/c/include/io/kbd_event.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/c/include/io/kbd_event.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -39,4 +39,5 @@
 #include <inttypes.h>
 #include <io/keycode.h>
+#include <types/common.h>
 
 typedef enum {
@@ -49,4 +50,7 @@
 	/** List handle */
 	link_t link;
+
+	/** Keyboard device ID */
+	sysarg_t kbd_id;
 
 	/** Press or release event. */
Index: uspace/lib/c/include/ipc/services.h
===================================================================
--- uspace/lib/c/include/ipc/services.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ 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,5 +55,7 @@
 #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"
 #define SERVICE_NAME_DHCP     "net/dhcp"
 #define SERVICE_NAME_DNSR     "net/dnsr"
Index: uspace/lib/ddev/src/ddev_srv.c
===================================================================
--- uspace/lib/ddev/src/ddev_srv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ddev/src/ddev_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2019 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -101,4 +101,5 @@
 	rc = async_data_read_finalize(&call, &info, sizeof(ddev_info_t));
 	if (rc != EOK) {
+		async_answer_0(&call, rc);
 		async_answer_0(icall, rc);
 		return;
Index: uspace/lib/device/src/bd_srv.c
===================================================================
--- uspace/lib/device/src/bd_srv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/device/src/bd_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -56,4 +56,5 @@
 	ipc_call_t rcall;
 	if (!async_data_read_receive(&rcall, &size)) {
+		async_answer_0(&rcall, EINVAL);
 		async_answer_0(call, EINVAL);
 		return;
@@ -99,4 +100,5 @@
 	ipc_call_t rcall;
 	if (!async_data_read_receive(&rcall, &size)) {
+		async_answer_0(&rcall, EINVAL);
 		async_answer_0(call, EINVAL);
 		return;
Index: uspace/lib/device/src/io/chardev_srv.c
===================================================================
--- uspace/lib/device/src/io/chardev_srv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/device/src/io/chardev_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2014 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -55,4 +55,5 @@
 	ipc_call_t call;
 	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EINVAL);
 		async_answer_0(icall, EINVAL);
 		return;
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/display/include/disp_srv.h
===================================================================
--- uspace/lib/display/include/disp_srv.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/include/disp_srv.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -64,7 +64,9 @@
 	errno_t (*window_get_max_rect)(void *, sysarg_t, gfx_rect_t *);
 	errno_t (*window_resize)(void *, sysarg_t, gfx_coord2_t *, gfx_rect_t *);
+	errno_t (*window_minimize)(void *, sysarg_t);
 	errno_t (*window_maximize)(void *, sysarg_t);
 	errno_t (*window_unmaximize)(void *, sysarg_t);
 	errno_t (*window_set_cursor)(void *, sysarg_t, display_stock_cursor_t);
+	errno_t (*window_set_caption)(void *, sysarg_t, const char *);
 	errno_t (*get_event)(void *, sysarg_t *, display_wnd_ev_t *);
 	errno_t (*get_info)(void *, display_info_t *);
Index: uspace/lib/display/include/display.h
===================================================================
--- uspace/lib/display/include/display.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/include/display.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -62,8 +62,10 @@
 extern errno_t display_window_resize(display_window_t *,
     gfx_coord2_t *, gfx_rect_t *);
+extern errno_t display_window_minimize(display_window_t *);
 extern errno_t display_window_maximize(display_window_t *);
 extern errno_t display_window_unmaximize(display_window_t *);
 extern errno_t display_window_set_cursor(display_window_t *,
     display_stock_cursor_t);
+extern errno_t display_window_set_caption(display_window_t *, const char *);
 
 #endif
Index: uspace/lib/display/include/ipc/display.h
===================================================================
--- uspace/lib/display/include/ipc/display.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/include/ipc/display.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -42,4 +42,5 @@
 	DISPLAY_WINDOW_CREATE,
 	DISPLAY_WINDOW_DESTROY,
+	DISPLAY_WINDOW_MINIMIZE,
 	DISPLAY_WINDOW_MAXIMIZE,
 	DISPLAY_WINDOW_MOVE,
@@ -50,4 +51,5 @@
 	DISPLAY_WINDOW_RESIZE_REQ,
 	DISPLAY_WINDOW_SET_CURSOR,
+	DISPLAY_WINDOW_SET_CAPTION,
 	DISPLAY_WINDOW_UNMAXIMIZE,
 	DISPLAY_GET_EVENT,
Index: uspace/lib/display/include/types/display/wndparams.h
===================================================================
--- uspace/lib/display/include/types/display/wndparams.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/include/types/display/wndparams.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -42,8 +42,16 @@
 	/** Popup window (capture events, no focus) */
 	wndf_popup = 0x1,
+	/** Topmost window */
+	wndf_topmost = 0x2,
 	/** Set specific initial window position */
-	wndf_setpos = 0x2,
+	wndf_setpos = 0x4,
+	/** Window is minimized */
+	wndf_minimized = 0x8,
 	/** Window is maximized */
-	wndf_maximized = 0x4
+	wndf_maximized = 0x10,
+	/** Special system window */
+	wndf_system = 0x20,
+	/** Maximized windows should avoid this window */
+	wndf_avoid = 0x40
 } display_wnd_flags_t;
 
@@ -58,4 +66,6 @@
 	/** Bounding rectangle */
 	gfx_rect_t rect;
+	/** Window caption */
+	const char *caption;
 	/** Minimum size (when being resized) */
 	gfx_coord2_t min_size;
Index: uspace/lib/display/private/params.h
===================================================================
--- uspace/lib/display/private/params.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/private/params.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2020 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -46,4 +46,21 @@
 } display_wnd_resize_t;
 
+/** Display window parameters encoded for transport
+ *
+ * This omits variable-length fields (window caption).
+ */
+typedef struct {
+	/** Bounding rectangle */
+	gfx_rect_t rect;
+	/** Window caption size in bytes */
+	size_t caption_size;
+	/** Minimum size (when being resized) */
+	gfx_coord2_t min_size;
+	/** Initial position (if flag wndf_setpos is set) */
+	gfx_coord2_t pos;
+	/** Flags */
+	display_wnd_flags_t flags;
+} display_wnd_params_enc_t;
+
 #endif
 
Index: uspace/lib/display/src/disp_srv.c
===================================================================
--- uspace/lib/display/src/disp_srv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/src/disp_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -63,5 +63,7 @@
 	sysarg_t wnd_id;
 	ipc_call_t call;
+	display_wnd_params_enc_t eparams;
 	display_wnd_params_t params;
+	char *caption;
 	size_t size;
 	errno_t rc;
@@ -73,12 +75,40 @@
 	}
 
-	if (size != sizeof(display_wnd_params_t)) {
-		async_answer_0(&call, EINVAL);
-		async_answer_0(icall, EINVAL);
-		return;
-	}
-
-	rc = async_data_write_finalize(&call, &params, size);
-	if (rc != EOK) {
+	if (size != sizeof(display_wnd_params_enc_t)) {
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_write_finalize(&call, &eparams, size);
+	if (rc != EOK) {
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	caption = calloc(eparams.caption_size + 1, 1);
+	if (caption == NULL) {
+		async_answer_0(icall, ENOMEM);
+		return;
+	}
+
+	if (!async_data_write_receive(&call, &size)) {
+		free(caption);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != eparams.caption_size) {
+		free(caption);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_write_finalize(&call, caption, eparams.caption_size);
+	if (rc != EOK) {
+		free(caption);
 		async_answer_0(&call, rc);
 		async_answer_0(icall, rc);
@@ -87,7 +117,15 @@
 
 	if (srv->ops->window_create == NULL) {
-		async_answer_0(icall, ENOTSUP);
-		return;
-	}
+		free(caption);
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	/* Decode the parameters from transport */
+	params.rect = eparams.rect;
+	params.caption = caption;
+	params.min_size = eparams.min_size;
+	params.pos = eparams.pos;
+	params.flags = eparams.flags;
 
 	rc = srv->ops->window_create(srv->arg, &params, &wnd_id);
@@ -203,4 +241,5 @@
 
 	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
 		async_answer_0(icall, EREFUSED);
 		return;
@@ -247,4 +286,5 @@
 
 	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
 		async_answer_0(icall, EREFUSED);
 		return;
@@ -353,4 +393,20 @@
 }
 
+static void display_window_minimize_srv(display_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	wnd_id = ipc_get_arg1(icall);
+
+	if (srv->ops->window_minimize == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->window_minimize(srv->arg, wnd_id);
+	async_answer_0(icall, rc);
+}
+
 static void display_window_maximize_srv(display_srv_t *srv, ipc_call_t *icall)
 {
@@ -401,4 +457,47 @@
 	rc = srv->ops->window_set_cursor(srv->arg, wnd_id, cursor);
 	async_answer_0(icall, rc);
+}
+
+static void display_window_set_caption_srv(display_srv_t *srv,
+    ipc_call_t *icall)
+{
+	sysarg_t wnd_id;
+	ipc_call_t call;
+	char *caption;
+	size_t size;
+	errno_t rc;
+
+	wnd_id = ipc_get_arg1(icall);
+
+	if (!async_data_write_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	caption = calloc(size + 1, 1);
+	if (caption == NULL) {
+		async_answer_0(&call, ENOMEM);
+		async_answer_0(icall, ENOMEM);
+		return;
+	}
+
+	rc = async_data_write_finalize(&call, caption, size);
+	if (rc != EOK) {
+		free(caption);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	if (srv->ops->window_set_caption == NULL) {
+		free(caption);
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->window_set_caption(srv->arg, wnd_id, caption);
+	async_answer_0(icall, rc);
+	free(caption);
 }
 
@@ -424,4 +523,5 @@
 	/* Transfer event data */
 	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
 		async_answer_0(icall, EREFUSED);
 		return;
@@ -458,4 +558,5 @@
 	/* Transfer information */
 	if (!async_data_read_receive(&call, &size)) {
+		async_answer_0(&call, EREFUSED);
 		async_answer_0(icall, EREFUSED);
 		return;
@@ -530,4 +631,7 @@
 			display_window_resize_srv(srv, &call);
 			break;
+		case DISPLAY_WINDOW_MINIMIZE:
+			display_window_minimize_srv(srv, &call);
+			break;
 		case DISPLAY_WINDOW_MAXIMIZE:
 			display_window_maximize_srv(srv, &call);
@@ -538,4 +642,7 @@
 		case DISPLAY_WINDOW_SET_CURSOR:
 			display_window_set_cursor_srv(srv, &call);
+			break;
+		case DISPLAY_WINDOW_SET_CAPTION:
+			display_window_set_caption_srv(srv, &call);
 			break;
 		case DISPLAY_GET_EVENT:
Index: uspace/lib/display/src/display.c
===================================================================
--- uspace/lib/display/src/display.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/src/display.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -38,4 +38,5 @@
 #include <mem.h>
 #include <stdlib.h>
+#include <str.h>
 #include "../private/display.h"
 #include "../private/params.h"
@@ -147,4 +148,5 @@
 {
 	memset(params, 0, sizeof(*params));
+	params->caption = "";
 }
 
@@ -162,8 +164,16 @@
 {
 	display_window_t *window;
-	async_exch_t *exch;
-	aid_t req;
-	ipc_call_t answer;
-	errno_t rc;
+	display_wnd_params_enc_t eparams;
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	errno_t rc;
+
+	/* Encode the parameters for transport */
+	eparams.rect = params->rect;
+	eparams.caption_size = str_size(params->caption);
+	eparams.min_size = params->min_size;
+	eparams.pos = params->pos;
+	eparams.flags = params->flags;
 
 	window = calloc(1, sizeof(display_window_t));
@@ -173,5 +183,18 @@
 	exch = async_exchange_begin(display->sess);
 	req = async_send_0(exch, DISPLAY_WINDOW_CREATE, &answer);
-	rc = async_data_write_start(exch, params, sizeof (display_wnd_params_t));
+
+	/* Write fixed fields */
+	rc = async_data_write_start(exch, &eparams,
+	    sizeof (display_wnd_params_enc_t));
+	if (rc != EOK) {
+		async_exchange_end(exch);
+		async_forget(req);
+		free(window);
+		return rc;
+	}
+
+	/* Write caption */
+	rc = async_data_write_start(exch, params->caption,
+	    eparams.caption_size);
 	async_exchange_end(exch);
 	if (rc != EOK) {
@@ -487,4 +510,21 @@
 }
 
+/** Minimize window.
+ *
+ * @param window Window
+ * @return EOK on success or an error code
+ */
+errno_t display_window_minimize(display_window_t *window)
+{
+	async_exch_t *exch;
+	errno_t rc;
+
+	exch = async_exchange_begin(window->display->sess);
+	rc = async_req_1_0(exch, DISPLAY_WINDOW_MINIMIZE, window->id);
+	async_exchange_end(exch);
+
+	return rc;
+}
+
 /** Maximize window.
  *
@@ -540,4 +580,37 @@
 	    cursor);
 	async_exchange_end(exch);
+	return rc;
+}
+
+/** Set display window caption.
+ *
+ * @param window Window
+ * @param caption New caption
+ * @return EOK on success or an error code
+ */
+errno_t display_window_set_caption(display_window_t *window,
+    const char *caption)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	size_t cap_size;
+	errno_t rc;
+
+	cap_size = str_size(caption);
+
+	exch = async_exchange_begin(window->display->sess);
+	req = async_send_1(exch, DISPLAY_WINDOW_SET_CAPTION, window->id,
+	    &answer);
+
+	/* Write caption */
+	rc = async_data_write_start(exch, caption, cap_size);
+	async_exchange_end(exch);
+	if (rc != EOK) {
+		async_forget(req);
+		return rc;
+	}
+
+	async_wait_for(req, &rc);
 	return rc;
 }
Index: uspace/lib/display/test/display.c
===================================================================
--- uspace/lib/display/test/display.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/display/test/display.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -38,4 +38,5 @@
 #include <loc.h>
 #include <pcut/pcut.h>
+#include <str.h>
 #include "../private/display.h"
 
@@ -65,7 +66,9 @@
 static errno_t test_window_resize(void *, sysarg_t, gfx_coord2_t *,
     gfx_rect_t *);
+static errno_t test_window_minimize(void *, sysarg_t);
 static errno_t test_window_maximize(void *, sysarg_t);
 static errno_t test_window_unmaximize(void *, sysarg_t);
 static errno_t test_window_set_cursor(void *, sysarg_t, display_stock_cursor_t);
+static errno_t test_window_set_caption(void *, sysarg_t, const char *);
 static errno_t test_get_event(void *, sysarg_t *, display_wnd_ev_t *);
 static errno_t test_get_info(void *, display_info_t *);
@@ -82,7 +85,9 @@
 	.window_resize_req = test_window_resize_req,
 	.window_resize = test_window_resize,
+	.window_minimize = test_window_minimize,
 	.window_maximize = test_window_maximize,
 	.window_unmaximize = test_window_unmaximize,
 	.window_set_cursor = test_window_set_cursor,
+	.window_set_caption = test_window_set_caption,
 	.get_event = test_get_event,
 	.get_info = test_get_info
@@ -142,4 +147,5 @@
 	sysarg_t resize_wnd_id;
 
+	bool window_minimize_called;
 	bool window_maximize_called;
 	bool window_unmaximize_called;
@@ -148,4 +154,8 @@
 	sysarg_t set_cursor_wnd_id;
 	display_stock_cursor_t set_cursor_cursor;
+
+	bool window_set_caption_called;
+	sysarg_t set_caption_wnd_id;
+	char *set_caption_caption;
 
 	bool get_event_called;
@@ -1050,4 +1060,100 @@
 }
 
+/** display_window_minimize() with server returning error response works. */
+PCUT_TEST(window_minimize_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	display_t *disp = NULL;
+	display_wnd_params_t params;
+	display_window_t *wnd;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_display_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_display_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_display_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = display_open(test_display_svc, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(disp);
+
+	resp.rc = EOK;
+	display_wnd_params_init(&params);
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p0.x = 100;
+	params.rect.p0.y = 100;
+
+	rc = display_window_create(disp, &params, &test_display_wnd_cb,
+	    (void *) &resp, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wnd);
+
+	resp.rc = EIO;
+	resp.window_minimize_called = false;
+
+	rc = display_window_minimize(wnd);
+	PCUT_ASSERT_TRUE(resp.window_minimize_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	display_window_destroy(wnd);
+	display_close(disp);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** display_window_minimize() with server returning success response works. */
+PCUT_TEST(window_minimize_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	display_t *disp = NULL;
+	display_wnd_params_t params;
+	display_window_t *wnd;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_display_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_display_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_display_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = display_open(test_display_svc, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(disp);
+
+	resp.rc = EOK;
+	display_wnd_params_init(&params);
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p0.x = 100;
+	params.rect.p0.y = 100;
+
+	rc = display_window_create(disp, &params, &test_display_wnd_cb,
+	    (void *) &resp, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wnd);
+
+	resp.rc = EOK;
+	resp.window_minimize_called = false;
+
+	rc = display_window_minimize(wnd);
+	PCUT_ASSERT_TRUE(resp.window_minimize_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	display_window_destroy(wnd);
+	display_close(disp);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
 /** display_window_maximize() with server returning error response works. */
 PCUT_TEST(window_maximize_failure)
@@ -1240,4 +1346,112 @@
 	PCUT_ASSERT_INT_EQUALS(dcurs_size_ud, resp.set_cursor_cursor);
 
+	display_window_destroy(wnd);
+	display_close(disp);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** display_window_set_caption() with server returning error response works. */
+PCUT_TEST(window_set_caption_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	display_t *disp = NULL;
+	display_wnd_params_t params;
+	display_window_t *wnd;
+	const char *caption;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_display_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_display_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_display_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = display_open(test_display_svc, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(disp);
+
+	resp.rc = EOK;
+	display_wnd_params_init(&params);
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p0.x = 100;
+	params.rect.p0.y = 100;
+
+	rc = display_window_create(disp, &params, &test_display_wnd_cb,
+	    (void *) &resp, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wnd);
+
+	caption = "Hello";
+
+	resp.rc = EIO;
+	resp.window_set_caption_called = false;
+
+	rc = display_window_set_caption(wnd, caption);
+	PCUT_ASSERT_INT_EQUALS(wnd->id, resp.set_caption_wnd_id);
+	PCUT_ASSERT_TRUE(resp.window_set_caption_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+	PCUT_ASSERT_INT_EQUALS(0, str_cmp(caption, resp.set_caption_caption));
+
+	//free(resp.set_caption_caption);
+	display_window_destroy(wnd);
+	display_close(disp);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** display_window_set_caption() with server returning success response works. */
+PCUT_TEST(window_set_caption_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	display_t *disp = NULL;
+	display_wnd_params_t params;
+	display_window_t *wnd;
+	const char *caption;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_display_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_display_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_display_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = display_open(test_display_svc, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(disp);
+
+	resp.rc = EOK;
+	display_wnd_params_init(&params);
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p0.x = 100;
+	params.rect.p0.y = 100;
+
+	rc = display_window_create(disp, &params, &test_display_wnd_cb,
+	    (void *) &resp, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wnd);
+
+	caption = "Hello";
+
+	resp.rc = EOK;
+	resp.window_set_caption_called = false;
+
+	rc = display_window_set_caption(wnd, caption);
+	PCUT_ASSERT_INT_EQUALS(wnd->id, resp.set_caption_wnd_id);
+	PCUT_ASSERT_TRUE(resp.window_set_caption_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+	PCUT_ASSERT_INT_EQUALS(0, str_cmp(caption, resp.set_caption_caption));
+
+	//free(resp.set_caption_caption);
 	display_window_destroy(wnd);
 	display_close(disp);
@@ -1988,4 +2202,13 @@
 }
 
+static errno_t test_window_minimize(void *arg, sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->window_minimize_called = true;
+	resp->resize_wnd_id = wnd_id;
+	return resp->rc;
+}
+
 static errno_t test_window_maximize(void *arg, sysarg_t wnd_id)
 {
@@ -2014,4 +2237,16 @@
 	resp->set_cursor_wnd_id = wnd_id;
 	resp->set_cursor_cursor = cursor;
+
+	return resp->rc;
+}
+
+static errno_t test_window_set_caption(void *arg, sysarg_t wnd_id,
+    const char *caption)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->window_set_caption_called = true;
+	resp->set_caption_wnd_id = wnd_id;
+	resp->set_caption_caption = str_dup(caption);
 
 	return resp->rc;
Index: uspace/lib/ext4/src/extent.c
===================================================================
--- uspace/lib/ext4/src/extent.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ext4/src/extent.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -643,5 +643,5 @@
 
 	uint16_t delete_count = block_count -
-	    (ext4_extent_get_start(path_ptr->extent) - first_fblock);
+	    (first_fblock - ext4_extent_get_start(path_ptr->extent));
 
 	/* Release all blocks */
Index: uspace/lib/gfxfont/include/types/gfx/text.h
===================================================================
--- uspace/lib/gfxfont/include/types/gfx/text.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/gfxfont/include/types/gfx/text.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -73,8 +73,10 @@
 	/** Horizontal alignment */
 	gfx_halign_t halign;
-	/** Justification width (for gfx_halign_justify) */
-	gfx_coord_t justify_width;
+	/** Width available for the text */
+	gfx_coord_t width;
 	/** Vertical alignment */
 	gfx_valign_t valign;
+	/** Abbreviate the text with ellipsis if it does not fit @c width */
+	bool abbreviate;
 	/** Underline */
 	bool underline;
Index: uspace/lib/gfxfont/src/text.c
===================================================================
--- uspace/lib/gfxfont/src/text.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/gfxfont/src/text.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -97,24 +97,39 @@
 /** Print string using text characters in text mode.
  *
- * @param font Font
  * @param pos Position of top-left corner of text
- * @param color Text color
+ * @param fmt Formatting
  * @param str String
  * @return EOK on success or an error code
  */
-static errno_t gfx_puttext_textmode(gfx_font_t *font, gfx_coord2_t *pos,
-    gfx_color_t *color, const char *str)
-{
-	gfx_context_t *gc = font->typeface->gc;
+static errno_t gfx_puttext_textmode(gfx_coord2_t *pos, gfx_text_fmt_t *fmt,
+    const char *str)
+{
+	gfx_context_t *gc = fmt->font->typeface->gc;
 	gfx_bitmap_params_t params;
 	gfx_bitmap_t *bitmap;
 	gfx_bitmap_alloc_t alloc;
+	gfx_coord_t width;
 	uint8_t attr;
 	pixelmap_t pmap;
 	gfx_coord_t x;
+	gfx_coord_t rmargin;
 	pixel_t pixel;
 	char32_t c;
 	size_t off;
+	bool ellipsis;
 	errno_t rc;
+
+	width = str_width(str);
+	if (fmt->abbreviate && width > fmt->width) {
+		ellipsis = true;
+		width = fmt->width;
+		if (width > 3)
+			rmargin = width - 3;
+		else
+			rmargin = width;
+	} else {
+		ellipsis = false;
+		rmargin = width;
+	}
 
 	/*
@@ -123,10 +138,10 @@
 	 */
 
-	gfx_color_get_ega(color, &attr);
+	gfx_color_get_ega(fmt->color, &attr);
 
 	gfx_bitmap_params_init(&params);
 	params.rect.p0.x = 0;
 	params.rect.p0.y = 0;
-	params.rect.p1.x = str_width(str);
+	params.rect.p1.x = width;
 	params.rect.p1.y = 1;
 
@@ -151,5 +166,5 @@
 
 	off = 0;
-	for (x = 0; x < params.rect.p1.x; x++) {
+	for (x = 0; x < rmargin; x++) {
 		c = str_decode(str, &off, STR_NO_LIMIT);
 		pixel = PIXEL(attr,
@@ -160,4 +175,15 @@
 	}
 
+	if (ellipsis) {
+		for (x = rmargin; x < params.rect.p1.x; x++) {
+			c = '.';
+			pixel = PIXEL(attr,
+			    (c >> 16) & 0xff,
+			    (c >> 8) & 0xff,
+			    c & 0xff);
+			pixelmap_put_pixel(&pmap, x, 0, pixel);
+		}
+	}
+
 	rc = gfx_bitmap_render(bitmap, NULL, pos);
 
@@ -183,5 +209,9 @@
 	/* Adjust position for horizontal alignment */
 	if (fmt->halign != gfx_halign_left) {
+		/* Compute text width */
 		width = gfx_text_width(fmt->font, str);
+		if (fmt->abbreviate && width > fmt->width)
+			width = fmt->width;
+
 		switch (fmt->halign) {
 		case gfx_halign_center:
@@ -233,4 +263,7 @@
 	gfx_coord2_t spos;
 	gfx_rect_t rect;
+	gfx_coord_t width;
+	gfx_coord_t rmargin;
+	bool ellipsis;
 	errno_t rc;
 
@@ -239,9 +272,20 @@
 	/* Text mode */
 	if ((fmt->font->finfo->props.flags & gff_text_mode) != 0)
-		return gfx_puttext_textmode(fmt->font, &spos, fmt->color, str);
+		return gfx_puttext_textmode(&spos, fmt, str);
 
 	rc = gfx_set_color(fmt->font->typeface->gc, fmt->color);
 	if (rc != EOK)
 		return rc;
+
+	width = gfx_text_width(fmt->font, str);
+
+	if (fmt->abbreviate && width > fmt->width) {
+		/* Need to append ellipsis */
+		ellipsis = true;
+		rmargin = spos.x + fmt->width - gfx_text_width(fmt->font, "...");
+	} else {
+		ellipsis = false;
+		rmargin = spos.x + width;
+	}
 
 	cpos = spos;
@@ -256,4 +300,8 @@
 		gfx_glyph_get_metrics(glyph, &gmetrics);
 
+		/* Stop if we would run over the right margin */
+		if (fmt->abbreviate && cpos.x + gmetrics.advance > rmargin)
+			break;
+
 		rc = gfx_glyph_render(glyph, &cpos);
 		if (rc != EOK)
@@ -274,4 +322,29 @@
 
 		rc = gfx_fill_rect(fmt->font->typeface->gc, &rect);
+		if (rc != EOK)
+			return rc;
+	}
+
+	/* Render ellipsis, if required */
+	if (ellipsis) {
+		rc = gfx_font_search_glyph(fmt->font, ".", &glyph, &stradv);
+		if (rc != EOK)
+			return EOK;
+
+		gfx_glyph_get_metrics(glyph, &gmetrics);
+
+		rc = gfx_glyph_render(glyph, &cpos);
+		if (rc != EOK)
+			return rc;
+
+		cpos.x += gmetrics.advance;
+
+		rc = gfx_glyph_render(glyph, &cpos);
+		if (rc != EOK)
+			return rc;
+
+		cpos.x += gmetrics.advance;
+
+		rc = gfx_glyph_render(glyph, &cpos);
 		if (rc != EOK)
 			return rc;
@@ -376,4 +449,7 @@
 	tfmt.valign = gfx_valign_baseline;
 
+	/* Remaining available width */
+	tfmt.width = fmt->width - (cpos->x - spos.x);
+
 	*cfmt = tfmt;
 }
@@ -390,10 +466,14 @@
 {
 	gfx_coord2_t spos;
+	gfx_coord_t width;
 
 	gfx_text_start_pos(pos, fmt, str, &spos);
+	width = gfx_text_width(fmt->font, str);
+	if (fmt->abbreviate && width > fmt->width)
+		width = fmt->width;
 
 	rect->p0.x = spos.x;
 	rect->p0.y = spos.y - fmt->font->metrics.ascent;
-	rect->p1.x = spos.x + gfx_text_width(fmt->font, str);
+	rect->p1.x = spos.x + width;
 	rect->p1.y = spos.y +  fmt->font->metrics.descent + 1;
 }
Index: uspace/lib/inet/src/iplink_srv.c
===================================================================
--- uspace/lib/inet/src/iplink_srv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/inet/src/iplink_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -74,6 +74,9 @@
 
 	rc = async_data_read_finalize(&call, &mac, size);
-	if (rc != EOK)
-		async_answer_0(&call, rc);
+	if (rc != EOK) {
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
 
 	async_answer_0(icall, rc);
@@ -94,4 +97,5 @@
 	rc = srv->ops->set_mac48(srv, &mac);
 	if (rc != EOK) {
+		async_answer_0(&call, rc);
 		async_answer_0(icall, rc);
 		return;
@@ -99,6 +103,9 @@
 
 	rc = async_data_read_finalize(&call, &mac, sizeof(eth_addr_t));
-	if (rc != EOK)
-		async_answer_0(&call, rc);
+	if (rc != EOK) {
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
 
 	async_answer_0(icall, rc);
@@ -109,4 +116,5 @@
 	ipc_call_t call;
 	size_t size;
+
 	if (!async_data_write_receive(&call, &size)) {
 		async_answer_0(&call, EREFUSED);
@@ -126,4 +134,5 @@
 		async_answer_0(&call, rc);
 		async_answer_0(icall, rc);
+		return;
 	}
 
@@ -136,4 +145,5 @@
 	ipc_call_t call;
 	size_t size;
+
 	if (!async_data_write_receive(&call, &size)) {
 		async_answer_0(&call, EREFUSED);
@@ -153,4 +163,5 @@
 		async_answer_0(&call, rc);
 		async_answer_0(icall, rc);
+		return;
 	}
 
@@ -181,7 +192,7 @@
 {
 	iplink_sdu6_t sdu;
-
-	ipc_call_t call;
-	size_t size;
+	ipc_call_t call;
+	size_t size;
+
 	if (!async_data_write_receive(&call, &size)) {
 		async_answer_0(&call, EREFUSED);
Index: uspace/lib/ipcgfx/src/server.c
===================================================================
--- uspace/lib/ipcgfx/src/server.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ipcgfx/src/server.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -151,4 +151,5 @@
 
 	if (!async_share_out_receive(&call, &size, &flags)) {
+		async_answer_0(&call, EINVAL);
 		async_answer_0(icall, EINVAL);
 		return;
@@ -157,4 +158,5 @@
 	/* Check size */
 	if (size != PAGES2SIZE(SIZE2PAGES(dim.x * dim.y * sizeof(uint32_t)))) {
+		async_answer_0(&call, EINVAL);
 		async_answer_0(icall, EINVAL);
 		return;
@@ -163,4 +165,5 @@
 	rc = async_share_out_finalize(&call, &pixels);
 	if (rc != EOK || pixels == AS_MAP_FAILED) {
+		async_answer_0(&call, ENOMEM);
 		async_answer_0(icall, ENOMEM);
 		return;
Index: uspace/lib/meson.build
===================================================================
--- uspace/lib/meson.build	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -102,5 +102,7 @@
 	'ieee80211',
 	'ddev',
+	'dispcfg',
 	'display',
+	'wndmgt',
 
 	'ui',
Index: uspace/lib/ui/include/types/ui/paint.h
===================================================================
--- uspace/lib/ui/include/types/ui/paint.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/types/ui/paint.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -94,6 +94,6 @@
 	/** Horizontal alignment */
 	gfx_halign_t halign;
-	/** Justification width (for gfx_halign_justify) */
-	gfx_coord_t justify_width;
+	/** Width available for the text */
+	gfx_coord_t width;
 	/** Vertical alignment */
 	gfx_valign_t valign;
Index: uspace/lib/ui/include/types/ui/wdecor.h
===================================================================
--- uspace/lib/ui/include/types/ui/wdecor.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/types/ui/wdecor.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -51,12 +51,15 @@
 	/** Window has a title bar */
 	ui_wds_titlebar = 0x2,
+	/** Window has a minimize button */
+	ui_wds_minimize_btn = 0x4,
 	/** Window has a maximize button */
-	ui_wds_maximize_btn = 0x4,
+	ui_wds_maximize_btn = 0x8,
 	/** Window has a close button */
-	ui_wds_close_btn = 0x8,
+	ui_wds_close_btn = 0x10,
 	/** Window is resizable */
-	ui_wds_resizable = 0x10,
+	ui_wds_resizable = 0x20,
 	/** Window is decorated (default decoration) */
-	ui_wds_decorated = ui_wds_frame | ui_wds_titlebar | ui_wds_close_btn
+	ui_wds_decorated = ui_wds_frame | ui_wds_titlebar |
+	    ui_wds_minimize_btn | ui_wds_close_btn
 } ui_wdecor_style_t;
 
@@ -78,4 +81,5 @@
 /** Window decoration callbacks */
 typedef struct ui_wdecor_cb {
+	void (*minimize)(ui_wdecor_t *, void *);
 	void (*maximize)(ui_wdecor_t *, void *);
 	void (*unmaximize)(ui_wdecor_t *, void *);
Index: uspace/lib/ui/include/types/ui/window.h
===================================================================
--- uspace/lib/ui/include/types/ui/window.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/types/ui/window.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -66,5 +66,11 @@
 typedef enum {
 	/** Popup window */
-	ui_wndf_popup = 0x1
+	ui_wndf_popup = 0x1,
+	/** Topmost window */
+	ui_wndf_topmost = 0x2,
+	/** Special system window */
+	ui_wndf_system = 0x4,
+	/** Maximized windows should avoid this window */
+	ui_wndf_avoid = 0x8
 } ui_wnd_flags_t;
 
@@ -87,4 +93,5 @@
 /** Window callbacks */
 typedef struct ui_window_cb {
+	void (*minimize)(ui_window_t *, void *);
 	void (*maximize)(ui_window_t *, void *);
 	void (*unmaximize)(ui_window_t *, void *);
Index: uspace/lib/ui/include/ui/paint.h
===================================================================
--- uspace/lib/ui/include/ui/paint.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/ui/paint.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -65,4 +65,6 @@
 extern errno_t ui_paint_cross(gfx_context_t *, gfx_coord2_t *, gfx_coord_t,
     gfx_coord_t, gfx_coord_t);
+extern errno_t ui_paint_minicon(ui_resource_t *, gfx_coord2_t *, gfx_coord_t,
+    gfx_coord_t);
 extern errno_t ui_paint_maxicon(ui_resource_t *, gfx_coord2_t *, gfx_coord_t,
     gfx_coord_t);
Index: uspace/lib/ui/include/ui/pbutton.h
===================================================================
--- uspace/lib/ui/include/ui/pbutton.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/ui/pbutton.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -56,4 +56,7 @@
 extern void ui_pbutton_set_rect(ui_pbutton_t *, gfx_rect_t *);
 extern void ui_pbutton_set_default(ui_pbutton_t *, bool);
+extern bool ui_pbutton_get_light(ui_pbutton_t *);
+extern void ui_pbutton_set_light(ui_pbutton_t *, bool);
+extern errno_t ui_pbutton_set_caption(ui_pbutton_t *, const char *);
 extern errno_t ui_pbutton_paint(ui_pbutton_t *);
 extern void ui_pbutton_press(ui_pbutton_t *);
Index: uspace/lib/ui/include/ui/resource.h
===================================================================
--- uspace/lib/ui/include/ui/resource.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/ui/resource.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -38,4 +38,5 @@
 
 #include <errno.h>
+#include <gfx/color.h>
 #include <gfx/context.h>
 #include <gfx/font.h>
@@ -49,4 +50,7 @@
 extern void ui_resource_expose(ui_resource_t *);
 extern gfx_font_t *ui_resource_get_font(ui_resource_t *);
+extern bool ui_resource_is_textmode(ui_resource_t *);
+extern gfx_color_t *ui_resource_get_wnd_face_color(ui_resource_t *);
+extern gfx_color_t *ui_resource_get_wnd_text_color(ui_resource_t *);
 
 #endif
Index: uspace/lib/ui/include/ui/ui.h
===================================================================
--- uspace/lib/ui/include/ui/ui.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/ui/ui.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -39,4 +39,5 @@
 #include <display.h>
 #include <errno.h>
+#include <gfx/coord.h>
 #include <io/console.h>
 #include <stdbool.h>
@@ -53,4 +54,5 @@
 extern bool ui_is_textmode(ui_t *);
 extern bool ui_is_fullscreen(ui_t *);
+extern errno_t ui_get_rect(ui_t *, gfx_rect_t *);
 extern errno_t ui_suspend(ui_t *);
 extern errno_t ui_resume(ui_t *);
Index: uspace/lib/ui/include/ui/window.h
===================================================================
--- uspace/lib/ui/include/ui/window.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/include/ui/window.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -65,4 +65,5 @@
 extern void ui_window_set_ctl_cursor(ui_window_t *, ui_stock_cursor_t);
 extern errno_t ui_window_paint(ui_window_t *);
+extern errno_t ui_window_def_minimize(ui_window_t *);
 extern errno_t ui_window_def_maximize(ui_window_t *);
 extern errno_t ui_window_def_unmaximize(ui_window_t *);
Index: uspace/lib/ui/private/pbutton.h
===================================================================
--- uspace/lib/ui/private/pbutton.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/private/pbutton.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -62,7 +62,9 @@
 	gfx_rect_t rect;
 	/** Caption */
-	const char *caption;
+	char *caption;
 	/** Button is selected as default */
 	bool isdefault;
+	/** Button light is on */
+	bool light;
 	/** Button is currently held down */
 	bool held;
Index: uspace/lib/ui/private/resource.h
===================================================================
--- uspace/lib/ui/private/resource.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/private/resource.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -66,4 +66,6 @@
 	/** Button face color */
 	gfx_color_t *btn_face_color;
+	/** Lit button face color */
+	gfx_color_t *btn_face_lit_color;
 	/** Button text color */
 	gfx_color_t *btn_text_color;
Index: uspace/lib/ui/private/wdecor.h
===================================================================
--- uspace/lib/ui/private/wdecor.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/private/wdecor.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -65,4 +65,6 @@
 	/** Window is maximized */
 	bool maximized;
+	/** Minimize button */
+	struct ui_pbutton *btn_min;
 	/** Maximize button */
 	struct ui_pbutton *btn_max;
@@ -80,4 +82,8 @@
 	/** Title bar rectangle */
 	gfx_rect_t title_bar_rect;
+	/** Window caption rectangle */
+	gfx_rect_t caption_rect;
+	/** Minimize button rectangle */
+	gfx_rect_t btn_min_rect;
 	/** Maximize button rectangle */
 	gfx_rect_t btn_max_rect;
@@ -88,4 +94,5 @@
 } ui_wdecor_geom_t;
 
+extern void ui_wdecor_minimize(ui_wdecor_t *);
 extern void ui_wdecor_maximize(ui_wdecor_t *);
 extern void ui_wdecor_unmaximize(ui_wdecor_t *);
Index: uspace/lib/ui/private/window.h
===================================================================
--- uspace/lib/ui/private/window.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/private/window.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -112,4 +112,5 @@
 
 extern display_stock_cursor_t wnd_dcursor_from_cursor(ui_stock_cursor_t);
+extern void ui_window_send_minimize(ui_window_t *);
 extern void ui_window_send_maximize(ui_window_t *);
 extern void ui_window_send_unmaximize(ui_window_t *);
Index: uspace/lib/ui/src/paint.c
===================================================================
--- uspace/lib/ui/src/paint.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/paint.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -529,4 +529,33 @@
 }
 
+/** Paint minimize icon.
+ *
+ * @param resource UI resource
+ * @param pos Center position
+ * @param w Icon width
+ * @param h Icon height
+ * @return EOK on success or an error code
+ */
+errno_t ui_paint_minicon(ui_resource_t *resource, gfx_coord2_t *pos,
+    gfx_coord_t w, gfx_coord_t h)
+{
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = gfx_set_color(resource->gc, resource->btn_text_color);
+	if (rc != EOK)
+		return rc;
+
+	rect.p0.x = pos->x - w / 2;
+	rect.p0.y = pos->y + h / 2 - 2;
+	rect.p1.x = rect.p0.x + w;
+	rect.p1.y = rect.p0.y + 2;
+	rc = gfx_fill_rect(resource->gc, &rect);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
 /** Paint maximize icon.
  *
@@ -922,5 +951,5 @@
 	tfmt.color = fmt->color;
 	tfmt.halign = fmt->halign;
-	tfmt.justify_width = fmt->justify_width;
+	tfmt.width = fmt->width;
 	tfmt.valign = fmt->valign;
 	tfmt.underline = false;
Index: uspace/lib/ui/src/pbutton.c
===================================================================
--- uspace/lib/ui/src/pbutton.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/pbutton.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -54,5 +54,7 @@
 enum {
 	ui_pb_press_dx = 1,
-	ui_pb_press_dy = 1
+	ui_pb_press_dy = 1,
+	ui_pb_pad_x = 2,
+	ui_pb_pad_x_text = 1
 };
 
@@ -114,4 +116,5 @@
 
 	ui_control_delete(pbutton->control);
+	free(pbutton->caption);
 	free(pbutton);
 }
@@ -183,4 +186,43 @@
 {
 	pbutton->isdefault = isdefault;
+}
+
+/** Get button light status.
+ *
+ * @param pbutton Button
+ * @return @c true iff light is on
+ */
+bool ui_pbutton_get_light(ui_pbutton_t *pbutton)
+{
+	return pbutton->light;
+}
+
+/** Turn button light on or off.
+ *
+ * @param pbutton Button
+ * @param light @c true iff button should be lit
+ */
+void ui_pbutton_set_light(ui_pbutton_t *pbutton, bool light)
+{
+	pbutton->light = light;
+}
+
+/** Set push button caption.
+ *
+ * @param pbutton Button
+ * @param caption New caption
+ * @return EOK on success, ENOMEM if out of memory
+ */
+errno_t ui_pbutton_set_caption(ui_pbutton_t *pbutton, const char *caption)
+{
+	char *dcaption;
+
+	dcaption = str_dup(caption);
+	if (dcaption == NULL)
+		return ENOMEM;
+
+	free(pbutton->caption);
+	pbutton->caption = dcaption;
+	return EOK;
 }
 
@@ -303,5 +345,7 @@
 	gfx_text_fmt_t fmt;
 	gfx_rect_t rect;
+	gfx_rect_t irect;
 	gfx_coord_t thickness;
+	gfx_color_t *color;
 	bool depressed;
 	errno_t rc;
@@ -315,5 +359,8 @@
 	rect.p1.y = pbutton->rect.p1.y - thickness;
 
-	rc = gfx_set_color(pbutton->res->gc, pbutton->res->btn_face_color);
+	color = pbutton->light ? pbutton->res->btn_face_lit_color :
+	    pbutton->res->btn_face_color;
+
+	rc = gfx_set_color(pbutton->res->gc, color);
 	if (rc != EOK)
 		goto error;
@@ -340,4 +387,5 @@
 	} else {
 		/* Text decoration */
+		ui_paint_get_inset_frame_inside(pbutton->res, &rect, &irect);
 		gfx_text_fmt_init(&fmt);
 		fmt.font = pbutton->res->font;
@@ -345,4 +393,6 @@
 		fmt.halign = gfx_halign_center;
 		fmt.valign = gfx_valign_center;
+		fmt.abbreviate = true;
+		fmt.width = irect.p1.x - irect.p0.x - 2 * ui_pb_pad_x;
 
 		rc = gfx_puttext(&pos, &fmt, pbutton->caption);
@@ -422,4 +472,8 @@
 	fmt.halign = gfx_halign_center;
 	fmt.valign = gfx_valign_center;
+	fmt.abbreviate = true;
+	fmt.width = rect.p1.x - rect.p0.x - 2 * ui_pb_pad_x_text;
+	if (fmt.width < 1)
+		fmt.width = 1;
 
 	rc = gfx_puttext(&pos, &fmt, pbutton->caption);
Index: uspace/lib/ui/src/resource.c
===================================================================
--- uspace/lib/ui/src/resource.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/resource.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -62,4 +62,5 @@
 	gfx_color_t *btn_frame_color = NULL;
 	gfx_color_t *btn_face_color = NULL;
+	gfx_color_t *btn_face_lit_color = NULL;
 	gfx_color_t *btn_text_color = NULL;
 	gfx_color_t *btn_highlight_color = NULL;
@@ -114,4 +115,8 @@
 		goto error;
 
+	rc = gfx_color_new_rgb_i16(0xe8e8, 0xe8e8, 0xe8e8, &btn_face_lit_color);
+	if (rc != EOK)
+		goto error;
+
 	rc = gfx_color_new_rgb_i16(0, 0, 0, &btn_text_color);
 	if (rc != EOK)
@@ -227,4 +232,5 @@
 	resource->btn_frame_color = btn_frame_color;
 	resource->btn_face_color = btn_face_color;
+	resource->btn_face_lit_color = btn_face_lit_color;
 	resource->btn_text_color = btn_text_color;
 	resource->btn_highlight_color = btn_highlight_color;
@@ -263,4 +269,6 @@
 	if (btn_face_color != NULL)
 		gfx_color_delete(btn_face_color);
+	if (btn_face_lit_color != NULL)
+		gfx_color_delete(btn_face_lit_color);
 	if (btn_text_color != NULL)
 		gfx_color_delete(btn_text_color);
@@ -336,4 +344,5 @@
 	gfx_color_t *btn_frame_color = NULL;
 	gfx_color_t *btn_face_color = NULL;
+	gfx_color_t *btn_face_lit_color = NULL;
 	gfx_color_t *btn_text_color = NULL;
 	gfx_color_t *btn_highlight_color = NULL;
@@ -383,4 +392,8 @@
 		goto error;
 
+	rc = gfx_color_new_ega(0x30, &btn_face_lit_color);
+	if (rc != EOK)
+		goto error;
+
 	rc = gfx_color_new_ega(0x20, &btn_text_color);
 	if (rc != EOK)
@@ -486,4 +499,5 @@
 	resource->btn_frame_color = btn_frame_color;
 	resource->btn_face_color = btn_face_color;
+	resource->btn_face_lit_color = btn_face_lit_color;
 	resource->btn_text_color = btn_text_color;
 	resource->btn_highlight_color = btn_highlight_color;
@@ -522,4 +536,6 @@
 	if (btn_face_color != NULL)
 		gfx_color_delete(btn_face_color);
+	if (btn_face_lit_color != NULL)
+		gfx_color_delete(btn_face_lit_color);
 	if (btn_text_color != NULL)
 		gfx_color_delete(btn_text_color);
@@ -607,4 +623,5 @@
 	gfx_color_delete(resource->btn_frame_color);
 	gfx_color_delete(resource->btn_face_color);
+	gfx_color_delete(resource->btn_face_lit_color);
 	gfx_color_delete(resource->btn_text_color);
 	gfx_color_delete(resource->btn_highlight_color);
@@ -677,4 +694,34 @@
 }
 
+/** Determine if resource is textmode.
+ *
+ * @param resource UI resource
+ * @return @c true iff resource is textmode
+ */
+bool ui_resource_is_textmode(ui_resource_t *resource)
+{
+	return resource->textmode;
+}
+
+/** Get the UI window face color.
+ *
+ * @param resource UI resource
+ * @return UI window face color
+ */
+gfx_color_t *ui_resource_get_wnd_face_color(ui_resource_t *resource)
+{
+	return resource->wnd_face_color;
+}
+
+/** Get the UI window text color.
+ *
+ * @param resource UI resource
+ * @return UI window text color
+ */
+gfx_color_t *ui_resource_get_wnd_text_color(ui_resource_t *resource)
+{
+	return resource->wnd_text_color;
+}
+
 /** @}
  */
Index: uspace/lib/ui/src/scrollbar.c
===================================================================
--- uspace/lib/ui/src/scrollbar.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/scrollbar.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -840,5 +840,5 @@
 
 		ui_scrollbar_set_pos(scrollbar, spos);
-		ui_scrollbar_moved(scrollbar, spos);
+		ui_scrollbar_moved(scrollbar, scrollbar->pos);
 	}
 
Index: uspace/lib/ui/src/ui.c
===================================================================
--- uspace/lib/ui/src/ui.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/ui.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -524,4 +524,37 @@
 }
 
+/** Get UI screen rectangle.
+ *
+ * @param ui User interface
+ * @param rect Place to store bounding rectangle
+ */
+errno_t ui_get_rect(ui_t *ui, gfx_rect_t *rect)
+{
+	display_info_t info;
+	sysarg_t cols, rows;
+	errno_t rc;
+
+	if (ui->display != NULL) {
+		rc = display_get_info(ui->display, &info);
+		if (rc != EOK)
+			return rc;
+
+		*rect = info.rect;
+	} else if (ui->console != NULL) {
+		rc = console_get_size(ui->console, &cols, &rows);
+		if (rc != EOK)
+			return rc;
+
+		rect->p0.x = 0;
+		rect->p0.y = 0;
+		rect->p1.x = cols;
+		rect->p1.y = rows;
+	} else {
+		return ENOTSUP;
+	}
+
+	return EOK;
+}
+
 /** Get clickmatic from UI.
  *
Index: uspace/lib/ui/src/wdecor.c
===================================================================
--- uspace/lib/ui/src/wdecor.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/wdecor.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -49,4 +49,8 @@
 #include "../private/wdecor.h"
 
+static void ui_wdecor_btn_min_clicked(ui_pbutton_t *, void *);
+static errno_t ui_wdecor_btn_min_paint(ui_pbutton_t *, void *,
+    gfx_coord2_t *);
+
 static void ui_wdecor_btn_max_clicked(ui_pbutton_t *, void *);
 static errno_t ui_wdecor_btn_max_paint(ui_pbutton_t *, void *,
@@ -56,4 +60,12 @@
 static errno_t ui_wdecor_btn_close_paint(ui_pbutton_t *, void *,
     gfx_coord2_t *);
+
+static ui_pbutton_cb_t ui_wdecor_btn_min_cb = {
+	.clicked = ui_wdecor_btn_min_clicked
+};
+
+static ui_pbutton_decor_ops_t ui_wdecor_btn_min_decor_ops = {
+	.paint = ui_wdecor_btn_min_paint
+};
 
 static ui_pbutton_cb_t ui_wdecor_btn_max_cb = {
@@ -88,4 +100,8 @@
 	/** Window frame width in text mode */
 	wdecor_frame_w_text = 1,
+	/** Window caption horizontal margin */
+	wdecor_cap_hmargin = 4,
+	/** Window caption horizontal margin in text mode */
+	wdecor_cap_hmargin_text = 1,
 	/** Close button cross leg length */
 	wdecor_close_cross_n = 5,
@@ -94,4 +110,8 @@
 	/** Close button cross pen height */
 	wdecor_close_cross_h = 1,
+	/** Minimize icon width */
+	wdecor_min_w = 10,
+	/** Minimize icon height */
+	wdecor_min_h = 10,
 	/** Maximize icon width */
 	wdecor_max_w = 10,
@@ -132,4 +152,18 @@
 	}
 
+	if ((style & ui_wds_minimize_btn) != 0) {
+		rc = ui_pbutton_create(resource, "_", &wdecor->btn_min);
+		if (rc != EOK) {
+			ui_wdecor_destroy(wdecor);
+			return rc;
+		}
+
+		ui_pbutton_set_cb(wdecor->btn_min, &ui_wdecor_btn_min_cb,
+		    (void *)wdecor);
+
+		ui_pbutton_set_decor_ops(wdecor->btn_min,
+		    &ui_wdecor_btn_min_decor_ops, (void *)wdecor);
+	}
+
 	if ((style & ui_wds_maximize_btn) != 0) {
 		rc = ui_pbutton_create(resource, "^", &wdecor->btn_max);
@@ -176,4 +210,5 @@
 		return;
 
+	ui_pbutton_destroy(wdecor->btn_min);
 	ui_pbutton_destroy(wdecor->btn_max);
 	ui_pbutton_destroy(wdecor->btn_close);
@@ -207,4 +242,6 @@
 	ui_wdecor_get_geom(wdecor, &geom);
 
+	if (wdecor->btn_min != NULL)
+		ui_pbutton_set_rect(wdecor->btn_min, &geom.btn_min_rect);
 	if (wdecor->btn_max != NULL)
 		ui_pbutton_set_rect(wdecor->btn_max, &geom.btn_max_rect);
@@ -326,7 +363,10 @@
 		fmt.halign = gfx_halign_center;
 		fmt.valign = gfx_valign_center;
-
-		pos.x = (trect.p0.x + trect.p1.x) / 2;
-		pos.y = (trect.p0.y + trect.p1.y) / 2;
+		fmt.abbreviate = true;
+		fmt.width = geom.caption_rect.p1.x -
+		    geom.caption_rect.p0.x;
+
+		pos.x = (geom.caption_rect.p0.x + geom.caption_rect.p1.x) / 2;
+		pos.y = (geom.caption_rect.p0.y + geom.caption_rect.p1.y) / 2;
 
 		if (wdecor->res->textmode) {
@@ -355,4 +395,10 @@
 			return rc;
 
+		if (wdecor->btn_min != NULL) {
+			rc = ui_pbutton_paint(wdecor->btn_min);
+			if (rc != EOK)
+				return rc;
+		}
+
 		if (wdecor->btn_max != NULL) {
 			rc = ui_pbutton_paint(wdecor->btn_max);
@@ -375,4 +421,14 @@
 }
 
+/** Send decoration minimize event.
+ *
+ * @param wdecor Window decoration
+ */
+void ui_wdecor_minimize(ui_wdecor_t *wdecor)
+{
+	if (wdecor->cb != NULL && wdecor->cb->minimize != NULL)
+		wdecor->cb->minimize(wdecor, wdecor->arg);
+}
+
 /** Send decoration maximize event.
  *
@@ -450,4 +506,5 @@
 	gfx_coord_t btn_x;
 	gfx_coord_t btn_y;
+	gfx_coord_t cap_hmargin;
 
 	/* Does window have a frame? */
@@ -529,4 +586,6 @@
 			geom->btn_max_rect.p1.x = btn_x;
 			geom->btn_max_rect.p1.y = btn_y + 20;
+
+			btn_x -= 20;
 		} else {
 			geom->btn_max_rect.p0.x = btn_x - 3;
@@ -534,4 +593,6 @@
 			geom->btn_max_rect.p1.x = btn_x;
 			geom->btn_max_rect.p1.y = btn_y + 1;
+
+			btn_x -= 3;
 		}
 	} else {
@@ -541,4 +602,39 @@
 		geom->btn_max_rect.p1.y = 0;
 	}
+
+	/* Does window have a minimize button? */
+	if ((wdecor->style & ui_wds_minimize_btn) != 0) {
+		if (wdecor->res->textmode == false) {
+			geom->btn_min_rect.p0.x = btn_x - 20;
+			geom->btn_min_rect.p0.y = btn_y;
+			geom->btn_min_rect.p1.x = btn_x;
+			geom->btn_min_rect.p1.y = btn_y + 20;
+
+			btn_x -= 20;
+		} else {
+			geom->btn_min_rect.p0.x = btn_x - 3;
+			geom->btn_min_rect.p0.y = btn_y;
+			geom->btn_min_rect.p1.x = btn_x;
+			geom->btn_min_rect.p1.y = btn_y + 1;
+
+			btn_x -= 3;
+		}
+	} else {
+		geom->btn_min_rect.p0.x = 0;
+		geom->btn_min_rect.p0.y = 0;
+		geom->btn_min_rect.p1.x = 0;
+		geom->btn_min_rect.p1.y = 0;
+	}
+
+	if (wdecor->res->textmode == false)
+		cap_hmargin = wdecor_cap_hmargin;
+	else
+		cap_hmargin = wdecor_cap_hmargin_text;
+
+	geom->caption_rect.p0.x = geom->title_bar_rect.p0.x +
+	    cap_hmargin;
+	geom->caption_rect.p0.y = geom->title_bar_rect.p0.y;
+	geom->caption_rect.p1.x = btn_x - cap_hmargin;
+	geom->caption_rect.p1.y = geom->title_bar_rect.p1.y;
 }
 
@@ -750,4 +846,10 @@
 	ui_wdecor_get_geom(wdecor, &geom);
 
+	if (wdecor->btn_min != NULL) {
+		claim = ui_pbutton_pos_event(wdecor->btn_min, event);
+		if (claim == ui_claimed)
+			return ui_claimed;
+	}
+
 	if (wdecor->btn_max != NULL) {
 		claim = ui_pbutton_pos_event(wdecor->btn_max, event);
@@ -775,7 +877,21 @@
 }
 
+/** Window decoration minimize button was clicked.
+ *
+ * @param pbutton Minimize button
+ * @param arg Argument (ui_wdecor_t)
+ */
+static void ui_wdecor_btn_min_clicked(ui_pbutton_t *pbutton, void *arg)
+{
+	ui_wdecor_t *wdecor = (ui_wdecor_t *) arg;
+
+	(void) pbutton;
+
+	ui_wdecor_minimize(wdecor);
+}
+
 /** Window decoration (un)maximize button was clicked.
  *
- * @param pbutton Close button
+ * @param pbutton (Un)maximize button
  * @param arg Argument (ui_wdecor_t)
  */
@@ -790,4 +906,22 @@
 	else
 		ui_wdecor_maximize(wdecor);
+}
+
+/** Paint minimize button decoration.
+ *
+ * @param pbutton Push button
+ * @param arg Argument (ui_wdecor_t *)
+ * @param pos Center position
+ */
+static errno_t ui_wdecor_btn_min_paint(ui_pbutton_t *pbutton,
+    void *arg, gfx_coord2_t *pos)
+{
+	ui_wdecor_t *wdecor = (ui_wdecor_t *)arg;
+	errno_t rc;
+
+	rc = ui_paint_minicon(wdecor->res, pos, wdecor_min_w,
+	    wdecor_min_h);
+
+	return rc;
 }
 
Index: uspace/lib/ui/src/window.c
===================================================================
--- uspace/lib/ui/src/window.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/src/window.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -74,4 +74,5 @@
 };
 
+static void wd_minimize(ui_wdecor_t *, void *);
 static void wd_maximize(ui_wdecor_t *, void *);
 static void wd_unmaximize(ui_wdecor_t *, void *);
@@ -83,4 +84,5 @@
 
 static ui_wdecor_cb_t wdecor_cb = {
+	.minimize = wd_minimize,
 	.maximize = wd_maximize,
 	.unmaximize = wd_unmaximize,
@@ -222,4 +224,5 @@
 	display_wnd_params_init(&dparams);
 	dparams.rect = params->rect;
+	dparams.caption = params->caption;
 	/* Only allow making the window larger */
 	gfx_rect_dims(&params->rect, &dparams.min_size);
@@ -227,4 +230,10 @@
 	if ((params->flags & ui_wndf_popup) != 0)
 		dparams.flags |= wndf_popup;
+	if ((params->flags & ui_wndf_topmost) != 0)
+		dparams.flags |= wndf_topmost;
+	if ((params->flags & ui_wndf_system) != 0)
+		dparams.flags |= wndf_system;
+	if ((params->flags & ui_wndf_avoid) != 0)
+		dparams.flags |= wndf_avoid;
 
 	if (ui->display != NULL) {
@@ -651,4 +660,21 @@
 errno_t ui_window_set_caption(ui_window_t *window, const char *caption)
 {
+	errno_t rc;
+
+	/* Set console caption if fullscreen window on console */
+	if (window->ui->console != NULL && window->placement ==
+	    ui_wnd_place_full_screen) {
+		rc = console_set_caption(window->ui->console, caption);
+		if (rc != EOK)
+			return rc;
+	}
+
+	/* Set display window caption if running on display service */
+	if (window->dwindow != NULL) {
+		rc = display_window_set_caption(window->dwindow, caption);
+		if (rc != EOK)
+			return rc;
+	}
+
 	return ui_wdecor_set_caption(window->wdecor, caption);
 }
@@ -891,4 +917,16 @@
 	ui_window_send_unfocus(window);
 	ui_unlock(ui);
+}
+
+/** Window decoration requested window minimization.
+ *
+ * @param wdecor Window decoration
+ * @param arg Argument (window)
+ */
+static void wd_minimize(ui_wdecor_t *wdecor, void *arg)
+{
+	ui_window_t *window = (ui_window_t *) arg;
+
+	ui_window_send_minimize(window);
 }
 
@@ -1017,4 +1055,16 @@
 }
 
+/** Send window minimize event.
+ *
+ * @param window Window
+ */
+void ui_window_send_minimize(ui_window_t *window)
+{
+	if (window->cb != NULL && window->cb->maximize != NULL)
+		window->cb->minimize(window, window->arg);
+	else
+		ui_window_def_minimize(window);
+}
+
 /** Send window maximize event.
  *
@@ -1107,4 +1157,24 @@
 	else
 		return ui_window_def_unfocus(window);
+}
+
+/** Default window minimize routine.
+ *
+ * @param window Window
+ * @return EOK on success or an error code
+ */
+errno_t ui_window_def_minimize(ui_window_t *window)
+{
+	errno_t rc;
+
+	if (window->dwindow != NULL) {
+		rc = display_window_minimize(window->dwindow);
+		if (rc != EOK)
+			goto error;
+	}
+
+	return EOK;
+error:
+	return rc;
 }
 
Index: uspace/lib/ui/test/paint.c
===================================================================
--- uspace/lib/ui/test/paint.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/paint.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -354,4 +354,32 @@
 }
 
+/** Paint mimimize icon */
+PCUT_TEST(minicon)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	ui_resource_t *resource = NULL;
+	test_gc_t tgc;
+	gfx_coord2_t center;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	center.x = 0;
+	center.y = 0;
+
+	rc = ui_paint_minicon(resource, &center, 8, 6);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_resource_destroy(resource);
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
 /** Paint maximize icon */
 PCUT_TEST(maxicon)
Index: uspace/lib/ui/test/pbutton.c
===================================================================
--- uspace/lib/ui/test/pbutton.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/pbutton.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -190,4 +190,59 @@
 }
 
+/** Get light gets internal field */
+PCUT_TEST(get_light)
+{
+	ui_pbutton_t *pbutton;
+	errno_t rc;
+
+	rc = ui_pbutton_create(NULL, "Hello", &pbutton);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	pbutton->light = true;
+	PCUT_ASSERT_TRUE(ui_pbutton_get_light(pbutton));
+
+	pbutton->light = false;
+	PCUT_ASSERT_FALSE(ui_pbutton_get_light(pbutton));
+
+	ui_pbutton_destroy(pbutton);
+}
+
+/** Set light sets internal field */
+PCUT_TEST(set_light)
+{
+	ui_pbutton_t *pbutton;
+	errno_t rc;
+
+	rc = ui_pbutton_create(NULL, "Hello", &pbutton);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_pbutton_set_light(pbutton, true);
+	PCUT_ASSERT_TRUE(pbutton->light);
+
+	ui_pbutton_set_light(pbutton, false);
+	PCUT_ASSERT_FALSE(pbutton->light);
+
+	ui_pbutton_destroy(pbutton);
+}
+
+/** Set caption sets internal field */
+PCUT_TEST(set_caption)
+{
+	ui_pbutton_t *pbutton;
+	errno_t rc;
+
+	rc = ui_pbutton_create(NULL, "Hello", &pbutton);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_STR_EQUALS("Hello", pbutton->caption);
+
+	rc = ui_pbutton_set_caption(pbutton, "World");
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_STR_EQUALS("World", pbutton->caption);
+
+	ui_pbutton_destroy(pbutton);
+}
+
 /** Paint button */
 PCUT_TEST(paint)
Index: uspace/lib/ui/test/resource.c
===================================================================
--- uspace/lib/ui/test/resource.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/resource.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -134,4 +134,110 @@
 }
 
+/** ui_resource_get_font() returns the font */
+PCUT_TEST(get_font)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	test_gc_t tgc;
+	ui_resource_t *resource = NULL;
+	gfx_font_t *font;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	font = ui_resource_get_font(resource);
+	PCUT_ASSERT_EQUALS(resource->font, font);
+
+	ui_resource_destroy(resource);
+
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** ui_resource_is_textmode() returns the textmode flag */
+PCUT_TEST(is_textmode)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	test_gc_t tgc;
+	ui_resource_t *resource = NULL;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	/* To make sure let's test both true and false case */
+	resource->textmode = true;
+	PCUT_ASSERT_TRUE(ui_resource_is_textmode(resource));
+	resource->textmode = false;
+	PCUT_ASSERT_FALSE(ui_resource_is_textmode(resource));
+
+	ui_resource_destroy(resource);
+
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** ui_resource_get_wnd_face_color() returns window face color */
+PCUT_TEST(get_wnd_face_color)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	test_gc_t tgc;
+	ui_resource_t *resource = NULL;
+	gfx_color_t *color;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	color = ui_resource_get_wnd_face_color(resource);
+	PCUT_ASSERT_EQUALS(resource->wnd_face_color, color);
+
+	ui_resource_destroy(resource);
+
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** ui_resource_get_wnd_text_color() returns window text color */
+PCUT_TEST(get_wnd_text_color)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	test_gc_t tgc;
+	ui_resource_t *resource = NULL;
+	gfx_color_t *color;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	color = ui_resource_get_wnd_text_color(resource);
+	PCUT_ASSERT_EQUALS(resource->wnd_text_color, color);
+
+	ui_resource_destroy(resource);
+
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
 static errno_t testgc_bitmap_create(void *arg, gfx_bitmap_params_t *params,
     gfx_bitmap_alloc_t *alloc, void **rbm)
Index: uspace/lib/ui/test/ui.c
===================================================================
--- uspace/lib/ui/test/ui.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/ui.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -169,4 +169,22 @@
 }
 
+/** ui_is_get_rect() */
+PCUT_TEST(get_rect)
+{
+	ui_t *ui = NULL;
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(ui);
+
+	/* This won't work without a display service */
+	rc = ui_get_rect(ui, &rect);
+	PCUT_ASSERT_ERRNO_VAL(ENOTSUP, rc);
+
+	ui_destroy(ui);
+}
+
 /** ui_lock(), ui_unlock() */
 PCUT_TEST(lock_unlock)
Index: uspace/lib/ui/test/wdecor.c
===================================================================
--- uspace/lib/ui/test/wdecor.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/wdecor.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -62,4 +62,5 @@
 };
 
+static void test_wdecor_minimize(ui_wdecor_t *, void *);
 static void test_wdecor_maximize(ui_wdecor_t *, void *);
 static void test_wdecor_unmaximize(ui_wdecor_t *, void *);
@@ -71,4 +72,5 @@
 
 static ui_wdecor_cb_t test_wdecor_cb = {
+	.minimize = test_wdecor_minimize,
 	.maximize = test_wdecor_maximize,
 	.unmaximize = test_wdecor_unmaximize,
@@ -100,4 +102,5 @@
 
 typedef struct {
+	bool minimize;
 	bool maximize;
 	bool unmaximize;
@@ -222,4 +225,30 @@
 	rc = gfx_context_delete(gc);
 	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Test ui_wdecor_minimize() */
+PCUT_TEST(minimize)
+{
+	errno_t rc;
+	ui_wdecor_t *wdecor;
+	test_cb_resp_t resp;
+
+	rc = ui_wdecor_create(NULL, "Hello", ui_wds_none, &wdecor);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Minimize callback with no callbacks set */
+	ui_wdecor_minimize(wdecor);
+
+	/* Minimize callback with minimize callback not implemented */
+	ui_wdecor_set_cb(wdecor, &dummy_wdecor_cb, NULL);
+	ui_wdecor_minimize(wdecor);
+
+	/* Minimize callback with real callback set */
+	resp.minimize = false;
+	ui_wdecor_set_cb(wdecor, &test_wdecor_cb, &resp);
+	ui_wdecor_minimize(wdecor);
+	PCUT_ASSERT_TRUE(resp.minimize);
+
+	ui_wdecor_destroy(wdecor);
 }
 
@@ -1014,4 +1043,11 @@
 }
 
+static void test_wdecor_minimize(ui_wdecor_t *wdecor, void *arg)
+{
+	test_cb_resp_t *resp = (test_cb_resp_t *) arg;
+
+	resp->minimize = true;
+}
+
 static void test_wdecor_maximize(ui_wdecor_t *wdecor, void *arg)
 {
Index: uspace/lib/ui/test/window.c
===================================================================
--- uspace/lib/ui/test/window.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/ui/test/window.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -45,4 +45,5 @@
 PCUT_TEST_SUITE(window);
 
+static void test_window_minimize(ui_window_t *, void *);
 static void test_window_maximize(ui_window_t *, void *);
 static void test_window_unmaximize(ui_window_t *, void *);
@@ -55,4 +56,5 @@
 
 static ui_window_cb_t test_window_cb = {
+	.minimize = test_window_minimize,
 	.maximize = test_window_maximize,
 	.unmaximize = test_window_unmaximize,
@@ -80,4 +82,5 @@
 typedef struct {
 	errno_t rc;
+	bool minimize;
 	bool maximize;
 	bool unmaximize;
@@ -537,4 +540,40 @@
 }
 
+/** ui_window_send_minimize() calls minimize callback set via ui_window_set_cb() */
+PCUT_TEST(send_minimize)
+{
+	errno_t rc;
+	ui_t *ui = NULL;
+	ui_wnd_params_t params;
+	ui_window_t *window = NULL;
+	test_cb_resp_t resp;
+
+	rc = ui_create_disp(NULL, &ui);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_wnd_params_init(&params);
+	params.caption = "Hello";
+
+	rc = ui_window_create(ui, &params, &window);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(window);
+
+	/* Minimize callback with no callbacks set */
+	ui_window_send_minimize(window);
+
+	/* Minimize callback with minimize callback not implemented */
+	ui_window_set_cb(window, &dummy_window_cb, NULL);
+	ui_window_send_minimize(window);
+
+	/* Minimize callback with real callback set */
+	resp.minimize = false;
+	ui_window_set_cb(window, &test_window_cb, &resp);
+	ui_window_send_minimize(window);
+	PCUT_ASSERT_TRUE(resp.minimize);
+
+	ui_window_destroy(window);
+	ui_destroy(ui);
+}
+
 /** ui_window_send_maximize() calls maximize callback set via ui_window_set_cb() */
 PCUT_TEST(send_maximize)
@@ -849,4 +888,11 @@
 }
 
+static void test_window_minimize(ui_window_t *window, void *arg)
+{
+	test_cb_resp_t *resp = (test_cb_resp_t *) arg;
+
+	resp->minimize = true;
+}
+
 static void test_window_maximize(ui_window_t *window, void *arg)
 {
Index: uspace/lib/usbvirt/src/ipc_dev.c
===================================================================
--- uspace/lib/usbvirt/src/ipc_dev.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/lib/usbvirt/src/ipc_dev.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2023 Jiri Svoboda
  * Copyright (c) 2011 Vojtech Horky
  * All rights reserved.
@@ -59,4 +60,5 @@
 	size_t accepted_size;
 	if (!async_data_read_receive(&call, &accepted_size)) {
+		async_answer_0(&call, EINVAL);
 		async_answer_0(icall, EINVAL);
 		return;
@@ -94,4 +96,5 @@
 	ipc_call_t data;
 	if (!async_data_read_receive(&data, &data_len)) {
+		async_answer_0(&data, EPARTY);
 		async_answer_0(icall, EPARTY);
 		free(setup_packet);
@@ -101,4 +104,5 @@
 	void *buffer = malloc(data_len);
 	if (buffer == NULL) {
+		async_answer_0(&data, EPARTY);
 		async_answer_0(icall, ENOMEM);
 		free(setup_packet);
@@ -184,4 +188,5 @@
 	ipc_call_t data;
 	if (!async_data_read_receive(&data, &data_len)) {
+		async_answer_0(&data, EPARTY);
 		async_answer_0(icall, EPARTY);
 		return;
@@ -190,4 +195,5 @@
 	void *buffer = malloc(data_len);
 	if (buffer == NULL) {
+		async_answer_0(&data, EPARTY);
 		async_answer_0(icall, ENOMEM);
 		return;
Index: uspace/lib/wndmgt/doc/doxygroups.h
===================================================================
--- uspace/lib/wndmgt/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/doc/doxygroups.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,3 @@
+/** @addtogroup libwndmgt libwndmgt
+ * @ingroup libs
+ */
Index: uspace/lib/wndmgt/include/ipc/wndmgt.h
===================================================================
--- uspace/lib/wndmgt/include/ipc/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/include/ipc/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2022 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 LIBWNDMGT_IPC_WNDMGT_H
+#define LIBWNDMGT_IPC_WNDMGT_H
+
+#include <ipc/common.h>
+
+typedef enum {
+	WNDMGT_CALLBACK_CREATE = IPC_FIRST_USER_METHOD,
+	WNDMGT_GET_WINDOW_LIST,
+	WNDMGT_GET_WINDOW_INFO,
+	WNDMGT_ACTIVATE_WINDOW,
+	WNDMGT_CLOSE_WINDOW,
+	WNDMGT_GET_EVENT,
+} wndmgt_request_t;
+
+typedef enum {
+	WNDMGT_EV_PENDING = IPC_FIRST_USER_METHOD
+} wndmgt_event_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/wndmgt/include/types/wndmgt.h
===================================================================
--- uspace/lib/wndmgt/include/types/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/include/types/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,98 @@
+/*
+ * 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 _LIBWNDMGT_TYPES_WNDMGT_H_
+#define _LIBWNDMGT_TYPES_WNDMGT_H_
+
+#include <ipc/services.h>
+#include <types/common.h>
+
+/** Use the default window management service (argument to wndmgt_open() */
+#define WNDMGT_DEFAULT SERVICE_NAME_WNDMGT
+
+struct wndmgt;
+
+/** Window management session */
+typedef struct wndmgt wndmgt_t;
+
+/** Window management callbacks */
+typedef struct {
+	/** Window added */
+	void (*window_added)(void *, sysarg_t);
+	/** Window removed */
+	void (*window_removed)(void *, sysarg_t);
+	/** Window changed */
+	void (*window_changed)(void *, sysarg_t);
+} wndmgt_cb_t;
+
+/** Window management event type */
+typedef enum {
+	/** Window added */
+	wmev_window_added,
+	/** Window removed */
+	wmev_window_removed,
+	/** Window changed */
+	wmev_window_changed
+} wndmgt_ev_type_t;
+
+/** Window management event */
+typedef struct {
+	/** Event type */
+	wndmgt_ev_type_t etype;
+	/** Window ID */
+	sysarg_t wnd_id;
+} wndmgt_ev_t;
+
+/** Window list */
+typedef struct {
+	/** Number of windows */
+	size_t nwindows;
+	/** ID for each window */
+	sysarg_t *windows;
+} wndmgt_window_list_t;
+
+/** Window information */
+typedef struct {
+	/** Window caption */
+	char *caption;
+	/** Window flags */
+	unsigned flags;
+	/** Number of foci on this window */
+	unsigned nfocus;
+} wndmgt_window_info_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/wndmgt/include/wndmgt.h
===================================================================
--- uspace/lib/wndmgt/include/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/include/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022 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 _LIBWNDMGT_WNDMGT_H_
+#define _LIBWNDMGT_WNDMGT_H_
+
+#include <errno.h>
+#include <types/common.h>
+#include "types/wndmgt.h"
+
+extern errno_t wndmgt_open(const char *, wndmgt_cb_t *, void *, wndmgt_t **);
+extern void wndmgt_close(wndmgt_t *);
+extern errno_t wndmgt_get_window_list(wndmgt_t *, wndmgt_window_list_t **);
+extern void wndmgt_free_window_list(wndmgt_window_list_t *);
+extern errno_t wndmgt_get_window_info(wndmgt_t *, sysarg_t,
+    wndmgt_window_info_t **);
+extern void wndmgt_free_window_info(wndmgt_window_info_t *);
+extern errno_t wndmgt_activate_window(wndmgt_t *, sysarg_t, sysarg_t);
+extern errno_t wndmgt_close_window(wndmgt_t *, sysarg_t);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/wndmgt/include/wndmgt_srv.h
===================================================================
--- uspace/lib/wndmgt/include/wndmgt_srv.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/include/wndmgt_srv.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 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 _LIBWNDMGT_WNDMGT_SRV_H_
+#define _LIBWNDMGT_WNDMGT_SRV_H_
+
+#include <async.h>
+#include <errno.h>
+#include "types/wndmgt.h"
+
+typedef struct wndmgt_ops wndmgt_ops_t;
+
+/** Window management server structure (per client session) */
+typedef struct {
+	async_sess_t *client_sess;
+	wndmgt_ops_t *ops;
+	void *arg;
+} wndmgt_srv_t;
+
+struct wndmgt_ops {
+	errno_t (*get_window_list)(void *, wndmgt_window_list_t **);
+	errno_t (*get_window_info)(void *, sysarg_t, wndmgt_window_info_t **);
+	errno_t (*activate_window)(void *, sysarg_t, sysarg_t);
+	errno_t (*close_window)(void *, sysarg_t);
+	errno_t (*get_event)(void *, wndmgt_ev_t *);
+};
+
+extern void wndmgt_conn(ipc_call_t *, wndmgt_srv_t *);
+extern void wndmgt_srv_initialize(wndmgt_srv_t *);
+extern void wndmgt_srv_ev_pending(wndmgt_srv_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/wndmgt/meson.build
===================================================================
--- uspace/lib/wndmgt/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2022 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/wndmgt.c',
+	'src/wndmgt_srv.c',
+)
+
+test_src = files(
+	'test/main.c',
+	'test/wndmgt.c',
+)
Index: uspace/lib/wndmgt/private/wndmgt.h
===================================================================
--- uspace/lib/wndmgt/private/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/private/wndmgt.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2022 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 _LIBWNDMGT_PRIVATE_WNDMGT_H_
+#define _LIBWNDMGT_PRIVATE_WNDMGT_H_
+
+#include <async.h>
+#include <fibril_synch.h>
+#include <stdbool.h>
+
+/** Window management session structure */
+struct wndmgt {
+	/** Session with window management service */
+	async_sess_t *sess;
+	/** Callbacks */
+	wndmgt_cb_t *cb;
+	/** Calback argument */
+	void *cb_arg;
+	/** Synchronize access to window management 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/wndmgt/src/wndmgt.c
===================================================================
--- uspace/lib/wndmgt/src/wndmgt.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/src/wndmgt.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,445 @@
+/*
+ * 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
+ * @brief Window management protocol client
+ */
+
+#include <async.h>
+#include <wndmgt.h>
+#include <errno.h>
+#include <fibril_synch.h>
+#include <ipc/wndmgt.h>
+#include <ipc/services.h>
+#include <loc.h>
+#include <mem.h>
+#include <stdlib.h>
+#include "../private/wndmgt.h"
+
+static errno_t wndmgt_callback_create(wndmgt_t *);
+static void wndmgt_cb_conn(ipc_call_t *, void *);
+
+/** Open window management service.
+ *
+ * @param wmname Window management service name or @c NULL to use default
+ * @param cb Window management callbacks
+ * @param cb_arg Callback argument
+ * @param rwndmgt Place to store pointer to window management session
+ * @return EOK on success or an error code
+ */
+errno_t wndmgt_open(const char *wmname, wndmgt_cb_t *cb, void *cb_arg,
+    wndmgt_t **rwndmgt)
+{
+	service_id_t wndmgt_svc;
+	wndmgt_t *wndmgt;
+	errno_t rc;
+
+	wndmgt = calloc(1, sizeof(wndmgt_t));
+	if (wndmgt == NULL)
+		return ENOMEM;
+
+	wndmgt->cb = cb;
+	wndmgt->cb_arg = cb_arg;
+
+	fibril_mutex_initialize(&wndmgt->lock);
+	fibril_condvar_initialize(&wndmgt->cv);
+
+	if (wmname == NULL)
+		wmname = SERVICE_NAME_WNDMGT;
+
+	rc = loc_service_get_id(wmname, &wndmgt_svc, 0);
+	if (rc != EOK) {
+		free(wndmgt);
+		return ENOENT;
+	}
+
+	wndmgt->sess = loc_service_connect(wndmgt_svc, INTERFACE_WNDMGT,
+	    0);
+	if (wndmgt->sess == NULL) {
+		free(wndmgt);
+		return ENOENT;
+	}
+
+	rc = wndmgt_callback_create(wndmgt);
+	if (rc != EOK) {
+		async_hangup(wndmgt->sess);
+		free(wndmgt);
+		return EIO;
+	}
+
+	*rwndmgt = wndmgt;
+	return EOK;
+}
+
+/** Create callback connection from window management service.
+ *
+ * @param wndmgt Window management
+ * @return EOK on success or an error code
+ */
+static errno_t wndmgt_callback_create(wndmgt_t *wndmgt)
+{
+	async_exch_t *exch = async_exchange_begin(wndmgt->sess);
+
+	aid_t req = async_send_0(exch, WNDMGT_CALLBACK_CREATE, NULL);
+
+	port_id_t port;
+	errno_t rc = async_create_callback_port(exch, INTERFACE_WNDMGT_CB, 0, 0,
+	    wndmgt_cb_conn, wndmgt, &port);
+
+	async_exchange_end(exch);
+
+	if (rc != EOK)
+		return rc;
+
+	errno_t retval;
+	async_wait_for(req, &retval);
+
+	return retval;
+}
+
+/** Close window management service.
+ *
+ * @param wndmgt Window management
+ */
+void wndmgt_close(wndmgt_t *wndmgt)
+{
+	fibril_mutex_lock(&wndmgt->lock);
+	async_hangup(wndmgt->sess);
+	wndmgt->sess = NULL;
+
+	/* Wait for callback handler to terminate */
+
+	while (!wndmgt->cb_done)
+		fibril_condvar_wait(&wndmgt->cv, &wndmgt->lock);
+	fibril_mutex_unlock(&wndmgt->lock);
+
+	free(wndmgt);
+}
+
+/** Get window list.
+ *
+ * @param wndmgt Window management
+ * @param rlist Place to store pointer to new window list structure
+ * @return EOK on success or an error code
+ */
+errno_t wndmgt_get_window_list(wndmgt_t *wndmgt, wndmgt_window_list_t **rlist)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	wndmgt_window_list_t *list;
+	sysarg_t nwindows;
+	sysarg_t *windows;
+	errno_t rc;
+
+	exch = async_exchange_begin(wndmgt->sess);
+	req = async_send_0(exch, WNDMGT_GET_WINDOW_LIST, &answer);
+
+	/* Receive window list length */
+	rc = async_data_read_start(exch, &nwindows, sizeof (nwindows));
+	if (rc != EOK) {
+		async_exchange_end(exch);
+		async_wait_for(req, &rc);
+		return rc;
+	}
+
+	windows = calloc(nwindows, sizeof(sysarg_t));
+	if (windows == NULL) {
+		async_exchange_end(exch);
+		async_forget(req);
+		return ENOMEM;
+	}
+
+	/* Receive window list */
+	rc = async_data_read_start(exch, windows, nwindows * 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(wndmgt_window_list_t));
+	if (list == NULL)
+		return ENOMEM;
+
+	list->nwindows = nwindows;
+	list->windows = windows;
+	*rlist = list;
+	return EOK;
+}
+
+/** Free window list.
+ *
+ * @param list Window management list
+ */
+void wndmgt_free_window_list(wndmgt_window_list_t *list)
+{
+	free(list->windows);
+	free(list);
+}
+
+/** Get window information.
+ *
+ * @param wndmgt Window management
+ * @param wnd_id Window ID
+ * @param rinfo Place to store pointer to new window information structure
+ * @return EOK on success or an error code
+ */
+errno_t wndmgt_get_window_info(wndmgt_t *wndmgt, sysarg_t wnd_id,
+    wndmgt_window_info_t **rinfo)
+{
+	async_exch_t *exch;
+	aid_t req;
+	ipc_call_t answer;
+	wndmgt_window_info_t *info;
+	sysarg_t capsize;
+	char *caption;
+	errno_t rc;
+
+	exch = async_exchange_begin(wndmgt->sess);
+	req = async_send_1(exch, WNDMGT_GET_WINDOW_INFO, wnd_id, &answer);
+
+	/* Receive caption size */
+	rc = async_data_read_start(exch, &capsize, sizeof (capsize));
+	if (rc != EOK) {
+		async_exchange_end(exch);
+		async_wait_for(req, &rc);
+		return rc;
+	}
+
+	caption = calloc(capsize + 1, sizeof(char));
+	if (caption == NULL) {
+		async_exchange_end(exch);
+		async_forget(req);
+		return ENOMEM;
+	}
+
+	/* Receive caption */
+	rc = async_data_read_start(exch, caption, capsize);
+	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 */
+	caption[capsize] = '\0';
+
+	info = calloc(1, sizeof(wndmgt_window_info_t));
+	if (info == NULL)
+		return ENOMEM;
+
+	info->caption = caption;
+	info->flags = ipc_get_arg1(&answer);
+	info->nfocus = ipc_get_arg2(&answer);
+	*rinfo = info;
+	return EOK;
+}
+
+/** Free window information.
+ *
+ * @param info Window management information
+ */
+void wndmgt_free_window_info(wndmgt_window_info_t *info)
+{
+	free(info->caption);
+	free(info);
+}
+
+/** Activate window.
+ *
+ * @param wndmgt Window management session
+ * @param dev_id ID of input device belonging to seat whose
+ *               focus is to be switched
+ * @param wnd_id Window ID
+ * @return EOK on success or an error code
+ */
+errno_t wndmgt_activate_window(wndmgt_t *wndmgt, sysarg_t dev_id,
+    sysarg_t wnd_id)
+{
+	async_exch_t *exch;
+	errno_t rc;
+
+	exch = async_exchange_begin(wndmgt->sess);
+	rc = async_req_2_0(exch, WNDMGT_ACTIVATE_WINDOW, dev_id,
+	    wnd_id);
+
+	async_exchange_end(exch);
+	return rc;
+}
+
+/** Close window.
+ *
+ * @param wndmgt Window management
+ * @param wnd_id Window ID
+ * @return EOK on success or an error code
+ */
+errno_t wndmgt_close_window(wndmgt_t *wndmgt, sysarg_t wnd_id)
+{
+	async_exch_t *exch;
+	errno_t rc;
+
+	exch = async_exchange_begin(wndmgt->sess);
+	rc = async_req_1_0(exch, WNDMGT_CLOSE_WINDOW, wnd_id);
+
+	async_exchange_end(exch);
+	return rc;
+}
+
+/** Get window management event.
+ *
+ * @param wndmgt Window management
+ * @param event Place to store event
+ * @return EOK on success or an error code
+ */
+static errno_t wndmgt_get_event(wndmgt_t *wndmgt, wndmgt_ev_t *event)
+{
+	async_exch_t *exch;
+	ipc_call_t answer;
+	aid_t req;
+	errno_t rc;
+
+	exch = async_exchange_begin(wndmgt->sess);
+	req = async_send_0(exch, WNDMGT_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;
+}
+
+/** Window management events are pending.
+ *
+ * @param wndmgt Window management
+ * @param icall Call data
+ */
+static void wndmgt_ev_pending(wndmgt_t *wndmgt, ipc_call_t *icall)
+{
+	errno_t rc;
+	wndmgt_ev_t event;
+
+	while (true) {
+		fibril_mutex_lock(&wndmgt->lock);
+
+		if (wndmgt->sess != NULL)
+			rc = wndmgt_get_event(wndmgt, &event);
+		else
+			rc = ENOENT;
+
+		fibril_mutex_unlock(&wndmgt->lock);
+
+		if (rc != EOK)
+			break;
+
+		switch (event.etype) {
+		case wmev_window_added:
+			if (wndmgt->cb != NULL &&
+			    wndmgt->cb->window_added != NULL) {
+				wndmgt->cb->window_added(wndmgt->cb_arg,
+				    event.wnd_id);
+			}
+			break;
+		case wmev_window_removed:
+			if (wndmgt->cb != NULL &&
+			    wndmgt->cb->window_removed != NULL) {
+				wndmgt->cb->window_removed(wndmgt->cb_arg,
+				    event.wnd_id);
+			}
+			break;
+		case wmev_window_changed:
+			if (wndmgt->cb != NULL &&
+			    wndmgt->cb->window_changed != NULL) {
+				wndmgt->cb->window_changed(wndmgt->cb_arg,
+				    event.wnd_id);
+			}
+			break;
+		}
+	}
+
+	async_answer_0(icall, EOK);
+}
+
+/** Callback connection handler.
+ *
+ * @param icall Connect call data
+ * @param arg   Argument, wndmgt_t *
+ */
+static void wndmgt_cb_conn(ipc_call_t *icall, void *arg)
+{
+	wndmgt_t *wndmgt = (wndmgt_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 WNDMGT_EV_PENDING:
+			wndmgt_ev_pending(wndmgt, &call);
+			break;
+		default:
+			async_answer_0(&call, ENOTSUP);
+			break;
+		}
+	}
+
+out:
+	fibril_mutex_lock(&wndmgt->lock);
+	wndmgt->cb_done = true;
+	fibril_mutex_unlock(&wndmgt->lock);
+	fibril_condvar_broadcast(&wndmgt->cv);
+}
+
+/** @}
+ */
Index: uspace/lib/wndmgt/src/wndmgt_srv.c
===================================================================
--- uspace/lib/wndmgt/src/wndmgt_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/src/wndmgt_srv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,352 @@
+/*
+ * 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
+ * @brief Window management protocol server stub
+ */
+
+#include <wndmgt_srv.h>
+#include <errno.h>
+#include <io/log.h>
+#include <ipc/wndmgt.h>
+#include <mem.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <str.h>
+#include <wndmgt.h>
+#include "../private/wndmgt.h"
+
+static void wndmgt_callback_create_srv(wndmgt_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 wndmgt_get_window_list_srv(wndmgt_srv_t *srv, ipc_call_t *icall)
+{
+	ipc_call_t call;
+	wndmgt_window_list_t *list = NULL;
+	size_t size;
+	errno_t rc;
+
+	if (srv->ops->get_window_list == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->get_window_list(srv->arg, &list);
+	if (rc != EOK) {
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send list size */
+
+	if (!async_data_read_receive(&call, &size)) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != sizeof(list->nwindows)) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, &list->nwindows, size);
+	if (rc != EOK) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send window list */
+
+	if (!async_data_read_receive(&call, &size)) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != list->nwindows * sizeof(sysarg_t)) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, list->windows, size);
+	if (rc != EOK) {
+		wndmgt_free_window_list(list);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	async_answer_0(icall, EOK);
+	wndmgt_free_window_list(list);
+}
+
+static void wndmgt_get_window_info_srv(wndmgt_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t wnd_id;
+	ipc_call_t call;
+	wndmgt_window_info_t *info = NULL;
+	size_t capsize;
+	size_t size;
+	errno_t rc;
+
+	wnd_id = ipc_get_arg1(icall);
+
+	if (srv->ops->get_window_info == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->get_window_info(srv->arg, wnd_id, &info);
+	if (rc != EOK) {
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send caption size */
+
+	if (!async_data_read_receive(&call, &size)) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != sizeof(size_t)) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	capsize = str_size(info->caption);
+
+	rc = async_data_read_finalize(&call, &capsize, size);
+	if (rc != EOK) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	/* Send caption */
+
+	if (!async_data_read_receive(&call, &size)) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, EREFUSED);
+		async_answer_0(icall, EREFUSED);
+		return;
+	}
+
+	if (size != capsize) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, EINVAL);
+		async_answer_0(icall, EINVAL);
+		return;
+	}
+
+	rc = async_data_read_finalize(&call, info->caption, size);
+	if (rc != EOK) {
+		wndmgt_free_window_info(info);
+		async_answer_0(&call, rc);
+		async_answer_0(icall, rc);
+		return;
+	}
+
+	async_answer_2(icall, EOK, info->flags, info->nfocus);
+	wndmgt_free_window_info(info);
+}
+
+static void wndmgt_activate_window_srv(wndmgt_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t dev_id;
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	dev_id = ipc_get_arg1(icall);
+	wnd_id = ipc_get_arg2(icall);
+
+	if (srv->ops->activate_window == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->activate_window(srv->arg, dev_id, wnd_id);
+	async_answer_0(icall, rc);
+}
+
+static void wndmgt_close_window_srv(wndmgt_srv_t *srv, ipc_call_t *icall)
+{
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	wnd_id = ipc_get_arg1(icall);
+
+	if (srv->ops->close_window == NULL) {
+		async_answer_0(icall, ENOTSUP);
+		return;
+	}
+
+	rc = srv->ops->close_window(srv->arg, wnd_id);
+	async_answer_0(icall, rc);
+}
+
+static void wndmgt_get_event_srv(wndmgt_srv_t *srv, ipc_call_t *icall)
+{
+	wndmgt_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 wndmgt_conn(ipc_call_t *icall, wndmgt_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 WNDMGT_CALLBACK_CREATE:
+			wndmgt_callback_create_srv(srv, &call);
+			break;
+		case WNDMGT_GET_WINDOW_LIST:
+			wndmgt_get_window_list_srv(srv, &call);
+			break;
+		case WNDMGT_GET_WINDOW_INFO:
+			wndmgt_get_window_info_srv(srv, &call);
+			break;
+		case WNDMGT_ACTIVATE_WINDOW:
+			wndmgt_activate_window_srv(srv, &call);
+			break;
+		case WNDMGT_CLOSE_WINDOW:
+			wndmgt_close_window_srv(srv, &call);
+			break;
+		case WNDMGT_GET_EVENT:
+			wndmgt_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 window management server structure
+ *
+ * @param srv Window management server structure to initialize
+ */
+void wndmgt_srv_initialize(wndmgt_srv_t *srv)
+{
+	memset(srv, 0, sizeof(*srv));
+}
+
+/** Send 'pending' event to client.
+ *
+ * @param srv Window management server structure
+ */
+void wndmgt_srv_ev_pending(wndmgt_srv_t *srv)
+{
+	async_exch_t *exch;
+
+	exch = async_exchange_begin(srv->client_sess);
+	async_msg_0(exch, WNDMGT_EV_PENDING);
+	async_exchange_end(exch);
+}
+
+/** @}
+ */
Index: uspace/lib/wndmgt/test/main.c
===================================================================
--- uspace/lib/wndmgt/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022 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(wndmgt);
+
+PCUT_MAIN();
Index: uspace/lib/wndmgt/test/wndmgt.c
===================================================================
--- uspace/lib/wndmgt/test/wndmgt.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/lib/wndmgt/test/wndmgt.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,713 @@
+/*
+ * 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 <wndmgt.h>
+#include <wndmgt_srv.h>
+#include <fibril_synch.h>
+#include <loc.h>
+#include <pcut/pcut.h>
+#include <str.h>
+#include "../private/wndmgt.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(wndmgt);
+
+static const char *test_wndmgt_server = "test-wndmgt";
+static const char *test_wndmgt_svc = "test/wndmgt";
+
+static void test_wndmgt_conn(ipc_call_t *, void *);
+
+static errno_t test_get_window_list(void *, wndmgt_window_list_t **);
+static errno_t test_get_window_info(void *, sysarg_t, wndmgt_window_info_t **);
+static errno_t test_activate_window(void *, sysarg_t, sysarg_t);
+static errno_t test_close_window(void *, sysarg_t);
+static errno_t test_get_event(void *, wndmgt_ev_t *);
+
+static void test_window_added(void *, sysarg_t);
+static void test_window_removed(void *, sysarg_t);
+static void test_window_changed(void *, sysarg_t);
+
+static wndmgt_ops_t test_wndmgt_srv_ops = {
+	.get_window_list = test_get_window_list,
+	.get_window_info = test_get_window_info,
+	.activate_window = test_activate_window,
+	.close_window = test_close_window,
+	.get_event = test_get_event
+};
+
+static wndmgt_cb_t test_wndmgt_cb = {
+	.window_added = test_window_added,
+	.window_removed = test_window_removed,
+	.window_changed = test_window_changed
+};
+
+/** 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 wnd_id;
+	wndmgt_ev_t event;
+	wndmgt_ev_t revent;
+	int event_cnt;
+
+	bool get_window_list_called;
+	wndmgt_window_list_t *get_window_list_rlist;
+
+	bool get_window_info_called;
+	sysarg_t get_window_info_wnd_id;
+	wndmgt_window_info_t *get_window_info_rinfo;
+
+	bool activate_window_called;
+	sysarg_t activate_window_seat_id;
+	sysarg_t activate_window_wnd_id;
+
+	bool close_window_called;
+	sysarg_t close_window_wnd_id;
+
+	bool get_event_called;
+
+	bool window_added_called;
+	sysarg_t window_added_wnd_id;
+
+	bool window_removed_called;
+	sysarg_t window_removed_wnd_id;
+
+	bool window_changed_called;
+	sysarg_t window_changed_wnd_id;
+
+	fibril_condvar_t event_cv;
+	fibril_mutex_t event_lock;
+	wndmgt_srv_t *srv;
+} test_response_t;
+
+/** wndmgt_open(), wndmgt_close() work for valid window management service */
+PCUT_TEST(open_close)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_get_window_list() with server returning error response works */
+PCUT_TEST(get_window_list_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	wndmgt_window_list_t *list;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	resp.rc = ENOMEM;
+	resp.get_window_list_called = false;
+
+	rc = wndmgt_get_window_list(wndmgt, &list);
+	PCUT_ASSERT_TRUE(resp.get_window_list_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_get_window_list() with server returning success response works */
+PCUT_TEST(get_window_list_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	wndmgt_window_list_t *list;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	resp.rc = EOK;
+	resp.get_window_list_called = false;
+	resp.get_window_list_rlist = calloc(1, sizeof(wndmgt_window_list_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_window_list_rlist);
+	resp.get_window_list_rlist->nwindows = 2;
+	resp.get_window_list_rlist->windows = calloc(2, sizeof(sysarg_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_window_list_rlist->windows);
+	resp.get_window_list_rlist->windows[0] = 42;
+	resp.get_window_list_rlist->windows[1] = 43;
+
+	rc = wndmgt_get_window_list(wndmgt, &list);
+	PCUT_ASSERT_TRUE(resp.get_window_list_called);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	PCUT_ASSERT_INT_EQUALS(2, list->nwindows);
+	PCUT_ASSERT_INT_EQUALS(42, list->windows[0]);
+	PCUT_ASSERT_INT_EQUALS(43, list->windows[1]);
+
+	wndmgt_free_window_list(list);
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_get_window_infp() with server returning error response works */
+PCUT_TEST(get_window_info_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t wnd_id;
+	wndmgt_window_info_t *info;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	resp.rc = ENOMEM;
+	resp.get_window_info_called = false;
+	wnd_id = 1;
+
+	rc = wndmgt_get_window_info(wndmgt, wnd_id, &info);
+	PCUT_ASSERT_TRUE(resp.get_window_info_called);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.get_window_info_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_get_window_info() with server returning success response works */
+PCUT_TEST(get_window_info_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t wnd_id;
+	wndmgt_window_info_t *info;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	resp.rc = EOK;
+	resp.get_window_info_called = false;
+	resp.get_window_info_rinfo = calloc(1, sizeof(wndmgt_window_info_t));
+	PCUT_ASSERT_NOT_NULL(resp.get_window_info_rinfo);
+	resp.get_window_info_rinfo->caption = str_dup("Hello");
+	resp.get_window_info_rinfo->flags = 42;
+	resp.get_window_info_rinfo->nfocus = 123;
+	PCUT_ASSERT_NOT_NULL(resp.get_window_info_rinfo->caption);
+	wnd_id = 1;
+
+	rc = wndmgt_get_window_info(wndmgt, wnd_id, &info);
+	PCUT_ASSERT_TRUE(resp.get_window_info_called);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.get_window_info_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	PCUT_ASSERT_STR_EQUALS("Hello", info->caption);
+	PCUT_ASSERT_INT_EQUALS(42, info->flags);
+	PCUT_ASSERT_INT_EQUALS(123, info->nfocus);
+
+	wndmgt_free_window_info(info);
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_activate_window() with server returning error response works */
+PCUT_TEST(activate_window_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t seat_id;
+	sysarg_t wnd_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	seat_id = 13;
+	wnd_id = 42;
+	resp.rc = ENOMEM;
+	resp.activate_window_called = false;
+
+	rc = wndmgt_activate_window(wndmgt, seat_id, wnd_id);
+	PCUT_ASSERT_TRUE(resp.activate_window_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.activate_window_seat_id);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.activate_window_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_activate_window() with server returning success response works */
+PCUT_TEST(activate_window_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t seat_id;
+	sysarg_t wnd_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	seat_id = 13;
+	wnd_id = 42;
+	resp.rc = EOK;
+	resp.activate_window_called = false;
+
+	rc = wndmgt_activate_window(wndmgt, seat_id, wnd_id);
+	PCUT_ASSERT_TRUE(resp.activate_window_called);
+	PCUT_ASSERT_INT_EQUALS(seat_id, resp.activate_window_seat_id);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.activate_window_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_close_window() with server returning error response works */
+PCUT_TEST(close_window_failure)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t wnd_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	wnd_id = 42;
+	resp.rc = ENOMEM;
+	resp.close_window_called = false;
+
+	rc = wndmgt_close_window(wndmgt, wnd_id);
+	PCUT_ASSERT_TRUE(resp.close_window_called);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.close_window_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** wndmgt_close_window() with server returning success response works */
+PCUT_TEST(close_window_success)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	sysarg_t wnd_id;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, NULL, NULL, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+
+	wnd_id = 42;
+	resp.rc = EOK;
+	resp.close_window_called = false;
+
+	rc = wndmgt_close_window(wndmgt, wnd_id);
+	PCUT_ASSERT_TRUE(resp.close_window_called);
+	PCUT_ASSERT_INT_EQUALS(wnd_id, resp.close_window_wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(resp.rc, rc);
+
+	wndmgt_close(wndmgt);
+	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(window_added_deliver)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, &test_wndmgt_cb, &resp, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+	PCUT_ASSERT_NOT_NULL(resp.srv);
+
+	resp.event_cnt = 1;
+	resp.event.etype = wmev_window_added;
+	resp.event.wnd_id = 42;
+	resp.window_added_called = false;
+	fibril_mutex_initialize(&resp.event_lock);
+	fibril_condvar_initialize(&resp.event_cv);
+	wndmgt_srv_ev_pending(resp.srv);
+
+	/* Wait for the event handler to be called. */
+	fibril_mutex_lock(&resp.event_lock);
+	while (!resp.window_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);
+
+	wndmgt_close(wndmgt);
+
+	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(window_removed_deliver)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, &test_wndmgt_cb, &resp, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+	PCUT_ASSERT_NOT_NULL(resp.srv);
+
+	resp.event_cnt = 1;
+	resp.event.etype = wmev_window_removed;
+	resp.event.wnd_id = 42;
+	resp.window_removed_called = false;
+	fibril_mutex_initialize(&resp.event_lock);
+	fibril_condvar_initialize(&resp.event_cv);
+	wndmgt_srv_ev_pending(resp.srv);
+
+	/* Wait for the event handler to be called. */
+	fibril_mutex_lock(&resp.event_lock);
+	while (!resp.window_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);
+
+	wndmgt_close(wndmgt);
+
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Window changed event can be delivered from server to client callback function */
+PCUT_TEST(window_changed_deliver)
+{
+	errno_t rc;
+	service_id_t sid;
+	wndmgt_t *wndmgt = NULL;
+	test_response_t resp;
+
+	async_set_fallback_port_handler(test_wndmgt_conn, &resp);
+
+	// FIXME This causes this test to be non-reentrant!
+	rc = loc_server_register(test_wndmgt_server);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = loc_service_register(test_wndmgt_svc, &sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = wndmgt_open(test_wndmgt_svc, &test_wndmgt_cb, &resp, &wndmgt);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(wndmgt);
+	PCUT_ASSERT_NOT_NULL(resp.srv);
+
+	resp.event_cnt = 1;
+	resp.event.etype = wmev_window_changed;
+	resp.event.wnd_id = 42;
+	resp.window_changed_called = false;
+	fibril_mutex_initialize(&resp.event_lock);
+	fibril_condvar_initialize(&resp.event_cv);
+	wndmgt_srv_ev_pending(resp.srv);
+
+	/* Wait for the event handler to be called. */
+	fibril_mutex_lock(&resp.event_lock);
+	while (!resp.window_changed_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);
+
+	wndmgt_close(wndmgt);
+
+	rc = loc_service_unregister(sid);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+}
+
+/** Test window management service connection. */
+static void test_wndmgt_conn(ipc_call_t *icall, void *arg)
+{
+	test_response_t *resp = (test_response_t *) arg;
+	wndmgt_srv_t srv;
+
+	/* Set up protocol structure */
+	wndmgt_srv_initialize(&srv);
+	srv.ops = &test_wndmgt_srv_ops;
+	srv.arg = arg;
+	resp->srv = &srv;
+
+	/* Handle connection */
+	wndmgt_conn(icall, &srv);
+
+	resp->srv = NULL;
+}
+
+static void test_window_added(void *arg, sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->revent.etype = wmev_window_added;
+
+	fibril_mutex_lock(&resp->event_lock);
+	resp->window_added_called = true;
+	resp->window_added_wnd_id = wnd_id;
+	fibril_condvar_broadcast(&resp->event_cv);
+	fibril_mutex_unlock(&resp->event_lock);
+}
+
+static void test_window_removed(void *arg, sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->revent.etype = wmev_window_removed;
+
+	fibril_mutex_lock(&resp->event_lock);
+	resp->window_removed_called = true;
+	resp->window_removed_wnd_id = wnd_id;
+	fibril_condvar_broadcast(&resp->event_cv);
+	fibril_mutex_unlock(&resp->event_lock);
+}
+
+static void test_window_changed(void *arg, sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->revent.etype = wmev_window_changed;
+
+	fibril_mutex_lock(&resp->event_lock);
+	resp->window_changed_called = true;
+	resp->window_changed_wnd_id = wnd_id;
+	fibril_condvar_broadcast(&resp->event_cv);
+	fibril_mutex_unlock(&resp->event_lock);
+}
+
+static errno_t test_get_window_list(void *arg, wndmgt_window_list_t **rlist)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->get_window_list_called = true;
+
+	if (resp->rc != EOK)
+		return resp->rc;
+
+	*rlist = resp->get_window_list_rlist;
+	return EOK;
+}
+
+static errno_t test_get_window_info(void *arg, sysarg_t wnd_id,
+    wndmgt_window_info_t **rinfo)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->get_window_info_called = true;
+	resp->get_window_info_wnd_id = wnd_id;
+
+	if (resp->rc != EOK)
+		return resp->rc;
+
+	*rinfo = resp->get_window_info_rinfo;
+	return EOK;
+}
+
+static errno_t test_activate_window(void *arg, sysarg_t seat_id,
+    sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->activate_window_called = true;
+	resp->activate_window_seat_id = seat_id;
+	resp->activate_window_wnd_id = wnd_id;
+	return resp->rc;
+}
+
+static errno_t test_close_window(void *arg, sysarg_t wnd_id)
+{
+	test_response_t *resp = (test_response_t *) arg;
+
+	resp->close_window_called = true;
+	resp->close_window_wnd_id = wnd_id;
+	return resp->rc;
+}
+
+static errno_t test_get_event(void *arg, wndmgt_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(wndmgt);
Index: uspace/srv/audio/hound/audio_sink.c
===================================================================
--- uspace/srv/audio/hound/audio_sink.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/audio/hound/audio_sink.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -36,4 +36,5 @@
 #include <assert.h>
 #include <errno.h>
+#include <fibril_synch.h>
 #include <stdlib.h>
 #include <str.h>
@@ -64,4 +65,5 @@
 		return EINVAL;
 	link_initialize(&sink->link);
+	fibril_mutex_initialize(&sink->lock);
 	list_initialize(&sink->connections);
 	sink->name = str_dup(name);
@@ -139,4 +141,5 @@
 
 	pcm_format_silence(dest, size, &sink->format);
+	fibril_mutex_lock(&sink->lock);
 	list_foreach(sink->connections, sink_link, connection_t, conn) {
 		const errno_t ret = connection_add_source_data(
@@ -147,4 +150,5 @@
 		}
 	}
+	fibril_mutex_unlock(&sink->lock);
 }
 
Index: uspace/srv/audio/hound/audio_sink.h
===================================================================
--- uspace/srv/audio/hound/audio_sink.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/audio/hound/audio_sink.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -41,4 +41,5 @@
 #include <stdbool.h>
 #include <fibril.h>
+#include <fibril_synch.h>
 #include <pcm/format.h>
 
@@ -51,4 +52,6 @@
 	/** Link in hound's sink list */
 	link_t link;
+	/** Protect sink including list of connections */
+	fibril_mutex_t lock;
 	/** List of all related connections */
 	list_t connections;
Index: uspace/srv/audio/hound/connection.c
===================================================================
--- uspace/srv/audio/hound/connection.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/audio/hound/connection.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -61,5 +61,7 @@
 		conn->source = source;
 		list_append(&conn->source_link, &source->connections);
+		fibril_mutex_lock(&sink->lock);
 		list_append(&conn->sink_link, &sink->connections);
+		fibril_mutex_unlock(&sink->lock);
 		audio_sink_set_format(sink, audio_source_format(source));
 		if (source->connection_change)
@@ -83,5 +85,7 @@
 	assert(!link_in_use(&connection->hound_link));
 	list_remove(&connection->source_link);
+	fibril_mutex_lock(&connection->sink->lock);
 	list_remove(&connection->sink_link);
+	fibril_mutex_unlock(&connection->sink->lock);
 	if (connection->sink && connection->sink->connection_change)
 		connection->sink->connection_change(connection->sink, false);
Index: uspace/srv/audio/hound/hound.c
===================================================================
--- uspace/srv/audio/hound/hound.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/audio/hound/hound.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -126,4 +126,5 @@
 	assert(fibril_mutex_is_locked(&hound->list_guard));
 	log_verbose("Removing sink '%s'.", sink->name);
+	fibril_mutex_lock(&sink->lock);
 	if (!list_empty(&sink->connections))
 		log_warning("Removing sink '%s' while still connected.", sink->name);
@@ -135,4 +136,5 @@
 	}
 	list_remove(&sink->link);
+	fibril_mutex_unlock(&sink->lock);
 }
 
Index: uspace/srv/hid/console/console.c
===================================================================
--- uspace/srv/hid/console/console.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/console/console.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * Copyright (c) 2011 Martin Decky
  * All rights reserved.
@@ -120,9 +120,11 @@
 static errno_t input_ev_active(input_t *);
 static errno_t input_ev_deactive(input_t *);
-static errno_t input_ev_key(input_t *, kbd_event_type_t, keycode_t, keymod_t, char32_t);
-static errno_t input_ev_move(input_t *, int, int);
-static errno_t input_ev_abs_move(input_t *, unsigned, unsigned, unsigned, unsigned);
-static errno_t input_ev_button(input_t *, int, int);
-static errno_t input_ev_dclick(input_t *, int);
+static errno_t input_ev_key(input_t *, unsigned, kbd_event_type_t, keycode_t,
+    keymod_t, char32_t);
+static errno_t input_ev_move(input_t *, unsigned, int, int);
+static errno_t input_ev_abs_move(input_t *, unsigned, unsigned, unsigned,
+    unsigned, unsigned);
+static errno_t input_ev_button(input_t *, unsigned, int, int);
+static errno_t input_ev_dclick(input_t *, unsigned, int);
 
 static input_ev_ops_t input_ev_ops = {
@@ -357,6 +359,6 @@
 }
 
-static errno_t input_ev_key(input_t *input, kbd_event_type_t type, keycode_t key,
-    keymod_t mods, char32_t c)
+static errno_t input_ev_key(input_t *input, unsigned kbd_id,
+    kbd_event_type_t type, keycode_t key, keymod_t mods, char32_t c)
 {
 	cons_event_t event;
@@ -369,4 +371,5 @@
 		event.type = CEV_KEY;
 
+		(void)kbd_id;
 		event.ev.key.type = type;
 		event.ev.key.key = key;
@@ -417,20 +420,25 @@
 }
 
-static errno_t input_ev_move(input_t *input, int dx, int dy)
-{
+static errno_t input_ev_move(input_t *input, unsigned pos_id, int dx, int dy)
+{
+	(void) pos_id;
 	pointer_update(pointer_x + dx, pointer_y + dy);
 	return EOK;
 }
 
-static errno_t input_ev_abs_move(input_t *input, unsigned x, unsigned y,
-    unsigned max_x, unsigned max_y)
-{
+static errno_t input_ev_abs_move(input_t *input, unsigned pos_id, unsigned x,
+    unsigned y, unsigned max_x, unsigned max_y)
+{
+	(void)pos_id;
 	pointer_update(mouse_scale_x * cols * x / max_x, mouse_scale_y * rows * y / max_y);
 	return EOK;
 }
 
-static errno_t input_ev_button(input_t *input, int bnum, int bpress)
+static errno_t input_ev_button(input_t *input, unsigned pos_id, int bnum,
+    int bpress)
 {
 	cons_event_t event;
+
+	(void)pos_id;
 
 	event.type = CEV_POS;
@@ -444,7 +452,9 @@
 }
 
-static errno_t input_ev_dclick(input_t *input, int bnum)
+static errno_t input_ev_dclick(input_t *input, unsigned pos_id, int bnum)
 {
 	cons_event_t event;
+
+	(void)pos_id;
 
 	event.type = CEV_POS;
Index: uspace/srv/hid/display/client.c
===================================================================
--- uspace/srv/hid/display/client.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/client.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -181,5 +181,5 @@
  * @param ewindow Place to store pointer to window receiving the event
  * @param event Place to store event
- * @return Graphic context
+ * @return EOK on success, ENOENT if event queue is empty
  */
 errno_t ds_client_get_event(ds_client_t *client, ds_window_t **ewindow,
Index: uspace/srv/hid/display/display.c
===================================================================
--- uspace/srv/hid/display/display.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/display.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -45,7 +45,8 @@
 #include "cursimg.h"
 #include "cursor.h"
+#include "display.h"
 #include "seat.h"
 #include "window.h"
-#include "display.h"
+#include "wmclient.h"
 
 static gfx_context_t *ds_display_get_unbuf_gc(ds_display_t *);
@@ -96,4 +97,5 @@
 	fibril_mutex_initialize(&disp->lock);
 	list_initialize(&disp->clients);
+	list_initialize(&disp->wmclients);
 	disp->next_wnd_id = 1;
 	list_initialize(&disp->ddevs);
@@ -114,7 +116,19 @@
 void ds_display_destroy(ds_display_t *disp)
 {
+	int i;
+
 	assert(list_empty(&disp->clients));
+	assert(list_empty(&disp->wmclients));
 	assert(list_empty(&disp->seats));
-	/* XXX destroy cursors */
+	assert(list_empty(&disp->ddevs));
+	assert(list_empty(&disp->seats));
+	assert(list_empty(&disp->windows));
+
+	/* Destroy cursors */
+	for (i = 0; i < dcurs_limit; i++) {
+		ds_cursor_destroy(disp->cursor[i]);
+		disp->cursor[i] = NULL;
+	}
+
 	gfx_color_delete(disp->bg_color);
 	free(disp);
@@ -203,4 +217,59 @@
 
 	return list_get_instance(link, ds_client_t, lclients);
+}
+
+/** Add WM client to display.
+ *
+ * @param disp Display
+ * @param wmclient WM client
+ */
+void ds_display_add_wmclient(ds_display_t *disp, ds_wmclient_t *wmclient)
+{
+	assert(wmclient->display == NULL);
+	assert(!link_used(&wmclient->lwmclients));
+
+	wmclient->display = disp;
+	list_append(&wmclient->lwmclients, &disp->wmclients);
+}
+
+/** Remove WM client from display.
+ *
+ * @param wmclient WM client
+ */
+void ds_display_remove_wmclient(ds_wmclient_t *wmclient)
+{
+	list_remove(&wmclient->lwmclients);
+	wmclient->display = NULL;
+}
+
+/** Get first WM client in display.
+ *
+ * @param disp Display
+ * @return First WM client or @c NULL if there is none
+ */
+ds_wmclient_t *ds_display_first_wmclient(ds_display_t *disp)
+{
+	link_t *link = list_first(&disp->wmclients);
+
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ds_wmclient_t, lwmclients);
+}
+
+/** Get next WM client in display.
+ *
+ * @param wmclient Current WM client
+ * @return Next WM client or @c NULL if there is none
+ */
+ds_wmclient_t *ds_display_next_wmclient(ds_wmclient_t *wmclient)
+{
+	link_t *link = list_next(&wmclient->lwmclients,
+	    &wmclient->display->wmclients);
+
+	if (link == NULL)
+		return NULL;
+
+	return list_get_instance(link, ds_wmclient_t, lwmclients);
 }
 
@@ -246,5 +315,6 @@
 		gfx_rect_translate(&wnd->dpos, &wnd->rect, &drect);
 
-		if (gfx_pix_inside_rect(pos, &drect))
+		if (gfx_pix_inside_rect(pos, &drect) &&
+		    ds_window_is_visible(wnd))
 			return wnd;
 
@@ -255,4 +325,36 @@
 }
 
+/** Add window to window list.
+ *
+ * Topmost windows are enlisted before any other window. Non-topmost
+ * windows are enlisted before any other non-topmost window.
+ *
+ * @param display Display
+ * @param wnd Window
+ */
+void ds_display_enlist_window(ds_display_t *display, ds_window_t *wnd)
+{
+	ds_window_t *w;
+
+	assert(wnd->display == display);
+	assert(!link_used(&wnd->ldwindows));
+
+	if ((wnd->flags & wndf_topmost) == 0) {
+		/* Find the first non-topmost window */
+		w = ds_display_first_window(display);
+		while (w != NULL && (w->flags & wndf_topmost) != 0)
+			w = ds_display_next_window(w);
+
+		if (w != NULL)
+			list_insert_before(&wnd->ldwindows, &w->ldwindows);
+		else
+			list_append(&wnd->ldwindows, &display->windows);
+	} else {
+		/* Insert at the beginning */
+		list_prepend(&wnd->ldwindows, &display->windows);
+	}
+
+}
+
 /** Add window to display.
  *
@@ -262,9 +364,18 @@
 void ds_display_add_window(ds_display_t *display, ds_window_t *wnd)
 {
+	ds_wmclient_t *wmclient;
+
 	assert(wnd->display == NULL);
 	assert(!link_used(&wnd->ldwindows));
 
 	wnd->display = display;
-	list_prepend(&wnd->ldwindows, &display->windows);
+	ds_display_enlist_window(display, wnd);
+
+	/* Notify window managers about the new window */
+	wmclient = ds_display_first_wmclient(display);
+	while (wmclient != NULL) {
+		ds_wmclient_post_wnd_added_event(wmclient, wnd->id);
+		wmclient = ds_display_next_wmclient(wmclient);
+	}
 }
 
@@ -275,6 +386,32 @@
 void ds_display_remove_window(ds_window_t *wnd)
 {
+	ds_wmclient_t *wmclient;
+	ds_display_t *display;
+
+	display = wnd->display;
+
 	list_remove(&wnd->ldwindows);
 	wnd->display = NULL;
+
+	/* Notify window managers about the removed window */
+	wmclient = ds_display_first_wmclient(display);
+	while (wmclient != NULL) {
+		ds_wmclient_post_wnd_removed_event(wmclient, wnd->id);
+		wmclient = ds_display_next_wmclient(wmclient);
+	}
+}
+
+/** Move window to top.
+ *
+ * @param display Display
+ * @param wnd Window
+ */
+void ds_display_window_to_top(ds_window_t *wnd)
+{
+	assert(wnd->display != NULL);
+	assert(link_used(&wnd->ldwindows));
+
+	list_remove(&wnd->ldwindows);
+	ds_display_enlist_window(wnd->display, wnd);
 }
 
@@ -352,6 +489,6 @@
 	ds_seat_t *seat;
 
-	// TODO Determine which seat the event belongs to
-	seat = ds_display_first_seat(display);
+	/* Determine which seat the event belongs to */
+	seat = ds_display_seat_by_idev(display, event->kbd_id);
 	if (seat == NULL)
 		return EOK;
@@ -369,6 +506,6 @@
 	ds_seat_t *seat;
 
-	// TODO Determine which seat the event belongs to
-	seat = ds_display_first_seat(display);
+	/* Determine which seat the event belongs to */
+	seat = ds_display_seat_by_idev(display, event->pos_id);
 	if (seat == NULL)
 		return EOK;
@@ -429,4 +566,18 @@
 
 	return list_get_instance(link, ds_seat_t, lseats);
+}
+
+/** Get seat which owns the specified input device.
+ *
+ * @param disp Display
+ * @param idev_id Input device ID
+ * @return Seat which owns device with ID @a idev_id or @c NULL if not found
+ */
+ds_seat_t *ds_display_seat_by_idev(ds_display_t *disp, ds_idev_id_t idev_id)
+{
+	// TODO Multi-seat
+	(void) idev_id;
+
+	return ds_display_first_seat(disp);
 }
 
@@ -493,7 +644,10 @@
 {
 	errno_t rc;
+	gfx_rect_t old_disp_rect;
 
 	assert(ddev->display == NULL);
 	assert(!link_used(&ddev->lddevs));
+
+	old_disp_rect = disp->rect;
 
 	ddev->display = disp;
@@ -507,14 +661,12 @@
 		/* Create cloning GC */
 		rc = ds_clonegc_create(ddev->gc, &disp->fbgc);
-		if (rc != EOK) {
-			// XXX Remove output
-			return ENOMEM;
-		}
+		if (rc != EOK)
+			goto error;
 
 		/* Allocate backbuffer */
 		rc = ds_display_alloc_backbuf(disp);
 		if (rc != EOK) {
-			// XXX Remove output
-			// XXX Delete clone GC
+			ds_clonegc_delete(disp->fbgc);
+			disp->fbgc = NULL;
 			goto error;
 		}
@@ -526,10 +678,9 @@
 	}
 
+	ds_display_update_max_rect(disp);
+
 	return EOK;
 error:
-	disp->rect.p0.x = 0;
-	disp->rect.p0.y = 0;
-	disp->rect.p1.x = 0;
-	disp->rect.p1.y = 0;
+	disp->rect = old_disp_rect;
 	list_remove(&ddev->lddevs);
 	return rc;
@@ -598,4 +749,69 @@
 	list_remove(&cursor->ldisplay);
 	cursor->display = NULL;
+}
+
+/** Update display maximize rectangle.
+ *
+ * Recalculate the maximize rectangle (the rectangle used for maximized
+ * windows).
+ *
+ * @param display Display
+ */
+void ds_display_update_max_rect(ds_display_t *display)
+{
+	ds_window_t *wnd;
+	gfx_rect_t max_rect;
+	gfx_rect_t drect;
+
+	/* Start with the entire display */
+	max_rect = display->rect;
+
+	wnd = ds_display_first_window(display);
+	while (wnd != NULL) {
+		/* Should maximized windows avoid this window? */
+		if ((wnd->flags & wndf_avoid) != 0) {
+			/* Window bounding rectangle on display */
+			gfx_rect_translate(&wnd->dpos, &wnd->rect, &drect);
+
+			/* Crop maximized rectangle */
+			ds_display_crop_max_rect(&drect, &max_rect);
+		}
+
+		wnd = ds_display_next_window(wnd);
+	}
+
+	/* Update the maximize rectangle */
+	display->max_rect = max_rect;
+}
+
+/** Crop maximize rectangle.
+ *
+ * Use the avoid rectangle @a arect to crop off maximization rectangle
+ * @a mrect. If @a arect covers the top, bottom, left or right part
+ * of @a mrect, it will be cropped off. Otherwise there will be
+ * no effect.
+ *
+ * @param arect Avoid rectangle
+ * @param mrect Maximize rectangle to be modified
+ */
+void ds_display_crop_max_rect(gfx_rect_t *arect, gfx_rect_t *mrect)
+{
+	if (arect->p0.x == mrect->p0.x && arect->p0.y == mrect->p0.y &&
+	    arect->p1.x == mrect->p1.x) {
+		/* Cropp off top part */
+		mrect->p0.y = arect->p1.y;
+	} else if (arect->p0.x == mrect->p0.x && arect->p1.x == mrect->p1.x &&
+	    arect->p1.y == mrect->p1.y) {
+		/* Cropp off bottom part */
+		mrect->p1.y = arect->p0.y;
+	} else if (arect->p0.x == mrect->p0.x && arect->p0.y == mrect->p0.y &&
+	    arect->p1.y == mrect->p1.y) {
+		/* Cropp off left part */
+		mrect->p0.x = arect->p1.x;
+	} else if (arect->p0.y == mrect->p0.y && arect->p1.x == mrect->p1.x &&
+	    arect->p1.y == mrect->p1.y) {
+		/* Cropp off right part */
+		mrect->p1.x = arect->p0.x;
+	}
 }
 
Index: uspace/srv/hid/display/display.h
===================================================================
--- uspace/srv/hid/display/display.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/display.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2019 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -46,6 +46,8 @@
 #include "types/display/ddev.h"
 #include "types/display/display.h"
+#include "types/display/idev.h"
 #include "types/display/ptd_event.h"
 #include "types/display/seat.h"
+#include "types/display/wmclient.h"
 
 extern errno_t ds_display_create(gfx_context_t *, ds_display_flags_t,
@@ -59,8 +61,14 @@
 extern ds_client_t *ds_display_first_client(ds_display_t *);
 extern ds_client_t *ds_display_next_client(ds_client_t *);
+extern void ds_display_add_wmclient(ds_display_t *, ds_wmclient_t *);
+extern void ds_display_remove_wmclient(ds_wmclient_t *);
+extern ds_wmclient_t *ds_display_first_wmclient(ds_display_t *);
+extern ds_wmclient_t *ds_display_next_wmclient(ds_wmclient_t *);
 extern ds_window_t *ds_display_find_window(ds_display_t *, ds_wnd_id_t);
 extern ds_window_t *ds_display_window_by_pos(ds_display_t *, gfx_coord2_t *);
+extern void ds_display_enlist_window(ds_display_t *, ds_window_t *);
 extern void ds_display_add_window(ds_display_t *, ds_window_t *);
 extern void ds_display_remove_window(ds_window_t *);
+extern void ds_display_window_to_top(ds_window_t *);
 extern ds_window_t *ds_display_first_window(ds_display_t *);
 extern ds_window_t *ds_display_last_window(ds_display_t *);
@@ -73,4 +81,5 @@
 extern ds_seat_t *ds_display_first_seat(ds_display_t *);
 extern ds_seat_t *ds_display_next_seat(ds_seat_t *);
+extern ds_seat_t *ds_display_seat_by_idev(ds_display_t *, ds_idev_id_t);
 extern errno_t ds_display_add_ddev(ds_display_t *, ds_ddev_t *);
 extern void ds_display_remove_ddev(ds_ddev_t *);
@@ -79,4 +88,6 @@
 extern void ds_display_add_cursor(ds_display_t *, ds_cursor_t *);
 extern void ds_display_remove_cursor(ds_cursor_t *);
+extern void ds_display_update_max_rect(ds_display_t *);
+extern void ds_display_crop_max_rect(gfx_rect_t *, gfx_rect_t *);
 extern gfx_context_t *ds_display_get_gc(ds_display_t *);
 extern errno_t ds_display_paint_bg(ds_display_t *, gfx_rect_t *);
Index: uspace/srv/hid/display/dsops.c
===================================================================
--- uspace/srv/hid/display/dsops.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/dsops.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -54,7 +54,9 @@
 static errno_t disp_window_resize(void *, sysarg_t, gfx_coord2_t *,
     gfx_rect_t *);
+static errno_t disp_window_minimize(void *, sysarg_t);
 static errno_t disp_window_maximize(void *, sysarg_t);
 static errno_t disp_window_unmaximize(void *, sysarg_t);
 static errno_t disp_window_set_cursor(void *, sysarg_t, display_stock_cursor_t);
+static errno_t disp_window_set_caption(void *, sysarg_t, const char *);
 static errno_t disp_get_event(void *, sysarg_t *, display_wnd_ev_t *);
 static errno_t disp_get_info(void *, display_info_t *);
@@ -69,7 +71,9 @@
 	.window_resize_req = disp_window_resize_req,
 	.window_resize = disp_window_resize,
+	.window_minimize = disp_window_minimize,
 	.window_maximize = disp_window_maximize,
 	.window_unmaximize = disp_window_unmaximize,
 	.window_set_cursor = disp_window_set_cursor,
+	.window_set_caption = disp_window_set_caption,
 	.get_event = disp_get_event,
 	.get_info = disp_get_info
@@ -245,4 +249,24 @@
 }
 
+static errno_t disp_window_minimize(void *arg, sysarg_t wnd_id)
+{
+	ds_client_t *client = (ds_client_t *) arg;
+	ds_window_t *wnd;
+	errno_t rc;
+
+	ds_display_lock(client->display);
+
+	wnd = ds_client_find_window(client, wnd_id);
+	if (wnd == NULL) {
+		ds_display_unlock(client->display);
+		return ENOENT;
+	}
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "disp_window_maximize()");
+	rc = ds_window_minimize(wnd);
+	ds_display_unlock(client->display);
+	return rc;
+}
+
 static errno_t disp_window_maximize(void *arg, sysarg_t wnd_id)
 {
@@ -306,4 +330,25 @@
 }
 
+static errno_t disp_window_set_caption(void *arg, sysarg_t wnd_id,
+    const char *caption)
+{
+	ds_client_t *client = (ds_client_t *) arg;
+	ds_window_t *wnd;
+	errno_t rc;
+
+	ds_display_lock(client->display);
+
+	wnd = ds_client_find_window(client, wnd_id);
+	if (wnd == NULL) {
+		ds_display_unlock(client->display);
+		return ENOENT;
+	}
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "disp_window_set_caption()");
+	rc = ds_window_set_caption(wnd, caption);
+	ds_display_unlock(client->display);
+	return rc;
+}
+
 static errno_t disp_get_event(void *arg, sysarg_t *wnd_id,
     display_wnd_ev_t *event)
@@ -313,5 +358,5 @@
 	errno_t rc;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "disp_window_get_event()");
+	log_msg(LOG_DEFAULT, LVL_DEBUG2, "disp_window_get_event()");
 
 	ds_display_lock(client->display);
Index: uspace/srv/hid/display/input.c
===================================================================
--- uspace/srv/hid/display/input.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/input.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -45,9 +45,11 @@
 static errno_t ds_input_ev_active(input_t *);
 static errno_t ds_input_ev_deactive(input_t *);
-static errno_t ds_input_ev_key(input_t *, kbd_event_type_t, keycode_t, keymod_t, char32_t);
-static errno_t ds_input_ev_move(input_t *, int, int);
-static errno_t ds_input_ev_abs_move(input_t *, unsigned, unsigned, unsigned, unsigned);
-static errno_t ds_input_ev_button(input_t *, int, int);
-static errno_t ds_input_ev_dclick(input_t *, int);
+static errno_t ds_input_ev_key(input_t *, unsigned, kbd_event_type_t, keycode_t,
+    keymod_t, char32_t);
+static errno_t ds_input_ev_move(input_t *, unsigned, int, int);
+static errno_t ds_input_ev_abs_move(input_t *, unsigned, unsigned, unsigned,
+    unsigned, unsigned);
+static errno_t ds_input_ev_button(input_t *, unsigned, int, int);
+static errno_t ds_input_ev_dclick(input_t *, unsigned, int);
 
 static input_ev_ops_t ds_input_ev_ops = {
@@ -71,6 +73,6 @@
 }
 
-static errno_t ds_input_ev_key(input_t *input, kbd_event_type_t type,
-    keycode_t key, keymod_t mods, char32_t c)
+static errno_t ds_input_ev_key(input_t *input, unsigned kbd_id,
+    kbd_event_type_t type, keycode_t key, keymod_t mods, char32_t c)
 {
 	ds_display_t *disp = (ds_display_t *) input->user;
@@ -78,4 +80,5 @@
 	errno_t rc;
 
+	event.kbd_id = kbd_id;
 	event.type = type;
 	event.key = key;
@@ -89,10 +92,11 @@
 }
 
-static errno_t ds_input_ev_move(input_t *input, int dx, int dy)
-{
-	ds_display_t *disp = (ds_display_t *) input->user;
-	ptd_event_t event;
-	errno_t rc;
-
+static errno_t ds_input_ev_move(input_t *input, unsigned pos_id, int dx, int dy)
+{
+	ds_display_t *disp = (ds_display_t *) input->user;
+	ptd_event_t event;
+	errno_t rc;
+
+	event.pos_id = pos_id;
 	event.type = PTD_MOVE;
 	event.dmove.x = dx;
@@ -105,11 +109,12 @@
 }
 
-static errno_t ds_input_ev_abs_move(input_t *input, unsigned x, unsigned y,
-    unsigned max_x, unsigned max_y)
-{
-	ds_display_t *disp = (ds_display_t *) input->user;
-	ptd_event_t event;
-	errno_t rc;
-
+static errno_t ds_input_ev_abs_move(input_t *input, unsigned pos_id, unsigned x,
+    unsigned y, unsigned max_x, unsigned max_y)
+{
+	ds_display_t *disp = (ds_display_t *) input->user;
+	ptd_event_t event;
+	errno_t rc;
+
+	event.pos_id = pos_id;
 	event.type = PTD_ABS_MOVE;
 	event.apos.x = x;
@@ -126,10 +131,12 @@
 }
 
-static errno_t ds_input_ev_button(input_t *input, int bnum, int bpress)
-{
-	ds_display_t *disp = (ds_display_t *) input->user;
-	ptd_event_t event;
-	errno_t rc;
-
+static errno_t ds_input_ev_button(input_t *input, unsigned pos_id, int bnum,
+    int bpress)
+{
+	ds_display_t *disp = (ds_display_t *) input->user;
+	ptd_event_t event;
+	errno_t rc;
+
+	event.pos_id = pos_id;
 	event.type = bpress ? PTD_PRESS : PTD_RELEASE;
 	event.btn_num = bnum;
@@ -143,10 +150,11 @@
 }
 
-static errno_t ds_input_ev_dclick(input_t *input, int bnum)
-{
-	ds_display_t *disp = (ds_display_t *) input->user;
-	ptd_event_t event;
-	errno_t rc;
-
+static errno_t ds_input_ev_dclick(input_t *input, unsigned pos_id, int bnum)
+{
+	ds_display_t *disp = (ds_display_t *) input->user;
+	ptd_event_t event;
+	errno_t rc;
+
+	event.pos_id = pos_id;
 	event.type = PTD_DCLICK;
 	event.btn_num = bnum;
Index: uspace/srv/hid/display/main.c
===================================================================
--- uspace/srv/hid/display/main.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2019 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -47,4 +47,5 @@
 #include <stdio.h>
 #include <task.h>
+#include <wndmgt_srv.h>
 #include "client.h"
 #include "display.h"
@@ -55,7 +56,12 @@
 #include "seat.h"
 #include "window.h"
+#include "wmclient.h"
+#include "wmops.h"
 
 static void display_client_conn(ipc_call_t *, void *);
 static void display_client_ev_pending(void *);
+static void display_wmclient_ev_pending(void *);
+static void display_gc_conn(ipc_call_t *, void *);
+static void display_wndmgt_conn(ipc_call_t *, void *);
 
 #ifdef CONFIG_DISP_DOUBLE_BUF
@@ -76,4 +82,8 @@
 };
 
+static ds_wmclient_cb_t display_wmclient_cb = {
+	.ev_pending = display_wmclient_ev_pending
+};
+
 static void display_client_ev_pending(void *arg)
 {
@@ -81,4 +91,11 @@
 
 	display_srv_ev_pending(srv);
+}
+
+static void display_wmclient_ev_pending(void *arg)
+{
+	wndmgt_srv_t *srv = (wndmgt_srv_t *) arg;
+
+	wndmgt_srv_ev_pending(srv);
 }
 
@@ -90,4 +107,7 @@
 	ds_output_t *output = NULL;
 	gfx_context_t *gc = NULL;
+	port_id_t disp_port;
+	port_id_t gc_port;
+	port_id_t wm_port;
 	errno_t rc;
 
@@ -115,5 +135,17 @@
 		goto error;
 
-	async_set_fallback_port_handler(display_client_conn, disp);
+	rc = async_create_port(INTERFACE_DISPLAY, display_client_conn, disp,
+	    &disp_port);
+	if (rc != EOK)
+		goto error;
+
+	rc = async_create_port(INTERFACE_GC, display_gc_conn, disp, &gc_port);
+	if (rc != EOK)
+		goto error;
+
+	rc = async_create_port(INTERFACE_WNDMGT, display_wndmgt_conn, disp,
+	    &wm_port);
+	if (rc != EOK)
+		goto error;
 
 	rc = loc_server_register(NAME);
@@ -135,4 +167,7 @@
 	return EOK;
 error:
+	// XXX destroy disp_port
+	// XXX destroy gc_port
+	// XXX destroy wm_port
 #if 0
 	if (disp->input != NULL)
@@ -154,10 +189,7 @@
 {
 	display_srv_t srv;
-	sysarg_t wnd_id;
 	sysarg_t svc_id;
 	ds_client_t *client = NULL;
-	ds_window_t *wnd;
 	ds_display_t *disp = (ds_display_t *) arg;
-	gfx_context_t *gc;
 	errno_t rc;
 
@@ -166,13 +198,11 @@
 	    ipc_get_arg4(icall));
 
-	(void) icall;
-	(void) arg;
-
 	svc_id = ipc_get_arg2(icall);
-	wnd_id = ipc_get_arg3(icall);
 
 	if (svc_id != 0) {
 		/* Create client object */
+		ds_display_lock(disp);
 		rc = ds_client_create(disp, &display_client_cb, &srv, &client);
+		ds_display_unlock(disp);
 		if (rc != EOK) {
 			async_answer_0(icall, ENOMEM);
@@ -191,20 +221,65 @@
 		ds_client_destroy(client);
 		ds_display_unlock(disp);
-	} else {
-		/* Window GC connection */
-
-		wnd = ds_display_find_window(disp, wnd_id);
-		if (wnd == NULL) {
-			async_answer_0(icall, ENOENT);
-			return;
-		}
-
-		/*
-		 * XXX We need a way to make sure that the connection does
-		 * not stay active after the window had been destroyed
-		 */
-		gc = ds_window_get_ctx(wnd);
-		gc_conn(icall, gc);
-	}
+	}
+}
+
+/** Handle GC connection to display server */
+static void display_gc_conn(ipc_call_t *icall, void *arg)
+{
+	sysarg_t wnd_id;
+	ds_window_t *wnd;
+	ds_display_t *disp = (ds_display_t *) arg;
+	gfx_context_t *gc;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "display_gc_conn arg1=%zu arg2=%zu arg3=%zu arg4=%zu.",
+	    ipc_get_arg1(icall), ipc_get_arg2(icall), ipc_get_arg3(icall),
+	    ipc_get_arg4(icall));
+
+	wnd_id = ipc_get_arg3(icall);
+
+	/* Window GC connection */
+
+	wnd = ds_display_find_window(disp, wnd_id);
+	if (wnd == NULL) {
+		async_answer_0(icall, ENOENT);
+		return;
+	}
+
+	/*
+	 * XXX We need a way to make sure that the connection does
+	 * not stay active after the window had been destroyed
+	 */
+	gc = ds_window_get_ctx(wnd);
+	gc_conn(icall, gc);
+}
+
+/** Handle window management connection to display server */
+static void display_wndmgt_conn(ipc_call_t *icall, void *arg)
+{
+	ds_display_t *disp = (ds_display_t *) arg;
+	errno_t rc;
+	wndmgt_srv_t srv;
+	ds_wmclient_t *wmclient = NULL;
+
+	/* Create WM client object */
+	ds_display_lock(disp);
+	rc = ds_wmclient_create(disp, &display_wmclient_cb, &srv, &wmclient);
+	ds_display_unlock(disp);
+	if (rc != EOK) {
+		async_answer_0(icall, ENOMEM);
+		return;
+	}
+
+	/* Set up protocol structure */
+	wndmgt_srv_initialize(&srv);
+	srv.ops = &wndmgt_srv_ops;
+	srv.arg = wmclient;
+
+	/* Handle connection */
+	wndmgt_conn(icall, &srv);
+
+	ds_display_lock(disp);
+	ds_wmclient_destroy(wmclient);
+	ds_display_unlock(disp);
 }
 
Index: uspace/srv/hid/display/meson.build
===================================================================
--- uspace/srv/hid/display/meson.build	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/meson.build	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 #
-# Copyright (c) 2019 Jiri Svoboda
+# Copyright (c) 2022 Jiri Svoboda
 # All rights reserved.
 #
@@ -27,5 +27,5 @@
 #
 
-deps = [ 'ipcgfx', 'memgfx', 'display', 'ddev' ]
+deps = [ 'ipcgfx', 'memgfx', 'display', 'ddev', 'wndmgt' ]
 
 src = files(
@@ -42,4 +42,6 @@
 	'seat.c',
 	'window.c',
+	'wmclient.c',
+	'wmops.c',
 )
 
@@ -53,4 +55,5 @@
 	'seat.c',
 	'window.c',
+	'wmclient.c',
 	'test/client.c',
 	'test/clonegc.c',
@@ -60,3 +63,4 @@
 	'test/seat.c',
 	'test/window.c',
+	'test/wmclient.c',
 )
Index: uspace/srv/hid/display/seat.c
===================================================================
--- uspace/srv/hid/display/seat.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/seat.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -90,7 +90,15 @@
 void ds_seat_set_focus(ds_seat_t *seat, ds_window_t *wnd)
 {
+	errno_t rc;
+
 	if (wnd == seat->focus) {
 		/* Focus is not changing */
 		return;
+	}
+
+	if (wnd != NULL) {
+		rc = ds_window_unminimize(wnd);
+		if (rc != EOK)
+			return;
 	}
 
@@ -130,23 +138,14 @@
 /** Evacuate seat references to window.
  *
- * If seat's focus is @a wnd, it will be set to a different window.
+ * If seat's focus is @a wnd, it will be set to NULL.
  * If seat's popup window is @a wnd, it will be set to NULL.
  *
  * @param seat Seat
- * @param wnd Window to evacuate focus from
+ * @param wnd Window to evacuate references from
  */
 void ds_seat_evac_wnd_refs(ds_seat_t *seat, ds_window_t *wnd)
 {
-	ds_window_t *nwnd;
-
-	if (seat->focus == wnd) {
-		nwnd = ds_display_prev_window(wnd);
-		if (nwnd == NULL)
-			nwnd = ds_display_last_window(wnd->display);
-		if (nwnd == wnd)
-			nwnd = NULL;
-
-		ds_seat_set_focus(seat, nwnd);
-	}
+	if (seat->focus == wnd)
+		ds_seat_set_focus(seat, NULL);
 
 	if (seat->popup == wnd)
@@ -154,4 +153,30 @@
 }
 
+/** Unfocus window.
+ *
+ * If seat's focus is @a wnd, it will be set to a different window
+ * that is not minimized, preferably not a system window.
+ *
+ * @param seat Seat
+ * @param wnd Window to remove focus from
+ */
+void ds_seat_unfocus_wnd(ds_seat_t *seat, ds_window_t *wnd)
+{
+	ds_window_t *nwnd;
+
+	if (seat->focus != wnd)
+		return;
+
+	/* Find alternate window that is neither system nor minimized */
+	nwnd = ds_window_find_alt(wnd, ~(wndf_minimized | wndf_system));
+
+	if (nwnd == NULL) {
+		/* Find alternate window that is not minimized */
+		nwnd = ds_window_find_alt(wnd, ~wndf_minimized);
+	}
+
+	ds_seat_set_focus(seat, nwnd);
+}
+
 /** Switch focus to another window.
  *
@@ -163,12 +188,8 @@
 	ds_window_t *nwnd;
 
-	if (seat->focus != NULL)
-		nwnd = ds_display_prev_window(seat->focus);
-	else
-		nwnd = NULL;
-
-	if (nwnd == NULL)
-		nwnd = ds_display_last_window(seat->display);
-
+	/* Find alternate window that is not a system window */
+	nwnd = ds_window_find_alt(seat->focus, ~wndf_system);
+
+	/* Only switch focus if there is another window */
 	if (nwnd != NULL)
 		ds_seat_set_focus(seat, nwnd);
@@ -364,5 +385,5 @@
 	if (event->type == PTD_PRESS || event->type == PTD_RELEASE ||
 	    event->type == PTD_DCLICK) {
-		pevent.pos_id = 0;
+		pevent.pos_id = event->pos_id;
 		switch (event->type) {
 		case PTD_PRESS:
@@ -395,5 +416,5 @@
 		seat->pntpos = npos;
 
-		pevent.pos_id = 0;
+		pevent.pos_id = event->pos_id;
 		pevent.type = POS_UPDATE;
 		pevent.btn_num = 0;
@@ -423,5 +444,5 @@
 		seat->pntpos = npos;
 
-		pevent.pos_id = 0;
+		pevent.pos_id = event->pos_id;
 		pevent.type = POS_UPDATE;
 		pevent.btn_num = 0;
Index: uspace/srv/hid/display/seat.h
===================================================================
--- uspace/srv/hid/display/seat.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/seat.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -51,4 +51,5 @@
 extern void ds_seat_set_popup(ds_seat_t *, ds_window_t *);
 extern void ds_seat_evac_wnd_refs(ds_seat_t *, ds_window_t *);
+extern void ds_seat_unfocus_wnd(ds_seat_t *, ds_window_t *);
 extern void ds_seat_switch_focus(ds_seat_t *);
 extern errno_t ds_seat_post_kbd_event(ds_seat_t *, kbd_event_t *);
Index: uspace/srv/hid/display/test/cursor.c
===================================================================
--- uspace/srv/hid/display/test/cursor.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/test/cursor.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2020 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -117,4 +117,5 @@
 	PCUT_ASSERT_TRUE(resp.render_called);
 
+	ds_ddev_close(ddev);
 	ds_cursor_destroy(cursor);
 	ds_display_destroy(disp);
@@ -167,4 +168,5 @@
 	PCUT_ASSERT_FALSE(resp.render_called);
 
+	ds_ddev_close(ddev);
 	ds_cursor_destroy(cursor);
 	ds_display_destroy(disp);
Index: uspace/srv/hid/display/test/display.c
===================================================================
--- uspace/srv/hid/display/test/display.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/test/display.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2019 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -36,4 +36,5 @@
 #include "../seat.h"
 #include "../window.h"
+#include "../wmclient.h"
 
 PCUT_INIT;
@@ -47,4 +48,10 @@
 };
 
+static void test_ds_wmev_pending(void *);
+
+static ds_wmclient_cb_t test_ds_wmclient_cb = {
+	.ev_pending = test_ds_wmev_pending
+};
+
 static void test_ds_ev_pending(void *arg)
 {
@@ -53,4 +60,10 @@
 }
 
+static void test_ds_wmev_pending(void *arg)
+{
+	bool *called_cb = (bool *) arg;
+	*called_cb = true;
+}
+
 /** Display creation and destruction. */
 PCUT_TEST(display_create_destroy)
@@ -86,4 +99,28 @@
 
 	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Basic WM client operation. */
+PCUT_TEST(display_wmclient)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	ds_wmclient_t *c0, *c1;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, NULL, &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	c0 = ds_display_first_wmclient(disp);
+	PCUT_ASSERT_EQUALS(c0, wmclient);
+
+	c1 = ds_display_next_wmclient(c0);
+	PCUT_ASSERT_NULL(c1);
+
+	ds_wmclient_destroy(wmclient);
 	ds_display_destroy(disp);
 }
@@ -150,4 +187,120 @@
 	wnd = ds_display_find_window(disp, w0->id + 1);
 	PCUT_ASSERT_NULL(wnd);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_display_enlist_window() */
+PCUT_TEST(display_enlist_window)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	ds_window_t *w2;
+	ds_window_t *w3;
+	ds_window_t *w;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 100;
+
+	/* Regular windows */
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Topmost windows */
+
+	params.flags |= wndf_topmost;
+
+	rc = ds_window_create(client, &params, &w2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w3);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Delist w1 and w2 */
+	list_remove(&w1->ldwindows);
+	list_remove(&w2->ldwindows);
+
+	/* Enlist the windows back and check their order */
+	ds_display_enlist_window(disp, w1);
+	ds_display_enlist_window(disp, w2);
+
+	w = ds_display_first_window(disp);
+	PCUT_ASSERT_EQUALS(w2, w);
+	w = ds_display_next_window(w);
+	PCUT_ASSERT_EQUALS(w3, w);
+	w = ds_display_next_window(w);
+	PCUT_ASSERT_EQUALS(w1, w);
+	w = ds_display_next_window(w);
+	PCUT_ASSERT_EQUALS(w0, w);
+	w = ds_display_next_window(w);
+	PCUT_ASSERT_EQUALS(NULL, w);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_window_destroy(w2);
+	ds_window_destroy(w3);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_display_window_to_top() */
+PCUT_TEST(display_window_to_top)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 100;
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w1, ds_display_first_window(disp));
+	ds_display_window_to_top(w0);
+	PCUT_ASSERT_EQUALS(w0, ds_display_first_window(disp));
 
 	ds_window_destroy(w0);
@@ -236,4 +389,10 @@
 	ds_seat_destroy(seat);
 	ds_display_destroy(disp);
+}
+
+/** ds_display_seat_by_idev() returns the correct seat. */
+PCUT_TEST(display_seat_by_idev)
+{
+	// XXX TODO
 }
 
@@ -442,3 +601,169 @@
 }
 
+/** ds_display_update_max_rect() updates maximization rectangle */
+PCUT_TEST(display_update_max_rect)
+{
+	ds_display_t *disp;
+	ds_seat_t *seat;
+	ds_client_t *client;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/*
+	 * We need to set display dimensions Here we do it directly
+	 * instead of adding a display device.
+	 */
+	disp->rect.p0.x = 0;
+	disp->rect.p0.y = 0;
+	disp->rect.p1.x = 500;
+	disp->rect.p1.y = 500;
+
+	/* Set maximize rectangle as well */
+	disp->max_rect = disp->rect;
+
+	/* A panel-like window at the bottom */
+	display_wnd_params_init(&params);
+	params.flags |= wndf_setpos;
+	params.pos.x = 0;
+	params.pos.y = 450;
+	params.rect.p0.x = 0;
+	params.rect.p0.y = 0;
+	params.rect.p1.x = 500;
+	params.rect.p1.y = 50;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/*
+	 * At this point the maximize rect should be unaltered because
+	 * the avoid flag has not been set.
+	 */
+	PCUT_ASSERT_INT_EQUALS(0, disp->max_rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(0, disp->max_rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(500, disp->max_rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(500, disp->max_rect.p1.y);
+
+	wnd->flags |= wndf_avoid;
+
+	/* Update maximize rectangle */
+	ds_display_update_max_rect(disp);
+
+	/* Verify maximize rectangle */
+	PCUT_ASSERT_INT_EQUALS(0, disp->max_rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(0, disp->max_rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(500, disp->max_rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(450, disp->max_rect.p1.y);
+
+	ds_window_destroy(wnd);
+	ds_client_destroy(client);
+	ds_seat_destroy(seat);
+	ds_display_destroy(disp);
+}
+
+/** Cropping maximization rectangle from the top */
+PCUT_TEST(display_crop_max_rect_top)
+{
+	gfx_rect_t arect;
+	gfx_rect_t mrect;
+
+	arect.p0.x = 10;
+	arect.p0.y = 20;
+	arect.p1.x = 30;
+	arect.p1.y = 5;
+
+	mrect.p0.x = 10;
+	mrect.p0.y = 20;
+	mrect.p1.x = 30;
+	mrect.p1.y = 40;
+
+	ds_display_crop_max_rect(&arect, &mrect);
+
+	PCUT_ASSERT_INT_EQUALS(10, mrect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(5, mrect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(30, mrect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(40, mrect.p1.y);
+}
+
+/** Cropping maximization rectangle from the bottom */
+PCUT_TEST(display_crop_max_rect_bottom)
+{
+	gfx_rect_t arect;
+	gfx_rect_t mrect;
+
+	arect.p0.x = 10;
+	arect.p0.y = 35;
+	arect.p1.x = 30;
+	arect.p1.y = 40;
+
+	mrect.p0.x = 10;
+	mrect.p0.y = 20;
+	mrect.p1.x = 30;
+	mrect.p1.y = 40;
+
+	ds_display_crop_max_rect(&arect, &mrect);
+
+	PCUT_ASSERT_INT_EQUALS(10, mrect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(20, mrect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(30, mrect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(35, mrect.p1.y);
+}
+
+/** Cropping maximization rectangle from the left */
+PCUT_TEST(display_crop_max_rect_left)
+{
+	gfx_rect_t arect;
+	gfx_rect_t mrect;
+
+	arect.p0.x = 10;
+	arect.p0.y = 20;
+	arect.p1.x = 15;
+	arect.p1.y = 40;
+
+	mrect.p0.x = 10;
+	mrect.p0.y = 20;
+	mrect.p1.x = 30;
+	mrect.p1.y = 40;
+
+	ds_display_crop_max_rect(&arect, &mrect);
+
+	PCUT_ASSERT_INT_EQUALS(15, mrect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(20, mrect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(30, mrect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(40, mrect.p1.y);
+}
+
+/** Cropping maximization rectangle from the right */
+PCUT_TEST(display_crop_max_rect_right)
+{
+	gfx_rect_t arect;
+	gfx_rect_t mrect;
+
+	arect.p0.x = 25;
+	arect.p0.y = 20;
+	arect.p1.x = 30;
+	arect.p1.y = 40;
+
+	mrect.p0.x = 10;
+	mrect.p0.y = 20;
+	mrect.p1.x = 30;
+	mrect.p1.y = 40;
+
+	ds_display_crop_max_rect(&arect, &mrect);
+
+	PCUT_ASSERT_INT_EQUALS(10, mrect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(20, mrect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(25, mrect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(40, mrect.p1.y);
+}
+
 PCUT_EXPORT(display);
Index: uspace/srv/hid/display/test/main.c
===================================================================
--- uspace/srv/hid/display/test/main.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/test/main.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -37,4 +37,5 @@
 PCUT_IMPORT(seat);
 PCUT_IMPORT(window);
+PCUT_IMPORT(wmclient);
 
 PCUT_MAIN();
Index: uspace/srv/hid/display/test/seat.c
===================================================================
--- uspace/srv/hid/display/test/seat.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/test/seat.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -90,52 +90,5 @@
 }
 
-/** Evacuate focus when another window is available. */
-PCUT_TEST(evac_focus_two_windows)
-{
-	ds_display_t *disp;
-	ds_client_t *client;
-	ds_seat_t *seat;
-	ds_window_t *w0;
-	ds_window_t *w1;
-	display_wnd_params_t params;
-	bool called_cb = false;
-	errno_t rc;
-
-	rc = ds_display_create(NULL, df_none, &disp);
-	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
-
-	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
-	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
-
-	rc = ds_seat_create(disp, &seat);
-	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
-
-	display_wnd_params_init(&params);
-	params.rect.p0.x = params.rect.p0.y = 0;
-	params.rect.p1.x = params.rect.p1.y = 1;
-
-	rc = ds_window_create(client, &params, &w1);
-	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
-
-	rc = ds_window_create(client, &params, &w0);
-	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
-
-	ds_seat_set_focus(seat, w1);
-	PCUT_ASSERT_EQUALS(w1, seat->focus);
-	PCUT_ASSERT_TRUE(called_cb);
-	called_cb = false;
-
-	ds_seat_evac_wnd_refs(seat, w1);
-	PCUT_ASSERT_EQUALS(w0, seat->focus);
-	PCUT_ASSERT_TRUE(called_cb);
-
-	ds_window_destroy(w0);
-	ds_window_destroy(w1);
-	ds_seat_destroy(seat);
-	ds_client_destroy(client);
-	ds_display_destroy(disp);
-}
-
-/** Evacuate focus from the only existing window.
+/** Evacuate focus from window.
  *
  * After evacuating no window should be focused
@@ -219,4 +172,191 @@
 
 	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Unfocus window when three windows are available */
+PCUT_TEST(unfocus_wnd_three_windows)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	ds_window_t *w2;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &w2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w0, seat->focus);
+
+	ds_window_unfocus(w0);
+
+	/* The previous window, w2, should be focused now */
+	PCUT_ASSERT_EQUALS(w2, seat->focus);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Unfocus window when two windows and one system window are available */
+PCUT_TEST(unfocus_wnd_two_windows_one_sys)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	ds_window_t *w2;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	params.flags |= wndf_system;
+	rc = ds_window_create(client, &params, &w2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	params.flags &= ~wndf_system;
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w0, seat->focus);
+
+	ds_window_unfocus(w0);
+
+	/* The previous non-system window is w1, w2 should be skipped */
+	PCUT_ASSERT_EQUALS(w1, seat->focus);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Unfocus window when one window and one system window is available */
+PCUT_TEST(unfocus_wnd_one_window_one_sys)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	params.flags |= wndf_system;
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	params.flags &= ~wndf_system;
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w0, seat->focus);
+
+	ds_window_unfocus(w0);
+
+	/* Since no non-system window is available, w1 should be focused */
+	PCUT_ASSERT_EQUALS(w1, seat->focus);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Unfocus window when one window is available */
+PCUT_TEST(unfocus_wnd_one_window)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	display_wnd_params_t params;
+	bool called_cb = false;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, &test_ds_client_cb, &called_cb, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w0, seat->focus);
+
+	ds_window_unfocus(w0);
+
+	/* Since no other window is availabe, no window should be focused */
+	PCUT_ASSERT_EQUALS(NULL, seat->focus);
+
+	ds_window_destroy(w0);
 	ds_seat_destroy(seat);
 	ds_client_destroy(client);
Index: uspace/srv/hid/display/test/window.c
===================================================================
--- uspace/srv/hid/display/test/window.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/test/window.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -51,4 +51,82 @@
 };
 
+/** Test creating and destroying window */
+PCUT_TEST(create_destroy)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 10;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_window_bring_to_top() brings window to top */
+PCUT_TEST(bring_to_top)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w1;
+	ds_window_t *w2;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 10;
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* w2 should be on the top */
+	PCUT_ASSERT_EQUALS(w2, ds_display_first_window(disp));
+
+	/* Bring w1 to top */
+	ds_window_bring_to_top(w1);
+
+	/* Now w1 should be on the top */
+	PCUT_ASSERT_EQUALS(w1, ds_display_first_window(disp));
+
+	ds_window_destroy(w1);
+	ds_window_destroy(w2);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
 /** Test ds_window_resize(). */
 PCUT_TEST(window_resize)
@@ -183,4 +261,79 @@
 }
 
+/** Test ds_window_minimize(). */
+PCUT_TEST(window_minimize)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 10;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_minimize(wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_INT_EQUALS(wndf_minimized, wnd->flags & wndf_minimized);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_window_unminimize(). */
+PCUT_TEST(window_unminimize)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.flags |= wndf_minimized;
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 10;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_unminimize(wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_INT_EQUALS(0, wnd->flags & wndf_minimized);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
 /** Test ds_window_maximize(). */
 PCUT_TEST(window_maximize)
@@ -295,4 +448,42 @@
 	gc = ds_window_get_ctx(wnd);
 	PCUT_ASSERT_NOT_NULL(gc);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_window_is_visible() */
+PCUT_TEST(is_visible)
+{
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 10;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_TRUE(ds_window_is_visible(wnd));
+
+	wnd->flags |= wndf_minimized;
+
+	PCUT_ASSERT_FALSE(ds_window_is_visible(wnd));
 
 	ds_window_destroy(wnd);
@@ -425,4 +616,82 @@
 	PCUT_ASSERT_INT_EQUALS(13, wnd->dpos.x);
 	PCUT_ASSERT_INT_EQUALS(14, wnd->dpos.y);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_window_post_focus_event() */
+PCUT_TEST(window_post_focus_event)
+{
+	gfx_context_t *gc;
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = gfx_context_new(&dummy_ops, NULL, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_display_create(gc, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_post_focus_event(wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_window_post_unfocus_event() */
+PCUT_TEST(window_post_unfocus_event)
+{
+	gfx_context_t *gc;
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = gfx_context_new(&dummy_ops, NULL, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_display_create(gc, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_post_focus_event(wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
 
 	ds_window_destroy(wnd);
@@ -851,4 +1120,151 @@
 }
 
+/** Test ds_window_set_caption() */
+PCUT_TEST(window_set_caption)
+{
+	gfx_context_t *gc;
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = gfx_context_new(&dummy_ops, NULL, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_display_create(gc, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &wnd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(wnd->display->cursor[dcurs_arrow], wnd->cursor);
+
+	rc = ds_window_set_caption(wnd, "Hello");
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_INT_EQUALS(0, str_cmp("Hello", wnd->caption));
+
+	ds_window_destroy(wnd);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** ds_window_find_alt() finds alternate window by flags */
+PCUT_TEST(window_find_alt)
+{
+	gfx_context_t *gc;
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	ds_window_t *w2;
+	ds_window_t *wnd;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = gfx_context_new(&dummy_ops, NULL, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_display_create(gc, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	w1->flags |= wndf_minimized;
+
+	rc = ds_window_create(client, &params, &w2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	w2->flags |= wndf_system;
+
+	wnd = ds_window_find_alt(w0, wndf_minimized);
+	PCUT_ASSERT_EQUALS(w1, wnd);
+
+	wnd = ds_window_find_alt(w0, wndf_system);
+	PCUT_ASSERT_EQUALS(w2, wnd);
+
+	wnd = ds_window_find_alt(w0, wndf_maximized);
+	PCUT_ASSERT_NULL(wnd);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_window_destroy(w2);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
+/** ds_window_unfocus() switches to another window */
+PCUT_TEST(window_unfocus)
+{
+	gfx_context_t *gc;
+	ds_display_t *disp;
+	ds_client_t *client;
+	ds_seat_t *seat;
+	ds_window_t *w0;
+	ds_window_t *w1;
+	display_wnd_params_t params;
+	errno_t rc;
+
+	rc = gfx_context_new(&dummy_ops, NULL, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_display_create(gc, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_client_create(disp, NULL, NULL, &client);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_seat_create(disp, &seat);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	display_wnd_params_init(&params);
+	params.rect.p0.x = params.rect.p0.y = 0;
+	params.rect.p1.x = params.rect.p1.y = 1;
+
+	rc = ds_window_create(client, &params, &w1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_window_create(client, &params, &w0);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	PCUT_ASSERT_EQUALS(w0, seat->focus);
+
+	ds_window_unfocus(w0);
+
+	PCUT_ASSERT_EQUALS(w1, seat->focus);
+
+	ds_window_destroy(w0);
+	ds_window_destroy(w1);
+	ds_seat_destroy(seat);
+	ds_client_destroy(client);
+	ds_display_destroy(disp);
+}
+
 static errno_t dummy_set_color(void *arg, gfx_color_t *color)
 {
Index: uspace/srv/hid/display/test/wmclient.c
===================================================================
--- uspace/srv/hid/display/test/wmclient.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/test/wmclient.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2022 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 <disp_srv.h>
+#include <errno.h>
+#include <pcut/pcut.h>
+#include <str.h>
+
+#include "../display.h"
+#include "../wmclient.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(wmclient);
+
+static void test_ds_wmev_pending(void *);
+
+static ds_wmclient_cb_t test_ds_wmclient_cb = {
+	.ev_pending = test_ds_wmev_pending
+};
+
+static void test_ds_wmev_pending(void *arg)
+{
+	bool *called_cb = (bool *) arg;
+	*called_cb = true;
+}
+
+/** WM client creation and destruction. */
+PCUT_TEST(create_destroy)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, NULL, &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_wmclient_destroy(wmclient);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_wmclient_get_event(), ds_wmclient_post_wnd_added_event(). */
+PCUT_TEST(client_get_post_wnd_added_event)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	wndmgt_ev_t revent;
+	bool called_cb = NULL;
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, &called_cb,
+	    &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	called_cb = false;
+	wnd_id = 42;
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	rc = ds_wmclient_post_wnd_added_event(wmclient, wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_TRUE(called_cb);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_EQUALS(wnd_id, revent.wnd_id);
+	PCUT_ASSERT_EQUALS(wmev_window_added, revent.etype);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	ds_wmclient_destroy(wmclient);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_wmclient_get_event(), ds_wmclient_post_wnd_removed_event(). */
+PCUT_TEST(client_get_post_wnd_removed_event)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	wndmgt_ev_t revent;
+	bool called_cb = NULL;
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, &called_cb,
+	    &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	called_cb = false;
+	wnd_id = 42;
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	rc = ds_wmclient_post_wnd_removed_event(wmclient, wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_TRUE(called_cb);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_EQUALS(wnd_id, revent.wnd_id);
+	PCUT_ASSERT_EQUALS(wmev_window_removed, revent.etype);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	ds_wmclient_destroy(wmclient);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_wmclient_get_event(), ds_wmclient_post_wnd_changed_event(). */
+PCUT_TEST(client_get_post_wnd_changed_event)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	wndmgt_ev_t revent;
+	bool called_cb = NULL;
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, &called_cb,
+	    &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	called_cb = false;
+	wnd_id = 42;
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	rc = ds_wmclient_post_wnd_changed_event(wmclient, wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_TRUE(called_cb);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_EQUALS(wnd_id, revent.wnd_id);
+	PCUT_ASSERT_EQUALS(wmev_window_changed, revent.etype);
+
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	ds_wmclient_destroy(wmclient);
+	ds_display_destroy(disp);
+}
+
+/** Test ds_wmclient_purge_events() */
+PCUT_TEST(purge_events)
+{
+	ds_display_t *disp;
+	ds_wmclient_t *wmclient;
+	wndmgt_ev_t revent;
+	bool called_cb = NULL;
+	sysarg_t wnd_id;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_wmclient_create(disp, &test_ds_wmclient_cb, &called_cb,
+	    &wmclient);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Post window added event */
+	wnd_id = 42;
+	rc = ds_wmclient_post_wnd_added_event(wmclient, wnd_id);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	/* Purge it */
+	ds_wmclient_purge_events(wmclient);
+
+	/* The queue should be empty now */
+	rc = ds_wmclient_get_event(wmclient, &revent);
+	PCUT_ASSERT_ERRNO_VAL(ENOENT, rc);
+
+	ds_wmclient_destroy(wmclient);
+	ds_display_destroy(disp);
+}
+
+PCUT_EXPORT(wmclient);
Index: uspace/srv/hid/display/types/display/display.h
===================================================================
--- uspace/srv/hid/display/types/display/display.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/types/display/display.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2019 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -62,4 +62,6 @@
 	/** Clients (of ds_client_t) */
 	list_t clients;
+	/** WM clients (of ds_wmclient_t) */
+	list_t wmclients;
 
 	/** Next ID to assign to a window.
@@ -95,4 +97,7 @@
 	gfx_rect_t rect;
 
+	/** Maximize rectangle */
+	gfx_rect_t max_rect;
+
 	/** Backbuffer bitmap or @c NULL if not double-buffering */
 	gfx_bitmap_t *backbuf;
Index: uspace/srv/hid/display/types/display/idev.h
===================================================================
--- uspace/srv/hid/display/types/display/idev.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/types/display/idev.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022 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 display
+ * @{
+ */
+/**
+ * @file Display server input device type
+ */
+
+#ifndef TYPES_DISPLAY_IDEV_H
+#define TYPES_DISPLAY_IDEV_H
+
+#include <types/common.h>
+
+typedef sysarg_t ds_idev_id_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/display/types/display/ptd_event.h
===================================================================
--- uspace/srv/hid/display/types/display/ptd_event.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/types/display/ptd_event.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * All rights reserved.
  *
@@ -48,4 +48,7 @@
 /** Pointing device event */
 typedef struct {
+	/** Positioning device ID */
+	unsigned pos_id;
+	/** PTD event type */
 	ptd_event_type_t type;
 	/** Button number for PTD_PRESS or PTD_RELEASE */
Index: uspace/srv/hid/display/types/display/window.h
===================================================================
--- uspace/srv/hid/display/types/display/window.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/types/display/window.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -105,4 +105,8 @@
 	/** Window resize type (if state is dsw_resizing) */
 	display_wnd_rsztype_t rsztype;
+	/** Window caption */
+	char *caption;
+	/** Number of foci */
+	unsigned nfocus;
 } ds_window_t;
 
Index: uspace/srv/hid/display/types/display/wmclient.h
===================================================================
--- uspace/srv/hid/display/types/display/wmclient.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/types/display/wmclient.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 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 display
+ * @{
+ */
+/**
+ * @file Display server WM client type
+ */
+
+#ifndef TYPES_DISPLAY_WMCLIENT_H
+#define TYPES_DISPLAY_WMCLIENT_H
+
+#include <adt/list.h>
+#include <wndmgt.h>
+
+/** Display server WM client callbacks */
+typedef struct {
+	void (*ev_pending)(void *);
+} ds_wmclient_cb_t;
+
+/** Display server WM client */
+typedef struct ds_wmclient {
+	/** Parent display */
+	struct ds_display *display;
+	/** Callbacks */
+	ds_wmclient_cb_t *cb;
+	/** Callback argument */
+	void *cb_arg;
+	/** Link to @c display->wmclients */
+	link_t lwmclients;
+	/** Event queue (of ds_window_ev_t) */
+	list_t events;
+} ds_wmclient_t;
+
+/** WM client event queue entry */
+typedef struct {
+	/** Link to event queue */
+	link_t levents;
+	/** WM client to which the event is delivered */
+	ds_wmclient_t *wmclient;
+	/** Event */
+	wndmgt_ev_t event;
+} ds_wmclient_ev_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/display/window.c
===================================================================
--- uspace/srv/hid/display/window.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/window.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -44,8 +44,11 @@
 #include <memgfx/memgc.h>
 #include <stdlib.h>
+#include <str.h>
+#include <wndmgt.h>
 #include "client.h"
 #include "display.h"
 #include "seat.h"
 #include "window.h"
+#include "wmclient.h"
 
 static void ds_window_invalidate_cb(void *, gfx_rect_t *);
@@ -64,10 +67,10 @@
  * @param client Client owning the window
  * @param params Window parameters
- * @param rgc Place to store pointer to new GC.
+ * @param rwnd Place to store pointer to new window.
  *
  * @return EOK on success or an error code
  */
 errno_t ds_window_create(ds_client_t *client, display_wnd_params_t *params,
-    ds_window_t **rgc)
+    ds_window_t **rwnd)
 {
 	ds_window_t *wnd = NULL;
@@ -81,4 +84,10 @@
 	wnd = calloc(1, sizeof(ds_window_t));
 	if (wnd == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	wnd->caption = str_dup(params->caption);
+	if (wnd->caption == NULL) {
 		rc = ENOMEM;
 		goto error;
@@ -133,4 +142,5 @@
 	}
 
+	// TODO Multi-seat: which seat should own the new window?
 	seat = ds_display_first_seat(client->display);
 
@@ -140,7 +150,10 @@
 		ds_seat_set_focus(seat, wnd);
 
+	if ((params->flags & wndf_avoid) != 0)
+		ds_display_update_max_rect(wnd->display);
+
 	(void) ds_display_paint(wnd->display, NULL);
 
-	*rgc = wnd;
+	*rwnd = wnd;
 	return EOK;
 error:
@@ -152,4 +165,6 @@
 		if (wnd->bitmap != NULL)
 			gfx_bitmap_destroy(wnd->bitmap);
+		if (wnd->caption != NULL)
+			free(wnd->caption);
 		free(wnd);
 	}
@@ -167,8 +182,13 @@
 
 	disp = wnd->display;
+
+	ds_window_unfocus(wnd);
 
 	ds_client_remove_window(wnd);
 	ds_display_remove_window(wnd);
 
+	if ((wnd->flags & wndf_avoid) != 0)
+		ds_display_update_max_rect(disp);
+
 	mem_gc_delete(wnd->mgc);
 
@@ -176,4 +196,5 @@
 		gfx_bitmap_destroy(wnd->bitmap);
 
+	free(wnd->caption);
 	free(wnd);
 
@@ -187,8 +208,5 @@
 void ds_window_bring_to_top(ds_window_t *wnd)
 {
-	ds_display_t *disp = wnd->display;
-
-	ds_display_remove_window(wnd);
-	ds_display_add_window(disp, wnd);
+	ds_display_window_to_top(wnd);
 	(void) ds_display_paint(wnd->display, NULL);
 }
@@ -202,4 +220,14 @@
 {
 	return wnd->gc;
+}
+
+/** Determine if window is visible.
+ *
+ * @param wnd Window
+ * @return @c true iff window is visible
+ */
+bool ds_window_is_visible(ds_window_t *wnd)
+{
+	return (wnd->flags & wndf_minimized) == 0;
 }
 
@@ -216,5 +244,9 @@
 	gfx_rect_t crect;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_paint");
+	log_msg(LOG_DEFAULT, LVL_DEBUG2, "ds_window_paint");
+
+	/* Skip painting the window if not visible */
+	if (!ds_window_is_visible(wnd))
+		return EOK;
 
 	if (rect != NULL) {
@@ -361,5 +393,5 @@
 	bool newr;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_repaint_preview");
+	log_msg(LOG_DEFAULT, LVL_DEBUG2, "ds_window_repaint_preview");
 
 	/*
@@ -459,5 +491,5 @@
 	gfx_rect_t old_rect;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_update_move (%d, %d)",
+	log_msg(LOG_DEFAULT, LVL_DEBUG2, "ds_window_update_move (%d, %d)",
 	    (int) pos->x, (int) pos->y);
 
@@ -496,5 +528,5 @@
 	wnd->preview_rect = wnd->rect;
 
-	// XXX Need client to tell us which seat started the resize!
+	// TODO Multi-seat: need client to tell us which seat started the resize!
 	seat = ds_display_first_seat(wnd->display);
 	ctype = display_cursor_from_wrsz(rsztype);
@@ -527,5 +559,5 @@
 	ds_client_post_resize_event(wnd->client, wnd, &nrect);
 
-	// XXX Need to know which seat started the resize!
+	// TODO Multi-seat: Need to know which seat started the resize!
 	seat = ds_display_first_seat(wnd->display);
 	ds_seat_set_wm_cursor(seat, NULL);
@@ -545,5 +577,5 @@
 	gfx_rect_t old_rect;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_update_resize (%d, %d)",
+	log_msg(LOG_DEFAULT, LVL_DEBUG2, "ds_window_update_resize (%d, %d)",
 	    (int) pos->x, (int) pos->y);
 
@@ -594,5 +626,5 @@
 	bool inside;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG,
+	log_msg(LOG_DEFAULT, LVL_DEBUG2,
 	    "ds_window_post_pos_event type=%d pos=%d,%d", event->type,
 	    (int) event->hpos, (int) event->vpos);
@@ -648,7 +680,24 @@
 errno_t ds_window_post_focus_event(ds_window_t *wnd)
 {
+	errno_t rc;
+	ds_wmclient_t *wmclient;
+
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_post_focus_event");
 
-	return ds_client_post_focus_event(wnd->client, wnd);
+	rc = ds_client_post_focus_event(wnd->client, wnd);
+	if (rc != EOK)
+		return rc;
+
+	/* Increase focus counter */
+	++wnd->nfocus;
+
+	/* Notify window managers about window information change */
+	wmclient = ds_display_first_wmclient(wnd->display);
+	while (wmclient != NULL) {
+		ds_wmclient_post_wnd_changed_event(wmclient, wnd->id);
+		wmclient = ds_display_next_wmclient(wmclient);
+	}
+
+	return EOK;
 }
 
@@ -660,7 +709,24 @@
 errno_t ds_window_post_unfocus_event(ds_window_t *wnd)
 {
+	errno_t rc;
+	ds_wmclient_t *wmclient;
+
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "ds_window_post_unfocus_event");
 
-	return ds_client_post_unfocus_event(wnd->client, wnd);
+	rc = ds_client_post_unfocus_event(wnd->client, wnd);
+	if (rc != EOK)
+		return rc;
+
+	/* Decrease focus counter */
+	--wnd->nfocus;
+
+	/* Notify window managers about window information change */
+	wmclient = ds_display_first_wmclient(wnd->display);
+	while (wmclient != NULL) {
+		ds_wmclient_post_wnd_changed_event(wmclient, wnd->id);
+		wmclient = ds_display_next_wmclient(wmclient);
+	}
+
+	return EOK;
 }
 
@@ -708,5 +774,5 @@
 void ds_window_get_max_rect(ds_window_t *wnd, gfx_rect_t *rect)
 {
-	*rect = wnd->display->rect;
+	*rect = wnd->display->max_rect;
 }
 
@@ -785,4 +851,41 @@
 	wnd->rect = *nrect;
 
+	if ((wnd->flags & wndf_avoid) != 0)
+		ds_display_update_max_rect(wnd->display);
+
+	(void) ds_display_paint(wnd->display, NULL);
+	return EOK;
+}
+
+/** Minimize window.
+ *
+ * @param wnd Window
+ * @return EOK on success or an error code
+ */
+errno_t ds_window_minimize(ds_window_t *wnd)
+{
+	/* If already minimized, do nothing and return success. */
+	if ((wnd->flags & wndf_minimized) != 0)
+		return EOK;
+
+	ds_window_unfocus(wnd);
+
+	wnd->flags |= wndf_minimized;
+	(void) ds_display_paint(wnd->display, NULL);
+	return EOK;
+}
+
+/** Unminimize window.
+ *
+ * @param wnd Window
+ * @return EOK on success or an error code
+ */
+errno_t ds_window_unminimize(ds_window_t *wnd)
+{
+	/* If not minimized, do nothing and return success. */
+	if ((wnd->flags & wndf_minimized) == 0)
+		return EOK;
+
+	wnd->flags &= ~wndf_minimized;
 	(void) ds_display_paint(wnd->display, NULL);
 	return EOK;
@@ -902,4 +1005,5 @@
  *
  * @param wnd Window
+ * @param cursor New cursor
  * @return EOK on success, EINVAL if @a cursor is invalid
  */
@@ -915,4 +1019,90 @@
 }
 
+/** Set window caption.
+ *
+ * @param wnd Window
+ * @param caption New caption
+ *
+ * @return EOK on success, EINVAL if @a cursor is invalid
+ */
+errno_t ds_window_set_caption(ds_window_t *wnd, const char *caption)
+{
+	char *dcaption;
+	ds_wmclient_t *wmclient;
+
+	dcaption = str_dup(caption);
+	if (dcaption == NULL)
+		return ENOMEM;
+
+	free(wnd->caption);
+	wnd->caption = dcaption;
+
+	/* Notify window managers about window information change */
+	wmclient = ds_display_first_wmclient(wnd->display);
+	while (wmclient != NULL) {
+		ds_wmclient_post_wnd_changed_event(wmclient, wnd->id);
+		wmclient = ds_display_next_wmclient(wmclient);
+	}
+
+	return EOK;
+}
+
+/** Find alternate window with the allowed flags.
+ *
+ * An alternate window is a *different* window that is preferably previous
+ * in the display order and only has the @a allowed flags.
+ *
+ * @param wnd Window
+ * @param allowed_flags Bitmask of flags that the window is allowed to have
+ *
+ * @return Alternate window matching the criteria or @c NULL if there is none
+ */
+ds_window_t *ds_window_find_alt(ds_window_t *wnd,
+    display_wnd_flags_t allowed_flags)
+{
+	ds_window_t *nwnd;
+
+	/* Try preceding windows in display order */
+	nwnd = ds_display_prev_window(wnd);
+	while (nwnd != NULL && (nwnd->flags & ~allowed_flags) != 0) {
+		nwnd = ds_display_prev_window(nwnd);
+	}
+
+	/* Do we already have a matching window? */
+	if (nwnd != NULL && (nwnd->flags & ~allowed_flags) == 0) {
+		return nwnd;
+	}
+
+	/* Try succeeding windows in display order */
+	nwnd = ds_display_last_window(wnd->display);
+	while (nwnd != NULL && nwnd != wnd &&
+	    (nwnd->flags & ~allowed_flags) != 0) {
+		nwnd = ds_display_prev_window(nwnd);
+	}
+
+	if (nwnd == wnd)
+		return NULL;
+
+	return nwnd;
+}
+
+/** Remove focus from window.
+ *
+ * Used to switch focus to another window when closing or minimizing window.
+ *
+ * @param wnd Window
+ */
+void ds_window_unfocus(ds_window_t *wnd)
+{
+	ds_seat_t *seat;
+
+	/* Make sure window is no longer focused in any seat */
+	seat = ds_display_first_seat(wnd->display);
+	while (seat != NULL) {
+		ds_seat_unfocus_wnd(seat, wnd);
+		seat = ds_display_next_seat(seat);
+	}
+}
+
 /** Window memory GC invalidate callback.
  *
Index: uspace/srv/hid/display/window.h
===================================================================
--- uspace/srv/hid/display/window.h	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/display/window.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2023 Jiri Svoboda
  * All rights reserved.
  *
@@ -41,4 +41,5 @@
 #include <errno.h>
 #include <io/pos_event.h>
+#include <stdbool.h>
 #include <types/gfx/context.h>
 #include <types/gfx/coord.h>
@@ -55,4 +56,5 @@
 extern void ds_window_bring_to_top(ds_window_t *);
 extern gfx_context_t *ds_window_get_ctx(ds_window_t *);
+extern bool ds_window_is_visible(ds_window_t *);
 extern errno_t ds_window_paint(ds_window_t *, gfx_rect_t *);
 errno_t ds_window_paint_preview(ds_window_t *, gfx_rect_t *);
@@ -68,4 +70,6 @@
     gfx_coord2_t *);
 extern errno_t ds_window_resize(ds_window_t *, gfx_coord2_t *, gfx_rect_t *);
+extern errno_t ds_window_minimize(ds_window_t *);
+extern errno_t ds_window_unminimize(ds_window_t *);
 extern errno_t ds_window_maximize(ds_window_t *);
 extern errno_t ds_window_unmaximize(ds_window_t *);
@@ -73,4 +77,7 @@
     gfx_rect_t *);
 extern errno_t ds_window_set_cursor(ds_window_t *, display_stock_cursor_t);
+extern errno_t ds_window_set_caption(ds_window_t *, const char *);
+extern ds_window_t *ds_window_find_alt(ds_window_t *, display_wnd_flags_t);
+extern void ds_window_unfocus(ds_window_t *);
 
 #endif
Index: uspace/srv/hid/display/wmclient.c
===================================================================
--- uspace/srv/hid/display/wmclient.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/wmclient.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2022 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 display
+ * @{
+ */
+/**
+ * @file Display server WM client
+ */
+
+#include <adt/list.h>
+#include <errno.h>
+#include <stdlib.h>
+#include "display.h"
+#include "window.h"
+#include "wmclient.h"
+
+/** Create WM client.
+ *
+ * @param display Parent display
+ * @param cb WM client callbacks
+ * @param cb_arg Callback argument
+ * @param rwmclient Place to store pointer to new WM client.
+ * @return EOK on success, ENOMEM if out of memory
+ */
+errno_t ds_wmclient_create(ds_display_t *display, ds_wmclient_cb_t *cb,
+    void *cb_arg, ds_wmclient_t **rwmclient)
+{
+	ds_wmclient_t *wmclient;
+
+	wmclient = calloc(1, sizeof(ds_wmclient_t));
+	if (wmclient == NULL)
+		return ENOMEM;
+
+	list_initialize(&wmclient->events);
+	wmclient->cb = cb;
+	wmclient->cb_arg = cb_arg;
+
+	ds_display_add_wmclient(display, wmclient);
+
+	*rwmclient = wmclient;
+	return EOK;
+}
+
+/** Destroy WM client.
+ *
+ * @param wmclient WM client
+ */
+void ds_wmclient_destroy(ds_wmclient_t *wmclient)
+{
+	ds_wmclient_purge_events(wmclient);
+	ds_display_remove_wmclient(wmclient);
+	free(wmclient);
+}
+
+/** Get next event from WM client event queue.
+ *
+ * @param wmclient WM client
+ * @param event Place to store event
+ * @return EOK on success, ENOENT if event queue is empty
+ */
+errno_t ds_wmclient_get_event(ds_wmclient_t *wmclient, wndmgt_ev_t *event)
+{
+	link_t *link;
+	ds_wmclient_ev_t *wevent;
+
+	link = list_first(&wmclient->events);
+	if (link == NULL)
+		return ENOENT;
+
+	wevent = list_get_instance(link, ds_wmclient_ev_t, levents);
+	list_remove(link);
+
+	*event = wevent->event;
+	free(wevent);
+	return EOK;
+}
+
+/** Purge events from WM client event queue.
+ *
+ * @param client Client
+ */
+void ds_wmclient_purge_events(ds_wmclient_t *wmclient)
+{
+	link_t *cur;
+	link_t *next;
+	ds_wmclient_ev_t *wevent;
+
+	cur = list_first(&wmclient->events);
+	while (cur != NULL) {
+		next = list_next(cur, &wmclient->events);
+		wevent = list_get_instance(cur, ds_wmclient_ev_t, levents);
+
+		list_remove(cur);
+		free(wevent);
+		cur = next;
+	}
+}
+#include <io/log.h>
+/** Post window added event to the WM client's message queue.
+ *
+ * @param wmclient WM client
+ * @param wnd_id Window ID of the added window
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ds_wmclient_post_wnd_added_event(ds_wmclient_t *wmclient,
+    sysarg_t wnd_id)
+{
+	ds_wmclient_ev_t *wevent;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "wmclient_pos_wnd_added_event "
+	    "wmclient=%p wnd_id=%zu\n", (void *)wmclient, wnd_id);
+
+	wevent = calloc(1, sizeof(ds_wmclient_ev_t));
+	if (wevent == NULL)
+		return ENOMEM;
+
+	wevent->wmclient = wmclient;
+	wevent->event.etype = wmev_window_added;
+	wevent->event.wnd_id = wnd_id;
+	list_append(&wevent->levents, &wmclient->events);
+
+	/* Notify the client */
+	// TODO Do not send more than once until client drains the queue
+	if (wmclient->cb != NULL && wmclient->cb->ev_pending != NULL)
+		wmclient->cb->ev_pending(wmclient->cb_arg);
+
+	return EOK;
+}
+
+/** Post window removed event to the WM client's message queue.
+ *
+ * @param wmclient WM client
+ * @param wnd_id Window ID of the added window
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ds_wmclient_post_wnd_removed_event(ds_wmclient_t *wmclient,
+    sysarg_t wnd_id)
+{
+	ds_wmclient_ev_t *wevent;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "wmclient_pos_wnd_removed_event "
+	    "wmclient=%p wnd_id=%zu\n", (void *)wmclient, wnd_id);
+
+	wevent = calloc(1, sizeof(ds_wmclient_ev_t));
+	if (wevent == NULL)
+		return ENOMEM;
+
+	wevent->wmclient = wmclient;
+	wevent->event.etype = wmev_window_removed;
+	wevent->event.wnd_id = wnd_id;
+	list_append(&wevent->levents, &wmclient->events);
+
+	/* Notify the client */
+	// TODO Do not send more than once until client drains the queue
+	if (wmclient->cb != NULL && wmclient->cb->ev_pending != NULL)
+		wmclient->cb->ev_pending(wmclient->cb_arg);
+
+	return EOK;
+}
+
+/** Post window changed event to the WM client's message queue.
+ *
+ * @param wmclient WM client
+ * @param wnd_id Window ID of the added window
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ds_wmclient_post_wnd_changed_event(ds_wmclient_t *wmclient,
+    sysarg_t wnd_id)
+{
+	ds_wmclient_ev_t *wevent;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "wmclient_pos_wnd_changed_event "
+	    "wmclient=%p wnd_id=%zu\n", (void *)wmclient, wnd_id);
+
+	wevent = calloc(1, sizeof(ds_wmclient_ev_t));
+	if (wevent == NULL)
+		return ENOMEM;
+
+	wevent->wmclient = wmclient;
+	wevent->event.etype = wmev_window_changed;
+	wevent->event.wnd_id = wnd_id;
+	list_append(&wevent->levents, &wmclient->events);
+
+	/* Notify the client */
+	// TODO Do not send more than once until client drains the queue
+	if (wmclient->cb != NULL && wmclient->cb->ev_pending != NULL)
+		wmclient->cb->ev_pending(wmclient->cb_arg);
+
+	return EOK;
+}
+
+/** @}
+ */
Index: uspace/srv/hid/display/wmclient.h
===================================================================
--- uspace/srv/hid/display/wmclient.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/wmclient.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2022 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 display
+ * @{
+ */
+/**
+ * @file Display server WM client
+ */
+
+#ifndef WMCLIENT_H
+#define WMCLIENT_H
+
+#include <errno.h>
+#include <wndmgt.h>
+#include "types/display/display.h"
+#include "types/display/wmclient.h"
+
+extern errno_t ds_wmclient_create(ds_display_t *, ds_wmclient_cb_t *, void *,
+    ds_wmclient_t **);
+extern void ds_wmclient_destroy(ds_wmclient_t *);
+extern errno_t ds_wmclient_get_event(ds_wmclient_t *,  wndmgt_ev_t *);
+extern errno_t ds_wmclient_post_wnd_added_event(ds_wmclient_t *, sysarg_t);
+extern errno_t ds_wmclient_post_wnd_removed_event(ds_wmclient_t *, sysarg_t);
+extern errno_t ds_wmclient_post_wnd_changed_event(ds_wmclient_t *, sysarg_t);
+extern void ds_wmclient_purge_events(ds_wmclient_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/display/wmops.c
===================================================================
--- uspace/srv/hid/display/wmops.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/wmops.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,225 @@
+/*
+ * 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 display
+ * @{
+ */
+/**
+ * @file Window management ops implementation
+ */
+
+#include <errno.h>
+#include <io/log.h>
+#include <stdlib.h>
+#include <str.h>
+#include <wndmgt_srv.h>
+#include "display.h"
+#include "seat.h"
+#include "window.h"
+#include "wmclient.h"
+
+static errno_t dispwm_get_window_list(void *, wndmgt_window_list_t **);
+static errno_t dispwm_get_window_info(void *, sysarg_t, wndmgt_window_info_t **);
+static errno_t dispwm_activate_window(void *, sysarg_t, sysarg_t);
+static errno_t dispwm_close_window(void *, sysarg_t);
+static errno_t dispwm_get_event(void *, wndmgt_ev_t *);
+
+wndmgt_ops_t wndmgt_srv_ops = {
+	.get_window_list = dispwm_get_window_list,
+	.get_window_info = dispwm_get_window_info,
+	.activate_window = dispwm_activate_window,
+	.close_window = dispwm_close_window,
+	.get_event = dispwm_get_event,
+};
+
+/** Get window list.
+ *
+ * @param arg Argument (WM client)
+ * @param rlist Place to store pointer to new list
+ * @return EOK on success or an error code
+ */
+static errno_t dispwm_get_window_list(void *arg, wndmgt_window_list_t **rlist)
+{
+	ds_wmclient_t *wmclient = (ds_wmclient_t *)arg;
+	wndmgt_window_list_t *list;
+	ds_window_t *wnd;
+	unsigned i;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "dispwm_get_window_list()");
+
+	list = calloc(1, sizeof(wndmgt_window_list_t));
+	if (list == NULL)
+		return ENOMEM;
+
+	ds_display_lock(wmclient->display);
+
+	/* Count the number of windows */
+	list->nwindows = 0;
+	wnd = ds_display_first_window(wmclient->display);
+	while (wnd != NULL) {
+		++list->nwindows;
+		wnd = ds_display_next_window(wnd);
+	}
+
+	/* Allocate array for window IDs */
+	list->windows = calloc(list->nwindows, sizeof(sysarg_t));
+	if (list->windows == NULL) {
+		ds_display_unlock(wmclient->display);
+		free(list);
+		return ENOMEM;
+	}
+
+	/* Fill in window IDs */
+	i = 0;
+	wnd = ds_display_first_window(wmclient->display);
+	while (wnd != NULL) {
+		list->windows[i++] = wnd->id;
+		wnd = ds_display_next_window(wnd);
+	}
+
+	ds_display_unlock(wmclient->display);
+	*rlist = list;
+	return EOK;
+}
+
+/** Get window information.
+ *
+ * @param arg Argument (WM client)
+ * @param wnd_id Window ID
+ * @param rinfo Place to store pointer to new window information structure
+ * @return EOK on success or an error code
+ */
+static errno_t dispwm_get_window_info(void *arg, sysarg_t wnd_id,
+    wndmgt_window_info_t **rinfo)
+{
+	ds_wmclient_t *wmclient = (ds_wmclient_t *)arg;
+	ds_window_t *wnd;
+	wndmgt_window_info_t *info;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "dispwm_get_window_info()");
+
+	ds_display_lock(wmclient->display);
+	wnd = ds_display_find_window(wmclient->display, wnd_id);
+	if (wnd == NULL) {
+		ds_display_unlock(wmclient->display);
+		return ENOENT;
+	}
+
+	info = calloc(1, sizeof(wndmgt_window_info_t));
+	if (info == NULL) {
+		ds_display_unlock(wmclient->display);
+		return ENOMEM;
+	}
+
+	info->caption = str_dup(wnd->caption);
+	if (info->caption == NULL) {
+		ds_display_unlock(wmclient->display);
+		free(info);
+		return ENOMEM;
+	}
+
+	info->flags = wnd->flags;
+	info->nfocus = wnd->nfocus;
+
+	ds_display_unlock(wmclient->display);
+	*rinfo = info;
+	return EOK;
+}
+
+/** Activate window.
+ *
+ * @param arg Argument (WM client)
+ * @param dev_id Input device ID
+ * @param wnd_id Window ID
+ * @return EOK on success or an error code
+ */
+static errno_t dispwm_activate_window(void *arg, sysarg_t dev_id,
+    sysarg_t wnd_id)
+{
+	ds_wmclient_t *wmclient = (ds_wmclient_t *)arg;
+	ds_window_t *wnd;
+	ds_seat_t *seat;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "dispwm_activate_window()");
+
+	ds_display_lock(wmclient->display);
+	wnd = ds_display_find_window(wmclient->display, wnd_id);
+	if (wnd == NULL) {
+		ds_display_unlock(wmclient->display);
+		return ENOENT;
+	}
+
+	/* Determine which seat's focus should be changed */
+	seat = ds_display_seat_by_idev(wnd->display, dev_id);
+	if (seat == NULL) {
+		ds_display_unlock(wmclient->display);
+		return ENOENT;
+	}
+
+	/* Switch focus */
+	ds_seat_set_focus(seat, wnd);
+
+	ds_display_unlock(wmclient->display);
+	return EOK;
+}
+
+/** Close window.
+ *
+ * @param arg Argument (WM client)
+ * @param wnd_id Window ID
+ * @return EOK on success or an error code
+ */
+static errno_t dispwm_close_window(void *arg, sysarg_t wnd_id)
+{
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "dispwm_close_window()");
+	(void)arg;
+	(void)wnd_id;
+	return EOK;
+}
+
+/** Get window management event.
+ *
+ * @param arg Argument (WM client)
+ * @param ev Place to store event
+ * @return EOK on success, ENOENT if there are no events
+ */
+static errno_t dispwm_get_event(void *arg, wndmgt_ev_t *ev)
+{
+	ds_wmclient_t *wmclient = (ds_wmclient_t *)arg;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "dispwm_get_event()");
+
+	ds_display_lock(wmclient->display);
+	rc = ds_wmclient_get_event(wmclient, ev);
+	ds_display_unlock(wmclient->display);
+	return rc;
+}
+
+/** @}
+ */
Index: uspace/srv/hid/display/wmops.h
===================================================================
--- uspace/srv/hid/display/wmops.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
+++ uspace/srv/hid/display/wmops.h	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022 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 display
+ * @{
+ */
+/**
+ * @file Window management ops implementation
+ */
+
+#ifndef WMOPS_H
+#define WMOPS_H
+
+#include <wndmgt_srv.h>
+
+extern wndmgt_ops_t wndmgt_srv_ops;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/input/input.c
===================================================================
--- uspace/srv/hid/input/input.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/hid/input/input.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2022 Jiri Svoboda
  * Copyright (c) 2006 Josef Cejka
  * All rights reserved.
@@ -253,5 +253,6 @@
 		if (client->active) {
 			async_exch_t *exch = async_exchange_begin(client->sess);
-			async_msg_4(exch, INPUT_EVENT_KEY, ev.type, ev.key, ev.mods, ev.c);
+			async_msg_5(exch, INPUT_EVENT_KEY, kdev->svc_id,
+			    ev.type, ev.key, ev.mods, ev.c);
 			async_exchange_end(exch);
 		}
@@ -267,5 +268,6 @@
 
 			if ((dx) || (dy))
-				async_msg_2(exch, INPUT_EVENT_MOVE, dx, dy);
+				async_msg_3(exch, INPUT_EVENT_MOVE,
+				    mdev->svc_id, dx, dy);
 
 			if (dz) {
@@ -274,7 +276,10 @@
 
 				for (unsigned int i = 0; i < 3; i++)
-					async_msg_4(exch, INPUT_EVENT_KEY, KEY_PRESS, code, 0, 0);
-
-				async_msg_4(exch, INPUT_EVENT_KEY, KEY_RELEASE, code, 0, 0);
+					async_msg_5(exch, INPUT_EVENT_KEY,
+					    0 /* XXX kbd_id */,
+					    KEY_PRESS, code, 0, 0);
+
+				async_msg_5(exch, INPUT_EVENT_KEY, KEY_RELEASE,
+				    0 /* XXX kbd_id */, code, 0, 0);
 			}
 
@@ -292,5 +297,6 @@
 			if ((max_x) && (max_y)) {
 				async_exch_t *exch = async_exchange_begin(client->sess);
-				async_msg_4(exch, INPUT_EVENT_ABS_MOVE, x, y, max_x, max_y);
+				async_msg_5(exch, INPUT_EVENT_ABS_MOVE,
+				    mdev->svc_id, x, y, max_x, max_y);
 				async_exchange_end(exch);
 			}
@@ -305,5 +311,6 @@
 		if (client->active) {
 			async_exch_t *exch = async_exchange_begin(client->sess);
-			async_msg_2(exch, INPUT_EVENT_BUTTON, bnum, press);
+			async_msg_3(exch, INPUT_EVENT_BUTTON, mdev->svc_id,
+			    bnum, press);
 			async_exchange_end(exch);
 		}
@@ -317,5 +324,6 @@
 		if (client->active) {
 			async_exch_t *exch = async_exchange_begin(client->sess);
-			async_msg_1(exch, INPUT_EVENT_DCLICK, bnum);
+			async_msg_2(exch, INPUT_EVENT_DCLICK, mdev->svc_id,
+			    bnum);
 			async_exchange_end(exch);
 		}
Index: uspace/srv/locsrv/locsrv.c
===================================================================
--- uspace/srv/locsrv/locsrv.c	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ uspace/srv/locsrv/locsrv.c	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -1378,7 +1378,4 @@
 	categ_dir_add_cat(&cdir, cat);
 
-	cat = category_new("renderer");
-	categ_dir_add_cat(&cdir, cat);
-
 	cat = category_new("display-device");
 	categ_dir_add_cat(&cdir, cat);
Index: version
===================================================================
--- version	(revision eda4323867bf5a8e98aa598c3cca1e3fb59f4e7a)
+++ version	(revision e04b72d69126c4074644c19c920bf771d9c72a1e)
@@ -28,5 +28,5 @@
 
 
-HELENOS_RELEASE = 0.11.2
-HELENOS_CODENAME = Kiev
-HELENOS_COPYRIGHT = Copyright (c) 2001-2021 HelenOS project
+HELENOS_RELEASE = 0.12.1
+HELENOS_CODENAME = Cathode
+HELENOS_COPYRIGHT = Copyright (c) 2001-2022 HelenOS project
