Give background agent seats independent keymaps and check foreground keyboard compatibility per operation. Package the downstream patch with separate source integrity and build provenance, retaining the upstream ABI verifier. Co-Authored-By: Codex XHigh <noreply@openai.com>
720 lines
41 KiB
Diff
720 lines
41 KiB
Diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 8a80de2..5dedb8f 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -39,8 +39,24 @@ target_compile_options(cua_hyprland_protocol PRIVATE -Wall -Wextra -Wpedantic -W
|
|
cua_hyprland_harden(cua_hyprland_protocol)
|
|
set_target_properties(cua_hyprland_protocol PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
|
|
|
+if(BUILD_TESTING OR CUA_HYPRLAND_INPUT OR CUA_HYPRLAND_TEST_INPUT)
|
|
+ find_package(PkgConfig REQUIRED)
|
|
+ pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
|
|
+endif()
|
|
+
|
|
if(BUILD_TESTING)
|
|
+ add_executable(cua_hyprland_keyboard_layout_test tests/keyboard_layout_test.cpp)
|
|
+ target_compile_features(cua_hyprland_keyboard_layout_test PRIVATE cxx_std_26)
|
|
+ target_include_directories(cua_hyprland_keyboard_layout_test PRIVATE src)
|
|
+ target_link_libraries(cua_hyprland_keyboard_layout_test PRIVATE PkgConfig::XKBCOMMON)
|
|
+ target_compile_options(cua_hyprland_keyboard_layout_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
|
+ add_test(NAME cua_hyprland_keyboard_layout_test COMMAND cua_hyprland_keyboard_layout_test)
|
|
+
|
|
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
|
+ add_test(NAME cua_hyprland_agent_keymap_test
|
|
+ COMMAND ${Python3_EXECUTABLE} -B ${CMAKE_CURRENT_SOURCE_DIR}/tests/agent_keymap_test.py)
|
|
+ set_tests_properties(cua_hyprland_agent_keymap_test PROPERTIES
|
|
+ ENVIRONMENT "CXX=${CMAKE_CXX_COMPILER}")
|
|
add_test(NAME cua_hyprland_desktop_fault_policy_test
|
|
COMMAND ${Python3_EXECUTABLE} -B
|
|
${CMAKE_CURRENT_SOURCE_DIR}/tests/desktop_fault_policy_test.py)
|
|
@@ -218,6 +234,7 @@ if(CUA_HYPRLAND_BUILD_PLUGIN)
|
|
message(FATAL_ERROR "Input is pinned to Hyprland 0.56.2")
|
|
endif()
|
|
target_sources(cua_hyprland_plugin PRIVATE src/input_experiment.cpp)
|
|
+ target_link_libraries(cua_hyprland_plugin PRIVATE PkgConfig::XKBCOMMON)
|
|
endif()
|
|
if(CUA_HYPRLAND_INPUT)
|
|
target_compile_definitions(cua_hyprland_plugin PRIVATE CUA_HYPRLAND_INPUT=1)
|
|
diff --git a/src/input_experiment.cpp b/src/input_experiment.cpp
|
|
index fc7e740..f256fc4 100644
|
|
--- a/src/input_experiment.cpp
|
|
+++ b/src/input_experiment.cpp
|
|
@@ -9,6 +9,8 @@
|
|
#include "seat_lifetime.hpp"
|
|
#include "owned_socket_path.hpp"
|
|
#include "foreground_route.hpp"
|
|
+#include "keyboard_layout.hpp"
|
|
+#include <sys/mman.h>
|
|
|
|
#include <src/Compositor.hpp>
|
|
#include <src/devices/IKeyboard.hpp>
|
|
@@ -214,7 +216,11 @@ struct InputExperiment::Impl {
|
|
xkb_keymap* keymap = nullptr;
|
|
xkb_state* keyboard_state = nullptr;
|
|
int keymap_fd = -1;
|
|
- bool retired = false, suspended = true, us_keymap = false, physical_keymap_present = false;
|
|
+ // Foreground state uses the actual primary map; never share it with agent seats.
|
|
+ xkb_keymap* physical_keymap = nullptr;
|
|
+ xkb_state* physical_state = nullptr;
|
|
+ std::string physical_keymap_text;
|
|
+ bool retired = false, suspended = true, physical_keymap_present = false;
|
|
WP<IKeyboard> physical_keyboard;
|
|
CHyprSignalListener keymap_listener;
|
|
unsigned lane;
|
|
@@ -243,23 +249,37 @@ struct InputExperiment::Impl {
|
|
// No private key or input-enabled default exists in this component.
|
|
}
|
|
|
|
- static bool canonical_us_keymap(xkb_context* context, xkb_keymap* map) {
|
|
- // Compare canonical compiled content, not a layout display name. This
|
|
- // deliberately excludes variants, options, remaps, and multiple groups.
|
|
- const xkb_rule_names names{"evdev", "pc105", "us", "", ""};
|
|
- auto* reference = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
- if (!reference) return false;
|
|
- char* actual = xkb_keymap_get_as_string(map, XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
- char* expected = xkb_keymap_get_as_string(reference, XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
- const bool matches = actual && expected && std::strcmp(actual, expected) == 0;
|
|
- std::free(actual); std::free(expected); xkb_keymap_unref(reference);
|
|
- return matches;
|
|
- }
|
|
- bool layout_qualified() const {
|
|
- if (!kProduction) return true;
|
|
+ bool physical_layout_ready() const {
|
|
const auto keyboard = g_pSeatManager->m_keyboard.lock();
|
|
- return physical_keymap_present && us_keymap && keyboard_state && keyboard &&
|
|
- keyboard->m_xkbKeymapV1FD.get() >= 0 && keyboard->m_xkbKeymapV1String == keymap_text;
|
|
+ return physical_keymap_present && physical_state && keyboard &&
|
|
+ keyboard->m_xkbKeymapV1FD.get() >= 0 && keyboard->m_xkbKeymapV1String == physical_keymap_text;
|
|
+ }
|
|
+ bool layout_qualified(InputRoute route, std::uint64_t capability) const {
|
|
+ return keyboard_layout_ready(route, capability, keyboard_state && keymap_fd >= 0, physical_layout_ready());
|
|
+ }
|
|
+ void initialize_agent_keymap() {
|
|
+ if (keymap) return;
|
|
+ xkb_context_ = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
+ keymap = xkb_context_ ? agent_keymap(xkb_context_) : nullptr;
|
|
+ keyboard_state = keymap ? xkb_state_new(keymap) : nullptr;
|
|
+ char* text = keymap ? xkb_keymap_get_as_string(keymap, XKB_KEYMAP_FORMAT_TEXT_V1) : nullptr;
|
|
+ if (!keyboard_state || !text) {
|
|
+ std::free(text);
|
|
+ throw std::runtime_error("agent XKB keymap unavailable");
|
|
+ }
|
|
+ keymap_text = text;
|
|
+ std::free(text);
|
|
+ keymap_fd = memfd_create("cua-agent-keymap", MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
|
+ if (keymap_fd < 0) throw std::runtime_error("agent keymap fd unavailable");
|
|
+ std::size_t offset = 0;
|
|
+ while (offset < keymap_text.size() + 1) {
|
|
+ const auto count = write(keymap_fd, keymap_text.c_str() + offset, keymap_text.size() + 1 - offset);
|
|
+ if (count < 0 && errno == EINTR) continue;
|
|
+ if (count <= 0) throw std::runtime_error("agent keymap write failed");
|
|
+ offset += static_cast<std::size_t>(count);
|
|
+ }
|
|
+ if (fcntl(keymap_fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) < 0)
|
|
+ throw std::runtime_error("agent keymap sealing failed");
|
|
}
|
|
void sync_keymap() {
|
|
const auto keyboard = g_pSeatManager->m_keyboard.lock();
|
|
@@ -278,38 +298,20 @@ struct InputExperiment::Impl {
|
|
return;
|
|
}
|
|
physical_keymap_present = true;
|
|
- if (keyboard_state && keymap_text == keyboard->m_xkbKeymapV1String) return;
|
|
- // Prepare a complete replacement before retiring the old independent
|
|
- // state. Keep our own fd: primary keyboard replacement must not leave
|
|
- // later seat bindings referring to a closed compositor fd.
|
|
- auto* context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
- auto* map = context ? xkb_keymap_new_from_string(context, keyboard->m_xkbKeymapV1String.c_str(),
|
|
- XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS) : nullptr;
|
|
- auto* state = map ? xkb_state_new(map) : nullptr;
|
|
- const auto fd = fcntl(keyboard->m_xkbKeymapV1FD.get(), F_DUPFD_CLOEXEC, 0);
|
|
- if (!state || fd < 0) {
|
|
- if (fd >= 0) close(fd);
|
|
- if (state) xkb_state_unref(state);
|
|
- if (map) xkb_keymap_unref(map);
|
|
- if (context) xkb_context_unref(context);
|
|
- throw std::runtime_error("independent XKB state unavailable");
|
|
- }
|
|
+ if (physical_keymap_text == keyboard->m_xkbKeymapV1String) return;
|
|
+ // Physical changes still revoke authority, but never replace the map
|
|
+ // advertised by either background keyboard.
|
|
desktop_transition();
|
|
- if (keyboard_state) xkb_state_unref(keyboard_state);
|
|
- if (keymap) xkb_keymap_unref(keymap);
|
|
- if (xkb_context_) xkb_context_unref(xkb_context_);
|
|
- if (keymap_fd >= 0) close(keymap_fd);
|
|
- keyboard_state = state; keymap = map; xkb_context_ = context; keymap_fd = fd;
|
|
- us_keymap = !kProduction || canonical_us_keymap(context, map);
|
|
- keymap_text = keyboard->m_xkbKeymapV1String;
|
|
- for (auto& k : keyboards)
|
|
- if (!k->dead && k->wl->resource())
|
|
- k->wl->sendKeymap(WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, keymap_fd, keymap_text.size() + 1);
|
|
- for (auto& seat : seats)
|
|
- if (!seat->dead && seat->wl->resource())
|
|
- seat->wl->sendCapabilities(static_cast<wl_seat_capability>(WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD));
|
|
+ auto* map = xkb_keymap_new_from_string(xkb_context_, keyboard->m_xkbKeymapV1String.c_str(),
|
|
+ XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
+ auto* state = map ? xkb_state_new(map) : nullptr;
|
|
+ if (physical_state) xkb_state_unref(physical_state);
|
|
+ if (physical_keymap) xkb_keymap_unref(physical_keymap);
|
|
+ physical_state = state; physical_keymap = map;
|
|
+ physical_keymap_text = keyboard->m_xkbKeymapV1String;
|
|
}
|
|
void start() {
|
|
+ initialize_agent_keymap();
|
|
sync_keymap();
|
|
timer = wl_event_loop_add_timer(g_pCompositor->m_wlEventLoop, tick, this);
|
|
if (!timer) throw std::runtime_error("input timer registration failed");
|
|
@@ -389,6 +391,8 @@ struct InputExperiment::Impl {
|
|
cleanup_socket();
|
|
if (keyboard_state) xkb_state_unref(keyboard_state);
|
|
if (keymap) xkb_keymap_unref(keymap);
|
|
+ if (physical_state) xkb_state_unref(physical_state);
|
|
+ if (physical_keymap) xkb_keymap_unref(physical_keymap);
|
|
if (xkb_context_) xkb_context_unref(xkb_context_);
|
|
if (keymap_fd >= 0) close(keymap_fd);
|
|
}
|
|
@@ -786,7 +790,7 @@ struct InputExperiment::Impl {
|
|
if (lease != &c) throw ForegroundFailure{ForegroundFailureReason::lease};
|
|
if (c.dead) throw ForegroundFailure{ForegroundFailureReason::client_dead};
|
|
if (!available()) throw ForegroundFailure{ForegroundFailureReason::session_unavailable};
|
|
- if (!layout_qualified()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
|
+ if (foreground_keyboard_used && !physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
|
if (Clock::now() >= expires) throw ForegroundFailure{ForegroundFailureReason::lease_expired};
|
|
const auto failure = foreground_guard(c).dispatch_failure(foreground_needs_pointer);
|
|
if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
|
@@ -813,6 +817,8 @@ struct InputExperiment::Impl {
|
|
foreground_pointers.clear(); foreground_keyboards.clear(); foreground_surface.reset(); foreground_seat.reset();
|
|
foreground_started = false;
|
|
foreground_keyboard_used = false;
|
|
+ if (physical_state) xkb_state_unref(physical_state);
|
|
+ physical_state = physical_keymap ? xkb_state_new(physical_keymap) : nullptr;
|
|
}
|
|
void start_foreground(Client& c, double x, double y, bool needs_pointer, bool needs_keyboard) {
|
|
const auto root = c.surface.lock();
|
|
@@ -820,7 +826,7 @@ struct InputExperiment::Impl {
|
|
const auto failure = foreground_guard(c).activation_failure();
|
|
if (failure != ForegroundFailureReason::none) throw ForegroundFailure{failure};
|
|
if (!physical) throw ForegroundFailure{ForegroundFailureReason::physical_keyboard};
|
|
- if (!keyboard_state) throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
|
+ if (needs_keyboard && !physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::keyboard_state};
|
|
if (needs_pointer && !g_pSeatManager->m_mouse) throw ForegroundFailure{ForegroundFailureReason::physical_pointer};
|
|
const Vector2D local{x + c.geometry[0] - c.geometry[4], y + c.geometry[1] - c.geometry[5]};
|
|
if (needs_pointer && (!point(c, x, y) || root->at(local, true).first != root)) throw ForegroundFailure{ForegroundFailureReason::pointer_target};
|
|
@@ -843,7 +849,7 @@ struct InputExperiment::Impl {
|
|
const auto modifier_failure = foreground_key_modifier_failure(foreground_modifiers);
|
|
if (modifier_failure != ForegroundFailureReason::none) throw ForegroundFailure{modifier_failure};
|
|
}
|
|
- xkb_state_update_mask(keyboard_state, foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], 0, 0, foreground_modifiers[3]);
|
|
+ if (needs_keyboard) xkb_state_update_mask(physical_state, foreground_modifiers[0], foreground_modifiers[1], foreground_modifiers[2], 0, 0, foreground_modifiers[3]);
|
|
foreground_surface = root;
|
|
foreground_seat = seat;
|
|
foreground_needs_pointer = needs_pointer;
|
|
@@ -893,16 +899,17 @@ struct InputExperiment::Impl {
|
|
}
|
|
void foreground_key(Client& c, std::uint32_t code, bool pressed) {
|
|
require_foreground(c);
|
|
+ if (!physical_layout_ready()) throw ForegroundFailure{ForegroundFailureReason::unsupported_layout};
|
|
foreground_keyboard_used = true;
|
|
- xkb_state_update_key(keyboard_state, code + 8, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
|
+ xkb_state_update_key(physical_state, code + 8, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
|
|
if (pressed) held_keys.push_back(code); else std::erase(held_keys, code);
|
|
for (const auto& weak : foreground_keyboards) {
|
|
const auto k = weak.lock(); if (!k || !k->good()) throw ForegroundFailure{ForegroundFailureReason::keyboard_resources};
|
|
k->sendKey(event_ms(), code, pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED);
|
|
- k->sendMods(xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_DEPRESSED),
|
|
- xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_LATCHED),
|
|
- xkb_state_serialize_mods(keyboard_state, XKB_STATE_MODS_LOCKED),
|
|
- xkb_state_serialize_layout(keyboard_state, XKB_STATE_LAYOUT_EFFECTIVE));
|
|
+ k->sendMods(xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_DEPRESSED),
|
|
+ xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_LATCHED),
|
|
+ xkb_state_serialize_mods(physical_state, XKB_STATE_MODS_LOCKED),
|
|
+ xkb_state_serialize_layout(physical_state, XKB_STATE_LAYOUT_EFFECTIVE));
|
|
}
|
|
}
|
|
bool pointer_enter(Client& c, double x, double y) {
|
|
@@ -942,8 +949,8 @@ struct InputExperiment::Impl {
|
|
held_button = pressed ? value : 0;
|
|
}
|
|
bool keyboard_enter(Client& c) {
|
|
- const auto root = c.surface.lock(); const auto physical = g_pSeatManager->m_keyboard.lock();
|
|
- if (!root || !physical || physical->m_xkbKeymapV1String != keymap_text || !keyboard_state) return false;
|
|
+ const auto root = c.surface.lock();
|
|
+ if (!root || !keyboard_state || keymap_fd < 0) return false;
|
|
unsigned count = 0;
|
|
for (auto& k : keyboards) {
|
|
if (k->dead || !k->wl->resource() || k->wl->client() != root->client()) continue;
|
|
@@ -1030,7 +1037,7 @@ struct InputExperiment::Impl {
|
|
if (kProduction && (!InputGrant::single_operation(requested_cap) ||
|
|
(requested_cap == 16 && route != InputRoute::primary_foreground))) { invalidate(c); send(c, refusal("unsupported")); return; }
|
|
if (kProduction && !available()) { invalidate(c, false); send(c, refusal("session_unavailable")); return; }
|
|
- if (!layout_qualified()) { invalidate(c, false); send(c, refusal("unsupported_layout")); return; }
|
|
+ if (!layout_qualified(route, requested_cap)) { invalidate(c, false); send(c, refusal("unsupported_layout")); return; }
|
|
const auto pid = number(f[1]); const auto address = number(f[2], 16);
|
|
PHLWINDOW window;
|
|
for (const auto& w : Desktop::windowState()->windows())
|
|
@@ -1090,7 +1097,7 @@ struct InputExperiment::Impl {
|
|
if (c.token.empty() || f[2] != c.token || !refresh(c)) { send(c, refusal("stale_target")); return; }
|
|
if (number(f[3]) != c.revision) { if (kProduction) revoke("stale_geometry"); send(c, refusal("stale_geometry")); return; }
|
|
if (!available()) { revoke("session_unavailable", true); send(c, refusal("session_unavailable")); return; }
|
|
- if (!layout_qualified()) { revoke("unsupported_layout", true); send(c, refusal("unsupported_layout")); return; }
|
|
+ if (!layout_qualified(c.route, cap)) { revoke("unsupported_layout", true); send(c, refusal("unsupported_layout")); return; }
|
|
if (lease && Clock::now() >= expires) revoke("lease_expired");
|
|
if (drag) { send(c, refusal("lease_busy")); return; }
|
|
if (lease != &c || !(capabilities & cap) || (kProduction && !grant.permits(cap, Clock::now()))) {
|
|
@@ -1184,6 +1191,10 @@ struct InputExperiment::Impl {
|
|
if (Clock::now() + std::chrono::milliseconds(duration + 50) >= expires) { send(c, refusal("lease_expired")); return; }
|
|
}
|
|
}
|
|
+ if (command == "KEY" && !foreground_chord_compatible(physical_keymap, keymap, code, mods)) {
|
|
+ revoke("unsupported_layout", true);
|
|
+ send(c, refusal("unsupported_layout")); return;
|
|
+ }
|
|
if (!consume_grant(c, cap)) return;
|
|
start_foreground(c, x, y, command != "KEY" && command != "ACTIVATE", command == "KEY");
|
|
if (command == "KEY") {
|
|
@@ -1227,7 +1238,7 @@ struct InputExperiment::Impl {
|
|
if (lease) {
|
|
if (lease->dead) revoke("disconnected", true);
|
|
else if (Clock::now() >= expires) revoke("lease_expired");
|
|
- else if (!available() || !layout_qualified()) revoke("cancelled", true);
|
|
+ else if (!available() || !layout_qualified(lease->route, capabilities)) revoke("cancelled", true);
|
|
else if (!refresh(*lease) || primary_conflict(*lease) || agent_conflict(*lease) ||
|
|
(drag && !drag->geometry.matches(lease->revision))) revoke("cancelled");
|
|
}
|
|
diff --git a/src/keyboard_layout.hpp b/src/keyboard_layout.hpp
|
|
new file mode 100644
|
|
index 0000000..ab7defc
|
|
--- /dev/null
|
|
+++ b/src/keyboard_layout.hpp
|
|
@@ -0,0 +1,102 @@
|
|
+#pragma once
|
|
+
|
|
+#include "foreground_route.hpp"
|
|
+#include <xkbcommon/xkbcommon.h>
|
|
+#include <array>
|
|
+#include <cstdint>
|
|
+#include <memory>
|
|
+
|
|
+namespace cua::hyprland {
|
|
+// Wire v3 carries evdev key positions plus fixed Shift/Ctrl/Alt/Super bits.
|
|
+// Background seats advertise this map, independently of the human keyboard.
|
|
+inline xkb_keymap* agent_keymap(xkb_context* context) {
|
|
+ const xkb_rule_names names{"evdev", "pc105", "us", "", ""};
|
|
+ return xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
+}
|
|
+
|
|
+inline bool keyboard_layout_ready(InputRoute route, std::uint64_t capability,
|
|
+ bool agent_ready, bool physical_ready) {
|
|
+ if (!(capability & 2)) return true; // Pointer/activation needs no key mapping.
|
|
+ return route == InputRoute::primary_foreground ? physical_ready : agent_ready;
|
|
+}
|
|
+
|
|
+inline bool same_key_symbols(xkb_state* actual, xkb_state* expected, xkb_keycode_t key) {
|
|
+ const xkb_keysym_t *a = nullptr, *b = nullptr;
|
|
+ const int na = xkb_state_key_get_syms(actual, key, &a);
|
|
+ const int nb = xkb_state_key_get_syms(expected, key, &b);
|
|
+ if (na != nb) return false;
|
|
+ for (int i = 0; i < na; ++i) if (a[i] != b[i]) return false;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+inline bool same_consumed_modifiers(xkb_state* actual, xkb_state* expected, xkb_keycode_t key) {
|
|
+ // Toolkits subtract consumed modifiers when matching shortcuts. Matching
|
|
+ // symbols and effective state alone does not preserve shortcut meaning.
|
|
+ for (auto* state : {actual, expected}) {
|
|
+ auto* map = xkb_state_get_keymap(state);
|
|
+ for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(map); ++i) {
|
|
+ const char* name = xkb_keymap_mod_get_name(map, i);
|
|
+ if (xkb_state_mod_name_is_active(state, name, XKB_STATE_MODS_EFFECTIVE) <= 0) continue;
|
|
+ const auto ai = xkb_keymap_mod_get_index(xkb_state_get_keymap(actual), name);
|
|
+ const auto bi = xkb_keymap_mod_get_index(xkb_state_get_keymap(expected), name);
|
|
+ for (auto mode : {XKB_CONSUMED_MODE_XKB, XKB_CONSUMED_MODE_GTK})
|
|
+ if ((xkb_state_mod_index_is_consumed2(actual, key, ai, mode) > 0) !=
|
|
+ (xkb_state_mod_index_is_consumed2(expected, key, bi, mode) > 0)) return false;
|
|
+ }
|
|
+ }
|
|
+ return true;
|
|
+}
|
|
+
|
|
+inline bool same_modifier_state(xkb_state* actual, xkb_state* expected) {
|
|
+ // Modifier indices can differ between maps. Compare names, in both directions,
|
|
+ // including nonstandard modifiers and lock/latch changes on key release.
|
|
+ for (auto* state : {actual, expected}) {
|
|
+ auto* map = xkb_state_get_keymap(state);
|
|
+ for (xkb_mod_index_t i = 0; i < xkb_keymap_num_mods(map); ++i) {
|
|
+ const char* name = xkb_keymap_mod_get_name(map, i);
|
|
+ for (auto component : {XKB_STATE_MODS_DEPRESSED, XKB_STATE_MODS_LATCHED,
|
|
+ XKB_STATE_MODS_LOCKED, XKB_STATE_MODS_EFFECTIVE}) {
|
|
+ const bool a = xkb_state_mod_name_is_active(actual, name, component) > 0;
|
|
+ const bool b = xkb_state_mod_name_is_active(expected, name, component) > 0;
|
|
+ if (a != b) return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ for (auto component : {XKB_STATE_LAYOUT_DEPRESSED, XKB_STATE_LAYOUT_LATCHED,
|
|
+ XKB_STATE_LAYOUT_LOCKED, XKB_STATE_LAYOUT_EFFECTIVE})
|
|
+ if (xkb_state_serialize_layout(actual, component) != xkb_state_serialize_layout(expected, component))
|
|
+ return false;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+// Simulate the complete chord before delivering any events or changing focus.
|
|
+// This is compatibility checking, not layout translation: physical key positions
|
|
+// remain unchanged. Unrelated remaps are harmless, requested remaps fail closed.
|
|
+inline bool foreground_chord_compatible(xkb_keymap* physical, xkb_keymap* canonical,
|
|
+ std::uint32_t code, std::uint32_t mods) {
|
|
+ if (!physical || !canonical) return false;
|
|
+ using State = std::unique_ptr<xkb_state, decltype(&xkb_state_unref)>;
|
|
+ State actual{xkb_state_new(physical), xkb_state_unref};
|
|
+ State expected{xkb_state_new(canonical), xkb_state_unref};
|
|
+ if (!actual || !expected) return false;
|
|
+ const auto event = [&](std::uint32_t key, xkb_key_direction direction) {
|
|
+ // A client interprets the press using the preceding modifiers event.
|
|
+ // Stock both_capslock_cancel gives Shift a Caps_Lock symbol at its
|
|
+ // shifted level; that level is not another press. Releases pair by
|
|
+ // keycode, but their lock/latch/group effects still must agree.
|
|
+ if (direction == XKB_KEY_DOWN &&
|
|
+ (!same_key_symbols(actual.get(), expected.get(), key + 8) ||
|
|
+ !same_consumed_modifiers(actual.get(), expected.get(), key + 8))) return false;
|
|
+ xkb_state_update_key(actual.get(), key + 8, direction);
|
|
+ xkb_state_update_key(expected.get(), key + 8, direction);
|
|
+ return same_modifier_state(actual.get(), expected.get());
|
|
+ };
|
|
+ constexpr std::array<std::uint32_t, 4> keys{42, 29, 56, 125};
|
|
+ for (unsigned i = 0; i < keys.size(); ++i)
|
|
+ if ((mods & (1u << i)) && keys[i] != code && !event(keys[i], XKB_KEY_DOWN)) return false;
|
|
+ if (!event(code, XKB_KEY_DOWN) || !event(code, XKB_KEY_UP)) return false;
|
|
+ for (int i = 3; i >= 0; --i)
|
|
+ if ((mods & (1u << i)) && keys[i] != code && !event(keys[i], XKB_KEY_UP)) return false;
|
|
+ return true;
|
|
+}
|
|
+} // namespace cua::hyprland
|
|
diff --git a/src/plugin.cpp b/src/plugin.cpp
|
|
index 88b9900..2d24471 100644
|
|
--- a/src/plugin.cpp
|
|
+++ b/src/plugin.cpp
|
|
@@ -218,6 +218,11 @@ std::string status_output(bool json) {
|
|
|
|
if (json) {
|
|
auto result = cua::hyprland::render_status_json(report);
|
|
+#ifdef CUA_HYPRLAND_INPUT
|
|
+ // Installation checks compiled support before enabling input seats.
|
|
+ result.pop_back();
|
|
+ result += ",\"keyboard_layout_independent\":true}";
|
|
+#endif
|
|
#if defined(CUA_HYPRLAND_TEST_INPUT) || defined(CUA_HYPRLAND_INPUT)
|
|
if (g_experiment) {
|
|
#ifdef CUA_HYPRLAND_INPUT
|
|
diff --git a/tests/agent_keymap_test.py b/tests/agent_keymap_test.py
|
|
new file mode 100644
|
|
index 0000000..78a3603
|
|
--- /dev/null
|
|
+++ b/tests/agent_keymap_test.py
|
|
@@ -0,0 +1,86 @@
|
|
+"""Exercise the production map initializer without a compositor or desktop changes."""
|
|
+import os
|
|
+from pathlib import Path
|
|
+import re
|
|
+import shlex
|
|
+import subprocess
|
|
+import tempfile
|
|
+import unittest
|
|
+
|
|
+ROOT = Path(__file__).resolve().parents[1]
|
|
+
|
|
+
|
|
+class AgentKeymapTest(unittest.TestCase):
|
|
+ def test_production_keymap_and_fd(self):
|
|
+ source = (ROOT / 'src/input_experiment.cpp').read_text()
|
|
+ body = re.search(r'^ void initialize_agent_keymap\(\).*?^ }', source, re.M | re.S)
|
|
+ self.assertIsNotNone(body)
|
|
+ fixture = r'''
|
|
+#include "keyboard_layout.hpp"
|
|
+#include <sys/mman.h>
|
|
+#include <sys/stat.h>
|
|
+#include <fcntl.h>
|
|
+#include <unistd.h>
|
|
+#include <cerrno>
|
|
+#include <cstdlib>
|
|
+#include <stdexcept>
|
|
+#include <string>
|
|
+#include <iostream>
|
|
+using namespace cua::hyprland;
|
|
+void check(bool v, const char* m) { if (!v) { std::cerr << m; std::exit(1); } }
|
|
+struct Lane {
|
|
+ xkb_context* xkb_context_ = nullptr;
|
|
+ xkb_keymap* keymap = nullptr;
|
|
+ xkb_state* keyboard_state = nullptr;
|
|
+ int keymap_fd = -1;
|
|
+ std::string keymap_text;
|
|
+ // INITIALIZER
|
|
+ ~Lane() {
|
|
+ if (keyboard_state) xkb_state_unref(keyboard_state);
|
|
+ if (keymap) xkb_keymap_unref(keymap);
|
|
+ if (xkb_context_) xkb_context_unref(xkb_context_);
|
|
+ if (keymap_fd >= 0) close(keymap_fd);
|
|
+ }
|
|
+};
|
|
+int main() {
|
|
+ Lane a, b;
|
|
+ a.initialize_agent_keymap(); b.initialize_agent_keymap();
|
|
+ check(a.keymap_fd != b.keymap_fd && a.keymap != b.keymap && a.keyboard_state != b.keyboard_state,
|
|
+ "lanes share owned map resources");
|
|
+ check(a.keymap_text == b.keymap_text, "lanes advertise different layouts");
|
|
+ for (Lane* lane : {&a, &b}) {
|
|
+ struct stat status{};
|
|
+ check(fstat(lane->keymap_fd, &status) == 0 && status.st_size == (off_t)lane->keymap_text.size() + 1,
|
|
+ "keymap fd is not terminated serialized map");
|
|
+ std::string text(status.st_size, '\0');
|
|
+ check(pread(lane->keymap_fd, text.data(), text.size(), 0) == status.st_size, "keymap fd cannot be read");
|
|
+ check(text == lane->keymap_text + '\0', "keymap fd differs from advertised map");
|
|
+ check(fcntl(lane->keymap_fd, F_GETFD) & FD_CLOEXEC, "keymap fd inherited by exec");
|
|
+ const int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL;
|
|
+ check((fcntl(lane->keymap_fd, F_GET_SEALS) & seals) == seals, "keymap fd not immutable");
|
|
+ const int fd = lane->keymap_fd;
|
|
+ lane->initialize_agent_keymap();
|
|
+ check(lane->keymap_fd == fd, "initialization replaced advertised map");
|
|
+ check(xkb_state_key_get_one_sym(lane->keyboard_state, 21 + 8) == XKB_KEY_y,
|
|
+ "background lane did not initialize US map");
|
|
+ }
|
|
+ xkb_state_update_key(a.keyboard_state, 42 + 8, XKB_KEY_DOWN);
|
|
+ check(xkb_state_key_get_one_sym(a.keyboard_state, 30 + 8) == XKB_KEY_A, "lane modifier not applied");
|
|
+ check(xkb_state_key_get_one_sym(b.keyboard_state, 30 + 8) == XKB_KEY_a, "lane modifier leaked");
|
|
+}
|
|
+'''.replace('// INITIALIZER', body.group())
|
|
+ compiler = shlex.split(os.environ.get('CXX', 'c++'))
|
|
+ flags = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'xkbcommon'], text=True))
|
|
+ with tempfile.TemporaryDirectory(prefix='cua-agent-keymap-') as directory:
|
|
+ cpp, binary = Path(directory) / 'fixture.cpp', Path(directory) / 'fixture'
|
|
+ cpp.write_text(fixture)
|
|
+ build = subprocess.run([*compiler, '-std=c++20', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
|
|
+ '-I', str(ROOT / 'src'), str(cpp), '-o', str(binary), *flags],
|
|
+ capture_output=True, text=True, timeout=60)
|
|
+ self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
|
|
+ result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
|
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
+
|
|
+
|
|
+if __name__ == '__main__':
|
|
+ unittest.main()
|
|
diff --git a/tests/desktop_fault_policy_fixture.cpp b/tests/desktop_fault_policy_fixture.cpp
|
|
index 45c48ef..36be139 100644
|
|
--- a/tests/desktop_fault_policy_fixture.cpp
|
|
+++ b/tests/desktop_fault_policy_fixture.cpp
|
|
@@ -1,6 +1,7 @@
|
|
// Used by desktop_fault_policy_test.py, which inserts actual production bodies.
|
|
// Transport effects are counted here; native protocol/app behavior is separate.
|
|
#include "drag_geometry.hpp"
|
|
+#include "keyboard_layout.hpp"
|
|
#include "input_grant.hpp"
|
|
#include "passive_pointer_target.hpp"
|
|
|
|
@@ -18,6 +19,7 @@ struct Window {};
|
|
struct Surface { int client() const { return 1; } };
|
|
using Target = PassivePointerTarget<std::weak_ptr<Window>, std::weak_ptr<Surface>>;
|
|
struct Client {
|
|
+ InputRoute route = InputRoute::independent;
|
|
bool dead = false;
|
|
void* source = nullptr;
|
|
int fd = -1;
|
|
@@ -34,8 +36,6 @@ struct Pointer {
|
|
};
|
|
struct Drag { Client* client; DragGeometry geometry{1}; };
|
|
struct Trace { void mark(const char*, unsigned) {} };
|
|
-enum class ForegroundFailureReason { none };
|
|
-struct ForegroundFailure { ForegroundFailureReason reason; };
|
|
void wl_event_source_remove(void*) {}
|
|
int close(int) { return 0; }
|
|
std::string refusal(std::string_view reason) { return std::string(reason); }
|
|
@@ -79,7 +79,7 @@ struct Lane {
|
|
drag.emplace(lease);
|
|
}
|
|
bool available() const { return !suspended && session; }
|
|
- bool layout_qualified() const { return layout; }
|
|
+ bool layout_qualified(InputRoute route, uint64_t cap) const { return keyboard_layout_ready(route, cap, layout, layout); }
|
|
bool refresh(Client&) const { return refresh_ok; }
|
|
template<typename T> bool primary_conflict(const T&) const { return primary_busy; }
|
|
template<typename T> bool agent_conflict(const T&) const { return peer_busy; }
|
|
@@ -122,7 +122,7 @@ int main() {
|
|
for (bool session_fault : {true, false}) {
|
|
for (int path = 0; path < 3; ++path) {
|
|
Lane l;
|
|
- if (session_fault) l.session = false; else l.layout = false;
|
|
+ if (session_fault) l.session = false; else { l.layout = false; l.capabilities = 2; }
|
|
if (path == 0) l.guard_targets();
|
|
if (path == 1) l.target_refusal(*l.lease);
|
|
if (path == 2) l.action_refusal(*l.lease);
|
|
@@ -133,6 +133,19 @@ int main() {
|
|
l.session = true; l.layout = true; l.guard_targets(); l.check_inert();
|
|
}
|
|
}
|
|
+ // Pointer admission, dispatch and ongoing drag ignore keyboard layout readiness.
|
|
+ for (auto route : {InputRoute::independent, InputRoute::primary_foreground}) {
|
|
+ for (uint64_t cap : {1, 4, 8, 16}) {
|
|
+ Lane l; l.layout = false; l.lease->route = route; l.capabilities = cap;
|
|
+ l.target_refusal(*l.lease, route, cap);
|
|
+ check(l.lease && l.responses.empty(), "pointer admission gated on layout");
|
|
+ l.action_refusal(*l.lease, cap);
|
|
+ check(l.lease && l.responses.empty(), "pointer dispatch gated on layout");
|
|
+ l.guard_targets();
|
|
+ check(l.lease && l.drag && l.held_button && l.responses.empty(),
|
|
+ "pointer operation was gated by keyboard layout");
|
|
+ }
|
|
+ }
|
|
// Every passive safety guard still applies during either desktop fault.
|
|
for (bool session_fault : {true, false}) {
|
|
for (int bad = 0; bad < 8; ++bad) {
|
|
diff --git a/tests/desktop_fault_policy_test.py b/tests/desktop_fault_policy_test.py
|
|
index 68ca6c8..7a4d1b4 100644
|
|
--- a/tests/desktop_fault_policy_test.py
|
|
+++ b/tests/desktop_fault_policy_test.py
|
|
@@ -35,10 +35,11 @@ def fixture(source):
|
|
):
|
|
block = source.split(start, 1)[1].split(end, 1)[0]
|
|
refusals = [line.strip() for line in block.splitlines()
|
|
- if 'if (' in line and ('!available()' in line or '!layout_qualified()' in line)]
|
|
+ if 'if (' in line and ('!available()' in line or '!layout_qualified(' in line)]
|
|
if len(refusals) != 2:
|
|
raise AssertionError(f'production refusal branches not found: {label}')
|
|
- methods += '\nvoid ' + label + '(Client& c) {\n' + '\n'.join(refusals) + '\n}'
|
|
+ params = ', InputRoute route = InputRoute::independent, uint64_t requested_cap = 2' if label == 'target_refusal' else ', uint64_t cap = 2'
|
|
+ methods += '\nvoid ' + label + '(Client& c' + params + ') {\n' + '\n'.join(refusals) + '\n}'
|
|
return (ROOT / 'tests/desktop_fault_policy_fixture.cpp').read_text().replace(
|
|
'// PRODUCTION_METHODS', methods)
|
|
|
|
diff --git a/tests/keyboard_layout_test.cpp b/tests/keyboard_layout_test.cpp
|
|
new file mode 100644
|
|
index 0000000..dd7c962
|
|
--- /dev/null
|
|
+++ b/tests/keyboard_layout_test.cpp
|
|
@@ -0,0 +1,97 @@
|
|
+#include "keyboard_layout.hpp"
|
|
+#include <cstdlib>
|
|
+#include <iostream>
|
|
+#include <memory>
|
|
+
|
|
+using namespace cua::hyprland;
|
|
+void check(bool condition, const char* message) {
|
|
+ if (!condition) { std::cerr << message << '\n'; std::exit(1); }
|
|
+}
|
|
+int main() {
|
|
+ using Context = std::unique_ptr<xkb_context, decltype(&xkb_context_unref)>;
|
|
+ using Map = std::unique_ptr<xkb_keymap, decltype(&xkb_keymap_unref)>;
|
|
+ using State = std::unique_ptr<xkb_state, decltype(&xkb_state_unref)>;
|
|
+ Context context{xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref};
|
|
+ Map agent{agent_keymap(context.get()), xkb_keymap_unref};
|
|
+ Map second{agent_keymap(context.get()), xkb_keymap_unref};
|
|
+ check(bool(agent) && bool(second), "canonical maps unavailable");
|
|
+ const auto map = [&](const char* layout, const char* options) {
|
|
+ const xkb_rule_names names{"evdev", "pc105", layout, "", options};
|
|
+ return Map{xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
|
+ };
|
|
+ auto stock = map("us", "compose:caps,shift:both_capslock_cancel");
|
|
+ auto nocaps = map("us", "ctrl:nocaps");
|
|
+ auto swapctrl = map("us", "ctrl:swapcaps");
|
|
+ auto swapalt = map("us", "altwin:swap_alt_win");
|
|
+ auto german = map("de", "");
|
|
+ check(stock && nocaps && swapctrl && swapalt && german, "test keymaps unavailable");
|
|
+ for (auto* physical : {stock.get(), nocaps.get(), swapctrl.get(), swapalt.get(), german.get()}) {
|
|
+ check(foreground_chord_compatible(physical, agent.get(), 30, 0), "unrelated remap blocked A");
|
|
+ check(foreground_chord_compatible(physical, agent.get(), 30, 1), "unrelated remap blocked Shift+A");
|
|
+ check(foreground_chord_compatible(physical, agent.get(), 28, 0), "unrelated remap blocked Return");
|
|
+ }
|
|
+ check(foreground_chord_compatible(stock.get(), agent.get(), 30, 2), "stock Omarchy blocked Ctrl+A");
|
|
+ check(foreground_chord_compatible(nocaps.get(), agent.get(), 30, 2), "Caps to Ctrl blocked Ctrl+A");
|
|
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 30, 2), "Ctrl/Caps swap sent wrong Ctrl+A");
|
|
+ check(!foreground_chord_compatible(swapctrl.get(), agent.get(), 29, 0), "remapped modifier key accepted");
|
|
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 4), "Alt/Super swap sent wrong Alt+A");
|
|
+ check(!foreground_chord_compatible(swapalt.get(), agent.get(), 30, 8), "Alt/Super swap sent wrong Super+A");
|
|
+ check(!foreground_chord_compatible(german.get(), agent.get(), 21, 0), "German Z accepted as US Y");
|
|
+ check(!foreground_chord_compatible(german.get(), agent.get(), 3, 1), "German shifted punctuation accepted");
|
|
+ check(!foreground_chord_compatible(nullptr, agent.get(), 30, 0), "missing physical map accepted");
|
|
+ // All supported wire chords remain compatible on the independent map.
|
|
+ for (unsigned code = 1; code <= 247; ++code)
|
|
+ if (code != 58 && code != 69 && code != 70)
|
|
+ for (unsigned mods = 0; mods < 16; ++mods)
|
|
+ check(foreground_chord_compatible(agent.get(), agent.get(), code, mods), "canonical chord rejected");
|
|
+ // Two lanes never share modifier state and never mutate the human state.
|
|
+ State first{xkb_state_new(agent.get()), xkb_state_unref};
|
|
+ State other{xkb_state_new(second.get()), xkb_state_unref};
|
|
+ State human{xkb_state_new(german.get()), xkb_state_unref};
|
|
+ xkb_state_update_key(first.get(), 42 + 8, XKB_KEY_DOWN);
|
|
+ check(xkb_state_key_get_one_sym(first.get(), 30 + 8) == XKB_KEY_A, "agent shift not active");
|
|
+ check(xkb_state_key_get_one_sym(other.get(), 30 + 8) == XKB_KEY_a, "agent shift leaked across lanes");
|
|
+ check(xkb_state_key_get_one_sym(human.get(), 21 + 8) == XKB_KEY_z, "human layout changed");
|
|
+ // Same symbols can hide a changed XKB action. Simulate an A that sets Mod3.
|
|
+ char* serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
+ std::string changed = serialized;
|
|
+ std::free(serialized);
|
|
+ const auto at = changed.find("key <AC01>");
|
|
+ const auto end = changed.find(';', at);
|
|
+ check(at != std::string::npos && end != std::string::npos, "test action fixture unavailable");
|
|
+ changed.replace(at, end - at + 1,
|
|
+ "key <AC01> { type=\"ALPHABETIC\", symbols[Group1]=[a,A], actions[Group1]=[LockMods(modifiers=Mod3),LockMods(modifiers=Mod3)] };");
|
|
+ Map lock{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
|
+ check(bool(lock), "action fixture failed to compile");
|
|
+ check(!foreground_chord_compatible(lock.get(), agent.get(), 30, 0), "hidden lock action accepted");
|
|
+ // Equal symbols and modifier state can still alter toolkit shortcut matching.
|
|
+ serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
+ changed = serialized;
|
|
+ std::free(serialized);
|
|
+ const auto type_at = changed.find("type \"ALPHABETIC\"");
|
|
+ const auto type_end = changed.find("};", type_at);
|
|
+ check(type_at != std::string::npos && type_end != std::string::npos, "key type fixture unavailable");
|
|
+ changed.insert(type_end, "preserve[Shift] = Shift;\n");
|
|
+ Map preserved{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
|
+ check(bool(preserved), "preserved modifier fixture failed to compile");
|
|
+ check(!foreground_chord_compatible(preserved.get(), agent.get(), 30, 1), "consumed modifier mismatch accepted");
|
|
+ // A modifier that preserves Shift state but emits another symbol must fail.
|
|
+ serialized = xkb_keymap_get_as_string(agent.get(), XKB_KEYMAP_FORMAT_TEXT_V1);
|
|
+ changed = serialized;
|
|
+ std::free(serialized);
|
|
+ const auto shift_at = changed.find("key <LFSH>");
|
|
+ const auto shift_end = changed.find(';', shift_at);
|
|
+ check(shift_at != std::string::npos && shift_end != std::string::npos, "shift fixture unavailable");
|
|
+ changed.replace(shift_at, shift_end - shift_at + 1,
|
|
+ "key <LFSH> { symbols[Group1]=[Delete], actions[Group1]=[SetMods(modifiers=Shift)] };");
|
|
+ Map badshift{xkb_keymap_new_from_string(context.get(), changed.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref};
|
|
+ check(bool(badshift), "shift fixture failed to compile");
|
|
+ check(!foreground_chord_compatible(badshift.get(), agent.get(), 30, 1), "modifier press symbols not checked");
|
|
+ for (auto route : {InputRoute::independent, InputRoute::primary_foreground}) {
|
|
+ for (uint64_t cap : {1, 4, 8, 16}) check(keyboard_layout_ready(route, cap, false, false), "pointer gated on layout");
|
|
+ check(!keyboard_layout_ready(route, 2, false, false), "key accepted without map");
|
|
+ }
|
|
+ check(keyboard_layout_ready(InputRoute::independent, 2, true, false), "background depends on human map");
|
|
+ check(!keyboard_layout_ready(InputRoute::primary_foreground, 2, true, false), "foreground uses agent readiness");
|
|
+ std::cout << "keyboard layout tests passed\n";
|
|
+}
|
|
diff --git a/tests/plugin_input_lifetime_test.cpp b/tests/plugin_input_lifetime_test.cpp
|
|
index ff09451..b999925 100644
|
|
--- a/tests/plugin_input_lifetime_test.cpp
|
|
+++ b/tests/plugin_input_lifetime_test.cpp
|
|
@@ -51,12 +51,17 @@ int main() {
|
|
check(setenv("XDG_RUNTIME_DIR", directory, 1) == 0 &&
|
|
setenv("HYPRLAND_INSTANCE_SIGNATURE", "mock", 1) == 0,
|
|
"select runtime");
|
|
- Config::Values::configured_bool_override = true;
|
|
+ Config::Values::configured_bool_override = false;
|
|
static_cast<void>(pluginInit(nullptr));
|
|
const auto toggle = [](bool enabled) {
|
|
HyprlandAPI::registered_bool->set_mock_value(enabled);
|
|
Event::bus()->m_events.config.reloaded.emit();
|
|
};
|
|
+#ifdef CUA_HYPRLAND_INPUT
|
|
+ const auto initial_status = HyprlandAPI::registered_legacy_command->fn(FORMAT_JSON, {});
|
|
+ check(created == 0 && initial_status.find("\"configured\":false") != std::string::npos && initial_status.find("\"keyboard_layout_independent\":true") != std::string::npos,
|
|
+ "disabled production module advertises compiled keyboard support before enable");
|
|
+#endif
|
|
for (unsigned i = 0; i < 20; ++i) {
|
|
toggle(true);
|
|
#ifdef CUA_HYPRLAND_INPUT
|
|
@@ -64,6 +69,7 @@ int main() {
|
|
check(status.find("\"state\":\"input_v3_candidate\"") != std::string::npos &&
|
|
status.find("trusted_local_per_action") != std::string::npos &&
|
|
status.find("\"input\":{}") != std::string::npos &&
|
|
+ status.find("\"keyboard_layout_independent\":true") != std::string::npos &&
|
|
status.find("operator") == std::string::npos,
|
|
"v3 status advertises its actual admission mode");
|
|
#endif
|