qt: parity tier 2 batch 3 — live XKB state via wl_keyboard (B22, B29)
New XkbTracker (XkbTracker.{h,cpp}): a process-singleton that binds
the compositor's wl_seat / wl_keyboard via the platform native
interface, listens for keymap, modifiers, enter/leave/key events,
and keeps a live xkb_keymap + xkb_state in sync with the user's
actual keyboard. Hooked in via QGuiApplication's wl_display.
B29 — Live XKB layout. XkbState now syncs from XkbTracker on
every public method call: when the live keymap pointer changes
(e.g. user switched layouts via the compositor's input-method
manager), we drop and rebuild m_unshifted + m_query from the new
keymap, and update the active group on m_unshifted. Result:
unshifted_codepoint and consumed_mods now honor the active layout
group. A us+ru user gets correct codepoints per active group
instead of always us. Mirrors GTK's event.getLayout() pattern.
B22 — Caps Lock and Num Lock state in mods. XkbTracker tracks
the locked-mods mask from wl_keyboard.modifiers; XkbState
exposes lockMods() returning GHOSTTY_MODS_CAPS / GHOSTTY_MODS_NUM
bits. sendKey ORs them into the event mods alongside sided bits
so kitty CSI-u and any binding that distinguishes caps-locked
letters now works.
The fallback for the early-startup window (compositor hasn't sent
the keymap yet) is a synthetic xkb_keymap_new_from_names with no
group; replaced as soon as the live keymap arrives via the
modifiers event.
This is Wayland-only (the only target the Qt frontend supports).
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
cd38f4bd5a
commit
913f192d87
|
|
@ -102,6 +102,7 @@ add_executable(ghastty
|
|||
src/TabWidget.cpp
|
||||
src/Util.cpp
|
||||
src/WindowBlur.cpp
|
||||
src/XkbTracker.cpp
|
||||
"${BLUR_CODE}"
|
||||
"${BLUR_HEADER}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "SearchBar.h"
|
||||
#include "TabWidget.h"
|
||||
#include "Util.h"
|
||||
#include "XkbTracker.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -595,7 +596,12 @@ public:
|
|||
|
||||
// Level-0 (unshifted) Unicode codepoint for `keycode`, or 0 if the
|
||||
// key has no associated UTF-32 (function keys, modifiers, etc.).
|
||||
//
|
||||
// Uses the live keymap from XkbTracker (synced via wl_keyboard) so
|
||||
// the active layout group is honored. A us+ru user gets the
|
||||
// correct codepoint per active group, instead of always us.
|
||||
uint32_t unshiftedCodepoint(uint32_t keycode) const {
|
||||
syncFromTracker();
|
||||
if (!m_unshifted) return 0;
|
||||
const xkb_keysym_t sym =
|
||||
xkb_state_key_get_one_sym(m_unshifted, keycode);
|
||||
|
|
@ -611,6 +617,7 @@ public:
|
|||
// bindings that distinguish left-vs-right modifier keys couldn't
|
||||
// fire.
|
||||
ghostty_input_mods_e sideBitsForKeycode(uint32_t keycode) const {
|
||||
syncFromTracker();
|
||||
if (!m_unshifted) return GHOSTTY_MODS_NONE;
|
||||
const xkb_keysym_t sym =
|
||||
xkb_state_key_get_one_sym(m_unshifted, keycode);
|
||||
|
|
@ -629,12 +636,23 @@ public:
|
|||
return static_cast<ghostty_input_mods_e>(r);
|
||||
}
|
||||
|
||||
// Caps Lock / Num Lock state from the live wl_keyboard tracker.
|
||||
ghostty_input_mods_e lockMods() const {
|
||||
int r = GHOSTTY_MODS_NONE;
|
||||
if (XkbTracker *t = XkbTracker::instance()) {
|
||||
if (t->capsLockOn()) r |= GHOSTTY_MODS_CAPS;
|
||||
if (t->numLockOn()) r |= GHOSTTY_MODS_NUM;
|
||||
}
|
||||
return static_cast<ghostty_input_mods_e>(r);
|
||||
}
|
||||
|
||||
// Modifiers consumed by the layout to produce `keycode`'s text given
|
||||
// `mods` are depressed. Returns the consumed subset, expressed as
|
||||
// ghostty mod bits. Mutates m_query (mutable) — see thread-safety
|
||||
// note on the class.
|
||||
ghostty_input_mods_e consumedMods(uint32_t keycode,
|
||||
ghostty_input_mods_e mods) const {
|
||||
syncFromTracker();
|
||||
if (!m_query) return GHOSTTY_MODS_NONE;
|
||||
xkb_mod_mask_t depressed = 0;
|
||||
if ((mods & GHOSTTY_MODS_SHIFT) && m_idxShift != XKB_MOD_INVALID)
|
||||
|
|
@ -645,11 +663,15 @@ public:
|
|||
depressed |= (1u << m_idxAlt);
|
||||
if ((mods & GHOSTTY_MODS_SUPER) && m_idxSuper != XKB_MOD_INVALID)
|
||||
depressed |= (1u << m_idxSuper);
|
||||
xkb_state_update_mask(m_query, depressed, 0, 0, 0, 0, 0);
|
||||
// Use the live group from the tracker so a layout switch (e.g.
|
||||
// us↔ru) takes effect immediately.
|
||||
const uint32_t group =
|
||||
XkbTracker::instance() ? XkbTracker::instance()->activeGroup() : 0;
|
||||
xkb_state_update_mask(m_query, depressed, 0, 0, 0, 0, group);
|
||||
const xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2(
|
||||
m_query, keycode, XKB_CONSUMED_MODE_XKB);
|
||||
// Reset so the next query starts from no-mods.
|
||||
xkb_state_update_mask(m_query, 0, 0, 0, 0, 0, 0);
|
||||
xkb_state_update_mask(m_query, 0, 0, 0, 0, 0, group);
|
||||
int r = GHOSTTY_MODS_NONE;
|
||||
if (m_idxShift != XKB_MOD_INVALID && (consumed & (1u << m_idxShift)))
|
||||
r |= GHOSTTY_MODS_SHIFT;
|
||||
|
|
@ -663,43 +685,76 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
XkbState() {
|
||||
m_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
if (!m_ctx) return;
|
||||
m_keymap = xkb_keymap_new_from_names(m_ctx, nullptr,
|
||||
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
if (!m_keymap) return;
|
||||
m_unshifted = xkb_state_new(m_keymap);
|
||||
m_query = xkb_state_new(m_keymap);
|
||||
m_idxShift = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_SHIFT);
|
||||
m_idxCtrl = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_CTRL);
|
||||
m_idxAlt = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_ALT);
|
||||
m_idxSuper = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_LOGO);
|
||||
// Lazy: build/rebuild m_unshifted + m_query from the live keymap.
|
||||
// Called from every public method; cheap when the keymap pointer
|
||||
// hasn't changed (a single comparison + early-return).
|
||||
void syncFromTracker() const {
|
||||
XkbTracker *t = XkbTracker::instance();
|
||||
xkb_keymap *liveKm = t ? t->keymap() : nullptr;
|
||||
xkb_keymap *km = liveKm ? liveKm : m_fallbackKeymap;
|
||||
|
||||
if (!km && t && t->ctx()) {
|
||||
// Compositor hasn't sent a keymap yet (early startup). Build a
|
||||
// throwaway from XKB defaults so the first key event isn't
|
||||
// dropped; it will be replaced on the next syncFromTracker
|
||||
// call once the tracker has the live keymap.
|
||||
m_fallbackKeymap = xkb_keymap_new_from_names(
|
||||
t->ctx(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
km = m_fallbackKeymap;
|
||||
}
|
||||
if (!km || km == m_keymap) {
|
||||
// Already synced (or no keymap available at all).
|
||||
// Update the live group on m_unshifted so the level-0 lookup
|
||||
// honors the active layout, even when the keymap pointer
|
||||
// hasn't changed.
|
||||
if (m_unshifted && t) {
|
||||
xkb_state_update_mask(m_unshifted, 0, 0, 0, 0, 0, t->activeGroup());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The live keymap was rebuilt by the tracker (or we're picking
|
||||
// up the first one). Drop our derived states and rebuild.
|
||||
if (m_unshifted) xkb_state_unref(m_unshifted);
|
||||
if (m_query) xkb_state_unref(m_query);
|
||||
m_unshifted = xkb_state_new(km);
|
||||
m_query = xkb_state_new(km);
|
||||
m_idxShift = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_SHIFT);
|
||||
m_idxCtrl = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_CTRL);
|
||||
m_idxAlt = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_ALT);
|
||||
m_idxSuper = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_LOGO);
|
||||
m_keymap = km; // pointer-identity comparison only; no ref taken
|
||||
if (t)
|
||||
xkb_state_update_mask(m_unshifted, 0, 0, 0, 0, 0, t->activeGroup());
|
||||
}
|
||||
|
||||
XkbState() = default;
|
||||
|
||||
~XkbState() {
|
||||
// Run on process exit when the static is destroyed. The OS would
|
||||
// reclaim regardless, but explicit teardown silences leak checkers
|
||||
// and documents the ownership chain.
|
||||
if (m_query) xkb_state_unref(m_query);
|
||||
if (m_unshifted) xkb_state_unref(m_unshifted);
|
||||
if (m_keymap) xkb_keymap_unref(m_keymap);
|
||||
if (m_ctx) xkb_context_unref(m_ctx);
|
||||
if (m_fallbackKeymap) xkb_keymap_unref(m_fallbackKeymap);
|
||||
}
|
||||
|
||||
XkbState(const XkbState &) = delete;
|
||||
XkbState &operator=(const XkbState &) = delete;
|
||||
|
||||
struct xkb_context *m_ctx = nullptr;
|
||||
struct xkb_keymap *m_keymap = nullptr;
|
||||
struct xkb_state *m_unshifted = nullptr; // permanent no-mods state
|
||||
// Reused across consumedMods calls (mutated then reset). Mutable so
|
||||
// consumedMods can stay logically const.
|
||||
// Pointer-identity reference to the keymap our derived states were
|
||||
// built from. NOT owned (the tracker or m_fallbackKeymap owns).
|
||||
mutable struct xkb_keymap *m_keymap = nullptr;
|
||||
// Throwaway keymap from XKB defaults, built when the live keymap
|
||||
// hasn't arrived yet. Owned. Released in dtor; never replaced.
|
||||
mutable struct xkb_keymap *m_fallbackKeymap = nullptr;
|
||||
mutable struct xkb_state *m_unshifted = nullptr; // no-mods state
|
||||
// Reused across consumedMods calls (mutated then reset).
|
||||
mutable struct xkb_state *m_query = nullptr;
|
||||
xkb_mod_index_t m_idxShift = XKB_MOD_INVALID;
|
||||
xkb_mod_index_t m_idxCtrl = XKB_MOD_INVALID;
|
||||
xkb_mod_index_t m_idxAlt = XKB_MOD_INVALID;
|
||||
xkb_mod_index_t m_idxSuper = XKB_MOD_INVALID;
|
||||
mutable xkb_mod_index_t m_idxShift = XKB_MOD_INVALID;
|
||||
mutable xkb_mod_index_t m_idxCtrl = XKB_MOD_INVALID;
|
||||
mutable xkb_mod_index_t m_idxAlt = XKB_MOD_INVALID;
|
||||
mutable xkb_mod_index_t m_idxSuper = XKB_MOD_INVALID;
|
||||
};
|
||||
|
||||
void GhosttySurface::sendKey(QKeyEvent *ev, ghostty_input_action_e action) {
|
||||
|
|
@ -725,11 +780,13 @@ void GhosttySurface::sendKey(QKeyEvent *ev, ghostty_input_action_e action) {
|
|||
k.action = action;
|
||||
k.mods = translateMods(ev->modifiers());
|
||||
// OR in any right-side bit for this keycode (e.g. Right-Shift sets
|
||||
// SHIFT_RIGHT alongside SHIFT). macOS + GTK populate these; without
|
||||
// SHIFT_RIGHT alongside SHIFT) and the live Caps/Num lock state
|
||||
// from XkbTracker. macOS + GTK populate all of these; without
|
||||
// them, keybinds like `right_shift+x` can't distinguish from
|
||||
// `left_shift+x`.
|
||||
// `left_shift+x` and the kitty CSI-u encoding loses the lock bits.
|
||||
k.mods = static_cast<ghostty_input_mods_e>(
|
||||
k.mods | XkbState::instance().sideBitsForKeycode(keycode));
|
||||
k.mods | XkbState::instance().sideBitsForKeycode(keycode) |
|
||||
XkbState::instance().lockMods());
|
||||
k.keycode = keycode;
|
||||
k.text = printable ? text.constData() : nullptr;
|
||||
// XKB lookups: unshifted codepoint (what this physical key would
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
#include "XkbTracker.h"
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Listener structs assembled from the static thunks below.
|
||||
const wl_keyboard_listener kKeyboardListener = {
|
||||
&XkbTracker::onKeymap, &XkbTracker::onEnter, &XkbTracker::onLeave,
|
||||
&XkbTracker::onKey, &XkbTracker::onModifiers,
|
||||
&XkbTracker::onRepeatInfo,
|
||||
};
|
||||
const wl_seat_listener kSeatListener = {
|
||||
&XkbTracker::onSeatCapabilities, &XkbTracker::onSeatName,
|
||||
};
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
&XkbTracker::onRegistryGlobal, &XkbTracker::onRegistryGlobalRemove,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
XkbTracker *XkbTracker::instance() {
|
||||
// Singleton initialised on first call. If Wayland binding fails
|
||||
// (e.g. running under XWayland with no exposed wl_seat) we return
|
||||
// a non-null tracker that simply has no state — callers handle a
|
||||
// null xkb_state gracefully.
|
||||
static XkbTracker self;
|
||||
return &self;
|
||||
}
|
||||
|
||||
XkbTracker::XkbTracker() {
|
||||
m_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
if (!m_ctx) return;
|
||||
|
||||
QPlatformNativeInterface *native =
|
||||
QGuiApplication::platformNativeInterface();
|
||||
if (!native) return;
|
||||
auto *display = static_cast<wl_display *>(
|
||||
native->nativeResourceForIntegration("wl_display"));
|
||||
if (!display) return;
|
||||
|
||||
// Enumerate the registry on a private event queue so we don't
|
||||
// disturb Qt's own queue. After we find wl_seat and get the
|
||||
// wl_keyboard, the keyboard proxy is moved back to the default
|
||||
// queue so Qt's event loop drives our listener callbacks.
|
||||
wl_event_queue *queue = wl_display_create_queue(display);
|
||||
wl_registry *registry = wl_display_get_registry(display);
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(registry), queue);
|
||||
wl_registry_add_listener(registry, &kRegistryListener, this);
|
||||
wl_display_roundtrip_queue(display, queue);
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
// Roundtrip again to receive seat capabilities and pick up the
|
||||
// wl_keyboard; the registry pass only binds the seat.
|
||||
if (m_keyboard == nullptr)
|
||||
wl_display_roundtrip_queue(display, queue);
|
||||
|
||||
// The keyboard proxy is hot — move it onto the default queue so
|
||||
// Qt's event loop dispatches our listeners alongside Qt's own
|
||||
// input events.
|
||||
if (m_keyboard) {
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(m_keyboard), nullptr);
|
||||
}
|
||||
wl_event_queue_destroy(queue);
|
||||
}
|
||||
|
||||
XkbTracker::~XkbTracker() {
|
||||
// Process-wide singleton; OS reclaims at exit. Explicit teardown
|
||||
// keeps leak checkers quiet and documents ownership.
|
||||
if (m_keyboard) wl_keyboard_destroy(m_keyboard);
|
||||
if (m_state) xkb_state_unref(m_state);
|
||||
if (m_keymap) xkb_keymap_unref(m_keymap);
|
||||
if (m_ctx) xkb_context_unref(m_ctx);
|
||||
}
|
||||
|
||||
bool XkbTracker::capsLockOn() const {
|
||||
if (m_idxCapsLock == ~0u) return false;
|
||||
return (m_modsLocked & (1u << m_idxCapsLock)) != 0;
|
||||
}
|
||||
|
||||
bool XkbTracker::numLockOn() const {
|
||||
if (m_idxNumLock == ~0u) return false;
|
||||
return (m_modsLocked & (1u << m_idxNumLock)) != 0;
|
||||
}
|
||||
|
||||
// --- Registry / seat binding ----------------------------------------
|
||||
|
||||
void XkbTracker::onRegistryGlobal(void *data, wl_registry *registry,
|
||||
uint32_t name, const char *interface,
|
||||
uint32_t /*version*/) {
|
||||
auto *self = static_cast<XkbTracker *>(data);
|
||||
if (std::strcmp(interface, wl_seat_interface.name) != 0) return;
|
||||
|
||||
// Bind the seat at version 5 (which exposes seat name + the
|
||||
// listener callbacks we need). If the compositor advertises an
|
||||
// older version, the bind silently downgrades; we only need
|
||||
// capabilities in either case.
|
||||
auto *seat = static_cast<wl_seat *>(
|
||||
wl_registry_bind(registry, name, &wl_seat_interface, 5));
|
||||
if (!seat) return;
|
||||
// Subscribe to capability changes; we'll grab the keyboard from
|
||||
// the capability callback once the seat tells us it has one.
|
||||
wl_seat_add_listener(seat, &kSeatListener, self);
|
||||
}
|
||||
|
||||
void XkbTracker::onRegistryGlobalRemove(void *, wl_registry *, uint32_t) {}
|
||||
|
||||
void XkbTracker::onSeatCapabilities(void *data, wl_seat *seat,
|
||||
uint32_t capabilities) {
|
||||
auto *self = static_cast<XkbTracker *>(data);
|
||||
const bool hasKbd = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0;
|
||||
if (hasKbd && !self->m_keyboard) {
|
||||
self->m_keyboard = wl_seat_get_keyboard(seat);
|
||||
if (self->m_keyboard)
|
||||
wl_keyboard_add_listener(self->m_keyboard, &kKeyboardListener, self);
|
||||
} else if (!hasKbd && self->m_keyboard) {
|
||||
wl_keyboard_destroy(self->m_keyboard);
|
||||
self->m_keyboard = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void XkbTracker::onSeatName(void *, wl_seat *, const char *) {}
|
||||
|
||||
// --- wl_keyboard listeners ------------------------------------------
|
||||
|
||||
void XkbTracker::onKeymap(void *data, wl_keyboard * /*kb*/, uint32_t format,
|
||||
int32_t fd, uint32_t size) {
|
||||
auto *self = static_cast<XkbTracker *>(data);
|
||||
// We can only handle XKB v1 keymaps. Anything else is a Wayland
|
||||
// protocol extension we don't support; close the FD and bail.
|
||||
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
// mmap the keymap text and feed it to xkb. MAP_PRIVATE so writes
|
||||
// don't propagate; PROT_READ is enough.
|
||||
void *map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (map == MAP_FAILED) {
|
||||
std::fprintf(stderr, "[ghastty] xkb keymap mmap failed\n");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
xkb_keymap *km = xkb_keymap_new_from_string(
|
||||
self->m_ctx, static_cast<const char *>(map),
|
||||
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
munmap(map, size);
|
||||
close(fd);
|
||||
if (!km) {
|
||||
std::fprintf(stderr, "[ghastty] xkb keymap compile failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the previous keymap+state. Anything that captured the
|
||||
// old xkb_state* must use XkbTracker::state() each call rather
|
||||
// than caching the pointer — we document that at the call site.
|
||||
if (self->m_state) {
|
||||
xkb_state_unref(self->m_state);
|
||||
self->m_state = nullptr;
|
||||
}
|
||||
if (self->m_keymap) xkb_keymap_unref(self->m_keymap);
|
||||
self->m_keymap = km;
|
||||
self->m_state = xkb_state_new(km);
|
||||
self->m_idxCapsLock = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_CAPS);
|
||||
self->m_idxNumLock = xkb_keymap_mod_get_index(km, XKB_MOD_NAME_NUM);
|
||||
self->m_modsLocked = 0;
|
||||
self->m_group = 0;
|
||||
}
|
||||
|
||||
void XkbTracker::onEnter(void *, wl_keyboard *, uint32_t, wl_surface *,
|
||||
wl_array *) {}
|
||||
void XkbTracker::onLeave(void *, wl_keyboard *, uint32_t, wl_surface *) {}
|
||||
void XkbTracker::onKey(void *, wl_keyboard *, uint32_t, uint32_t, uint32_t,
|
||||
uint32_t) {
|
||||
// Qt delivers key events; we don't want to double-process here.
|
||||
}
|
||||
|
||||
void XkbTracker::onModifiers(void *data, wl_keyboard *, uint32_t,
|
||||
uint32_t mods_depressed, uint32_t mods_latched,
|
||||
uint32_t mods_locked, uint32_t group) {
|
||||
auto *self = static_cast<XkbTracker *>(data);
|
||||
if (!self->m_state) return;
|
||||
// Keep the live state in sync so xkb_state_key_get_one_sym (used
|
||||
// for unshifted_codepoint) and xkb_state_key_get_consumed_mods2
|
||||
// see the active layout group and locked-modifier mask.
|
||||
xkb_state_update_mask(self->m_state, mods_depressed, mods_latched,
|
||||
mods_locked, 0, 0, group);
|
||||
self->m_modsLocked = mods_locked;
|
||||
self->m_group = group;
|
||||
}
|
||||
|
||||
void XkbTracker::onRepeatInfo(void *, wl_keyboard *, int32_t, int32_t) {}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct xkb_context;
|
||||
struct xkb_keymap;
|
||||
struct xkb_state;
|
||||
|
||||
// Tracks the user's live XKB state on Wayland: the active keymap, the
|
||||
// effective layout group, and the locked modifier mask (Caps Lock,
|
||||
// Num Lock).
|
||||
//
|
||||
// Qt does not expose any of this directly. We bind to the
|
||||
// process-wide wl_seat via the platform native interface, install a
|
||||
// wl_keyboard listener, rebuild our xkb_keymap from the compositor's
|
||||
// keymap FD on every keymap event, and keep an xkb_state synced via
|
||||
// the modifiers event.
|
||||
//
|
||||
// Read access (modsLocked / activeGroup / xkbState) is from the GUI
|
||||
// thread only — same as Qt's input event delivery — and these
|
||||
// methods do not mutate state.
|
||||
//
|
||||
// Wayland-only: this file's symbols are referenced from
|
||||
// GhosttySurface, which already runs only on Wayland.
|
||||
class XkbTracker {
|
||||
public:
|
||||
// Process-wide singleton; returns nullptr if Wayland binding
|
||||
// failed (e.g. running under XWayland with no wl_seat available
|
||||
// through Qt).
|
||||
static XkbTracker *instance();
|
||||
|
||||
// The live xkb_state. Owned by the tracker; do not unref. May be
|
||||
// null if the compositor hasn't sent a keymap yet.
|
||||
xkb_state *state() const { return m_state; }
|
||||
|
||||
// The live xkb_keymap from the compositor. Owned by the tracker;
|
||||
// do not unref. Replaced on every keymap event from the
|
||||
// compositor; consumers that cache derived state should compare
|
||||
// pointer identity to detect rebuilds.
|
||||
xkb_keymap *keymap() const { return m_keymap; }
|
||||
// The shared xkb_context. Lives as long as the tracker (process
|
||||
// lifetime).
|
||||
xkb_context *ctx() const { return m_ctx; }
|
||||
|
||||
// True if Caps Lock is on right now.
|
||||
bool capsLockOn() const;
|
||||
// True if Num Lock is on right now.
|
||||
bool numLockOn() const;
|
||||
|
||||
// The active layout group (0-based). 0 when the compositor hasn't
|
||||
// sent a modifiers event yet.
|
||||
uint32_t activeGroup() const { return m_group; }
|
||||
|
||||
// Listener entry points are public because they're addressed by C
|
||||
// function pointer in the wl_keyboard_listener / wl_seat_listener
|
||||
// / wl_registry_listener structs. They are not part of the public
|
||||
// API; treat as internal.
|
||||
static void onKeymap(void *data, struct wl_keyboard *kb, uint32_t format,
|
||||
int32_t fd, uint32_t size);
|
||||
static void onEnter(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
struct wl_surface *surface, struct wl_array *keys);
|
||||
static void onLeave(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
struct wl_surface *surface);
|
||||
static void onKey(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
uint32_t time, uint32_t key, uint32_t state);
|
||||
static void onModifiers(void *data, struct wl_keyboard *kb, uint32_t serial,
|
||||
uint32_t mods_depressed, uint32_t mods_latched,
|
||||
uint32_t mods_locked, uint32_t group);
|
||||
static void onRepeatInfo(void *data, struct wl_keyboard *kb, int32_t rate,
|
||||
int32_t delay);
|
||||
// Registry callbacks (used to find wl_seat).
|
||||
static void onRegistryGlobal(void *data, struct wl_registry *registry,
|
||||
uint32_t name, const char *interface,
|
||||
uint32_t version);
|
||||
static void onRegistryGlobalRemove(void *data, struct wl_registry *registry,
|
||||
uint32_t name);
|
||||
// wl_seat capability-changed callback.
|
||||
static void onSeatCapabilities(void *data, struct wl_seat *seat,
|
||||
uint32_t capabilities);
|
||||
static void onSeatName(void *data, struct wl_seat *seat, const char *name);
|
||||
|
||||
private:
|
||||
XkbTracker();
|
||||
~XkbTracker();
|
||||
XkbTracker(const XkbTracker &) = delete;
|
||||
XkbTracker &operator=(const XkbTracker &) = delete;
|
||||
|
||||
xkb_context *m_ctx = nullptr;
|
||||
xkb_keymap *m_keymap = nullptr;
|
||||
xkb_state *m_state = nullptr;
|
||||
uint32_t m_modsLocked = 0;
|
||||
uint32_t m_group = 0;
|
||||
// Indices into the keymap for the lock mods. XKB_MOD_INVALID until
|
||||
// a keymap is loaded.
|
||||
uint32_t m_idxCapsLock = ~0u;
|
||||
uint32_t m_idxNumLock = ~0u;
|
||||
// wl_keyboard handle, owned by us via wl_seat_get_keyboard.
|
||||
struct wl_keyboard *m_keyboard = nullptr;
|
||||
};
|
||||
Loading…
Reference in New Issue