Skip to content

Keyboard and paste

The keyboard tools type into the focused app. They need the lease, and each takes expect, the window the agent means to type into.

The server types through a virtual keyboard of its own, with the compositor’s keymap. The keys go to the app, not to niri: niri’s own keybinds don’t fire from them. They run through the same gate as the other action tools, then check, before anything is typed:

  1. the arguments: type_text refuses over 1000 characters, counted as Unicode scalar values, with text_too_long, and an empty text, an empty list of keys, more than 16 keys, or any combination it can’t read is an argument mistake
  2. expect against the window with keyboard focus: focus_mismatch when it doesn’t match. A lock screen, the overview or a shell panel leaves no window focused, so only "none" types there
  3. the focused window: app_denied if its app_id is on the policy’s deny list

expect is required, so the agent says where it means to type:

expect Means
{"window_id": <id>} that window, from desktop_state, must have keyboard focus
{"app_id": "<app_id>"} the focused window must have that app_id
"none" no check, for example to type into a shell panel or a dialog that holds focus outside the windows

Each key is pressed and released, and niri acknowledges it before the next one goes out. Before each key the server checks that keyboard focus is still on the window it started on and that niri’s active layout hasn’t changed, and if either did, sends nothing more; before anything was typed that is focus_mismatch. A focused app that goes on your deny list during the call gets nothing more either, with app_denied. The result has accepted: true, focus (matched, or unchecked for "none"), and observed: sent once niri has acknowledged the last key, or interrupted if keyboard focus moved off the window that had it, or the layout changed, during the call; an interrupted result comes with a screenshot. If niri’s event stream was lost meanwhile, observed is uncertain with accepted: true: the keys went out, but where focus went is unknown. A change to niri’s keyboard configuration during the call ends it uncertain too. What the keys did is for the next screenshot to show.

Before the first key, the server writes the input-dirty marker (pending), naming the keycodes the call may press, never the text. A key goes out only within 250 ms of the server’s last read of the lock state, which it reads again when it has grown older; otherwise the call ends lock_stale. A stop or a lost lease once a key went out ends the call uncertain, with the reason in detail, accepted: true and typed or pressed counting what went out; before that it is stopped or lease_required. A call gets 50 ms for each character or key, and at least three seconds. One still typing after that stops, before its next key or while a key waits for niri, with deadline_exceeded saying how much it typed. A stopped, cancelled or timed-out call releases its keys at once. Every call, however it ends, sends zero modifiers and puts niri’s latest keymap back in the layout niri has active, also when the keyboard configuration changed or you switched layouts during the call, and the marker comes off once niri has acknowledged that. If the server can’t confirm that clients hold that keymap, the marker stays and every action refuses with recovery_required until the user runs niri-computer-use recover. If the server is killed mid-call, its crash guardian releases the keys and sends the compositor’s keymap back; if the guardian is killed too, the next server to start does it. The marker stays either way until recover. The text is never logged: the audit log has text_len.

Text with symbols the active layout lacks, such as é or → on a US layout, is typed through a temporary keymap: the compositor’s, with the missing symbols added on keycodes the layout leaves empty and Chromium knows, then on F13 to F24, which few keyboards have. Every other key keeps its code and symbols, and a key you press during the call, F13 to F24 included, means what it always does: niri sends clients the compositor’s keymap before a key from your keyboard. The nested test suite checks this: F13 pressed during a call whose keymap had borrowed it arrived as XF86Tools. There are only so many spare keycodes, 14 in a US layout, so text with more distinct missing symbols goes out in parts, each with a keymap of its own that the server uploads right before the part’s first key. The compositor’s keymap comes back once, at the end. A part ends right before a missing symbol it has no spare keycode left for, so the parts follow the text and nothing is reordered. A missing control character is refused with refused.

A key combination, a paste’s key among them, goes out in the active layout when that layout has its key, and otherwise in the first of your other layouts that has it. So ctrl+v pastes and ctrl+s saves while a layout without Latin letters, such as Russian, is active. The application is back in the layout you had as soon as the key is released, and niri’s active layout never changes. A combination none of your layouts has is refused with refused.

Modifiers you hold and locks you have on, such as Shift, Caps Lock or Num Lock, don’t change what is typed: each key goes out with the modifiers it needs and no lock. Afterwards the focused application believes no modifier is down and no lock is on, until you press or release a modifier or a lock key, niri’s keymap changes, or focus moves.

So a lock that was on before the call reads as off in that application after every call, even if nobody touched the keyboard. With Num Lock on, a keypad digit you type afterwards acts as it does with Num Lock off and moves the cursor; with Caps Lock on, letters come out lowercase. A modifier still held after the call is lost the same way: a letter typed with Shift still down comes out lowercase. Pressing the lock key twice, or moving focus away and back, puts the application back in step with the keyboard. A modifier you press or release while a call types can reach the application between a key’s modifiers and the key itself, and change that key.

click, drag and scroll can hold modifiers with keys. Their release ends like a typing call’s.

Characters outside the Basic Multilingual Plane, emoji among them, arrive in Chromium and Electron apps cut to 16 bits, as another character: 🤖 (U+1F916) arrives as U+F916. That is Chromium’s handling of the key’s symbol, so send text holding them with via: "paste".

Chromium drops a key whose keycode it has no DOM code for, so the native backend puts symbols your layout lacks on keycodes Chromium knows. Typed into Vesktop (Discord) on a US layout, each of these arrived exactly: ab é ab ß ab → ab; a αβγδεζηθικλμν ok, 14 missing symbols in one keymap; αβγδεζηθικλμνξοπρστυφχψω ok ←↑→↓ done, 28 in two parts; café naïve façade Ærø; and Привет, мир. So in Chromium and Electron apps only characters outside the Basic Multilingual Plane need via: "paste", with one exception: Chromium reports a symbol typed on F13 to F24, the third and later missing symbols of a keymap on a US layout, as that F key too, so a shortcut the app has on F13 to F24, such as a Discord push-to-talk key or a VS Code keybinding, can run while the symbol is typed. If you have such a shortcut, have agents paste text with more than two distinct symbols your layout lacks.

Argument Value
keys (required) 1 to 16 combinations, pressed in order. Each is modifiers and one key joined by +, such as ctrl+s, ctrl+shift+t, alt+F4 or Return. The key is an XKB keysym name (a, Return, Escape, F5, slash, Page_Down); the modifiers are shift, ctrl, alt, altgr and super
expect (required) see above
screenshot see Action tools
element_changes default false; with true, element changes for the window focused when the call starts

For each combination, presses the modifiers, presses and releases the key, then releases the modifiers in reverse order. If focus moves after one, the rest aren’t pressed: observed is interrupted and pressed gives the number that were. A combination that fails ends the call with that error, its detail starting with how many were pressed before it, and saying so when the failed one may have gone out too, even if it was the first: pressed 0 of 1 keys, and more may have gone out; then ….

Argument Value
text (required) 1 to 1000 characters, or up to 1 MiB with via: "paste". Typed text with a control character is an argument mistake. A line break (\n, \r) or a tab would be pressed as Return or Tab, so a message would go out before the rest of it, or the rest would land in another field, and the others as keys such as BackSpace (\u0008), Escape (\u001b) or Delete (\u007f). Use submit, key with Return or Tab, or via: "paste" for text with several lines
expect (required) see above
via keys, the default, types the text; paste pastes it, see Pasting
paste_keys with via: "paste", required: the combination that pastes in the focused app, ctrl+v, ctrl+shift+v (terminals) or shift+Insert; with via: "keys", an argument mistake
submit default false; with true, Return is pressed once all of the text went out. With via: "paste", an argument mistake: press Return with key after looking at the result
screenshot see Action tools
element_changes default false; with true, element changes for the window focused when the call starts

The text goes out one key at a time. If focus moved, the rest isn’t typed: observed is interrupted and typed gives the number of characters sent. A key that fails ends the call with that error, its detail starting with how many characters were typed before it and, with submit, whether Return was pressed, as in typed 150 of 150 characters, not yet the submitting Return; then …. A key sent to niri that it didn’t acknowledge in time may still have gone out, and the detail says so: typed 150 of 150 characters, and the submitting Return may have gone out; then …. That holds for the first key too: typed 0 of 2 characters, and more may have gone out. Every failure whose detail has such a count also has may_have_sent: true. A result without typed means the whole text went out. With submit, the result has submitted: true once Return was pressed, false when the text stopped early and Return wasn’t pressed.

type_text with via: "paste" and paste_keys pastes up to 1 MiB of text, counted in UTF-8 bytes, through the clipboard, then puts the clipboard back. expect and the deny list are checked first, before the clipboard is touched. Then a keeper process, the server’s own binary as niri-computer-use paste-keeper, reads every type the clipboard offers through the wlr data-control protocol, up to 16 MiB in all within two seconds; if it can’t, if the clipboard holds x-kde-passwordManagerHint set to secret (other than an earlier paste’s text, below), or if something else was copied while it saved, the call ends with clipboard_unsaved and nothing changes. The server waits up to eleven seconds for the keeper to take the clipboard or refuse: a second to start, then two seconds each to find niri, connect, save, check for a copy made meanwhile and take the clipboard. The keeper takes the selection with the text, offered as text/plain;charset=utf-8, text/plain, UTF8_STRING, STRING and TEXT, marked with that hint so clipboard managers that honour it keep it out of their history, and with application/x-niri-computer-use-paste, which marks it as a paste’s. The server then presses paste_keys as key would, with the same checks. Right before the key goes out, it asks the keeper to admit it and waits up to half a second for the answer. The keeper admits the key only while it still holds the text on the clipboard and is still waiting for the key, which it does for ten seconds after taking the clipboard; after that it has already put the clipboard back, and it admits nothing. A key the keeper doesn’t admit isn’t sent. Neither is one admitted after the lock state’s last check has grown older than 250 ms: the call fails with lock_stale and the clipboard is put back.

Once the key went out, the keeper waits up to two seconds for an app to read the text, then until no new read has begun for 100 ms, and offers every saved type again. It keeps serving them, as wl-copy does, until something else is copied. If the key didn’t go out, it restores at once. A stop, a cancelled call or a client that goes away doesn’t cut short a key already on its way: the keeper still waits for that key and then for the read, and other input is refused until it has finished. If the server dies once the key may be on its way, the keeper can’t know whether the key will still arrive, so it doesn’t restore: the pasted text stays on the clipboard, and what you had copied before is lost, rather than pasted into the app by a late key. That text stays until something else is copied or the next paste, which replaces it although it is marked secret, and afterwards clears the clipboard rather than put it back. If something else was copied meanwhile, that copy stays, unless niri handles it in one of two moments: between the keeper’s check after saving and its taking the clipboard, or between its last check and its restore. The clipboard protocol can’t set the selection only if nothing else took it, so neither moment can be closed.

The result is key’s with paste: read, whether an app asked for the text after the key, and clipboard: restored; cleared, when nothing was copied before and nothing is again, or when the clipboard held an earlier paste’s text, which detail then says was dropped; replaced; failed, with detail; kept, when the server couldn’t say whether the key went out, so the pasted text stays; or unknown when the keeper didn’t report within six and a half seconds. A failed key’s error says what became of the clipboard in its detail. When the keeper didn’t admit the key, the call fails with upstream_error, and its detail says that the key wasn’t sent, that nothing was pasted, and what became of the clipboard, usually that it was restored. A read can’t be traced to the app it came from: a clipboard manager that reads every new selection and ignores the hint counts as a read. An app that reads the clipboard as soon as it changes and keeps what it read has the text before the key and reads nothing after it, so it pastes with read: false: Qt Quick text fields do this. The text is never logged: the audit log has text_len.