public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/kmscon] rawhide: Add localectl runtime support
@ 2026-06-05 11:19 Jocelyn Falempe
  0 siblings, 0 replies; only message in thread
From: Jocelyn Falempe @ 2026-06-05 11:19 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/kmscon
            Branch : rawhide
            Commit : acf9652b0f61ec90ae0042e92b6282c65f89d0d0
            Author : Jocelyn Falempe <jfalempe@redhat.com>
            Date   : 2026-06-05T13:17:24+02:00
            Stats  : +2810/-3 in 2 file(s)
            URL    : https://src.fedoraproject.org/rpms/kmscon/c/acf9652b0f61ec90ae0042e92b6282c65f89d0d0?branch=rawhide

            Log:
            Add localectl runtime support

This allows to change the keyboard layout at runtime using localectl.
OpenQA scripts use loadkeys on fbcon VT, and need a similar
functionality on kmscon

Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>

---
diff --git a/kmscon.spec b/kmscon.spec
index 94e94f5..7875ca9 100644
--- a/kmscon.spec
+++ b/kmscon.spec
@@ -21,7 +21,7 @@ BuildRequires:  pkgconfig(freetype2)
 BuildRequires:  pkgconfig(gbm)
 BuildRequires:  pkgconfig(glesv2)
 BuildRequires:  pkgconfig(libdrm)
-BuildRequires:  pkgconfig(libsystemd)
+BuildRequires:  pkgconfig(dbus-1)
 BuildRequires:  pkgconfig(libudev) >= 172
 BuildRequires:  pkgconfig(pango)
 BuildRequires:  pkgconfig(pangoft2)
@@ -29,6 +29,8 @@ BuildRequires:  pkgconfig(systemd)
 BuildRequires:  pkgconfig(xkbcommon) >= 0.5.0
 BuildRequires:  pkgconfig(zlib)
 
+Patch: localectl_support.patch
+
 %description
 Kmscon is a simple terminal emulator based on linux kernel mode setting (KMS).
 It is an attempt to replace the in-kernel VT implementation with a userspace
@@ -91,8 +93,6 @@ mod-gltex.so
 %{_bindir}/%{name}
 %{_bindir}/kmscon-launch-gui
 %{_libdir}/kmscon/mod-unifont.so
-%dir %{_libexecdir}/kmscon
-%{_libexecdir}/kmscon/kmscon
 %{_mandir}/man1/kmscon.1*
 %{_mandir}/man5/kmscon.conf.5*
 %{_unitdir}/kmscon.service

diff --git a/localectl_support.patch b/localectl_support.patch
new file mode 100644
index 0000000..3fd517d
--- /dev/null
+++ b/localectl_support.patch
@@ -0,0 +1,2807 @@
+From 519a57b8546977d0378ac491065797d3040f914a Mon Sep 17 00:00:00 2001
+From: Jocelyn Falempe <jfalempe@redhat.com>
+Date: Fri, 5 Jun 2026 12:08:56 +0200
+Subject: [PATCH] Squashed commit of the following:
+
+commit 21a7f41266a881169609452d3d3d7623c10404ae
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Fri Jun 5 11:11:42 2026 +0200
+
+    dbus: Listen to locale1 property changes
+
+    And update the keyboard layout accordingly.
+
+    Don't listen to locale1 property changes if the keyboard layout was
+    set in the config file or in command line argument.
+
+    This allows to use `localectl set-keymap` to change the keyboard
+    layout at runtime.
+    One difference from "loadkeys" is that this is a system-wide setting,
+    and that it will affect all kmscon instance on all tty.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 99b5e7e840035f9f6a437cb4d1c506be085cbd92
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Fri Jun 5 10:48:21 2026 +0200
+
+    conf: Use NULL for default xkb config variable
+
+    This better fit libxkbcommon API
+    Remove the workaround in input, as it's no longer necessary.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 3010008c5d810a794b47e38befbd780f406c4904
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Fri Jun 5 10:22:01 2026 +0200
+
+    input: Add input_update_keymap() to change the keymap at runtime
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit d716d72cae6331e681a0f0a37dfa4ad2cc49366d
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Fri Jun 5 10:16:09 2026 +0200
+
+    input: split keyboard layout and compose initialization
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 0dffbd7a27db7f23806cfab0ecd5369e5f7b15ca
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Thu Jun 4 00:38:20 2026 +0200
+
+    session: Simplify session management
+
+    Directly switch from one session to another.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit fa42d6d0adf806a05349efedceaa801df77aca6d
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Wed Jun 3 17:37:47 2026 +0200
+
+    terminal: pass directly the seat input, conf, name and eloop
+
+    This avoid to use kmscon_seat_get_xxx() for each.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 21011b1561c9199fdae2a1566d27e43b67bcc0b6
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Wed Jun 3 16:24:24 2026 +0200
+
+    session: Remove session callback
+
+    As terminal sessions are the only session available, remove the
+    callbacks, and call directly the corresponding terminal functions.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 12c1072d254e6b04819850630def9028e0cf8d19
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Wed Jun 3 12:26:13 2026 +0200
+
+    dummy_session: Remove dummy sessions
+
+    Kmscon wanted to provide different type of session, but only the
+    terminal session is used.
+    Dummy session is just a black screen, so has no real usage.
+
+    Let's focus on terminal sessions, and remove the dummy session.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit a6e78153a5690659d99e587fe770eca4b56d09ad
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Mon Jun 1 23:41:32 2026 +0200
+
+    Remove startup script, as it is no more necessary
+
+    kmscon now retrieve xkb keyboard configuration directly form dbus,
+    so there is no need for the startup script.
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 06bb7413292d7c556ffc817bc3075c3c3c96fb0c
+Author: Jocelyn Falempe <jfalempe@redhat.com>
+Date:   Thu May 21 18:09:51 2026 +0200
+
+    Use libdbus directly to get locale
+
+    Signed-off-by: Jocelyn Falempe <jfalempe@redhat.com>
+
+commit 84501684db7dda606209c4c7ae04f7e66d964809
+Author: Fabian Vogt <fvogt@suse.de>
+Date:   Tue Jun 2 12:31:05 2026 +0200
+
+    freetype: Enable light hinting
+
+    Not hinting at all results in blurry edges, especially visible with straight
+    lines in common characters such as "-[]".
+
+    Light hinting in FreeType has the nice property that it only tries to
+    align edges vertically, so the width of glyphs does not change, preserving
+    monospaceness.
+
+commit d789d1c3517d997f1271a38b4594c29898eaf199
+Author: Fabian Vogt <fvogt@suse.de>
+Date:   Mon Jun 1 17:03:47 2026 +0200
+
+    Define default issue search path in a single place
+
+    It was hardcoded in four different places, of which only one has an actual
+    effect. Consolidate it.
+
+commit 44c0df12ff202eb84394ea0c8e059894563fe501
+Author: Adam Duskett <adam.duskett@amarulasolutions.com>
+Date:   Thu May 21 16:24:23 2026 +0200
+
+    fix zlib cross-compiling errors
+
+    zlib compression was introduced in version 9.3.3. However, when
+    cross-compiling kmscon for an architecture other than x86_64, the
+    genunifont executable tries to use the build-systems zlib instead
+    of the hosts zlib.
+
+    This leads to the following error during compiling:
+    libz.so: error adding symbols: file in wrong format
+
+    Fix this by adding a new native zlib dependency specifically for the
+    genunifont executable.
+---
+ README.md                       |   2 -
+ docs/man/kmscon.1.xml.in        |  29 +-
+ docs/man/kmscon.conf.5.xml.in   |  14 -
+ meson.build                     |  10 +-
+ meson.options                   |   8 +-
+ scripts/etc/kmscon.conf.example |   1 -
+ scripts/kmscon.in               |  63 ----
+ src/dbus.c                      | 368 +++++++++++++++++++
+ src/{kmscon_dummy.h => dbus.h}  |  53 +--
+ src/font/font_freetype.c        |   4 +-
+ src/font/meson.build            |   4 +-
+ src/input/input.c               |  60 +++-
+ src/input/input.h               |   7 +-
+ src/input/input_internal.h      |  10 +-
+ src/input/input_uxkb.c          |  33 +-
+ src/kmscon_conf.c               |  26 +-
+ src/kmscon_conf.h               |   4 -
+ src/kmscon_dummy.c              | 154 --------
+ src/kmscon_issue.c              |  10 -
+ src/kmscon_issue.h              |   9 +
+ src/kmscon_main.c               |  33 ++
+ src/kmscon_seat.c               | 602 +++++++-------------------------
+ src/kmscon_seat.h               |  41 +--
+ src/kmscon_terminal.c           |  98 ++----
+ src/kmscon_terminal.h           |  34 +-
+ src/meson.build                 |  25 +-
+ src/shl/dlist.h                 |  10 +
+ tests/test_input.c              |   7 +-
+ 28 files changed, 751 insertions(+), 968 deletions(-)
+ delete mode 100755 scripts/kmscon.in
+ create mode 100644 src/dbus.c
+ rename src/{kmscon_dummy.h => dbus.h} (50%)
+ delete mode 100644 src/kmscon_dummy.c
+
+diff --git a/README.md b/README.md
+index 8675413..3bf4912 100644
+--- a/README.md
++++ b/README.md
+@@ -82,8 +82,6 @@ explicitly enable it via command line:
+ |`font_freetype`| `auto` | Freetype2 based scalable font renderer, also handle bitmap fonts |
+ |`font_pango`| `auto` | Pango based scalable font renderer |
+ |`renderer_gltex`| `auto` | OpenGLESv2 accelerated renderer |
+-|`session_dummy`| `auto` | Dummy fallback session |
+-|`session_terminal`| `auto` | Terminal-emulator sessions |
+ |`docs`|`auto`| Build manpages and documentation |
+ 
+ 
+diff --git a/docs/man/kmscon.1.xml.in b/docs/man/kmscon.1.xml.in
+index 0538186..25cf29a 100644
+--- a/docs/man/kmscon.1.xml.in
++++ b/docs/man/kmscon.1.xml.in
+@@ -187,14 +187,6 @@
+                 sessions via keyboard input. (default: off)</para>
+         </listitem>
+       </varlistentry>
+-
+-      <varlistentry>
+-        <term><option>--terminal-session</option></term>
+-        <listitem>
+-          <para>Start a terminal session after setup is done.
+-                (default: on)</para>
+-        </listitem>
+-      </varlistentry>
+     </variablelist>
+ 
+     <para>Terminal Options:</para>
+@@ -463,14 +455,6 @@
+         </listitem>
+       </varlistentry>
+ 
+-      <varlistentry>
+-        <term><option>--grab-session-dummy {grab}</option></term>
+-        <listitem>
+-          <para>Switch to a dummy session (blank screen).
+-                (default: &lt;Ctrl&gt;&lt;Logo&gt;Escape)</para>
+-        </listitem>
+-      </varlistentry>
+-
+       <varlistentry>
+         <term><option>--grab-session-close {grab}</option></term>
+         <listitem>
+@@ -886,16 +870,11 @@
+   <refsect1>
+     <title>Sessions</title>
+     <para>If KMSCON is active on a seat, the internal session-manager is woken
+-          up. At most times, only the terminal-session is active, but KMSCON can
+-          also support any other session type. You can switch between sessions
+-          with keyboard-shortcuts and you can create/destroy sessions during
+-          runtime.</para>
+-
+-    <para>Dummy sessions simply show a black screen and are used if no other
+-          session is available. Otherwise, dummy sessions are hidden from the
+-          user so they cannot switch to it.</para>
++          up. At most times, only the terminal-session is active. You can switch
++          between sessions with keyboard-shortcuts and you can create/destroy
++          terminal sessions during runtime.</para>
+ 
+-    <para>Terminal sessions provide a terminal emulator. They are the main
++    <para>Terminal sessions provide a terminal emulator. They are the only
+           session type and provide all the terminal-emulation
+           functionality.</para>
+   </refsect1>
+diff --git a/docs/man/kmscon.conf.5.xml.in b/docs/man/kmscon.conf.5.xml.in
+index dca056d..8af4580 100644
+--- a/docs/man/kmscon.conf.5.xml.in
++++ b/docs/man/kmscon.conf.5.xml.in
+@@ -161,13 +161,6 @@ font-name=Ubuntu Mono
+         </listitem>
+       </varlistentry>
+ 
+-      <varlistentry>
+-        <term><option>terminal-session</option></term>
+-        <listitem>
+-          <para>Enable terminal session. (default: on)</para>
+-        </listitem>
+-      </varlistentry>
+-
+       <varlistentry>
+         <term><option>login</option></term>
+         <listitem>
+@@ -368,13 +361,6 @@ font-name=Ubuntu Mono
+         </listitem>
+       </varlistentry>
+ 
+-      <varlistentry>
+-        <term><option>grab-session-dummy</option></term>
+-        <listitem>
+-          <para>Switch to dummy session. (default: &lt;Ctrl&gt;&lt;Logo&gt;Escape)</para>
+-        </listitem>
+-      </varlistentry>
+-
+       <varlistentry>
+         <term><option>grab-session-close</option></term>
+         <listitem>
+diff --git a/meson.build b/meson.build
+index de89d72..1aa72f2 100644
+--- a/meson.build
++++ b/meson.build
+@@ -53,6 +53,7 @@ libudev_deps = dependency('libudev', version: '>=172')
+ dl_deps = dependency('dl')
+ threads_deps = dependency('threads')
+ zlib_deps = dependency('zlib')
++zlib_deps_native = dependency('zlib', native: true)
+ 
+ python = find_program('python3')
+ 
+@@ -75,6 +76,7 @@ glesv2_deps = dependency('glesv2', disabler: true, required: require_glesv2)
+ pango_deps = dependency('pangoft2', disabler: true, required: get_option('font_pango'))
+ freetype_deps = dependency('freetype2', disabler: true, required: get_option('font_freetype'))
+ fontconfig_deps = dependency('fontconfig', disabler: true, required: get_option('font_freetype'))
++dbus_deps = dependency('dbus-1', disabler: true, required: get_option('dbus'))
+ xsltproc = find_program('xsltproc', native: true, disabler: true, required: get_option('docs'))
+ check_deps = dependency('check', disabler: true, required: get_option('tests'))
+ tic = find_program('tic', required: false)
+@@ -89,7 +91,6 @@ sections = {
+   'video': 'Video Backends',
+   'font': 'Font Backends',
+   'renderer': 'Renderers',
+-  'session': 'Session Types',
+ }
+ config = configuration_data()
+ # Note: keep this in sync with the dependencies above
+@@ -102,8 +103,7 @@ foreach name, reqs : {
+   'font_unifont': [],
+   'font_freetype': [freetype_deps, fontconfig_deps],
+   'font_pango': [pango_deps],
+-  'session_dummy': [],
+-  'session_terminal': [],
++  'dbus': [dbus_deps],
+ }
+   found = true
+   foreach req : reqs
+@@ -195,10 +195,6 @@ else
+   dirs_info.set('PAM_CFG', '')
+ endif
+ foreach filename, kwargs : {
+-  'scripts/kmscon.in': {
+-    'install_dir': bindir,
+-    'install_mode': 'rwxr-xr-x',
+-  },
+   'scripts/kmscon-launch-gui.sh': {
+     'install_dir': bindir,
+     'install_mode': 'rwxr-xr-x',
+diff --git a/meson.options b/meson.options
+index 5cafa60..afa5b32 100644
+--- a/meson.options
++++ b/meson.options
+@@ -29,8 +29,6 @@ option('font_freetype', type: 'feature', value: 'auto',
+ option('font_pango', type: 'feature', value: 'auto',
+   description: 'pango font backend')
+ 
+-# kmscon sessions
+-option('session_dummy', type: 'feature', value: 'auto',
+-  description: 'dummy session')
+-option('session_terminal', type: 'feature', value: 'auto',
+-  description: 'terminal session')
++# dbus integration
++option('dbus', type: 'feature', value: 'auto',
++  description: 'dbus integration')
+diff --git a/scripts/etc/kmscon.conf.example b/scripts/etc/kmscon.conf.example
+index ca2232e..be45a4a 100644
+--- a/scripts/etc/kmscon.conf.example
++++ b/scripts/etc/kmscon.conf.example
+@@ -66,7 +66,6 @@
+ #grab-zoom-out=<Ctrl>Minus
+ #grab-session-next=<Ctrl><Logo>Right
+ #grab-session-prev=<Ctrl><Logo>Left
+-#grab-session-dummy=<Ctrl><Logo>Escape
+ #grab-session-close=<Ctrl><Logo>BackSpace
+ #grab-terminal-new=<Ctrl><Logo>Return
+ #grab-rotate-cw=<Logo>Plus
+diff --git a/scripts/kmscon.in b/scripts/kmscon.in
+deleted file mode 100755
+index 6545399..0000000
+--- a/scripts/kmscon.in
++++ /dev/null
+@@ -1,63 +0,0 @@
+-#!/bin/sh
+-#
+-# Copyright (c) 2018 Aetf <aetf@unlimitedcodeworks.xyz>
+-# Copyright (c) 2018 Fabian Vogt <fvogt@suse.com>
+-#
+-# Permission is hereby granted, free of charge, to any person obtaining a copy
+-# of this software and associated documentation files (the "Software"), to deal
+-# in the Software without restriction, including without limitation the rights
+-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-# copies of the Software, and to permit persons to whom the Software is
+-# furnished to do so, subject to the following conditions:
+-#
+-# The above copyright notice and this permission notice shall be included in all
+-# copies or substantial portions of the Software.
+-#
+-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-# SOFTWARE.
+-
+-# configuration path
+-helperdir=@libexecdir@
+-
+-# Get a property from org.freedesktop.locale1
+-queryLocaledbus() {
+-    dbus-send --system --print-reply=literal --dest=org.freedesktop.locale1 /org/freedesktop/locale1 org.freedesktop.DBus.Properties.Get "string:org.freedesktop.locale1" "string:X11$1" 2>/dev/null | awk '{print $2}'
+-}
+-
+-queryLocalectl() {
+-    localectl status | awk "/X11 $1/ {print \$3}"
+-}
+-
+-# Query and setup system locale settings before start kmscon
+-setupLocale() {
+-    # Don't override existing values. Also there is no point in setting only some of them
+-    # as then they would not match anymore.
+-    if test -n "${XKB_DEFAULT_MODEL}" -o -n "${XKB_DEFAULT_LAYOUT}" -o -n "${XKB_DEFAULT_VARIANT}" -o -n "${XKB_DEFAULT_OPTIONS}"; then
+-        return
+-    fi
+-
+-    if command -v dbus-send >/dev/null 2>/dev/null; then
+-        querycmd="queryLocaledbus"
+-    elif command -v localectl >/dev/null 2>/dev/null; then
+-        querycmd="queryLocalectl"
+-    else
+-        return
+-    fi
+-
+-    X11MODEL="$(${querycmd} Model)"
+-    X11LAYOUT="$(${querycmd} Layout)"
+-    X11VARIANT="$(${querycmd} Variant)"
+-    X11OPTIONS="$(${querycmd} Options)"
+-    [ -n "${X11MODEL}" ] && export XKB_DEFAULT_MODEL="${X11MODEL}"
+-    [ -n "${X11LAYOUT}" ] && export XKB_DEFAULT_LAYOUT="${X11LAYOUT}"
+-    [ -n "${X11VARIANT}" ] && export XKB_DEFAULT_VARIANT="${X11VARIANT}"
+-    [ -n "${X11OPTIONS}" ] && export XKB_DEFAULT_OPTIONS="${X11OPTIONS}"
+-}
+-
+-setupLocale
+-exec ${helperdir}/kmscon "$@"
+diff --git a/src/dbus.c b/src/dbus.c
+new file mode 100644
+index 0000000..84c467d
+--- /dev/null
++++ b/src/dbus.c
+@@ -0,0 +1,368 @@
++/*
++ * dbus - D-Bus support
++ *
++ * Copyright (c) 2026 Red Hat.
++ * Author: Jocelyn Falempe <jfalempe@redhat.com>
++ *
++ * Permission is hereby granted, free of charge, to any person obtaining
++ * a copy of this software and associated documentation files
++ * (the "Software"), to deal in the Software without restriction, including
++ * without limitation the rights to use, copy, modify, merge, publish,
++ * distribute, sublicense, and/or sell copies of the Software, and to
++ * permit persons to whom the Software is furnished to do so, subject to
++ * the following conditions:
++ *
++ * The above copyright notice and this permission notice shall be included
++ * in all copies or substantial portions of the Software.
++ *
++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
++ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
++ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
++ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
++ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
++ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
++ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
++ */
++
++#include <dbus/dbus.h>
++#include <errno.h>
++#include <stdlib.h>
++#include <string.h>
++
++#include "dbus.h"
++#include "shl/eloop.h"
++#include "shl/log.h"
++
++#define LOG_SUBSYSTEM "dbus"
++
++struct rmlvo {
++	const char *model;
++	const char *layout;
++	const char *variant;
++	const char *options;
++};
++
++struct kmscon_dbus {
++	DBusConnection *conn;
++	struct ev_eloop *eloop;
++	struct ev_fd *watch_fd;
++	dbus_update_xkb_layout_cb cb;
++	void *data;
++};
++
++static void set_env_from_locale1_properties(DBusConnection *conn, const char *property_name,
++					    const char *env_name)
++{
++	DBusError error;
++	DBusMessage *msg;
++	DBusMessage *reply;
++	DBusMessageIter args, variant_iter;
++	const char *interface_name = "org.freedesktop.locale1";
++	char *value = NULL;
++
++	dbus_error_init(&error);
++
++	msg = dbus_message_new_method_call("org.freedesktop.locale1", "/org/freedesktop/locale1",
++					   "org.freedesktop.DBus.Properties", "Get");
++	if (!msg)
++		return;
++
++	dbus_message_iter_init_append(msg, &args);
++	if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &interface_name) ||
++	    !dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &property_name)) {
++		dbus_message_unref(msg);
++		return;
++	}
++
++	reply = dbus_connection_send_with_reply_and_block(conn, msg, 2000, &error);
++	dbus_message_unref(msg);
++
++	if (dbus_error_has_name(&error, DBUS_ERROR_UNKNOWN_INTERFACE) ||
++	    dbus_error_has_name(&error, DBUS_ERROR_UNKNOWN_PROPERTY)) {
++		/* This is normal if the interface is not supported by the system */
++		dbus_error_free(&error);
++		return;
++	} else if (dbus_error_is_set(&error)) {
++		log_warning("dbus error: %s / %s", error.name, error.message);
++		dbus_error_free(&error);
++		return;
++	} else if (!reply) {
++		log_warning("no reply from dbus");
++		return;
++	}
++
++	if (!dbus_message_iter_init(reply, &args)) {
++		log_debug("Reply message has no arguments.\n");
++	} else if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_VARIANT) {
++		log_debug("Reply argument is not a variant.\n");
++	} else {
++		dbus_message_iter_recurse(&args, &variant_iter);
++
++		if (dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_STRING) {
++			dbus_message_iter_get_basic(&variant_iter, &value);
++			if (strlen(value) > 0) {
++				setenv(env_name, value, 0);
++				log_debug("Set %s to %s\n", env_name, value);
++			}
++		} else {
++			log_debug("Unexpected inner data type in variant.\n");
++		}
++	}
++
++	dbus_message_unref(reply);
++	return;
++}
++
++static bool is_xkb_env_set(void)
++{
++	return getenv("XKB_DEFAULT_MODEL") || getenv("XKB_DEFAULT_LAYOUT") ||
++	       getenv("XKB_DEFAULT_VARIANT") || getenv("XKB_DEFAULT_OPTIONS");
++}
++
++/* This is called once at startup to set the XKB environment variables from the locale1 properties.
++ */
++void kmscon_dbus_set_xkb_env_from_locale1(struct kmscon_dbus *dbus)
++{
++	DBusError error;
++
++	if (is_xkb_env_set())
++		return;
++
++	dbus_error_init(&error);
++
++	set_env_from_locale1_properties(dbus->conn, "X11Model", "XKB_DEFAULT_MODEL");
++	set_env_from_locale1_properties(dbus->conn, "X11Layout", "XKB_DEFAULT_LAYOUT");
++	set_env_from_locale1_properties(dbus->conn, "X11Variant", "XKB_DEFAULT_VARIANT");
++	set_env_from_locale1_properties(dbus->conn, "X11Options", "XKB_DEFAULT_OPTIONS");
++}
++
++struct kmscon_dbus *kmscon_dbus_new(struct ev_eloop *eloop)
++{
++	struct kmscon_dbus *dbus;
++	DBusError error;
++
++	dbus = malloc(sizeof(*dbus));
++	if (!dbus)
++		return NULL;
++	memset(dbus, 0, sizeof(*dbus));
++
++	dbus->eloop = eloop;
++
++	dbus_error_init(&error);
++	dbus->conn = dbus_bus_get(DBUS_BUS_SYSTEM, &error);
++	if (dbus_error_is_set(&error)) {
++		log_debug("Connection Error: %s\n", error.message);
++		dbus_error_free(&error);
++		return NULL;
++	}
++	return dbus;
++}
++
++void kmscon_dbus_free(struct kmscon_dbus *dbus)
++{
++	if (!dbus)
++		return;
++	dbus_connection_unref(dbus->conn);
++	free(dbus);
++}
++
++static void set_rmlvo_property(struct rmlvo *rmlvo, const char *property_name,
++			       const char *property_value)
++{
++	if (!strcmp(property_name, "X11Model")) {
++		rmlvo->model = property_value;
++	} else if (!strcmp(property_name, "X11Layout")) {
++		rmlvo->layout = property_value;
++	} else if (!strcmp(property_name, "X11Variant")) {
++		rmlvo->variant = property_value;
++	} else if (!strcmp(property_name, "X11Options")) {
++		rmlvo->options = property_value;
++	}
++}
++
++/* Parse the locale1 properties changed message and update the rmlvo struct */
++void parse_locale_properties(DBusMessage *msg, struct rmlvo *rmlvo)
++{
++	DBusMessageIter root_iter;
++	DBusMessageIter dict_iter;
++	DBusMessageIter entry_iter;
++	DBusMessageIter variant_iter;
++	char *property_name;
++	char *property_value;
++
++	if (!dbus_message_iter_init(msg, &root_iter)) {
++		log_debug("Message has no arguments.\n");
++		return;
++	}
++
++	dbus_message_iter_next(&root_iter);
++	if (dbus_message_iter_get_arg_type(&root_iter) != DBUS_TYPE_ARRAY) {
++		log_debug("Not the container type we expect\n");
++		return;
++	}
++
++	dbus_message_iter_recurse(&root_iter, &dict_iter);
++	while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {
++
++		dbus_message_iter_recurse(&dict_iter, &entry_iter);
++
++		dbus_message_iter_get_basic(&entry_iter, &property_name);
++
++		dbus_message_iter_next(&entry_iter);
++
++		if (dbus_message_iter_get_arg_type(&entry_iter) == DBUS_TYPE_VARIANT) {
++
++			dbus_message_iter_recurse(&entry_iter, &variant_iter);
++
++			if (dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_STRING) {
++				dbus_message_iter_get_basic(&variant_iter, &property_value);
++				set_rmlvo_property(rmlvo, property_name, property_value);
++				log_debug("property %s: %s\n", property_name, property_value);
++			}
++		}
++		dbus_message_iter_next(&dict_iter);
++	}
++}
++
++static void handle_dispatch_status(DBusConnection *conn, DBusDispatchStatus status, void *data)
++{
++	struct kmscon_dbus *dbus = data;
++	struct rmlvo rmlvo = {NULL, NULL, NULL, NULL};
++
++	if (status == DBUS_DISPATCH_DATA_REMAINS) {
++		DBusMessage *msg = dbus_connection_pop_message(dbus->conn);
++		if (msg) {
++			log_debug("Received message %s\n", dbus_message_get_signature(msg));
++			if (dbus_message_is_signal(msg, "org.freedesktop.DBus.Properties",
++						   "PropertiesChanged")) {
++				parse_locale_properties(msg, &rmlvo);
++				if (dbus->cb)
++					dbus->cb(rmlvo.model, rmlvo.layout, rmlvo.variant,
++						 rmlvo.options, dbus->data);
++			}
++			dbus_message_unref(msg);
++		}
++	}
++}
++
++static void locale1_properties_changed(struct ev_fd *fd, int mask, void *data)
++{
++	struct DBusWatch *watch = data;
++	struct kmscon_dbus *dbus = dbus_watch_get_data(watch);
++
++	unsigned int flags = 0;
++
++	log_debug("locale1 property changed\n");
++
++	if (mask & EV_READABLE)
++		flags |= DBUS_WATCH_READABLE;
++	if (mask & EV_WRITEABLE)
++		flags |= DBUS_WATCH_WRITABLE;
++
++	dbus_watch_handle(watch, flags);
++
++	while (dbus_connection_get_dispatch_status(dbus->conn) == DBUS_DISPATCH_DATA_REMAINS) {
++		dbus_connection_dispatch(dbus->conn);
++		// DBUS-1 API is a pain, but at least that works for this simple use case
++		handle_dispatch_status(dbus->conn, DBUS_DISPATCH_DATA_REMAINS, dbus);
++	}
++}
++
++/* Only register one read watch, as we're not interested in writes */
++static unsigned int dbus_add_watch(DBusWatch *watch, void *data)
++{
++	struct kmscon_dbus *dbus = data;
++	int fd = dbus_watch_get_unix_fd(watch);
++	unsigned int flags = dbus_watch_get_flags(watch);
++	unsigned int ev_flags = 0;
++	int ret = 0;
++
++	log_debug("Adding watch for fd %d with flags %d %p %d\n", fd, flags, watch,
++		  dbus_watch_get_enabled(watch));
++
++	if (!dbus_watch_get_enabled(watch))
++		return TRUE;
++
++	if (dbus->watch_fd) {
++		log_warning("dbus watch already registered");
++		return TRUE;
++	}
++	if (flags & DBUS_WATCH_WRITABLE)
++		ev_flags |= EV_WRITEABLE;
++	if (flags & DBUS_WATCH_READABLE)
++		ev_flags |= EV_READABLE;
++
++	ret = ev_eloop_new_fd(dbus->eloop, &dbus->watch_fd, fd, ev_flags,
++			      locale1_properties_changed, watch);
++
++	dbus_watch_set_data(watch, dbus, NULL);
++
++	if (ret)
++		log_error("cannot add watch for fd %d: %d %d %p", fd, ret, ev_flags, watch);
++	return TRUE;
++}
++
++static void dbus_remove_watch(DBusWatch *watch, void *data)
++{
++	struct kmscon_dbus *dbus = data;
++	log_debug("Removing watch for fd %d\n", dbus_watch_get_unix_fd(watch));
++
++	if (dbus_watch_get_data(watch) != dbus)
++		return;
++
++	ev_eloop_rm_fd(dbus->watch_fd);
++	dbus->watch_fd = NULL;
++	dbus_watch_set_data(watch, NULL, NULL);
++	return;
++}
++
++static void dbus_toggle_watch(DBusWatch *watch, void *data)
++{
++	log_debug("Toggling watch for fd %d\n", dbus_watch_get_unix_fd(watch));
++
++	if (dbus_watch_get_enabled(watch))
++		dbus_add_watch(watch, data);
++	else
++		dbus_remove_watch(watch, data);
++}
++
++/* Listen to locale1 properties changed, and update the XKB layout if needed */
++int kmscon_dbus_listen_locale1(struct kmscon_dbus *dbus, dbus_update_xkb_layout_cb cb, void *data)
++{
++	DBusError error;
++
++	log_debug("Listening to locale1\n");
++
++	if (!cb)
++		return -EINVAL;
++
++	dbus->cb = cb;
++	dbus->data = data;
++
++	dbus_error_init(&error);
++
++	dbus_connection_set_watch_functions(dbus->conn, dbus_add_watch, dbus_remove_watch,
++					    dbus_toggle_watch, dbus, NULL);
++
++	dbus_connection_set_dispatch_status_function(dbus->conn, handle_dispatch_status, dbus,
++						     NULL);
++
++	const char *match_rule = "type='signal',"
++				 "sender='org.freedesktop.locale1',"
++				 "path='/org/freedesktop/locale1',"
++				 "interface='org.freedesktop.DBus.Properties',"
++				 "member='PropertiesChanged'";
++
++	dbus_bus_add_match(dbus->conn, match_rule, &error);
++
++	if (dbus_error_is_set(&error)) {
++		log_debug("Error adding match: %s\n", error.message);
++		dbus_error_free(&error);
++		return -EIO;
++	}
++	dbus_connection_flush(dbus->conn);
++
++	log_info("Listening to locale1 changes");
++
++	return 0;
++}
+diff --git a/src/kmscon_dummy.h b/src/dbus.h
+similarity index 50%
+rename from src/kmscon_dummy.h
+rename to src/dbus.h
+index b4f3aa2..272434f 100644
+--- a/src/kmscon_dummy.h
++++ b/src/dbus.h
+@@ -1,7 +1,8 @@
+ /*
+- * kmscon - Dummy Session
++ * dbus - D-Bus support
+  *
+- * Copyright (c) 2012 David Herrmann <dh.herrmann@googlemail.com>
++ * Copyright (c) 2026 Red Hat.
++ * Author: Jocelyn Falempe <jfalempe@redhat.com>
+  *
+  * Permission is hereby granted, free of charge, to any person obtaining
+  * a copy of this software and associated documentation files
+@@ -23,28 +24,38 @@
+  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+  */
+ 
+-/*
+- * Dummy Session
+- */
+-
+-#ifndef KMSCON_DUMMY_H
+-#define KMSCON_DUMMY_H
++#ifndef _KMSCON_DBUS_H
++#define _KMSCON_DBUS_H
+ 
+-#include <errno.h>
+-#include <stdlib.h>
+-#include "kmscon_seat.h"
++struct ev_eloop;
++struct kmscon_dbus;
+ 
+-#ifdef BUILD_ENABLE_SESSION_DUMMY
++typedef void (*dbus_update_xkb_layout_cb)(const char *model, const char *layout,
++					  const char *variant, const char *options, void *data);
+ 
+-int kmscon_dummy_register(struct kmscon_session **out, struct kmscon_seat *seat);
++#ifdef BUILD_ENABLE_DBUS
++void kmscon_dbus_set_xkb_env_from_locale1(struct kmscon_dbus *dbus);
++struct kmscon_dbus *kmscon_dbus_new(struct ev_eloop *eloop);
++void kmscon_dbus_free(struct kmscon_dbus *dbus);
++int kmscon_dbus_listen_locale1(struct kmscon_dbus *dbus, dbus_update_xkb_layout_cb cb, void *data);
+ 
+-#else /* !BUILD_ENABLE_SESSION_DUMMY */
+-
+-static inline int kmscon_dummy_register(struct kmscon_session **out, struct kmscon_seat *seat)
++#else
++static inline void kmscon_dbus_set_xkb_env_from_locale1(struct kmscon_dbus *dbus)
+ {
+-	return -EOPNOTSUPP;
++	return;
+ }
+-
+-#endif /* BUILD_ENABLE_SESSION_DUMMY */
+-
+-#endif /* KMSCON_DUMMY_H */
++static inline struct kmscon_dbus *kmscon_dbus_new(struct ev_eloop *eloop)
++{
++	return NULL;
++}
++static inline void kmscon_dbus_free(struct kmscon_dbus *dbus)
++{
++	return;
++}
++static inline int kmscon_dbus_listen_locale1(struct kmscon_dbus *dbus, dbus_update_xkb_layout_cb cb,
++					     void *data)
++{
++	return 0;
++}
++#endif /* BUILD_ENABLE_DBUS */
++#endif /* _KMSCON_DBUS_H */
+\ No newline at end of file
+diff --git a/src/font/font_freetype.c b/src/font/font_freetype.c
+index a879d64..d0e3ee9 100644
+--- a/src/font/font_freetype.c
++++ b/src/font/font_freetype.c
+@@ -114,7 +114,7 @@ static int font_get_width(FT_Face face)
+ {
+ 	FT_UInt glyph_index = FT_Get_Char_Index(face, 'M');
+ 
+-	if (FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT))
++	if (FT_Load_Glyph(face, glyph_index, FT_LOAD_TARGET_LIGHT))
+ 		return -1;
+ 
+ 	if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL))
+@@ -377,7 +377,7 @@ static struct kmscon_glyph *render_glyph(FT_Face face, FT_UInt index, const uint
+ 	if (!cwidth)
+ 		return NULL;
+ 
+-	if (FT_Load_Glyph(face, index, FT_LOAD_NO_HINTING)) {
++	if (FT_Load_Glyph(face, index, FT_LOAD_TARGET_LIGHT)) {
+ 		log_err("Failed to load glyph\n");
+ 		return NULL;
+ 	}
+diff --git a/src/font/meson.build b/src/font/meson.build
+index dd5d9dc..d265bb0 100644
+--- a/src/font/meson.build
++++ b/src/font/meson.build
+@@ -22,7 +22,7 @@ font_deps = declare_dependency(
+ # Unifont Generator
+ # This generates the unifont sources from raw hex-encoded font data.
+ #
+-genunifont = executable('genunifont', 'genunifont.c', dependencies: [zlib_deps], native: true)
++genunifont = executable('genunifont', 'genunifont.c', dependencies: [zlib_deps_native], native: true)
+ 
+ unifont_bin = custom_target('unifont-bin',
+   input: ['font_unifont_data.hex'],
+@@ -68,4 +68,4 @@ if enable_font_pango
+     install: true,
+     install_dir: moduledir,
+   )
+-endif
+\ No newline at end of file
++endif
+diff --git a/src/input/input.c b/src/input/input.c
+index 65c1d6f..71a053b 100644
+--- a/src/input/input.c
++++ b/src/input/input.c
+@@ -504,23 +504,48 @@ err_free:
+ 
+ SHL_EXPORT
+ int input_set_keymap(struct input *input, const char *model, const char *layout,
+-		     const char *variant, const char *options, const char *locale,
+-		     const char *keymap, const char *compose_file, size_t compose_file_len)
+-{
+-	/* xkbcommon won't use the XKB_DEFAULT_OPTIONS environment
+-	 * variable if options is an empty string.
+-	 * So if all variables are empty, use NULL instead.
+-	 */
+-	if (model && *model == 0 && layout && *layout == 0 && variant && *variant == 0 && options &&
+-	    *options == 0) {
+-		model = NULL;
+-		layout = NULL;
+-		variant = NULL;
+-		options = NULL;
+-	}
++		     const char *variant, const char *options, const char *keymap)
++{
++	return uxkb_layout_init(input, model, layout, variant, options, keymap);
++}
+ 
+-	return uxkb_desc_init(input, model, layout, variant, options, locale, keymap, compose_file,
+-			      compose_file_len);
++SHL_EXPORT
++void input_set_compose(struct input *input, const char *locale, const char *compose_file,
++		       size_t compose_file_len)
++{
++	uxkb_compose_table_init(input, compose_file, compose_file_len, locale);
++}
++
++SHL_EXPORT
++int input_update_keymap(struct input *input, const char *model, const char *layout,
++			const char *variant, const char *options)
++{
++	struct shl_dlist *iter;
++	struct input_dev *dev;
++
++	if (input->ctx) {
++		shl_dlist_for_each(iter, &input->devices)
++		{
++			dev = shl_dlist_entry(iter, struct input_dev, list);
++			if (dev->capabilities & DEVICE_HAS_KEYS) {
++				input_sleep_dev(dev);
++				input_exit_keyboard(dev);
++			}
++		}
++	}
++	uxkb_compose_table_destroy(input);
++	uxkb_layout_destroy(input);
++	uxkb_layout_init(input, model, layout, variant, options, NULL);
++	shl_dlist_for_each(iter, &input->devices)
++	{
++		dev = shl_dlist_entry(iter, struct input_dev, list);
++		if (dev->capabilities & DEVICE_HAS_KEYS) {
++			input_init_keyboard(dev);
++			if (input->awake)
++				input_wake_up_dev(dev);
++		}
++	}
++	return 0;
+ }
+ 
+ SHL_EXPORT
+@@ -565,7 +590,8 @@ void input_unref(struct input *input)
+ 		input_free_dev(dev);
+ 	}
+ 
+-	uxkb_desc_destroy(input);
++	uxkb_compose_table_destroy(input);
++	uxkb_layout_destroy(input);
+ 	shl_hook_free(input->key_hook);
+ 	shl_hook_free(input->pointer_hook);
+ 	ev_eloop_rm_timer(input->hide_pointer);
+diff --git a/src/input/input.h b/src/input/input.h
+index 8b81e00..9f269ff 100644
+--- a/src/input/input.h
++++ b/src/input/input.h
+@@ -92,8 +92,11 @@ typedef void (*uterm_close_cb)(int fd, int fd_id, void *data);
+ 
+ int input_new(struct input **out, struct ev_eloop *eloop);
+ int input_set_keymap(struct input *input, const char *model, const char *layout,
+-		     const char *variant, const char *options, const char *locale,
+-		     const char *keymap, const char *compose_file, size_t compose_file_len);
++		     const char *variant, const char *options, const char *keymap);
++void input_set_compose(struct input *input, const char *locale, const char *compose_file,
++		       size_t compose_file_len);
++int input_update_keymap(struct input *input, const char *model, const char *layout,
++			const char *variant, const char *options);
+ void input_set_conf(struct input *input, unsigned int repeat_delay, unsigned int repeat_rate,
+ 		    bool mouse_enabled);
+ void input_ref(struct input *input);
+diff --git a/src/input/input_internal.h b/src/input/input_internal.h
+index 559e8ec..fc932fa 100644
+--- a/src/input/input_internal.h
++++ b/src/input/input_internal.h
+@@ -140,10 +140,12 @@ static inline bool input_bit_is_set(const unsigned long *array, int bit)
+ 	return !!(array[bit / LONG_BIT] & (1UL << (bit % LONG_BIT)));
+ }
+ 
+-int uxkb_desc_init(struct input *input, const char *model, const char *layout, const char *variant,
+-		   const char *options, const char *locale, const char *keymap,
+-		   const char *compose_file, size_t compose_file_len);
+-void uxkb_desc_destroy(struct input *input);
++int uxkb_layout_init(struct input *input, const char *model, const char *layout,
++		     const char *variant, const char *options, const char *keymap);
++void uxkb_compose_table_init(struct input *input, const char *compose_file, size_t compose_file_len,
++			     const char *locale);
++void uxkb_layout_destroy(struct input *input);
++void uxkb_compose_table_destroy(struct input *input);
+ 
+ int uxkb_dev_init(struct input_dev *dev);
+ void uxkb_dev_destroy(struct input_dev *dev);
+diff --git a/src/input/input_uxkb.c b/src/input/input_uxkb.c
+index 377e935..1ae2565 100644
+--- a/src/input/input_uxkb.c
++++ b/src/input/input_uxkb.c
+@@ -69,11 +69,10 @@ static void uxkb_log(struct xkb_context *context, enum xkb_log_level level, cons
+ 	log_submit(LOG_DEFAULT, sev, format, args);
+ }
+ 
+-int uxkb_desc_init(struct input *input, const char *model, const char *layout, const char *variant,
+-		   const char *options, const char *locale, const char *keymap,
+-		   const char *compose_file, size_t compose_file_len)
++int uxkb_layout_init(struct input *input, const char *model, const char *layout,
++		     const char *variant, const char *options, const char *keymap)
+ {
+-	int ret;
++	int ret = 0;
+ 	struct xkb_rule_names rmlvo = {
+ 		.rules = "evdev",
+ 		.model = model,
+@@ -140,7 +139,15 @@ int uxkb_desc_init(struct input *input, const char *model, const char *layout, c
+ 		log_debug("new keyboard description (%s, %s, %s, %s)", model, layout, variant,
+ 			  options);
+ 	}
++	return 0;
++err_ctx:
++	xkb_context_unref(input->ctx);
++	return ret;
++}
+ 
++void uxkb_compose_table_init(struct input *input, const char *compose_file, size_t compose_file_len,
++			     const char *locale)
++{
+ 	if (compose_file && *compose_file) {
+ 		input->compose_table = xkb_compose_table_new_from_buffer(
+ 			input->ctx, compose_file, compose_file_len, locale,
+@@ -161,19 +168,20 @@ int uxkb_desc_init(struct input *input, const char *model, const char *layout, c
+ 				 "table, disabling compose support");
+ 		}
+ 	}
+-
+-	return 0;
+-
+-err_ctx:
+-	xkb_context_unref(input->ctx);
+-	return ret;
+ }
+ 
+-void uxkb_desc_destroy(struct input *input)
++void uxkb_compose_table_destroy(struct input *input)
+ {
+ 	xkb_compose_table_unref(input->compose_table);
++	input->compose_table = NULL;
++}
++
++void uxkb_layout_destroy(struct input *input)
++{
+ 	xkb_keymap_unref(input->keymap);
+ 	xkb_context_unref(input->ctx);
++	input->keymap = NULL;
++	input->ctx = NULL;
+ }
+ 
+ static void timer_event(struct ev_timer *timer, uint64_t num, void *data)
+@@ -218,6 +226,9 @@ void uxkb_dev_destroy(struct input_dev *dev)
+ 	xkb_compose_state_unref(dev->compose_state);
+ 	xkb_state_unref(dev->state);
+ 	ev_eloop_rm_timer(dev->repeat_timer);
++	dev->compose_state = NULL;
++	dev->state = NULL;
++	dev->repeat_timer = NULL;
+ }
+ 
+ #define EVDEV_KEYCODE_OFFSET 8
+diff --git a/src/kmscon_conf.c b/src/kmscon_conf.c
+index 6a8728a..4b01d4a 100644
+--- a/src/kmscon_conf.c
++++ b/src/kmscon_conf.c
+@@ -34,6 +34,7 @@
+ #include <xkbcommon/xkbcommon-keysyms.h>
+ #include "conf.h"
+ #include "kmscon_conf.h"
++#include "kmscon_issue.h"
+ #include "shl/githead.h"
+ #include "shl/log.h"
+ #include "shl/misc.h"
+@@ -78,14 +79,13 @@ static void print_help()
+ 		"Session Options:\n"
+ 		"\t    --session-max <max>         [50]  Maximum number of sessions\n"
+ 		"\t    --session-control           [off] Allow keyboard session-control\n"
+-		"\t    --terminal-session          [on]  Enable terminal session\n"
+ 		"\n"
+ 		"Terminal Options:\n"
+ 		"\t    --issue                 [on]\n"
+ 		"\t                              Display issue files before the\n"
+ 		"\t                              login prompt. Use --no-issue to\n"
+ 		"\t                              let the login process handle it.\n"
+-		"\t    --issue-path <path>     [/etc/issue:/etc/issue.d]\n"
++		"\t    --issue-path <path>     [" ISSUE_DEFAULT_PATH "]\n"
+ 		"\t                              Colon-separated list of issue\n"
+ 		"\t                              files and directories to read\n"
+ 		"\t-l, --login                 [/bin/login -p]\n"
+@@ -148,8 +148,6 @@ static void print_help()
+ 		"\t                                  Switch to the next session\n"
+ 		"\t    --grab-session-prev <grab>  [<Ctrl><Logo>Left]\n"
+ 		"\t                                  Switch to the previous session\n"
+-		"\t    --grab-session-dummy <grab> [<Ctrl><Logo>Escape]\n"
+-		"\t                                  Switch to a dummy session\n"
+ 		"\t    --grab-session-close <grab> [<Ctrl><Logo>BackSpace]\n"
+ 		"\t                                  Close current session\n"
+ 		"\t    --grab-terminal-new <grab>  [<Ctrl><Logo>Return]\n"
+@@ -659,9 +657,6 @@ static struct conf_grab def_grab_session_next =
+ static struct conf_grab def_grab_session_prev =
+ 	CONF_SINGLE_GRAB(SHL_CONTROL_MASK | SHL_LOGO_MASK, XKB_KEY_Left);
+ 
+-static struct conf_grab def_grab_session_dummy =
+-	CONF_SINGLE_GRAB(SHL_CONTROL_MASK | SHL_LOGO_MASK, XKB_KEY_Escape);
+-
+ static struct conf_grab def_grab_session_close =
+ 	CONF_SINGLE_GRAB(SHL_CONTROL_MASK | SHL_LOGO_MASK, XKB_KEY_BackSpace);
+ 
+@@ -728,11 +723,10 @@ int kmscon_conf_new(struct conf_ctx **out)
+ 		/* Session Options */
+ 		CONF_OPTION_UINT(0, "session-max", &conf->session_max, 50),
+ 		CONF_OPTION_BOOL(0, "session-control", &conf->session_control, false),
+-		CONF_OPTION_BOOL(0, "terminal-session", &conf->terminal_session, true),
+ 
+ 		/* Terminal Options */
+ 		CONF_OPTION_BOOL(0, "issue", &conf->issue, true),
+-		CONF_OPTION_STRING(0, "issue-path", &conf->issue_path, "/etc/issue:/etc/issue.d"),
++		CONF_OPTION_STRING(0, "issue-path", &conf->issue_path, ISSUE_DEFAULT_PATH),
+ 		CONF_OPTION(0, 'l', "login", &conf_login, aftercheck_login, NULL, file_login,
+ 			    &conf->login, false),
+ 		CONF_OPTION_STRING('t', "term", &conf->term, "kmscon"),
+@@ -742,12 +736,12 @@ int kmscon_conf_new(struct conf_ctx **out)
+ 		CONF_OPTION_BOOL(0, "bell", &conf->bell, false),
+ 
+ 		/* Input Options */
+-		CONF_OPTION_STRING(0, "xkb-model", &conf->xkb_model, ""),
+-		CONF_OPTION_STRING(0, "xkb-layout", &conf->xkb_layout, ""),
+-		CONF_OPTION_STRING(0, "xkb-variant", &conf->xkb_variant, ""),
+-		CONF_OPTION_STRING(0, "xkb-options", &conf->xkb_options, ""),
+-		CONF_OPTION_STRING(0, "xkb-keymap", &conf->xkb_keymap, ""),
+-		CONF_OPTION_STRING(0, "xkb-compose-file", &conf->xkb_compose_file, ""),
++		CONF_OPTION_STRING(0, "xkb-model", &conf->xkb_model, NULL),
++		CONF_OPTION_STRING(0, "xkb-layout", &conf->xkb_layout, NULL),
++		CONF_OPTION_STRING(0, "xkb-variant", &conf->xkb_variant, NULL),
++		CONF_OPTION_STRING(0, "xkb-options", &conf->xkb_options, NULL),
++		CONF_OPTION_STRING(0, "xkb-keymap", &conf->xkb_keymap, NULL),
++		CONF_OPTION_STRING(0, "xkb-compose-file", &conf->xkb_compose_file, NULL),
+ 		CONF_OPTION_UINT(0, "xkb-repeat-delay", &conf->xkb_repeat_delay, 250),
+ 		CONF_OPTION_UINT(0, "xkb-repeat-rate", &conf->xkb_repeat_rate, 50),
+ 		CONF_OPTION_BOOL(0, "mouse", &conf->mouse, true),
+@@ -767,8 +761,6 @@ int kmscon_conf_new(struct conf_ctx **out)
+ 				 &def_grab_session_next),
+ 		CONF_OPTION_GRAB(0, "grab-session-prev", &conf->grab_session_prev,
+ 				 &def_grab_session_prev),
+-		CONF_OPTION_GRAB(0, "grab-session-dummy", &conf->grab_session_dummy,
+-				 &def_grab_session_dummy),
+ 		CONF_OPTION_GRAB(0, "grab-session-close", &conf->grab_session_close,
+ 				 &def_grab_session_close),
+ 		CONF_OPTION_GRAB(0, "grab-terminal-new", &conf->grab_terminal_new,
+diff --git a/src/kmscon_conf.h b/src/kmscon_conf.h
+index 8dc7afd..377f8a3 100644
+--- a/src/kmscon_conf.h
++++ b/src/kmscon_conf.h
+@@ -79,8 +79,6 @@ struct kmscon_conf_t {
+ 	unsigned int session_max;
+ 	/* allow keyboard session control */
+ 	bool session_control;
+-	/* run terminal session */
+-	bool terminal_session;
+ 
+ 	/* Terminal Options */
+ 	/* display /etc/issue before login prompt */
+@@ -145,8 +143,6 @@ struct kmscon_conf_t {
+ 	struct conf_grab *grab_session_next;
+ 	/* session-prev grab */
+ 	struct conf_grab *grab_session_prev;
+-	/* session-dummy grab */
+-	struct conf_grab *grab_session_dummy;
+ 	/* session-close grab */
+ 	struct conf_grab *grab_session_close;
+ 	/* terminal-new grab */
+diff --git a/src/kmscon_dummy.c b/src/kmscon_dummy.c
+deleted file mode 100644
+index fcafb6c..0000000
+--- a/src/kmscon_dummy.c
++++ /dev/null
+@@ -1,154 +0,0 @@
+-/*
+- * kmscon - Dummy Session
+- *
+- * Copyright (c) 2012 David Herrmann <dh.herrmann@googlemail.com>
+- *
+- * Permission is hereby granted, free of charge, to any person obtaining
+- * a copy of this software and associated documentation files
+- * (the "Software"), to deal in the Software without restriction, including
+- * without limitation the rights to use, copy, modify, merge, publish,
+- * distribute, sublicense, and/or sell copies of the Software, and to
+- * permit persons to whom the Software is furnished to do so, subject to
+- * the following conditions:
+- *
+- * The above copyright notice and this permission notice shall be included
+- * in all copies or substantial portions of the Software.
+- *
+- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+- */
+-
+-/*
+- * Dummy Session
+- */
+-
+-#include <errno.h>
+-#include <stdlib.h>
+-#include <string.h>
+-#include "kmscon_dummy.h"
+-#include "kmscon_seat.h"
+-#include "shl/dlist.h"
+-#include "shl/log.h"
+-#include "video/video.h"
+-
+-#define LOG_SUBSYSTEM "dummy"
+-
+-struct display {
+-	struct shl_dlist list;
+-	struct display *disp;
+-};
+-
+-struct kmscon_dummy {
+-	struct kmscon_session *session;
+-	struct shl_dlist displays;
+-	bool active;
+-};
+-
+-static void dummy_redraw(struct kmscon_dummy *dummy, struct display *d)
+-{
+-	display_clear(d->disp, 0, 0, 0);
+-	display_swap(d->disp);
+-}
+-
+-static int dummy_session_event(struct kmscon_session *session, struct kmscon_session_event *ev,
+-			       void *data)
+-{
+-	struct kmscon_dummy *dummy = data;
+-	struct display *d;
+-	struct shl_dlist *iter;
+-
+-	switch (ev->type) {
+-	case KMSCON_SESSION_DISPLAY_NEW:
+-		d = malloc(sizeof(*d));
+-		if (!d) {
+-			log_error("cannot allocate memory for new display");
+-			break;
+-		}
+-		memset(d, 0, sizeof(*d));
+-		d->disp = ev->disp;
+-		shl_dlist_link_tail(&dummy->displays, &d->list);
+-		if (dummy->active)
+-			dummy_redraw(dummy, d);
+-		break;
+-	case KMSCON_SESSION_DISPLAY_GONE:
+-		shl_dlist_for_each(iter, &dummy->displays)
+-		{
+-			d = shl_dlist_entry(iter, struct display, list);
+-			if (d->disp != ev->disp)
+-				continue;
+-
+-			shl_dlist_unlink(&d->list);
+-			free(d);
+-			break;
+-		}
+-		break;
+-	case KMSCON_SESSION_DISPLAY_REFRESH:
+-		shl_dlist_for_each(iter, &dummy->displays)
+-		{
+-			d = shl_dlist_entry(iter, struct display, list);
+-			if (d->disp != ev->disp)
+-				continue;
+-
+-			if (dummy->active)
+-				dummy_redraw(dummy, d);
+-			break;
+-		}
+-		break;
+-	case KMSCON_SESSION_ACTIVATE:
+-		dummy->active = true;
+-		shl_dlist_for_each(iter, &dummy->displays)
+-		{
+-			d = shl_dlist_entry(iter, struct display, list);
+-			dummy_redraw(dummy, d);
+-		}
+-		break;
+-	case KMSCON_SESSION_DEACTIVATE:
+-		dummy->active = false;
+-		break;
+-	case KMSCON_SESSION_UNREGISTER:
+-		while (!shl_dlist_empty(&dummy->displays)) {
+-			d = shl_dlist_entry(dummy->displays.prev, struct display, list);
+-			shl_dlist_unlink(&d->list);
+-			free(d);
+-		}
+-
+-		free(dummy);
+-		break;
+-	}
+-
+-	return 0;
+-}
+-
+-int kmscon_dummy_register(struct kmscon_session **out, struct kmscon_seat *seat)
+-{
+-	struct kmscon_dummy *dummy;
+-	int ret;
+-
+-	if (!out || !seat)
+-		return -EINVAL;
+-
+-	dummy = malloc(sizeof(*dummy));
+-	if (!dummy)
+-		return -ENOMEM;
+-	memset(dummy, 0, sizeof(*dummy));
+-	shl_dlist_init(&dummy->displays);
+-
+-	ret = kmscon_seat_register_session(seat, &dummy->session, dummy_session_event, dummy);
+-	if (ret) {
+-		log_error("cannot register session for dummy: %d", ret);
+-		goto err_free;
+-	}
+-
+-	*out = dummy->session;
+-	log_debug("new dummy object %p", dummy);
+-	return 0;
+-
+-err_free:
+-	free(dummy);
+-	return ret;
+-}
+diff --git a/src/kmscon_issue.c b/src/kmscon_issue.c
+index 3846f89..bffb792 100644
+--- a/src/kmscon_issue.c
++++ b/src/kmscon_issue.c
+@@ -36,16 +36,6 @@
+ /* Cap total collected issue text to prevent runaway allocation. */
+ #define ISSUE_MAX_SIZE (256u * 1024u)
+ 
+-/*
+- * Default search path, matching agetty's default:
+- *   /etc/issue:/etc/issue.d
+- *
+- * Distros can override this with --issue-path or the issue-path config
+- * option using a colon-separated list.  Plain files are read directly;
+- * directories are scanned for *.issue files in lexicographic order.
+- */
+-#define ISSUE_DEFAULT_PATH "/etc/issue:/etc/issue.d"
+-
+ /* Color escape codes */
+ // clang-format off
+ #define UL_COLOR_RESET		"[0m"
+diff --git a/src/kmscon_issue.h b/src/kmscon_issue.h
+index f74382a..8900c95 100644
+--- a/src/kmscon_issue.h
++++ b/src/kmscon_issue.h
+@@ -29,6 +29,15 @@
+ #include <libtsm.h>
+ #include "pty.h"
+ 
++/*
++ * Default search path, matching agetty's default.
++ *
++ * Distros can override this with --issue-path or the issue-path config
++ * option using a colon-separated list.  Plain files are read directly;
++ * directories are scanned for *.issue files in lexicographic order.
++ */
++#define ISSUE_DEFAULT_PATH "/etc/issue:/etc/issue.d"
++
+ void kmscon_issue_write(struct tsm_vte *vte, struct kmscon_pty *pty, const char *search_path);
+ 
+ #endif /* KMSCON_ISSUE_H */
+diff --git a/src/kmscon_main.c b/src/kmscon_main.c
+index 6fb57a8..81343f4 100644
+--- a/src/kmscon_main.c
++++ b/src/kmscon_main.c
+@@ -31,6 +31,7 @@
+ #include <string.h>
+ #include <sys/signalfd.h>
+ #include "conf.h"
++#include "dbus.h"
+ #include "kmscon_conf.h"
+ #include "kmscon_seat.h"
+ #include "render/text.h"
+@@ -39,6 +40,8 @@
+ #include "shl/module.h"
+ #include "video/video.h"
+ 
++#define LOG_SUBSYSTEM "kmscon"
++
+ struct kmscon_app {
+ 	struct conf_ctx *conf_ctx;
+ 	struct kmscon_conf_t *conf;
+@@ -49,6 +52,7 @@ struct kmscon_app {
+ 	struct conf_ctx *seat_ctx;
+ 	struct kmscon_conf_t *seat_conf;
+ 	struct kmscon_seat *seat;
++	struct kmscon_dbus *dbus;
+ };
+ 
+ static int app_seat_event(struct kmscon_seat *s, unsigned int event, void *data)
+@@ -112,6 +116,30 @@ static void destroy_app(struct kmscon_app *app)
+ 	ev_eloop_unregister_signal_cb(app->eloop, SIGINT, app_sig_generic, app);
+ 	ev_eloop_unregister_signal_cb(app->eloop, SIGTERM, app_sig_generic, app);
+ 	ev_eloop_unref(app->eloop);
++	kmscon_dbus_free(app->dbus);
++}
++
++static void update_xkb_layout(const char *model, const char *layout, const char *variant,
++			      const char *options, void *data)
++{
++	struct kmscon_app *app = data;
++
++	log_debug("updating xkb layout: %s, %s, %s, %s", model, layout, variant, options);
++	if (kmscon_seat_update_xkb_layout(app->seat, model, layout, variant, options))
++		log_error("cannot update xkb layout");
++}
++
++static void listen_to_dbus_locale1(struct kmscon_app *app)
++{
++	int ret;
++
++	if (app->conf->xkb_keymap || app->conf->xkb_model || app->conf->xkb_layout ||
++	    app->conf->xkb_variant || app->conf->xkb_options)
++		return;
++
++	ret = kmscon_dbus_listen_locale1(app->dbus, update_xkb_layout, app);
++	if (ret)
++		log_warning("cannot listen to locale1: %d", ret);
+ }
+ 
+ static int setup_app(struct kmscon_app *app)
+@@ -142,6 +170,11 @@ static int setup_app(struct kmscon_app *app)
+ 		goto err_app;
+ 	}
+ 
++	app->dbus = kmscon_dbus_new(app->eloop);
++	if (app->dbus) {
++		kmscon_dbus_set_xkb_env_from_locale1(app->dbus);
++		listen_to_dbus_locale1(app);
++	}
+ 	return 0;
+ 
+ err_app:
+diff --git a/src/kmscon_seat.c b/src/kmscon_seat.c
+index 7b6759a..91b8db8 100644
+--- a/src/kmscon_seat.c
++++ b/src/kmscon_seat.c
+@@ -39,7 +39,6 @@
+ #include "conf.h"
+ #include "input/input.h"
+ #include "kmscon_conf.h"
+-#include "kmscon_dummy.h"
+ #include "kmscon_seat.h"
+ #include "kmscon_terminal.h"
+ #include "shl/dlist.h"
+@@ -53,17 +52,12 @@
+ 
+ struct kmscon_session {
+ 	struct shl_dlist list;
+-	unsigned long ref;
+ 	struct kmscon_seat *seat;
+ 
+-	bool enabled;
+ 	bool foreground;
+-	bool deactivating;
+ 
+ 	struct ev_timer *timer;
+-
+-	kmscon_session_cb_t cb;
+-	void *data;
++	struct kmscon_terminal *term;
+ };
+ 
+ struct kmscon_display {
+@@ -85,12 +79,6 @@ struct kmscon_video {
+ 	bool awake;
+ };
+ 
+-enum kmscon_async_schedule {
+-	SCHEDULE_SWITCH,
+-	SCHEDULE_VT,
+-	SCHEDULE_UNREGISTER,
+-};
+-
+ struct kmscon_seat {
+ 	struct ev_eloop *eloop;
+ 	struct conf_ctx *conf_ctx;
+@@ -110,10 +98,6 @@ struct kmscon_seat {
+ 	bool awake;
+ 	bool foreground;
+ 	struct kmscon_session *current_sess;
+-	struct kmscon_session *scheduled_sess;
+-	struct kmscon_session *dummy_sess;
+-
+-	unsigned int async_schedule;
+ 
+ 	/* DPMS timeout management */
+ 	struct ev_timer *dpms_timer;
+@@ -133,48 +117,10 @@ static int kmscon_seat_add_video(struct kmscon_seat *seat, enum uterm_monitor_de
+ static void kmscon_seat_remove_video(struct kmscon_seat *seat, void *data);
+ static void kmscon_seat_poll_video(void *data);
+ 
+-static int session_call(struct kmscon_session *sess, unsigned int event, struct display *disp)
+-{
+-	struct kmscon_session_event ev;
+-
+-	if (!sess->cb)
+-		return 0;
+-
+-	memset(&ev, 0, sizeof(ev));
+-	ev.type = event;
+-	ev.disp = disp;
+-	return sess->cb(sess, &ev, sess->data);
+-}
+-
+-static int session_call_activate(struct kmscon_session *sess)
+-{
+-	log_debug("activate session %p", sess);
+-	return session_call(sess, KMSCON_SESSION_ACTIVATE, NULL);
+-}
+-
+-static int session_call_deactivate(struct kmscon_session *sess)
+-{
+-	log_debug("deactivate session %p", sess);
+-	return session_call(sess, KMSCON_SESSION_DEACTIVATE, NULL);
+-}
+-
+-static void session_call_display_new(struct kmscon_session *sess, struct display *disp)
+-{
+-	session_call(sess, KMSCON_SESSION_DISPLAY_NEW, disp);
+-}
+-
+-static void session_call_display_gone(struct kmscon_session *sess, struct display *disp)
+-{
+-	session_call(sess, KMSCON_SESSION_DISPLAY_GONE, disp);
+-}
+-
+-static void session_call_display_refresh(struct kmscon_session *sess, struct display *disp)
+-{
+-	session_call(sess, KMSCON_SESSION_DISPLAY_REFRESH, disp);
+-}
+-
+ /* Forward declaration */
+ static void seat_dpms_reset_timer(struct kmscon_seat *seat);
++static struct kmscon_session *kmscon_seat_new_session(struct kmscon_seat *seat);
++static void kmscon_session_unregister(struct kmscon_session *sess);
+ 
+ static void activate_display(struct kmscon_display *d)
+ {
+@@ -203,12 +149,12 @@ static void activate_display(struct kmscon_display *d)
+ 		shl_dlist_for_each_safe(iter, tmp, &seat->sessions)
+ 		{
+ 			s = shl_dlist_entry(iter, struct kmscon_session, list);
+-			session_call_display_new(s, d->disp);
++			terminal_add_display(s->term, d->disp);
+ 		}
+ 	}
+ }
+ 
+-static int seat_go_foreground(struct kmscon_seat *seat, bool force)
++static int seat_go_foreground(struct kmscon_seat *seat)
+ {
+ 	struct shl_dlist *iter, *tmp;
+ 	struct kmscon_video *vid;
+@@ -217,7 +163,7 @@ static int seat_go_foreground(struct kmscon_seat *seat, bool force)
+ 
+ 	if (seat->foreground)
+ 		return 0;
+-	if (!seat->awake || (!force && seat->current_sess))
++	if (!seat->awake)
+ 		return -EBUSY;
+ 
+ 	seat->foreground = true;
+@@ -251,14 +197,14 @@ static int seat_go_foreground(struct kmscon_seat *seat, bool force)
+ 	return 0;
+ }
+ 
+-static int seat_go_background(struct kmscon_seat *seat, bool force)
++static int seat_go_background(struct kmscon_seat *seat)
+ {
+ 	struct shl_dlist *iter;
+ 	struct kmscon_video *vid;
+ 
+ 	if (!seat->foreground)
+ 		return 0;
+-	if (!seat->awake || (!force && seat->current_sess))
++	if (!seat->awake)
+ 		return -EBUSY;
+ 
+ 	shl_dlist_for_each(iter, &seat->videos)
+@@ -271,25 +217,15 @@ static int seat_go_background(struct kmscon_seat *seat, bool force)
+ 	return 0;
+ }
+ 
+-static int seat_go_asleep(struct kmscon_seat *seat, bool force)
++static int seat_go_asleep(struct kmscon_seat *seat)
+ {
+-	int err = 0;
+-
+ 	if (!seat->awake)
+ 		return 0;
+-	if (seat->current_sess || seat->foreground) {
+-		if (force) {
+-			seat->foreground = false;
+-			seat->current_sess = NULL;
+-			err = -EBUSY;
+-		} else {
+-			return -EBUSY;
+-		}
+-	}
++
+ 	seat->awake = false;
+ 	input_sleep(seat->input);
+ 
+-	return err;
++	return 0;
+ }
+ 
+ static int seat_go_awake(struct kmscon_seat *seat)
+@@ -313,196 +249,52 @@ static int seat_go_awake(struct kmscon_seat *seat)
+ 	return 0;
+ }
+ 
+-static int seat_run(struct kmscon_seat *seat)
++static void seat_switch(struct kmscon_seat *seat, struct kmscon_session *new)
+ {
+-	int ret;
+-	struct kmscon_session *session;
+-
+-	if (!seat->awake)
+-		return -EBUSY;
+-	if (seat->current_sess)
+-		return 0;
+-
+-	if (!seat->scheduled_sess) {
+-		log_debug("no session scheduled to run (num %zu)", seat->session_count);
+-		return -ENOENT;
+-	}
+-	session = seat->scheduled_sess;
+-
+-	if (session->foreground && !seat->foreground) {
+-		ret = seat_go_foreground(seat, false);
+-		if (ret) {
+-			log_warning("cannot put seat %s into foreground for session %p", seat->name,
+-				    session);
+-			return ret;
+-		}
+-	} else if (!session->foreground && seat->foreground) {
+-		ret = seat_go_background(seat, false);
+-		if (ret) {
+-			log_warning("cannot put seat %s into background for session %p", seat->name,
+-				    session);
+-			return ret;
+-		}
+-	}
+-
+-	ret = session_call_activate(session);
+-	if (ret) {
+-		log_warning("cannot activate session %p: %d", session, ret);
+-		return ret;
+-	}
+-
+-	seat->current_sess = session;
+-
+-	return 0;
+-}
+-
+-static void session_deactivate(struct kmscon_session *sess)
+-{
+-	if (sess->seat->current_sess != sess)
++	if (seat->current_sess == new)
+ 		return;
+ 
+-	sess->seat->async_schedule = SCHEDULE_SWITCH;
+-	sess->deactivating = false;
+-	sess->seat->current_sess = NULL;
+-}
+-
+-static int seat_pause(struct kmscon_seat *seat, bool force)
+-{
+-	int ret;
+-
+-	if (!seat->current_sess)
+-		return 0;
+-
+-	seat->current_sess->deactivating = true;
+-	ret = session_call_deactivate(seat->current_sess);
+-	if (ret) {
+-		if (ret == -EINPROGRESS)
+-			log_debug("pending deactivation for session %p", seat->current_sess);
+-		else
+-			log_warning("cannot deactivate session %p: %d", seat->current_sess, ret);
+-		if (!force)
+-			return ret;
+-	}
+-
+-	session_deactivate(seat->current_sess);
+-
+-	return ret;
+-}
+-
+-static void seat_reschedule(struct kmscon_seat *seat)
+-{
+-	struct shl_dlist *iter, *start;
+-	struct kmscon_session *sess;
+-
+-	if (seat->scheduled_sess && seat->scheduled_sess->enabled)
+-		return;
+-
+-	if (seat->current_sess && seat->current_sess->enabled) {
+-		seat->scheduled_sess = seat->current_sess;
+-		return;
+-	}
++	log_debug("switch session from %p to %p on seat %s", seat->current_sess, new, seat->name);
+ 
+ 	if (seat->current_sess)
+-		start = &seat->current_sess->list;
+-	else
+-		start = &seat->sessions;
+-
+-	shl_dlist_for_each_but_one(iter, start, &seat->sessions)
+-	{
+-		sess = shl_dlist_entry(iter, struct kmscon_session, list);
+-		if (sess == seat->dummy_sess || !sess->enabled)
+-			continue;
+-		seat->scheduled_sess = sess;
+-		return;
++		terminal_deactivate(seat->current_sess->term);
++	if (new) {
++		if (new->foreground && !seat->foreground) {
++			seat_go_foreground(seat);
++		} else if (!new->foreground && seat->foreground) {
++			seat_go_background(seat);
++		}
++		terminal_activate(new->term);
+ 	}
+-
+-	if (seat->dummy_sess && seat->dummy_sess->enabled)
+-		seat->scheduled_sess = seat->dummy_sess;
+-	else
+-		seat->scheduled_sess = NULL;
+-}
+-
+-static bool seat_has_schedule(struct kmscon_seat *seat)
+-{
+-	return seat->scheduled_sess && seat->scheduled_sess != seat->current_sess;
+-}
+-
+-static int seat_switch(struct kmscon_seat *seat)
+-{
+-	int ret;
+-
+-	seat->async_schedule = SCHEDULE_SWITCH;
+-	ret = seat_pause(seat, false);
+-	if (ret)
+-		return ret;
+-
+-	return seat_run(seat);
++	seat->current_sess = new;
+ }
+ 
+ static void seat_next(struct kmscon_seat *seat)
+ {
+-	struct shl_dlist *cur, *iter;
+-	struct kmscon_session *s, *next;
++	struct kmscon_session *next = NULL;
+ 
+ 	if (seat->current_sess)
+-		cur = &seat->current_sess->list;
+-	else if (seat->session_count)
+-		cur = &seat->sessions;
+-	else
+-		return;
+-
+-	next = NULL;
+-	if (!seat->current_sess && seat->dummy_sess && seat->dummy_sess->enabled)
+-		next = seat->dummy_sess;
+-
+-	shl_dlist_for_each_but_one(iter, cur, &seat->sessions)
+-	{
+-		s = shl_dlist_entry(iter, struct kmscon_session, list);
+-		if (!s->enabled || seat->dummy_sess == s)
+-			continue;
+-
+-		next = s;
+-		break;
+-	}
+-
++		next = shl_dlist_next(seat->current_sess, &seat->sessions, list);
+ 	if (!next)
+-		return;
++		next = shl_dlist_first(&seat->sessions, struct kmscon_session, list);
++	if (next && next == seat->current_sess)
++		next = NULL;
+ 
+-	seat->scheduled_sess = next;
+-	seat_switch(seat);
++	seat_switch(seat, next);
+ }
+ 
+ static void seat_prev(struct kmscon_seat *seat)
+ {
+-	struct shl_dlist *cur, *iter;
+-	struct kmscon_session *s, *prev;
++	struct kmscon_session *prev = NULL;
+ 
+ 	if (seat->current_sess)
+-		cur = &seat->current_sess->list;
+-	else if (seat->session_count)
+-		cur = &seat->sessions;
+-	else
+-		return;
+-
+-	prev = NULL;
+-	if (!seat->current_sess && seat->dummy_sess && seat->dummy_sess->enabled)
+-		prev = seat->dummy_sess;
+-
+-	shl_dlist_for_each_reverse_but_one(iter, cur, &seat->sessions)
+-	{
+-		s = shl_dlist_entry(iter, struct kmscon_session, list);
+-		if (!s->enabled || seat->dummy_sess == s)
+-			continue;
+-
+-		prev = s;
+-		break;
+-	}
+-
++		prev = shl_dlist_prev(seat->current_sess, &seat->sessions, list);
+ 	if (!prev)
+-		return;
++		prev = shl_dlist_last(&seat->sessions, struct kmscon_session, list);
++	if (prev && prev == seat->current_sess)
++		prev = NULL;
+ 
+-	seat->scheduled_sess = prev;
+-	seat_switch(seat);
++	seat_switch(seat, prev);
+ }
+ 
+ static void seat_add_display(struct kmscon_seat *seat, struct display *disp)
+@@ -536,7 +328,7 @@ static void seat_remove_display(struct kmscon_seat *seat, struct kmscon_display
+ 		shl_dlist_for_each_safe(iter, tmp, &seat->sessions)
+ 		{
+ 			s = shl_dlist_entry(iter, struct kmscon_session, list);
+-			session_call_display_gone(s, d->disp);
++			terminal_rm_display(s->term, d->disp);
+ 		}
+ 	}
+ 
+@@ -569,7 +361,7 @@ static void seat_refresh_display(struct kmscon_seat *seat, struct kmscon_display
+ 		shl_dlist_for_each(iter, &seat->sessions)
+ 		{
+ 			s = shl_dlist_entry(iter, struct kmscon_session, list);
+-			session_call_display_refresh(s, d->disp);
++			terminal_refresh_displays(s->term);
+ 		}
+ 	}
+ }
+@@ -578,9 +370,14 @@ static void seat_vt_activate(struct uterm_vt *vt, void *data)
+ {
+ 	struct kmscon_seat *seat = data;
+ 
++	log_debug("VT %d activated on seat %s", uterm_vt_get_num(vt), seat->name);
++
+ 	if (seat_go_awake(seat))
+ 		return;
+-	seat_run(seat);
++	if (seat_go_foreground(seat))
++		return;
++	if (seat->current_sess)
++		terminal_activate(seat->current_sess->term);
+ }
+ 
+ static int seat_vt_deactivate(struct uterm_vt *vt, bool force, void *data)
+@@ -588,14 +385,13 @@ static int seat_vt_deactivate(struct uterm_vt *vt, bool force, void *data)
+ 	struct kmscon_seat *seat = data;
+ 	int ret;
+ 
+-	seat->async_schedule = SCHEDULE_VT;
+-	ret = seat_pause(seat, false);
+-	if (ret)
+-		return ret;
+-	ret = seat_go_background(seat, false);
++	if (seat->current_sess)
++		terminal_deactivate(seat->current_sess->term);
++
++	ret = seat_go_background(seat);
+ 	if (ret)
+ 		return ret;
+-	ret = seat_go_asleep(seat, false);
++	ret = seat_go_asleep(seat);
+ 	if (ret)
+ 		return ret;
+ 	return 0;
+@@ -726,7 +522,6 @@ static void seat_input_event(struct input *input, struct input_key_event *ev, vo
+ {
+ 	struct kmscon_seat *seat = data;
+ 	struct kmscon_session *s;
+-	int ret;
+ 
+ 	/* Reset DPMS timer on any input event */
+ 	seat_dpms_reset_timer(seat);
+@@ -736,49 +531,27 @@ static void seat_input_event(struct input *input, struct input_key_event *ev, vo
+ 
+ 	if (conf_grab_matches(seat->conf->grab_session_next, ev->mods, ev->num_syms, ev->keysyms)) {
+ 		ev->handled = true;
+-		if (!seat->conf->session_control)
++		if (!seat->conf->session_control || seat->session_count < 2)
+ 			return;
+ 		seat_next(seat);
+ 		return;
+ 	}
+ 	if (conf_grab_matches(seat->conf->grab_session_prev, ev->mods, ev->num_syms, ev->keysyms)) {
+ 		ev->handled = true;
+-		if (!seat->conf->session_control)
++		if (!seat->conf->session_control || seat->session_count < 2)
+ 			return;
+ 		seat_prev(seat);
+ 		return;
+ 	}
+-	if (conf_grab_matches(seat->conf->grab_session_dummy, ev->mods, ev->num_syms,
+-			      ev->keysyms)) {
+-		ev->handled = true;
+-		if (!seat->conf->session_control)
+-			return;
+-		seat->scheduled_sess = seat->dummy_sess;
+-		seat_switch(seat);
+-		return;
+-	}
++
+ 	if (conf_grab_matches(seat->conf->grab_session_close, ev->mods, ev->num_syms,
+ 			      ev->keysyms)) {
+ 		ev->handled = true;
+-		if (!seat->conf->session_control)
++		if (!seat->conf->session_control || seat->session_count < 2)
+ 			return;
+ 		s = seat->current_sess;
+ 		if (!s)
+ 			return;
+-		if (s == seat->dummy_sess)
+-			return;
+-
+-		/* First time this is invoked on a session, we simply try
+-		 * unloading it. If it fails, we give it some time. If this is
+-		 * invoked a second time, we notice that we already tried
+-		 * removing it and so we go straight to unregistering the
+-		 * session unconditionally. */
+-		if (!s->deactivating) {
+-			seat->async_schedule = SCHEDULE_UNREGISTER;
+-			ret = seat_pause(seat, false);
+-			if (ret)
+-				return;
+-		}
+ 
+ 		kmscon_session_unregister(s);
+ 		return;
+@@ -787,16 +560,9 @@ static void seat_input_event(struct input *input, struct input_key_event *ev, vo
+ 		ev->handled = true;
+ 		if (!seat->conf->session_control)
+ 			return;
+-		ret = kmscon_terminal_register(&s, seat, uterm_vt_get_num(seat->vt));
+-		if (ret == -EOPNOTSUPP) {
+-			log_notice("terminal support not compiled in");
+-		} else if (ret) {
+-			log_error("cannot register terminal session: %d", ret);
+-		} else {
+-			s->enabled = true;
+-			seat->scheduled_sess = s;
+-			seat_switch(seat);
+-		}
++		s = kmscon_seat_new_session(seat);
++		if (s)
++			seat_switch(seat, s);
+ 		return;
+ 	}
+ 	if (seat->conf->grab_reboot &&
+@@ -829,12 +595,9 @@ static const char *find_locale(void)
+ 
+ static int kmscon_seat_set_keymap(struct kmscon_seat *seat)
+ {
+-	const char *locale;
+-	char *keymap, *compose_file;
+-	size_t compose_file_len;
+-	int ret;
+ 
+-	locale = find_locale();
++	char *keymap;
++	int ret;
+ 
+ 	/* TODO: The XKB-API currently requires zero-terminated strings as
+ 	 * keymap input. Hence, we have to read it in instead of using mmap().
+@@ -846,21 +609,49 @@ static int kmscon_seat_set_keymap(struct kmscon_seat *seat)
+ 			log_error("cannot read keymap file %s: %d", seat->conf->xkb_keymap, ret);
+ 	}
+ 
++	ret = input_set_keymap(seat->input, seat->conf->xkb_model, seat->conf->xkb_layout,
++			       seat->conf->xkb_variant, seat->conf->xkb_options, keymap);
++	if (ret)
++		log_error("cannot set keymap: %d", ret);
++
++	free(keymap);
++	return ret;
++}
++
++static void kmscon_seat_set_compose(struct kmscon_seat *seat)
++{
++	const char *locale;
++	char *compose_file;
++	size_t compose_file_len;
++	int ret;
++
++	locale = find_locale();
++
+ 	compose_file = NULL;
+ 	compose_file_len = 0;
+ 	if (seat->conf->xkb_compose_file && *seat->conf->xkb_compose_file) {
+ 		ret = shl_read_file(seat->conf->xkb_compose_file, &compose_file, &compose_file_len);
+-		if (ret)
++		if (ret) {
+ 			log_error("cannot read compose file %s: %d", seat->conf->xkb_compose_file,
+ 				  ret);
++			return;
++		}
+ 	}
+-	ret = input_set_keymap(seat->input, seat->conf->xkb_model, seat->conf->xkb_layout,
+-			       seat->conf->xkb_variant, seat->conf->xkb_options, locale, keymap,
+-			       compose_file, compose_file_len);
+-	if (ret)
+-		log_error("cannot set keymap: %d", ret);
++	input_set_compose(seat->input, locale, compose_file, compose_file_len);
++}
+ 
+-	free(keymap);
++int kmscon_seat_update_xkb_layout(struct kmscon_seat *seat, const char *model, const char *layout,
++				  const char *variant, const char *options)
++{
++	int ret;
++
++	ret = input_update_keymap(seat->input, model, layout, variant, options);
++	if (ret) {
++		log_error("cannot update keymap: %d", ret);
++		return ret;
++	}
++	/* Compose table is lost when updating the keymap, so we need to set it again */
++	kmscon_seat_set_compose(seat);
+ 	return ret;
+ }
+ 
+@@ -993,6 +784,8 @@ int kmscon_seat_new(struct kmscon_seat **out, struct conf_ctx *main_conf,
+ 	if (ret)
+ 		goto err_conf;
+ 
++	kmscon_seat_set_compose(seat);
++
+ 	ret = uterm_monitor_new(&seat->mon, seat->eloop, &seat_monitor_cb, seat);
+ 	if (ret) {
+ 		log_error("cannot create device monitor: %d", ret);
+@@ -1063,12 +856,9 @@ void kmscon_seat_free(struct kmscon_seat *seat)
+ 	if (!seat)
+ 		return;
+ 
+-	ret = seat_pause(seat, true);
+-	if (ret)
+-		log_warning("destroying seat %s while session %p is active", seat->name,
+-			    seat->current_sess);
++	seat_switch(seat, NULL);
+ 
+-	ret = seat_go_asleep(seat, true);
++	ret = seat_go_asleep(seat);
+ 	if (ret)
+ 		log_warning("destroying seat %s while still awake: %d", seat->name, ret);
+ 
+@@ -1306,31 +1096,18 @@ static void kmscon_seat_poll_video(void *data)
+ 
+ void kmscon_seat_startup(struct kmscon_seat *seat)
+ {
+-	int ret;
+ 	struct kmscon_session *s;
+ 
+ 	if (!seat)
+ 		return;
+ 
+-	ret = kmscon_dummy_register(&s, seat);
+-	if (ret == -EOPNOTSUPP) {
+-		log_notice("dummy sessions not compiled in");
+-	} else if (ret) {
+-		log_error("cannot register dummy session: %d", ret);
+-	} else {
+-		seat->dummy_sess = s;
+-		kmscon_session_enable(s);
+-	}
++	seat_go_awake(seat);
+ 
+-	if (seat->conf->terminal_session) {
+-		ret = kmscon_terminal_register(&s, seat, uterm_vt_get_num(seat->vt));
+-		if (ret == -EOPNOTSUPP)
+-			log_notice("terminal support not compiled in");
+-		else if (ret)
+-			log_error("cannot register terminal session");
+-		else
+-			kmscon_session_enable(s);
+-	}
++	s = kmscon_seat_new_session(seat);
++	if (s)
++		seat_switch(seat, s);
++	else
++		log_error("cannot create new session on seat %s", seat->name);
+ 
+ 	if (seat->conf->switchvt || uterm_vt_get_num(seat->vt) == 0)
+ 		uterm_vt_activate(seat->vt);
+@@ -1339,30 +1116,6 @@ void kmscon_seat_startup(struct kmscon_seat *seat)
+ 	uterm_monitor_scan(seat->mon, seat->name);
+ }
+ 
+-const char *kmscon_seat_get_name(struct kmscon_seat *seat)
+-{
+-	if (!seat)
+-		return NULL;
+-
+-	return seat->name;
+-}
+-
+-struct input *kmscon_seat_get_input(struct kmscon_seat *seat)
+-{
+-	if (!seat)
+-		return NULL;
+-
+-	return seat->input;
+-}
+-
+-struct ev_eloop *kmscon_seat_get_eloop(struct kmscon_seat *seat)
+-{
+-	if (!seat)
+-		return NULL;
+-
+-	return seat->eloop;
+-}
+-
+ struct conf_ctx *kmscon_seat_get_conf(struct kmscon_seat *seat)
+ {
+ 	if (!seat)
+@@ -1371,36 +1124,39 @@ struct conf_ctx *kmscon_seat_get_conf(struct kmscon_seat *seat)
+ 	return seat->conf_ctx;
+ }
+ 
+-int kmscon_seat_register_session(struct kmscon_seat *seat, struct kmscon_session **out,
+-				 kmscon_session_cb_t cb, void *data)
++static struct kmscon_session *kmscon_seat_new_session(struct kmscon_seat *seat)
+ {
+ 	struct kmscon_session *sess;
+ 	struct shl_dlist *iter;
+ 	struct kmscon_display *d;
+ 
+-	if (!seat || !out)
+-		return -EINVAL;
++	if (!seat)
++		return NULL;
+ 
+ 	if (seat->conf->session_max && seat->session_count >= seat->conf->session_max) {
+ 		log_warning("maximum number of sessions reached (%d), dropping new session",
+ 			    seat->conf->session_max);
+-		return -EOVERFLOW;
++		return NULL;
+ 	}
+ 
+ 	sess = malloc(sizeof(*sess));
+ 	if (!sess) {
+ 		log_error("cannot allocate memory for new session on seat %s", seat->name);
+-		return -ENOMEM;
++		return NULL;
+ 	}
+ 
+ 	log_debug("register session %p", sess);
+ 
+ 	memset(sess, 0, sizeof(*sess));
+-	sess->ref = 1;
+ 	sess->seat = seat;
+-	sess->cb = cb;
+-	sess->data = data;
+ 	sess->foreground = true;
++	sess->term = terminal_new(sess, uterm_vt_get_num(seat->vt), seat->conf_ctx, seat->eloop,
++				  seat->input, seat->name);
++	if (!sess->term) {
++		log_error("cannot create terminal for new session on seat %s", seat->name);
++		free(sess);
++		return NULL;
++	}
+ 
+ 	/* register new sessions next to the current one */
+ 	if (seat->current_sess)
+@@ -1409,39 +1165,18 @@ int kmscon_seat_register_session(struct kmscon_seat *seat, struct kmscon_session
+ 		shl_dlist_link_tail(&seat->sessions, &sess->list);
+ 
+ 	++seat->session_count;
+-	*out = sess;
+ 
+ 	shl_dlist_for_each(iter, &seat->displays)
+ 	{
+ 		d = shl_dlist_entry(iter, struct kmscon_display, list);
+-		session_call_display_new(sess, d->disp);
++		terminal_add_display(sess->term, d->disp);
+ 	}
+-
+-	return 0;
+-}
+-
+-void kmscon_session_ref(struct kmscon_session *sess)
+-{
+-	if (!sess || !sess->ref)
+-		return;
+-
+-	++sess->ref;
++	return sess;
+ }
+ 
+-void kmscon_session_unref(struct kmscon_session *sess)
+-{
+-	if (!sess || !sess->ref || --sess->ref)
+-		return;
+-
+-	kmscon_session_unregister(sess);
+-	free(sess);
+-}
+-
+-void kmscon_session_unregister(struct kmscon_session *sess)
++static void kmscon_session_unregister(struct kmscon_session *sess)
+ {
+ 	struct kmscon_seat *seat;
+-	int ret;
+-	bool forced = false;
+ 
+ 	if (!sess || !sess->seat)
+ 		return;
+@@ -1449,47 +1184,18 @@ void kmscon_session_unregister(struct kmscon_session *sess)
+ 	log_debug("unregister session %p", sess);
+ 
+ 	seat = sess->seat;
+-	sess->enabled = false;
+-	if (seat->dummy_sess == sess)
+-		seat->dummy_sess = NULL;
+-	seat_reschedule(seat);
+ 
+ 	if (seat->current_sess == sess) {
+-		ret = seat_pause(seat, true);
+-		if (ret) {
+-			forced = true;
+-			log_warning("unregistering active session %p; skipping automatic "
+-				    "session-switch",
+-				    sess);
+-		}
++		terminal_deactivate(seat->current_sess->term);
++		seat_next(seat);
+ 	}
+ 
+ 	shl_dlist_unlink(&sess->list);
+ 	--seat->session_count;
+ 	sess->seat = NULL;
+ 
+-	session_call(sess, KMSCON_SESSION_UNREGISTER, NULL);
+-	kmscon_session_unref(sess);
+-
+-	/* If this session was active and we couldn't deactivate it, then it
+-	 * might still have resources allocated that couldn't get freed. In this
+-	 * case we should not automatically switch to the next session as it is
+-	 * very likely that it will not be able to start.
+-	 * Instead, we stay inactive and wait for user/external input to switch
+-	 * to another session. This delay will then hopefully be long enough so
+-	 * all resources got freed. */
+-	if (!forced)
+-		seat_run(seat);
+-}
+-
+-bool kmscon_session_is_registered(struct kmscon_session *sess)
+-{
+-	return sess && sess->seat;
+-}
+-
+-bool kmscon_session_is_active(struct kmscon_session *sess)
+-{
+-	return sess && sess->seat && sess->seat->current_sess == sess;
++	terminal_destroy(sess->term);
++	free(sess);
+ }
+ 
+ int kmscon_session_set_foreground(struct kmscon_session *sess)
+@@ -1508,7 +1214,7 @@ int kmscon_session_set_foreground(struct kmscon_session *sess)
+ 		if (ret)
+ 			return ret;
+ 
+-		ret = seat_go_foreground(seat, true);
++		ret = seat_go_foreground(seat);
+ 		if (ret)
+ 			return ret;
+ 	}
+@@ -1529,7 +1235,7 @@ int kmscon_session_set_background(struct kmscon_session *sess)
+ 
+ 	seat = sess->seat;
+ 	if (seat && seat->current_sess == sess && seat->foreground) {
+-		ret = seat_go_background(seat, true);
++		ret = seat_go_background(seat);
+ 		if (ret)
+ 			return ret;
+ 	}
+@@ -1543,35 +1249,6 @@ bool kmscon_session_get_foreground(struct kmscon_session *sess)
+ 	return sess->foreground;
+ }
+ 
+-void kmscon_session_enable(struct kmscon_session *sess)
+-{
+-	if (!sess || sess->enabled)
+-		return;
+-
+-	log_debug("enable session %p", sess);
+-	sess->enabled = true;
+-	if (sess->seat &&
+-	    (!sess->seat->current_sess || sess->seat->current_sess == sess->seat->dummy_sess)) {
+-		sess->seat->scheduled_sess = sess;
+-		if (seat_has_schedule(sess->seat))
+-			seat_switch(sess->seat);
+-	}
+-}
+-
+-void kmscon_session_disable(struct kmscon_session *sess)
+-{
+-	if (!sess || !sess->enabled)
+-		return;
+-
+-	log_debug("disable session %p", sess);
+-	sess->enabled = false;
+-}
+-
+-bool kmscon_session_is_enabled(struct kmscon_session *sess)
+-{
+-	return sess && sess->enabled;
+-}
+-
+ void kmscon_session_bell(struct kmscon_session *sess)
+ {
+ 	if (!sess || !sess->seat)
+@@ -1588,36 +1265,3 @@ void kmscon_session_set_leds(struct kmscon_session *sess, unsigned int scroll_lo
+ 
+ 	input_set_leds(sess->seat->input, scroll_lock, num_lock, caps_lock);
+ }
+-
+-void kmscon_session_notify_deactivated(struct kmscon_session *sess)
+-{
+-	struct kmscon_seat *seat;
+-	int ret;
+-	unsigned int sched;
+-
+-	if (!sess || !sess->seat)
+-		return;
+-
+-	seat = sess->seat;
+-	if (seat->current_sess != sess)
+-		return;
+-
+-	sched = seat->async_schedule;
+-	log_debug("session %p notified core about deactivation (schedule: %u)", sess, sched);
+-	session_deactivate(sess);
+-	seat_reschedule(seat);
+-
+-	if (sched == SCHEDULE_VT) {
+-		ret = seat_go_background(seat, false);
+-		if (ret)
+-			return;
+-		ret = seat_go_asleep(seat, false);
+-		if (ret)
+-			return;
+-		uterm_vt_retry(seat->vt);
+-	} else if (sched == SCHEDULE_UNREGISTER) {
+-		kmscon_session_unregister(sess);
+-	} else {
+-		seat_switch(seat);
+-	}
+-}
+diff --git a/src/kmscon_seat.h b/src/kmscon_seat.h
+index c3fa707..31e8808 100644
+--- a/src/kmscon_seat.h
++++ b/src/kmscon_seat.h
+@@ -33,14 +33,9 @@
+ #define KMSCON_SEAT_H
+ 
+ #include <stdbool.h>
+-#include <stdlib.h>
+ #include "conf.h"
+-#include "input/input.h"
+ #include "kmscon_conf.h"
+ #include "shl/eloop.h"
+-#include "uterm_monitor.h"
+-#include "uterm_vt.h"
+-#include "video/video.h"
+ 
+ struct kmscon_seat;
+ struct kmscon_session;
+@@ -53,54 +48,22 @@ enum kmscon_seat_event {
+ 
+ typedef int (*kmscon_seat_cb_t)(struct kmscon_seat *seat, unsigned int event, void *data);
+ 
+-enum kmscon_session_event_type {
+-	KMSCON_SESSION_DISPLAY_NEW,
+-	KMSCON_SESSION_DISPLAY_GONE,
+-	KMSCON_SESSION_DISPLAY_REFRESH,
+-	KMSCON_SESSION_ACTIVATE,
+-	KMSCON_SESSION_DEACTIVATE,
+-	KMSCON_SESSION_UNREGISTER,
+-};
+-
+-struct kmscon_session_event {
+-	unsigned int type;
+-	struct display *disp;
+-};
+-
+-typedef int (*kmscon_session_cb_t)(struct kmscon_session *session,
+-				   struct kmscon_session_event *event, void *data);
+-
+ int kmscon_seat_new(struct kmscon_seat **out, struct conf_ctx *main_conf,
+ 		    struct kmscon_conf_t *conf, struct ev_eloop *eloop, kmscon_seat_cb_t cb,
+ 		    void *data);
+ void kmscon_seat_free(struct kmscon_seat *seat);
+ void kmscon_seat_startup(struct kmscon_seat *seat);
++int kmscon_seat_update_xkb_layout(struct kmscon_seat *seat, const char *model, const char *layout,
++				  const char *variant, const char *options);
+ 
+-const char *kmscon_seat_get_name(struct kmscon_seat *seat);
+-struct input *kmscon_seat_get_input(struct kmscon_seat *seat);
+-struct ev_eloop *kmscon_seat_get_eloop(struct kmscon_seat *seat);
+ struct conf_ctx *kmscon_seat_get_conf(struct kmscon_seat *seat);
+ 
+-int kmscon_seat_register_session(struct kmscon_seat *seat, struct kmscon_session **out,
+-				 kmscon_session_cb_t cb, void *data);
+-
+-void kmscon_session_ref(struct kmscon_session *sess);
+-void kmscon_session_unref(struct kmscon_session *sess);
+-void kmscon_session_unregister(struct kmscon_session *sess);
+-bool kmscon_session_is_registered(struct kmscon_session *sess);
+-
+-bool kmscon_session_is_active(struct kmscon_session *sess);
+ int kmscon_session_set_foreground(struct kmscon_session *sess);
+ int kmscon_session_set_background(struct kmscon_session *sess);
+ bool kmscon_session_get_foreground(struct kmscon_session *sess);
+ 
+-void kmscon_session_enable(struct kmscon_session *sess);
+-void kmscon_session_disable(struct kmscon_session *sess);
+-bool kmscon_session_is_enabled(struct kmscon_session *sess);
+ void kmscon_session_bell(struct kmscon_session *sess);
+ void kmscon_session_set_leds(struct kmscon_session *sess, unsigned int scroll_lock,
+ 			     unsigned int num_lock, unsigned int caps_lock);
+ 
+-void kmscon_session_notify_deactivated(struct kmscon_session *sess);
+-
+ #endif /* KMSCON_SEAT_H */
+diff --git a/src/kmscon_terminal.c b/src/kmscon_terminal.c
+index 391cd53..c5562f6 100644
+--- a/src/kmscon_terminal.c
++++ b/src/kmscon_terminal.c
+@@ -645,7 +645,7 @@ static void rotate_ccw_all(struct kmscon_terminal *term)
+ 	update_pointer_max_all(term);
+ }
+ 
+-static int add_display(struct kmscon_terminal *term, struct display *disp)
++int terminal_add_display(struct kmscon_terminal *term, struct display *disp)
+ {
+ 	struct shl_dlist *iter;
+ 	struct screen *scr;
+@@ -741,7 +741,7 @@ static void free_screen(struct screen *scr, bool update)
+ 	terminal_update_size_notify(term);
+ }
+ 
+-static void rm_display(struct kmscon_terminal *term, struct display *disp)
++void terminal_rm_display(struct kmscon_terminal *term, struct display *disp)
+ {
+ 	struct shl_dlist *iter;
+ 	struct screen *scr;
+@@ -1080,7 +1080,30 @@ static void terminal_close(struct kmscon_terminal *term)
+ 	term->opened = false;
+ }
+ 
+-static void terminal_destroy(struct kmscon_terminal *term)
++void terminal_refresh_displays(struct kmscon_terminal *term)
++{
++	if (term->pointer.visible)
++		hw_cursor_show(term, term->pointer.x, term->pointer.y);
++	redraw_all_text(term);
++}
++
++void terminal_activate(struct kmscon_terminal *term)
++{
++	term->awake = true;
++	if (!term->opened)
++		terminal_open(term);
++	if (term->pointer.visible)
++		hw_cursor_show(term, term->pointer.x, term->pointer.y);
++	redraw_all_text(term);
++}
++
++void terminal_deactivate(struct kmscon_terminal *term)
++{
++	term->awake = false;
++	hw_cursor_hide(term);
++}
++
++void terminal_destroy(struct kmscon_terminal *term)
+ {
+ 	log_debug("free terminal object %p", term);
+ 
+@@ -1099,43 +1122,6 @@ static void terminal_destroy(struct kmscon_terminal *term)
+ 	free(term);
+ }
+ 
+-static int session_event(struct kmscon_session *session, struct kmscon_session_event *ev,
+-			 void *data)
+-{
+-	struct kmscon_terminal *term = data;
+-
+-	switch (ev->type) {
+-	case KMSCON_SESSION_DISPLAY_NEW:
+-		add_display(term, ev->disp);
+-		break;
+-	case KMSCON_SESSION_DISPLAY_GONE:
+-		rm_display(term, ev->disp);
+-		break;
+-	case KMSCON_SESSION_DISPLAY_REFRESH:
+-		if (term->pointer.visible)
+-			hw_cursor_show(term, term->pointer.x, term->pointer.y);
+-		redraw_all_text(term);
+-		break;
+-	case KMSCON_SESSION_ACTIVATE:
+-		term->awake = true;
+-		if (!term->opened)
+-			terminal_open(term);
+-		if (term->pointer.visible)
+-			hw_cursor_show(term, term->pointer.x, term->pointer.y);
+-		redraw_all_text(term);
+-		break;
+-	case KMSCON_SESSION_DEACTIVATE:
+-		term->awake = false;
+-		hw_cursor_hide(term);
+-		break;
+-	case KMSCON_SESSION_UNREGISTER:
+-		terminal_destroy(term);
+-		break;
+-	}
+-
+-	return 0;
+-}
+-
+ static void pty_input(struct kmscon_pty *pty, const char *u8, size_t len, void *data)
+ {
+ 	struct kmscon_terminal *term = data;
+@@ -1163,26 +1149,25 @@ static void write_event(struct tsm_vte *vte, const char *u8, size_t len, void *d
+ 	kmscon_pty_write(term->pty, u8, len);
+ }
+ 
+-int kmscon_terminal_register(struct kmscon_session **out, struct kmscon_seat *seat,
+-			     unsigned int vtnr)
++struct kmscon_terminal *terminal_new(struct kmscon_session *session, unsigned int vtnr,
++				     struct conf_ctx *conf_ctx, struct ev_eloop *eloop,
++				     struct input *input, const char *seat_name)
+ {
+ 	struct kmscon_terminal *term;
+ 	int ret;
+ 
+-	if (!out || !seat)
+-		return -EINVAL;
+-
+ 	term = malloc(sizeof(*term));
+ 	if (!term)
+-		return -ENOMEM;
++		return NULL;
+ 
+ 	memset(term, 0, sizeof(*term));
+ 	term->ref = 1;
+-	term->eloop = kmscon_seat_get_eloop(seat);
+-	term->input = kmscon_seat_get_input(seat);
++	term->session = session;
++	term->eloop = eloop;
++	term->input = input;
+ 	shl_dlist_init(&term->screens);
+ 
+-	term->conf_ctx = kmscon_seat_get_conf(seat);
++	term->conf_ctx = conf_ctx;
+ 	term->conf = conf_ctx_get_mem(term->conf_ctx);
+ 
+ 	strncpy(term->font_attr.name, term->conf->font_name, KMSCON_FONT_MAX_NAME - 1);
+@@ -1221,7 +1206,7 @@ int kmscon_terminal_register(struct kmscon_session **out, struct kmscon_seat *se
+ 		goto err_font;
+ 
+ 	ret = kmscon_pty_set_conf(term->pty, term->conf->term, "truecolor", term->conf->argv,
+-				  kmscon_seat_get_name(seat), vtnr, term->conf->reset_env,
++				  seat_name, vtnr, term->conf->reset_env,
+ 				  term->conf->backspace_delete);
+ 	if (ret)
+ 		goto err_pty;
+@@ -1241,20 +1226,11 @@ int kmscon_terminal_register(struct kmscon_session **out, struct kmscon_seat *se
+ 			goto err_input;
+ 	}
+ 
+-	ret = kmscon_seat_register_session(seat, &term->session, session_event, term);
+-	if (ret) {
+-		log_error("cannot register session for terminal: %d", ret);
+-		goto err_pointer;
+-	}
+-
+ 	ev_eloop_ref(term->eloop);
+ 	input_ref(term->input);
+-	*out = term->session;
+ 	log_debug("new terminal object %p", term);
+-	return 0;
++	return term;
+ 
+-err_pointer:
+-	input_unregister_pointer_cb(term->input, pointer_event, term);
+ err_input:
+ 	input_unregister_key_cb(term->input, input_event, term);
+ err_ptyfd:
+@@ -1269,5 +1245,5 @@ err_con:
+ 	tsm_screen_unref(term->console);
+ err_free:
+ 	free(term);
+-	return ret;
++	return NULL;
+ }
+diff --git a/src/kmscon_terminal.h b/src/kmscon_terminal.h
+index cd74bab..552242c 100644
+--- a/src/kmscon_terminal.h
++++ b/src/kmscon_terminal.h
+@@ -33,23 +33,21 @@
+ #ifndef KMSCON_TERMINAL_H
+ #define KMSCON_TERMINAL_H
+ 
+-#include <errno.h>
+-#include <stdlib.h>
+-#include "kmscon_seat.h"
+-
+-#ifdef BUILD_ENABLE_SESSION_TERMINAL
+-
+-int kmscon_terminal_register(struct kmscon_session **out, struct kmscon_seat *seat,
+-			     unsigned int vtnr);
+-
+-#else /* !BUILD_ENABLE_SESSION_TERMINAL */
+-
+-static inline int kmscon_terminal_register(struct kmscon_session **out, struct kmscon_seat *seat,
+-					   unsigned int vtnr)
+-{
+-	return -EOPNOTSUPP;
+-}
+-
+-#endif /* BUILD_ENABLE_SESSION_TERMINAL */
++struct conf_ctx;
++struct display;
++struct ev_eloop;
++struct kmscon_terminal;
++struct kmscon_session;
++struct input;
++
++struct kmscon_terminal *terminal_new(struct kmscon_session *session, unsigned int vtnr,
++				     struct conf_ctx *conf_ctx, struct ev_eloop *eloop,
++				     struct input *input, const char *seat_name);
++void terminal_destroy(struct kmscon_terminal *term);
++int terminal_add_display(struct kmscon_terminal *term, struct display *disp);
++void terminal_rm_display(struct kmscon_terminal *term, struct display *disp);
++void terminal_refresh_displays(struct kmscon_terminal *term);
++void terminal_activate(struct kmscon_terminal *term);
++void terminal_deactivate(struct kmscon_terminal *term);
+ 
+ #endif /* KMSCON_TERMINAL_H */
+diff --git a/src/meson.build b/src/meson.build
+index 9547fe1..1c4ba12 100644
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -88,16 +88,27 @@ kmscon_srcs = [
+   'kmscon_conf.c',
+   'kmscon_issue.c',
+   'kmscon_main.c',
++  'kmscon_terminal.c',
+ ]
+-if enable_session_dummy
+-  kmscon_srcs += 'kmscon_dummy.c'
+-endif
+-if enable_session_terminal
+-  kmscon_srcs += 'kmscon_terminal.c'
++kmscon_dep = [xkbcommon_deps,
++  libtsm_deps,
++  threads_deps,
++  dl_deps,
++  conf_deps,
++  shl_deps,
++  eloop_deps,
++  uterm_deps,
++  render_deps,
++  font_deps,
++]
++
++if enable_dbus
++  kmscon_srcs += 'dbus.c'
++  kmscon_dep += dbus_deps
+ endif
+ kmscon = executable('kmscon', kmscon_srcs,
+-  dependencies: [xkbcommon_deps, libtsm_deps, threads_deps, dl_deps, conf_deps, shl_deps, eloop_deps, uterm_deps, render_deps, font_deps],
++  dependencies: kmscon_dep,
+   export_dynamic: true,
+   install: true,
+-  install_dir: libexecdir,
++  install_dir: bindir,
+ )
+diff --git a/src/shl/dlist.h b/src/shl/dlist.h
+index 83cd233..0af5094 100644
+--- a/src/shl/dlist.h
++++ b/src/shl/dlist.h
+@@ -102,6 +102,16 @@ static inline bool shl_dlist_empty(struct shl_dlist *head)
+ 
+ #define shl_dlist_last(head, type, member) shl_dlist_entry((head)->prev, type, member)
+ 
++#define shl_dlist_next(iter, head, member)                                                         \
++	((iter)->member.next == (head)                                                             \
++		 ? NULL                                                                            \
++		 : shl_dlist_entry((iter)->member.next, typeof(*iter), list))
++
++#define shl_dlist_prev(iter, head, member)                                                         \
++	((iter)->member.prev == (head)                                                             \
++		 ? NULL                                                                            \
++		 : shl_dlist_entry((iter)->member.prev, typeof(*iter), list))
++
+ #define shl_dlist_for_each(iter, head) for (iter = (head)->next; iter != (head); iter = iter->next)
+ 
+ #define shl_dlist_for_each_but_one(iter, start, head)                                              \
+diff --git a/tests/test_input.c b/tests/test_input.c
+index 0ecefc0..b218edc 100644
+--- a/tests/test_input.c
++++ b/tests/test_input.c
+@@ -128,11 +128,12 @@ static void setup_input(struct uterm_monitor *mon)
+ 	if (ret)
+ 		return;
+ 	ret = input_set_keymap(input, input_conf.xkb_model, input_conf.xkb_layout,
+-			       input_conf.xkb_variant, input_conf.xkb_options, input_conf.locale,
+-			       input_conf.xkb_keymap, input_conf.xkb_compose_file,
+-			       compose_file_len);
++			       input_conf.xkb_variant, input_conf.xkb_options,
++			       input_conf.xkb_keymap);
+ 	if (ret)
+ 		return;
++
++	input_set_compose(input, input_conf.locale, compose_file, compose_file_len);
+ 	input_set_conf(input, 250, 50, true);
+ 	ret = input_register_key_cb(input, input_arrived, NULL);
+ 	if (ret)
+-- 
+2.54.0
+

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-06-05 11:19 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-05 11:19 [rpms/kmscon] rawhide: Add localectl runtime support Jocelyn Falempe

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox