Keyboard grabs
gnoblin.input.grab(options?) takes the keyboard for your shell. While the grab is alive, every key press and release
goes to your code. No application, no shortcut and no other handler sees a key. Use it for a launcher, a password
prompt, a key-binding recorder or a modal overlay that types text. It returns a Grab.
local grab = gnoblin.input.grab({timeout = 10})
local typed = ""
grab:on("key", function(grab, key)
if not key.pressed then
return
end
if key.key == "Escape" then
grab:release()
elseif key.key == "BackSpace" then
typed = typed:sub(1, -2)
elseif key.text then
typed = typed .. key.text
end
end)
grab:on("closed", function(grab, reason)
print("grab ended:", reason, "typed:", typed)
end)Call gnoblin.input.grab while you handle an event, as with every operation that changes the session. No key may be
down when the grab starts. See what a grab cannot do.
The lease
A grab is a lease. It ends by itself when it has had no key event and no grab:renew() for timeout seconds. A script
that stops, hangs or forgets to release cannot keep the keyboard.
| Option | Value |
|---|---|
timeout |
Idle seconds, a whole number from 1 to 60. Default 10. |
- Every key event that reaches the grab starts the timeout again. Someone who types needs the grab, so a long entry
does not expire. A script that waits for a slow task, with no key, calls
grab:renew(). grab:renew()returns anOperationthat succeeds with{id, renewed = true}. It fails withnot_foundfor a grab that has ended.grab:release()gives the keyboard back. It returns anOperationthat succeeds with{id, released = true}, and fails withnot_foundfor a grab that has ended. The grab ends, and its"closed"listeners run with the reason"released", when the compositor has done it.
There is one grab for the whole compositor. A new grab ends the old one with the reason "replaced".
The Grab object
| Field | Value |
|---|---|
id |
Identifier of this grab, "grab-" and a number. |
alive |
true until the grab has ended. |
timeout |
The idle seconds you asked for. |
device |
What the grab holds. It is always "keyboard". |
The Grab exists at once, before the compositor confirms it. If the compositor refuses the grab, the object ends and its
"closed" listeners get the reason, which is the message of the compositor.
grab:on(signal, fn) takes one of these signals and returns a subscription with an unsubscribe() method:
| Signal | fn receives |
Runs when |
|---|---|---|
"key" |
grab, key |
A key is pressed or released, or a press repeats. |
"closed" |
grab, reason |
The grab has ended. Listening ends after this signal. |
Key events
key is a table with these fields:
| Field | Value |
|---|---|
key |
Name of the keysym: "a", "A", "Return", "Escape", "dead_acute". |
keysym |
The keysym number. |
keycode |
The hardware key code. |
modifiers |
The modifier mask when the key came. Shift is 1, Control 4, Alt 8 and Super 67108864. |
pressed |
true for a press, false for a release. |
repeated |
true for a press that key repeat made. |
text |
What the press types, or nil. See below. |
composing |
true while a dead key or Compose sequence is in progress. |
time |
The event time in milliseconds. |
key names the key as the layout in use reads it, so the same physical key gives "a" on one layout and "q" on
another, and a shifted key gives "A". Use keycode to name a physical key.
Text
A press has text when it types something. The text comes from the keysym. Gnoblin also feeds every press to the
compose table of the locale (libxkbcommon), so dead keys and Compose sequences work:
- A key that types a character has that character as
text:"a","A","7","é". - A dead key, or the Compose key, starts a sequence. It and the keys inside the sequence have
composing = trueand notext. - The key that completes the sequence has the composed character as
textandcomposing = false. Dead acute andegive"é". - A key that breaks the sequence has its own
text, if it types anything. The sequence is dropped, so the dead key's own character is lost. - A release, a modifier,
Return,Tab,BackSpace, an arrow and any other key that types nothing have notext. Usekey.keyfor these. - A press with Control, Alt or Super held has no
text, because it is a command and not typing.
The locale comes from LC_ALL, LC_CTYPE or LANG; the C locale uses the en_US.UTF-8 table. If the locale has no
compose table, a dead key has no text, and Gnoblin logs a warning once.
When the grab ends
The "closed" signal passes one of these reasons:
| Reason | Meaning |
|---|---|
"released" |
Your code called grab:release(). |
"expired" |
The grab had no key and no renewal for timeout seconds. |
"replaced" |
Another call to gnoblin.input.grab took the keyboard. |
"locked" |
The session locked. The lock screen owns the keyboard. |
"preempted" |
Mutter gave the keyboard to another owner, such as a popup grab or an input-capture portal. |
"config_reloaded" |
A configuration reload committed. |
"runtime_stopped" |
The Lua worker is shutting down. |
A refused grab passes the message of the compositor instead.
On "config_reloaded" and "runtime_stopped" the listener runs as the old runtime goes. It can read, but it cannot start
an operation. A Lua worker that restarts loses its grab without a signal, because the new worker is a different
runtime. The compositor gives the keyboard back in every case.
What a grab cannot do
- It refuses to start while the session is locked, and it ends when the session locks. The lock screen always owns the
keyboard. The call raises no error; the
Grabends when the compositor answers, with a reason that says so. - It never takes the keyboard from a shortcut session (a
modalorcapture_inputshortcut) or fromgnoblin.shortcuts.capture, and they cannot start while a grab holds the keyboard. The grab ends when the compositor answers, with a reason that says so. BareSuperand every other shortcut stop working until the grab ends, because the grab takes the keys before the shortcuts see them. - Its key events carry no
grant. A grab cannot move, resize or focus a window with the keys it holds, which a trusted shortcut can. - It does not change the focus. The focused window stays focused, but it gets no keys.
- It refuses to start while any key is down. Mutter would give the release of that key to the grab, and the focused
application, which saw the press, would never see the key lift. The
Grabends when the compositor answers, with a reason that names the key that is held. A shortcut that fires on a press has its keys down, so start the grab from a key release, from a timer, or from a later event. - The keys that count as down are the keys that are physically down. The compositor watches key events ahead of every other handler, so a key pressed or released while the diagnostic console is open counts as it would anywhere else: a key released under the console does not stay down.
- Each keyboard holds its own keys. A keyboard that is removed (unplugged, or a remote-desktop device that goes away) lets go of the keys it held. A grab is refused only for a key that is still down.
Why shortcut sessions keep their own timer
A modal or capture_input shortcut session still ends after 10 seconds, with the reason timed_out. It does not use
the grab lease.
A session belongs to a gesture of the user: it starts when the user holds a key and ends when the user lets go. The 10
seconds only guard against a lost key release. No script owns a session, so nothing can renew it. Its events carry a
grant for a trusted shortcut.
The two cannot share a lease. A lease that a script renews would let a script hold a session open for ever, and a grab
that gives a grant would let a script move windows without a user gesture. They have different owners and
different trust, so they share Mutter's one key-capture slot and nothing else.
Keyboard grabs in gnoblinctl lua
A gnoblinctl lua script has no runtime that receives key events, so gnoblin.input.grab raises an error there. Make
the grab in a configuration file.
Bridge
Lua reaches the grab with the runtime operations input.grab, input.grab.renew and input.grab.release (bridge API
1.91), and receives the events gnoblin.input.grab.key and gnoblin.input.grab.closed. Socket clients do not have
them.