diff options
| author | Adam Malczewski <[email protected]> | 2026-06-13 16:05:28 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-13 16:05:28 +0900 |
| commit | 9ee2d3ceee93ff0d0afdb47f594ea4fee95455fc (patch) | |
| tree | 4a1b73d8f140c88775d0487434ba23e807fc441f | |
| parent | 11812b0374d5de395e2c17532c6bf89a903ee043 (diff) | |
| download | unbox-9ee2d3ceee93ff0d0afdb47f594ea4fee95455fc.tar.gz unbox-9ee2d3ceee93ff0d0afdb47f594ea4fee95455fc.zip | |
Kernel: Ctrl+Alt+F1..F12 VT switching (session escape hatch)
Intercept the XF86Switch_VT_1..12 keysyms before the keybinding filter and call
wlr_session_change_vt, so the user can always switch consoles while unbox runs.
Clean no-op without a session (headless/nested). Pure vt_for_keysym helper +
doctest; wlroots reached via the wlr.hpp wrapper.
Real-seat verified on the CF-AX3.
| -rw-r--r-- | packages/kernel/include/unbox/kernel/wlr.hpp | 9 | ||||
| -rw-r--r-- | packages/kernel/src/input.cpp | 19 | ||||
| -rw-r--r-- | packages/kernel/src/server.cpp | 5 | ||||
| -rw-r--r-- | packages/kernel/src/server_impl.hpp | 4 | ||||
| -rw-r--r-- | packages/kernel/src/vt_core.hpp | 33 | ||||
| -rw-r--r-- | packages/kernel/tests/test_kernel.cpp | 32 |
6 files changed, 101 insertions, 1 deletions
diff --git a/packages/kernel/include/unbox/kernel/wlr.hpp b/packages/kernel/include/unbox/kernel/wlr.hpp index 496d737..c890774 100644 --- a/packages/kernel/include/unbox/kernel/wlr.hpp +++ b/packages/kernel/include/unbox/kernel/wlr.hpp @@ -27,6 +27,15 @@ extern "C" { // protocol/wlr headers (only `namespace` collides in the current set). #define namespace _namespace #include <wlr/backend.h> +// Session escape-hatch (kernel VT switching, Ctrl+Alt+Fn): wlr_session + +// wlr_session_change_vt. In wlroots 0.20 the session is NOT fetched from the +// backend (there is no wlr_backend_get_session); it is the out-param of +// wlr_backend_autocreate, which the kernel captures at init. NULL under the +// headless/nested backends (no libseat session) — the glue no-ops then. +// Static-blanking re-audit: this header is plain declarations only (no +// header-inline function with a function-local static, no `[static N]` +// array-param), so the surrounding `#define static` is inert across it. +#include <wlr/backend/session.h> #include <wlr/render/allocator.h> // Slice-3 spike (RMLUi -> wlr_scene bridge): EGL/dmabuf, the GLES2 renderer's // EGL accessors, buffer (dmabuf + data-ptr access for the shm fallback), diff --git a/packages/kernel/src/input.cpp b/packages/kernel/src/input.cpp index 336947e..6056c53 100644 --- a/packages/kernel/src/input.cpp +++ b/packages/kernel/src/input.cpp @@ -1,5 +1,7 @@ #include "server_impl.hpp" +#include "vt_core.hpp" + #include <xkbcommon/xkbcommon.h> namespace unbox::kernel { @@ -75,6 +77,23 @@ void Server::Impl::new_keyboard(wlr_input_device* device) { const std::uint32_t modifiers = wlr_keyboard_get_modifiers(keyboard->keyboard); const bool pressed = event->state == WL_KEYBOARD_KEY_STATE_PRESSED; + // SESSION ESCAPE HATCH: Ctrl+Alt+Fn (XF86Switch_VT_1..12) switches the + // Linux VT. Handled HERE, kernel-hardwired, BEFORE the key_filter, so no + // extension can intercept, consume, or block the only escape from the + // real DRM seat (user decision: not config-driven, not rebindable). On + // PRESS we change the VT; we CONSUME both press and release (the matching + // release carries the same keysym) so the filter never runs and the key + // never reaches the focused client. No session (headless/nested => + // session is NULL) is a clean no-op — never crash. + for (int i = 0; i < nsyms; ++i) { + if (const std::optional<unsigned> vt = vt_for_keysym(syms[i])) { + if (pressed && session != nullptr) { + wlr_session_change_vt(session, *vt); + } + return; // consume: no filter, no client forward (press or release) + } + } + // Thread each resolved keysym through the key_filter; a filter link may // CONSUME the key (set handled=true) — that is how extensions implement // compositor shortcuts. If any resolution is consumed, suppress the diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp index a0627f4..a5f8365 100644 --- a/packages/kernel/src/server.cpp +++ b/packages/kernel/src/server.cpp @@ -145,7 +145,10 @@ void Server::Impl::init() { wlr_log_init(WLR_INFO, nullptr); display = require(wl_display_create(), "wl_display"); - backend = require(wlr_backend_autocreate(wl_display_get_event_loop(display), nullptr), + // Capture the session out-param: on the real DRM seat it is the libseat + // session the VT-switch escape hatch (input.cpp) drives; NULL under + // headless/nested (no real seat), where VT switching no-ops cleanly. + backend = require(wlr_backend_autocreate(wl_display_get_event_loop(display), &session), "wlr_backend"); renderer = require(wlr_renderer_autocreate(backend), "wlr_renderer"); wlr_renderer_init_wl_display(renderer, display); diff --git a/packages/kernel/src/server_impl.hpp b/packages/kernel/src/server_impl.hpp index 3407883..ef4a479 100644 --- a/packages/kernel/src/server_impl.hpp +++ b/packages/kernel/src/server_impl.hpp @@ -69,6 +69,10 @@ struct Server::Impl : detail::DisableSink { wl_display* display = nullptr; wlr_backend* backend = nullptr; + // The libseat/logind session, captured from wlr_backend_autocreate's + // out-param at init. NULL under headless/nested backends (no real seat) — + // the VT-switch escape hatch no-ops cleanly then. Owned by the backend. + wlr_session* session = nullptr; wlr_renderer* renderer = nullptr; wlr_allocator* allocator = nullptr; wlr_scene* scene = nullptr; diff --git a/packages/kernel/src/vt_core.hpp b/packages/kernel/src/vt_core.hpp new file mode 100644 index 0000000..d08837d --- /dev/null +++ b/packages/kernel/src/vt_core.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <optional> + +#include <xkbcommon/xkbcommon.h> + +// Pure decision core for the kernel's VT-switch escape hatch — NO wlroots / GL, +// so it is doctest-able with nothing running (AGENTS.md: effects at the edges, +// pure cores tested hard). The keyboard glue (input.cpp) injects the effect +// (wlr_session_change_vt) around this. +// +// xkbcommon is allowed here: it is a pure value library (a keysym is an +// integer), not a wlroots/GL/wayland effect surface — the wlroots-include rule +// targets <wlr/...> + <wayland-server*.h>, and input.cpp already includes +// <xkbcommon/xkbcommon.h> for keysym resolution. + +namespace unbox::kernel { + +// Map a resolved keysym to the Linux VT it requests, or nullopt if it is not a +// VT-switch keysym. Ctrl+Alt+Fn is delivered by xkb as the contiguous range +// XKB_KEY_XF86Switch_VT_1 (0x1008FE01) .. XKB_KEY_XF86Switch_VT_12 +// (0x1008FE0C); the returned value is the 1-based VT number (1..12). These +// keysyms are ONLY produced with Ctrl+Alt held, so matching the keysym is +// sufficient — no separate modifier-mask check. Plain F1..F12 resolve to the +// ordinary XKB_KEY_F1.. keysyms and are NOT in this range, so they pass through. +[[nodiscard]] constexpr auto vt_for_keysym(xkb_keysym_t keysym) -> std::optional<unsigned> { + if (keysym >= XKB_KEY_XF86Switch_VT_1 && keysym <= XKB_KEY_XF86Switch_VT_12) { + return static_cast<unsigned>(keysym - XKB_KEY_XF86Switch_VT_1) + 1U; + } + return std::nullopt; +} + +} // namespace unbox::kernel diff --git a/packages/kernel/tests/test_kernel.cpp b/packages/kernel/tests/test_kernel.cpp index dbb5c02..912d35e 100644 --- a/packages/kernel/tests/test_kernel.cpp +++ b/packages/kernel/tests/test_kernel.cpp @@ -13,9 +13,12 @@ // state machine, implicit-grab ownership, hit-test geometry) are doctest-ed // directly, no wlroots. #include "../src/ui_core.hpp" +// The VT-switch escape hatch's pure core (keysym -> VT number), no wlroots. +#include "../src/vt_core.hpp" #include <cstdlib> #include <memory> +#include <optional> #include <stdexcept> #include <string> #include <vector> @@ -349,6 +352,35 @@ TEST_CASE("substrate: a click over a ui surface is CONSUMED (no click-through)") } // ============================================================================ +// VT-switch escape hatch — PURE CORE (no wlroots): keysym -> VT number. The +// glue (input.cpp) calls wlr_session_change_vt on a hit and consumes; this +// helper decides the hit. Ctrl+Alt+Fn arrives as XF86Switch_VT_1..12. +// ============================================================================ + +TEST_CASE("vt_for_keysym: maps the XF86Switch_VT range to 1..12") { + using unbox::kernel::vt_for_keysym; + + // Both endpoints of the range. + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_1) == 1U); + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_12) == 12U); + // A representative interior value. + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_2) == 2U); + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_7) == 7U); + + // Just outside the range on both sides => nullopt (no VT-switch). + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_1 - 1) == std::nullopt); + CHECK(vt_for_keysym(XKB_KEY_XF86Switch_VT_12 + 1) == std::nullopt); + + // Plain F1..F12 (no Ctrl+Alt) resolve to ordinary keysyms, NOT the + // XF86Switch_VT range — they must pass through untouched. + CHECK(vt_for_keysym(XKB_KEY_F1) == std::nullopt); + CHECK(vt_for_keysym(XKB_KEY_F12) == std::nullopt); + + // An unrelated keysym. + CHECK(vt_for_keysym(XKB_KEY_a) == std::nullopt); +} + +// ============================================================================ // The typed bus — PURE CORE (strict; zero mocks of unbox modules, no wlroots // running). A test DisableSink stands in for the kernel's isolation registry. // ============================================================================ |
