mirror of
https://github.com/hyprwm/Hyprland.git
synced 2026-08-24 02:24:14 -05:00
* feat: implement ext-hotkey-v1 (client-managed global shortcuts) * protocols: rename ext-hotkey-v1 to vicinae-hotkey-v1 * protocols: address feedback
306 lines
16 KiB
XML
306 lines
16 KiB
XML
<?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>
|