mirror of
https://github.com/hyprwm/Hyprland.git
synced 2026-08-24 02:24:14 -05:00
protocols/vicinae-hotkey-v1: implement client-managed global shortcuts (#15010)
* feat: implement ext-hotkey-v1 (client-managed global shortcuts) * protocols: rename ext-hotkey-v1 to vicinae-hotkey-v1 * protocols: address feedback
This commit is contained in:
@@ -537,6 +537,7 @@ protocolnew("protocols" "wlr-virtual-pointer-unstable-v1" true)
|
||||
protocolnew("protocols" "input-method-unstable-v2" true)
|
||||
protocolnew("protocols" "wlr-output-management-unstable-v1" true)
|
||||
protocolnew("protocols" "kde-server-decoration" true)
|
||||
protocolnew("protocols" "vicinae-hotkey-v1" true)
|
||||
protocolnew("protocols" "wlr-data-control-unstable-v1" true)
|
||||
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-focus-grab-v1" true)
|
||||
protocolnew("protocols" "wlr-layer-shell-unstable-v1" true)
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="vicinae_hotkey_v1">
|
||||
<copyright>
|
||||
Copyright © 2026 Aurelien Brabant
|
||||
|
||||
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 (including the next
|
||||
paragraph) 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.
|
||||
</copyright>
|
||||
|
||||
<description summary="client-managed global hotkeys">
|
||||
This protocol lets a client choose, and freely reconfigure, its own
|
||||
global hotkeys: it requests a specific key combination and the compositor
|
||||
accepts it or rejects it with a reason. The compositor remains the eventual
|
||||
arbiter (it may deny a request, revoke a binding at any time, or apply
|
||||
whatever policy it likes), but within that the application manages its own
|
||||
bindings, including changing them at runtime, with no out-of-band user
|
||||
configuration and no persisted compositor state.
|
||||
|
||||
A hotkey is a key combination that fires regardless of which surface holds
|
||||
keyboard focus. This is unrelated to keyboard-shortcuts-inhibit, which lets a
|
||||
focused client suppress the compositor's own shortcuts.
|
||||
|
||||
Whether a combination is exclusive is compositor policy. A compositor MAY
|
||||
treat combinations as exclusive and reject a clashing request with
|
||||
"already_bound", or MAY grant the same combination to more than one binding
|
||||
(each bound hotkey then receives the events), as some platforms do. Clients
|
||||
must cope with either.
|
||||
|
||||
It is intentionally a thin mechanism. It does NOT define which combinations
|
||||
are acceptable: that is policy and belongs to the compositor, expressed
|
||||
through the accept/deny channel. It does NOT persist hotkeys: a binding lives
|
||||
only as long as its vicinae_hotkey_v1 object and the client connection; there is
|
||||
no configure or storage step, a binding never outlives the client that created
|
||||
it, and reconfiguring is simply destroying a hotkey and binding the new
|
||||
combination.
|
||||
|
||||
Warning! The protocol described in this file is currently in the testing
|
||||
phase. Backward incompatible changes may be added together with the
|
||||
corresponding interface version bump. Backward compatible changes are added
|
||||
by bumping the interface version.
|
||||
|
||||
Security considerations:
|
||||
|
||||
A global hotkey lets an unfocused client observe that a specific key
|
||||
combination was pressed. Compositors are the security boundary and SHOULD
|
||||
apply policy when deciding whether to accept a request. In particular,
|
||||
implementations SHOULD reject combinations that do not include at least one
|
||||
non-latching modifier among Ctrl, Alt or Super, unless the trigger is a
|
||||
function key, because unmodified (and Shift-only) keys carry ordinary text
|
||||
entry and grabbing them turns this protocol into a keylogger.
|
||||
|
||||
The protocol's one hard guarantee is containment: a compositor MUST NOT
|
||||
deliver events for a combination the client did not successfully bind, and
|
||||
never forwards the raw key stream, so a client learns nothing about input it
|
||||
did not explicitly, and successfully, bind. Beyond that, a compositor MAY
|
||||
apply any further policy it sees fit (for example prompting the user,
|
||||
restricting which clients may bind, or limiting how many bindings a client may
|
||||
hold) and MAY revoke a binding at any time via the "revoked" event, including
|
||||
under user control.
|
||||
|
||||
These are recommendations, not wire requirements: a combination the
|
||||
compositor disallows is simply reported through the "denied" event with the
|
||||
"not_permitted" reason, so applications can rely on a uniform rejection path
|
||||
regardless of each compositor's policy.
|
||||
</description>
|
||||
|
||||
<interface name="vicinae_hotkey_manager_v1" version="1">
|
||||
<description summary="global hotkey factory">
|
||||
This interface is the entry point of the protocol. It is used to request
|
||||
global hotkeys.
|
||||
</description>
|
||||
|
||||
<enum name="modifiers" bitfield="true">
|
||||
<description summary="modifier keys required by a hotkey">
|
||||
A bitmask of the modifiers that must be held for the hotkey to fire.
|
||||
|
||||
These are fixed, keymap-independent semantic bits naming the standard
|
||||
modifiers, suitable for expressing a binding, unlike wl_keyboard.modifiers,
|
||||
which carries opaque, keymap-derived masks for runtime state. A compositor
|
||||
matches each bit against the corresponding modifier in its keyboard state
|
||||
as the keymap defines it; for the xkb_v1 keymap format that is shift ->
|
||||
XKB_MOD_NAME_SHIFT, ctrl -> XKB_MOD_NAME_CTRL, alt -> XKB_MOD_NAME_ALT
|
||||
("Mod1"), super -> XKB_MOD_NAME_LOGO ("Mod4").
|
||||
|
||||
Lock modifiers (Caps Lock, Num Lock) are never part of a binding and are
|
||||
ignored when matching.
|
||||
</description>
|
||||
<entry name="shift" value="1" summary="the Shift modifier"/>
|
||||
<entry name="ctrl" value="2" summary="the Control modifier"/>
|
||||
<entry name="alt" value="4" summary="the Alt modifier"/>
|
||||
<entry name="super" value="8" summary="the Super/Logo modifier"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
Destroy the manager object. Hotkey objects created through this manager
|
||||
are unaffected and remain valid; their bindings persist until they are
|
||||
themselves destroyed or the client disconnects.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="bind">
|
||||
<description summary="request a global hotkey">
|
||||
Request that the given key combination be bound as a global hotkey.
|
||||
|
||||
The keysym identifies the trigger key using the keysym numbering shared
|
||||
with the keymap delivered over wl_keyboard (for the xkb_v1 keymap format,
|
||||
an XKB_KEY_* value), taken in its unshifted form (XKB_KEY_b not XKB_KEY_B). A compositor SHOULD match
|
||||
it against any of the user's layout groups, so a binding keeps firing after
|
||||
the user switches layout group. This is one deliberately specified rule,
|
||||
not a universal one: other platforms differ (macOS matches a physical
|
||||
keycode, X11 re-grabs per layout, Windows tracks the active layout's key),
|
||||
and no rule serves a layout that never produces the keysym; the single
|
||||
documented rule is chosen for predictability across compositors.
|
||||
|
||||
The request creates an vicinae_hotkey_v1 object immediately, but the binding
|
||||
is not active yet. The compositor replies asynchronously with either the
|
||||
"bound" event (the hotkey is now active) or the "denied" event (the
|
||||
request was rejected, with a reason).
|
||||
|
||||
seat is the wl_seat whose keyboard should trigger the hotkey, or null to
|
||||
request it on all of the client's seats. Most clients pass null; seat is
|
||||
provided for completeness on multi-seat systems.
|
||||
|
||||
app_id identifies the requesting application, for use in the compositor's
|
||||
policy and any user-facing audit UI. Where the application has a desktop
|
||||
entry, app_id SHOULD be its desktop file ID as defined by the
|
||||
freedesktop.org Desktop Entry specification, that is the .desktop file
|
||||
name with the .desktop suffix removed (for example "org.example.Launcher"),
|
||||
matching the convention used by xdg_toplevel.set_app_id so a compositor
|
||||
can correlate the two. It is advisory and may be spoofed; a compositor
|
||||
that needs a trustworthy identity SHOULD obtain it through the
|
||||
security-context mechanism rather than relying on this string.
|
||||
|
||||
description is a human-readable, localized description of what the hotkey
|
||||
does (e.g. "Toggle the launcher"), for display in compositor UI or even in this protocol error messages.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="vicinae_hotkey_v1"
|
||||
summary="the new hotkey object"/>
|
||||
<arg name="keysym" type="uint" summary="keysym of the trigger key (see description)"/>
|
||||
<arg name="modifiers" type="uint" enum="modifiers"
|
||||
summary="bitmask of required modifiers"/>
|
||||
<arg name="seat" type="object" interface="wl_seat" allow-null="true"
|
||||
summary="target seat, or null for all of the client's seats"/>
|
||||
<arg name="app_id" type="string"
|
||||
summary="advisory application identifier (SHOULD be the desktop file ID)"/>
|
||||
<arg name="description" type="string"
|
||||
summary="human-readable action description"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="vicinae_hotkey_v1" version="1">
|
||||
<description summary="a single global hotkey">
|
||||
Represents one requested global hotkey. Its binding is ephemeral: it is
|
||||
released when this object is destroyed or when the client disconnects, and
|
||||
it is never written to any persistent store.
|
||||
|
||||
After bind, exactly one of "bound" or "denied" is sent. While the hotkey is
|
||||
bound, "pressed" and "released" are sent as the combination is activated.
|
||||
The compositor may send "revoked" at any time after "bound" to indicate the
|
||||
binding is no longer active (for example because the user removed it, or a
|
||||
higher-priority binding took the combination).
|
||||
</description>
|
||||
|
||||
<enum name="deny_reason">
|
||||
<description summary="why a bind request was denied">
|
||||
Carried by the "denied" event. This is a reason code, not a fatal
|
||||
protocol error: the object remains valid and the client should destroy
|
||||
it (or try a different combination).
|
||||
</description>
|
||||
<entry name="already_bound" value="0"
|
||||
summary="already held by another hotkey the compositor treats as exclusive. The compositor is not obligated to give this level of detail and can just give not_permitted as the general 'deny' reason."/>
|
||||
<entry name="not_permitted" value="1"
|
||||
summary="the combination is disallowed by compositor policy"/>
|
||||
<entry name="invalid" value="2"
|
||||
summary="the keysym or combination is not a valid trigger"/>
|
||||
</enum>
|
||||
|
||||
<enum name="revoke_reason">
|
||||
<description summary="why a previously bound hotkey was withdrawn">
|
||||
Carried by the "revoked" event. This is a reason code, not a fatal
|
||||
protocol error: the object remains valid and the client should destroy
|
||||
it.
|
||||
|
||||
The distinction is actionable: on "superseded" the combination may
|
||||
become available again, so a client may reasonably re-request it later;
|
||||
on "removed" the withdrawal is a deliberate user or compositor decision,
|
||||
so a client MUST NOT silently re-request the same combination. A client
|
||||
that receives a value it does not recognise (a future addition) MUST
|
||||
treat it conservatively as "removed" and not auto-rebind.
|
||||
</description>
|
||||
<entry name="removed" value="0"
|
||||
summary="withdrawn by the user or compositor; do not auto-rebind"/>
|
||||
<entry name="superseded" value="1"
|
||||
summary="a higher-priority binding took the combination; it may become available again"/>
|
||||
<entry name="not_permitted" value="2"
|
||||
summary="the combination is no longer allowed by compositor policy"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="release the hotkey">
|
||||
Release the binding and destroy the object. The combination becomes
|
||||
available again immediately. It is valid to destroy the object before
|
||||
receiving "bound" or "denied", which cancels the pending request.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="bound">
|
||||
<description summary="the hotkey is now active">
|
||||
The requested combination was accepted and is now active. "pressed" and
|
||||
"released" events may follow.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="denied">
|
||||
<description summary="the request was rejected">
|
||||
The requested combination was not bound. No input events will be sent.
|
||||
The client should destroy this object; it may then create a new request
|
||||
with a different combination.
|
||||
|
||||
message is an optional, advisory, human-readable explanation in the
|
||||
compositor's locale, intended for verbatim display in client UI (for
|
||||
example a "change shortcut" settings screen). It may be empty, and a
|
||||
compositor is never required to provide it. Clients MUST NOT parse it or
|
||||
rely on its contents in any way; all programmatic behavior keys off
|
||||
reason. A client with nowhere to show it simply ignores it.
|
||||
</description>
|
||||
<arg name="reason" type="uint" enum="deny_reason" summary="why it was rejected"/>
|
||||
<arg name="message" type="string"
|
||||
summary="optional human-readable detail for display (may be empty)"/>
|
||||
</event>
|
||||
|
||||
<event name="revoked">
|
||||
<description summary="a previously active hotkey was withdrawn">
|
||||
A binding that was previously "bound" is no longer active. No further
|
||||
input events will be sent for it. The client should destroy this object;
|
||||
it may request the combination again later.
|
||||
|
||||
message is an optional, advisory, human-readable explanation in the
|
||||
compositor's locale, intended for verbatim display in client UI. It may
|
||||
be empty, and a compositor is never required to provide it. Clients MUST
|
||||
NOT parse it or rely on its contents in any way; all programmatic
|
||||
behavior keys off reason. A client with nowhere to show it simply
|
||||
ignores it.
|
||||
</description>
|
||||
<arg name="reason" type="uint" enum="revoke_reason" summary="why it was withdrawn"/>
|
||||
<arg name="message" type="string"
|
||||
summary="optional human-readable detail for display (may be empty)"/>
|
||||
</event>
|
||||
|
||||
<event name="pressed">
|
||||
<description summary="the hotkey combination was pressed">
|
||||
The bound combination was activated.
|
||||
|
||||
This event is sent once per activation. While the combination is held
|
||||
down no further "pressed" events are sent, and a single "released"
|
||||
follows when the trigger key is released: the compositor does not
|
||||
auto-repeat the hotkey.
|
||||
|
||||
serial is a compositor-issued input serial that counts as a recent user
|
||||
interaction, so the client can act on the hotkey even though the
|
||||
triggering key press is not otherwise delivered to it (the compositor
|
||||
consumes it). In particular, a client may pass this serial to
|
||||
xdg_activation_token_v1.set_serial in order to raise or focus a surface in
|
||||
response to the hotkey; a compositor SHOULD honor activation carried by
|
||||
this serial, since the hotkey is itself a deliberate user action. The
|
||||
serial is drawn from the same space as other input event serials.
|
||||
|
||||
time is the event timestamp, with the same millisecond clock as wl_pointer
|
||||
and wl_keyboard input events; it is useful for ordering and for measuring
|
||||
press-to-release duration.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="input serial for the activation (e.g. xdg-activation)"/>
|
||||
<arg name="time" type="uint" summary="timestamp in milliseconds"/>
|
||||
</event>
|
||||
|
||||
<event name="released">
|
||||
<description summary="the hotkey combination was released">
|
||||
The trigger key of a previously pressed combination was released. This
|
||||
enables press-and-hold uses (e.g. push-to-talk); clients that only act
|
||||
on activation may ignore it.
|
||||
|
||||
serial and time have the same meaning as in the "pressed" event.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="input serial for the activation (e.g. xdg-activation)"/>
|
||||
<arg name="time" type="uint" summary="timestamp in milliseconds"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../devices/IKeyboard.hpp"
|
||||
#include "../managers/SeatManager.hpp"
|
||||
#include "../protocols/ShortcutsInhibit.hpp"
|
||||
#include "../protocols/Hotkey.hpp"
|
||||
#include "../protocols/core/DataDevice.hpp"
|
||||
#include "../errorOverlay/Overlay.hpp"
|
||||
#include "KeybindManager.hpp"
|
||||
@@ -257,6 +258,32 @@ uint32_t CKeybindManager::keycodeToModifier(xkb_keycode_t keycode) {
|
||||
}
|
||||
}
|
||||
|
||||
SP<SKeybind> CKeybindManager::findConflictingKeybind(xkb_keysym_t keysym, uint32_t modmask) {
|
||||
if (keysym == XKB_KEY_NoSymbol)
|
||||
return nullptr;
|
||||
|
||||
for (const auto& k : m_keybinds) {
|
||||
if (!k->enabled || k->mouse)
|
||||
continue;
|
||||
if (!k->submap.name.empty())
|
||||
continue;
|
||||
if (k->modmask != modmask)
|
||||
continue;
|
||||
|
||||
xkb_keysym_t bindSym = XKB_KEY_NoSymbol;
|
||||
if (!k->key.empty())
|
||||
bindSym = xkb_keysym_from_name(k->key.c_str(), XKB_KEYSYM_CASE_INSENSITIVE);
|
||||
else if (k->keycode != 0 && m_xkbTranslationState)
|
||||
// "code:NN" binds store the xkb keycode
|
||||
bindSym = xkb_state_key_get_one_sym(m_xkbTranslationState, k->keycode);
|
||||
|
||||
if (bindSym != XKB_KEY_NoSymbol && bindSym == keysym)
|
||||
return k;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CKeybindManager::updateXKBTranslationState() {
|
||||
if (m_xkbTranslationState) {
|
||||
xkb_state_unref(m_xkbTranslationState);
|
||||
@@ -350,6 +377,9 @@ bool CKeybindManager::onKeyEvent(std::any event, SP<IKeyboard> pKeyboard) {
|
||||
|
||||
const auto MODS = g_pInputManager->getModsFromAllKBs();
|
||||
|
||||
if (PROTO::hotkey && PROTO::hotkey->onKey(keysym, MODS, KEYCODE, e.state == WL_KEYBOARD_KEY_STATE_PRESSED, e.timeMs))
|
||||
return false;
|
||||
|
||||
Config::Actions::state()->m_timeLastMs = e.timeMs;
|
||||
Config::Actions::state()->m_lastCode = KEYCODE;
|
||||
Config::Actions::state()->m_lastMouseCode = 0;
|
||||
|
||||
@@ -124,6 +124,7 @@ class CKeybindManager {
|
||||
void removeKeybind(const std::string& displayKeys);
|
||||
uint32_t stringToModMask(std::string);
|
||||
uint32_t keycodeToModifier(xkb_keycode_t);
|
||||
SP<SKeybind> findConflictingKeybind(xkb_keysym_t keysym, uint32_t modmask);
|
||||
void clearKeybinds();
|
||||
void shadowKeybinds(const xkb_keysym_t& doesntHave = 0, const uint32_t doesntHaveCode = 0);
|
||||
SSubmap getCurrentSubmap();
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
#include "../protocols/ContentType.hpp"
|
||||
#include "../protocols/XDGTag.hpp"
|
||||
#include "../protocols/XDGBell.hpp"
|
||||
#include "../protocols/Hotkey.hpp"
|
||||
#include "../protocols/ExtWorkspace.hpp"
|
||||
#include "../protocols/ExtDataDevice.hpp"
|
||||
#include "../protocols/PointerWarp.hpp"
|
||||
@@ -193,6 +194,7 @@ CProtocolManager::CProtocolManager() {
|
||||
PROTO::contentType = makeUnique<CContentTypeProtocol>(&wp_content_type_manager_v1_interface, 1, "ContentType");
|
||||
PROTO::xdgTag = makeUnique<CXDGToplevelTagProtocol>(&xdg_toplevel_tag_manager_v1_interface, 1, "XDGTag");
|
||||
PROTO::xdgBell = makeUnique<CXDGSystemBellProtocol>(&xdg_system_bell_v1_interface, 1, "XDGBell");
|
||||
PROTO::hotkey = makeUnique<CHotkeyProtocol>(&vicinae_hotkey_manager_v1_interface, 1, "Hotkey");
|
||||
PROTO::extWorkspace = makeUnique<CExtWorkspaceProtocol>(&ext_workspace_manager_v1_interface, 1, "ExtWorkspace");
|
||||
PROTO::extDataDevice = makeUnique<CExtDataDeviceProtocol>(&ext_data_control_manager_v1_interface, 1, "ExtDataDevice");
|
||||
PROTO::pointerWarp = makeUnique<CPointerWarpProtocol>(&wp_pointer_warp_v1_interface, 1, "PointerWarp");
|
||||
@@ -303,6 +305,7 @@ CProtocolManager::~CProtocolManager() {
|
||||
PROTO::colorManagement.reset();
|
||||
PROTO::xdgTag.reset();
|
||||
PROTO::xdgBell.reset();
|
||||
PROTO::hotkey.reset();
|
||||
PROTO::extWorkspace.reset();
|
||||
PROTO::extDataDevice.reset();
|
||||
PROTO::pointerWarp.reset();
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "Hotkey.hpp"
|
||||
#include "../Compositor.hpp"
|
||||
#include "../devices/IKeyboard.hpp"
|
||||
#include "../managers/KeybindManager.hpp"
|
||||
#include "../event/EventBus.hpp"
|
||||
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
#include <format>
|
||||
|
||||
static constexpr uint32_t RELEVANT_MODS = HL_MODIFIER_SHIFT | HL_MODIFIER_CTRL | HL_MODIFIER_ALT | HL_MODIFIER_META;
|
||||
|
||||
static uint32_t protoModsToHL(uint32_t mods) {
|
||||
uint32_t out = 0;
|
||||
if (mods & VICINAE_HOTKEY_MANAGER_V1_MODIFIERS_SHIFT)
|
||||
out |= HL_MODIFIER_SHIFT;
|
||||
if (mods & VICINAE_HOTKEY_MANAGER_V1_MODIFIERS_CTRL)
|
||||
out |= HL_MODIFIER_CTRL;
|
||||
if (mods & VICINAE_HOTKEY_MANAGER_V1_MODIFIERS_ALT)
|
||||
out |= HL_MODIFIER_ALT;
|
||||
if (mods & VICINAE_HOTKEY_MANAGER_V1_MODIFIERS_SUPER)
|
||||
out |= HL_MODIFIER_META;
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isFunctionKey(xkb_keysym_t sym) {
|
||||
return sym >= XKB_KEY_F1 && sym <= XKB_KEY_F35;
|
||||
}
|
||||
|
||||
static bool isValidTrigger(xkb_keysym_t sym, uint32_t modmask) {
|
||||
if (isFunctionKey(sym))
|
||||
return true;
|
||||
return modmask & (HL_MODIFIER_CTRL | HL_MODIFIER_ALT | HL_MODIFIER_META);
|
||||
}
|
||||
|
||||
static std::string keybindLabel(const SP<SKeybind>& k) {
|
||||
if (!k->description.empty())
|
||||
return k->description;
|
||||
if (!k->arg.empty())
|
||||
return k->handler + ", " + k->arg;
|
||||
return k->handler;
|
||||
}
|
||||
|
||||
CVicinaeHotkeyManager::CVicinaeHotkeyManager(SP<CVicinaeHotkeyManagerV1> resource_) : m_resource(resource_) {
|
||||
if UNLIKELY (!good())
|
||||
return;
|
||||
|
||||
m_resource->setOnDestroy([this](CVicinaeHotkeyManagerV1*) { PROTO::hotkey->destroyManager(this); });
|
||||
m_resource->setDestroy([this](CVicinaeHotkeyManagerV1*) { PROTO::hotkey->destroyManager(this); });
|
||||
|
||||
m_resource->setBind([this](CVicinaeHotkeyManagerV1* mgr, uint32_t id, uint32_t keysym, vicinaeHotkeyManagerV1Modifiers mods, wl_resource* seat, const char* appid,
|
||||
const char* description) { PROTO::hotkey->onBind(m_resource, id, (xkb_keysym_t)keysym, (uint32_t)mods, appid, description); });
|
||||
}
|
||||
|
||||
bool CVicinaeHotkeyManager::good() {
|
||||
return m_resource->resource();
|
||||
}
|
||||
|
||||
CHotkeyProtocol::CHotkeyProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
|
||||
m_reloadListener = Event::bus()->m_events.config.reloaded.listen([this] { revokeConflicting(); });
|
||||
}
|
||||
|
||||
void CHotkeyProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) {
|
||||
const auto RESOURCE = m_managers.emplace_back(makeShared<CVicinaeHotkeyManager>(makeShared<CVicinaeHotkeyManagerV1>(client, ver, id)));
|
||||
|
||||
if UNLIKELY (!RESOURCE->good()) {
|
||||
wl_client_post_no_memory(client);
|
||||
m_managers.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CHotkeyProtocol::destroyManager(CVicinaeHotkeyManager* mgr) {
|
||||
// hotkeys outlive their manager
|
||||
std::erase_if(m_managers, [&](const auto& other) { return other.get() == mgr; });
|
||||
}
|
||||
|
||||
bool CHotkeyProtocol::comboTakenByHotkey(xkb_keysym_t keysym, uint32_t modmask) {
|
||||
for (const auto& hk : m_hotkeys) {
|
||||
if (hk->bound && hk->keysym == keysym && hk->modmask == modmask)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CHotkeyProtocol::onBind(SP<CVicinaeHotkeyManagerV1> mgr, uint32_t id, xkb_keysym_t keysym, uint32_t protoMods, const char* appid, const char* description) {
|
||||
auto hk = makeShared<SBoundHotkey>();
|
||||
hk->resource = makeShared<CVicinaeHotkeyV1>(mgr->client(), mgr->version(), id);
|
||||
|
||||
if UNLIKELY (!hk->resource->resource()) {
|
||||
mgr->noMemory();
|
||||
return;
|
||||
}
|
||||
|
||||
hk->keysym = keysym;
|
||||
hk->modmask = protoModsToHL(protoMods);
|
||||
hk->appid = appid ? appid : "";
|
||||
hk->description = description ? description : "";
|
||||
|
||||
SBoundHotkey* raw = hk.get();
|
||||
hk->resource->setDestroy([raw](CVicinaeHotkeyV1*) { std::erase_if(PROTO::hotkey->m_hotkeys, [&](const auto& other) { return other.get() == raw; }); });
|
||||
hk->resource->setOnDestroy([raw](CVicinaeHotkeyV1*) { std::erase_if(PROTO::hotkey->m_hotkeys, [&](const auto& other) { return other.get() == raw; }); });
|
||||
|
||||
m_hotkeys.emplace_back(hk);
|
||||
|
||||
if (keysym == XKB_KEY_NoSymbol) {
|
||||
hk->resource->sendDenied(VICINAE_HOTKEY_V1_DENY_REASON_INVALID, "the keysym is not a valid trigger");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidTrigger(keysym, hk->modmask)) {
|
||||
hk->resource->sendDenied(VICINAE_HOTKEY_V1_DENY_REASON_NOT_PERMITTED, "a non-latching modifier (Ctrl, Alt or Super) is required unless the trigger is a function key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (comboTakenByHotkey(keysym, hk->modmask)) {
|
||||
hk->resource->sendDenied(VICINAE_HOTKEY_V1_DENY_REASON_ALREADY_BOUND, "the combination is already bound by another hotkey");
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto CONFLICT = g_pKeybindManager->findConflictingKeybind(keysym, hk->modmask)) {
|
||||
hk->resource->sendDenied(VICINAE_HOTKEY_V1_DENY_REASON_ALREADY_BOUND,
|
||||
std::format("the combination is reserved by a compositor keybind ({})", keybindLabel(CONFLICT)).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
hk->bound = true;
|
||||
hk->resource->sendBound();
|
||||
}
|
||||
|
||||
bool CHotkeyProtocol::onKey(xkb_keysym_t keysym, uint32_t modmask, uint32_t keycode, bool pressed, uint32_t timeMs) {
|
||||
const uint32_t mods = modmask & RELEVANT_MODS;
|
||||
bool consumed = false;
|
||||
|
||||
if (pressed) {
|
||||
for (const auto& hk : m_hotkeys) {
|
||||
if (!hk->bound || hk->held)
|
||||
continue;
|
||||
if (hk->keysym != keysym || hk->modmask != mods)
|
||||
continue;
|
||||
|
||||
hk->held = true;
|
||||
hk->heldCode = keycode;
|
||||
const uint32_t serial = wl_display_next_serial(g_pCompositor->m_wlDisplay);
|
||||
hk->resource->sendPressed(serial, timeMs);
|
||||
consumed = true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& hk : m_hotkeys) {
|
||||
if (!hk->held || hk->heldCode != keycode)
|
||||
continue;
|
||||
|
||||
hk->held = false;
|
||||
hk->heldCode = 0;
|
||||
const uint32_t serial = wl_display_next_serial(g_pCompositor->m_wlDisplay);
|
||||
hk->resource->sendReleased(serial, timeMs);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void CHotkeyProtocol::revokeConflicting() {
|
||||
for (const auto& hk : m_hotkeys) {
|
||||
if (!hk->bound)
|
||||
continue;
|
||||
const auto CONFLICT = g_pKeybindManager->findConflictingKeybind(hk->keysym, hk->modmask);
|
||||
if (!CONFLICT)
|
||||
continue;
|
||||
|
||||
hk->bound = false;
|
||||
hk->held = false;
|
||||
hk->heldCode = 0;
|
||||
hk->resource->sendRevoked(VICINAE_HOTKEY_V1_REVOKE_REASON_SUPERSEDED,
|
||||
std::format("the combination is now reserved by a compositor keybind ({})", keybindLabel(CONFLICT)).c_str());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "../defines.hpp"
|
||||
#include "vicinae-hotkey-v1.hpp"
|
||||
#include "./WaylandProtocol.hpp"
|
||||
#include "../helpers/signal/Signal.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
// hotkeys are owned by the protocol, not this manager, so they outlive it
|
||||
class CVicinaeHotkeyManager {
|
||||
public:
|
||||
CVicinaeHotkeyManager(SP<CVicinaeHotkeyManagerV1> resource);
|
||||
|
||||
bool good();
|
||||
|
||||
private:
|
||||
SP<CVicinaeHotkeyManagerV1> m_resource;
|
||||
|
||||
friend class CHotkeyProtocol;
|
||||
};
|
||||
|
||||
class CHotkeyProtocol : public IWaylandProtocol {
|
||||
public:
|
||||
CHotkeyProtocol(const wl_interface* iface, const int& ver, const std::string& name);
|
||||
|
||||
void bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) override;
|
||||
|
||||
// returns true if a bound hotkey consumed the key
|
||||
bool onKey(xkb_keysym_t keysym, uint32_t modmask, uint32_t keycode, bool pressed, uint32_t timeMs);
|
||||
|
||||
void revokeConflicting();
|
||||
|
||||
private:
|
||||
struct SBoundHotkey {
|
||||
SP<CVicinaeHotkeyV1> resource;
|
||||
xkb_keysym_t keysym = XKB_KEY_NoSymbol;
|
||||
uint32_t modmask = 0; // HL_MODIFIER_* bits
|
||||
std::string appid;
|
||||
std::string description;
|
||||
bool bound = false;
|
||||
bool held = false;
|
||||
uint32_t heldCode = 0;
|
||||
};
|
||||
|
||||
void onBind(SP<CVicinaeHotkeyManagerV1> mgr, uint32_t id, xkb_keysym_t keysym, uint32_t protoMods, const char* appid, const char* description);
|
||||
void destroyManager(CVicinaeHotkeyManager* mgr);
|
||||
bool comboTakenByHotkey(xkb_keysym_t keysym, uint32_t modmask);
|
||||
|
||||
std::vector<SP<CVicinaeHotkeyManager>> m_managers;
|
||||
std::vector<SP<SBoundHotkey>> m_hotkeys;
|
||||
|
||||
CHyprSignalListener m_reloadListener;
|
||||
|
||||
friend class CVicinaeHotkeyManager;
|
||||
};
|
||||
|
||||
namespace PROTO {
|
||||
inline UP<CHotkeyProtocol> hotkey;
|
||||
};
|
||||
Reference in New Issue
Block a user