Desktop integration
What an app has to tell the desktop about itself, beyond drawing. Today that is startup notification, which is on by default and has nothing to call; the launcher's view of the app — a badge on its icon, a bounce for attention, the Dock's menu; what a password field has to do to be reachable by the desktop's password managers; and the one switch that turns off everything here that talks over D-Bus.
The menu bar is the other half of this and has a page of its own —
globalmenu.md — because on a desktop that shows application
menus in its panel, MenuBar hands the menu over and stops drawing. Same
default shape as startup notification: on with no configuration, and one
switch to turn it off.
Everything on this page is the app talking outwards. The one thing that
comes the other way — the desktop handing the app a myapp://… link to open,
and the single-instance question that goes with it — is
uri-schemes.md. It shares the launch timestamp described
below: ctx.timestamp there is the same _TIME suffix launchTimestamp()
parses here, and for the same reason.
Startup notification
When a launcher spawns an app it opens a startup sequence — that is what
the busy cursor is — and closes it when the app says it is up. An app that
never says so leaves the sequence running until the desktop gives up on it:
mutter's STARTUP_TIMEOUT_MS is 15 seconds, long after the window is on
screen and being clicked. The mutter source is blunt about what that looks
like, in a comment on the constant itself: people assume the launch failed
and start it again.
Two other things ride on the same handshake:
- Focus.
_NET_WM_USER_TIMEis the evidence focus-stealing prevention weighs when deciding whether a new window may come to the front. With none, a strict desktop opens the window behind whatever the user was doing — correctly, from its point of view, since nothing said a user asked for this. - Placement. The sequence records which workspace the launch happened on. Without one the window goes wherever new windows go.
react-x11 does all of it with no configuration:
const root = await createRoot(); // that is the whole of it
DESKTOP_STARTUP_ID is read from the environment and removed from it.
That is deliberate and it matters: the variable names one launch, so a child
process that inherits it would claim a sequence that is not its own and end
it early — the parent's cursor stops when the child starts. Every toolkit
that gets this wrong produces that same bug.
With no id in the environment — a terminal, CI, XQuartz — nothing is set and nothing is sent.
When is an app "started"?
The default is the first frame that actually painted.
GTK ends the sequence when the toplevel maps, and copying that here would be
subtly wrong. This renderer does not paint on map: invalidate() schedules
through the frame clock and the drawing lands in flush() a frame later. So
a mapped window is an empty rectangle, and stopping the busy cursor there is
compliant and dishonest — it says "ready" over a blank window.
A suspense fallback counts as painted, and should. If the first frame is a spinner because the tree is waiting on data, that is exactly the right moment to stop the system's spinner: the app is up and is telling the user what it is doing. There is no signal for "finished loading", and guessing at one is how this ends back at fifteen seconds.
completeOn is there for apps the default does not suit:
await createRoot({ startupNotification: { completeOn: 'map' } });
completeOn | |
|---|---|
'paint' | the first frame that drew (default) |
'map' | the first toplevel mapping — earlier, and what GTK does |
'manual' | nothing automatic; call notifyStartupComplete() |
'map' suits an app whose first frame is expensive enough that it would
rather the cursor stopped before it. 'manual' suits one that is not up
until it says so — restoring a session behind a splash, say:
import { notifyStartupComplete } from 'react-x11';
await createRoot({ startupNotification: { completeOn: 'manual' } });
await restoreSession();
notifyStartupComplete(); // idempotent, and a no-op if there is no sequence
A backstop ends the sequence regardless, ten seconds after the window maps, whichever mode is in force. An app that never paints, or that forgets to call, cannot leave the cursor spinning — which would be this feature reproducing the bug it exists to fix. Ten is chosen to beat mutter's fifteen by a margin while being far longer than any honest first frame.
Turning it off, and supplying an id
await createRoot({ startupNotification: false });
await createRoot({ startupNotification: 'launcher/app/1-0_TIME9876' });
false is for an app that runs its own sequence, or an embedder that owns
the toplevel. Note that opting out leaves DESKTOP_STARTUP_ID in the
environment — if you are managing the sequence yourself, the id is yours to
read and yours to clear.
A string supplies the id for a launch where it did not arrive in the
environment. A D-Bus-activated app gets it in platform_data instead.
launchTimestamp()
import { launchTimestamp } from 'react-x11';
const when = launchTimestamp(); // number | null
The X server timestamp of the user action that launched the app, parsed from
the id's _TIME suffix. null is a real answer, not a failure: an app
started from a shell has no launch timestamp and never will.
It is the "when" that any later request to come forward is weighed against.
Do not substitute 0 for a missing one — EWMH gives zero its own meaning,
"do not focus this window when it maps".
On macOS
quartz-wm implements none of this, so on XQuartz the messages go to a root
window nobody is listening at and _NET_WM_USER_TIME is ignored. Harmless,
and worth knowing before concluding from a Mac that the feature is broken.
What is deliberately not here
- The launcher half. An app that spawns another app should generate an
id, send
new:, putDESKTOP_STARTUP_IDin the child's environment and close the sequence itself. Same encoder, different persona. - Ongoing
_NET_WM_USER_TIMEmaintenance. Setting it once at launch is this. Keeping it current on every keypress is a separate design with a real cost — EWMH is explicit that storing a frequently-changing property on the toplevel wakes every client watching that window, which is what_NET_WM_USER_TIME_WINDOWexists to avoid.
Security
The id is broadcast to the root window, so every client on the display sees it. On X11 that is the pre-existing no-isolation story rather than a new exposure — see security.md — but it is the reason the messages carry the id the launcher gave us and nothing invented, and the reason the variable's value is never logged.
The launcher: a badge, attention, and the Dock menu
The one thing an app says to the desktop that the user reads without opening it is the mark on its icon — an unread count on the Dock tile, a dot on the taskbar entry. Three things live here, and they are on both backends only where both have a mechanism:
Linux (launcherentry) | macOS (cocoa) | Windows (taskbar) | |
|---|---|---|---|
| a badge | com.canonical.Unity.LauncherEntry — KDE, elementary, Cairo-Dock listen; GNOME needs an extension | NSDockTile.badgeLabel | an overlay icon with the label drawn into it |
| badge text | — the protocol carries a count and nothing else | any label | up to three glyphs; longer clamps to 99+, and it is ~16px on screen |
| progress | useProgress(0…1) — a bar across the tile | — NSDockTile has none | ITaskbarList3::SetProgressValue, across the taskbar button |
| attention | states={['demands_attention']}, or setUrgent() for the launcher entry itself | the same prop: the Dock icon bounces until activated | FlashWindowEx |
| the launcher menu | useLauncherMenu(items) — the launcher protocol's quicklist | useLauncherMenu(items) | — the taskbar button's menu is the jump list, whose entries start a new process, so it is features.tasks and useJumpList rather than this (windows-integrations.md) |
| tasks | — Actions= in the .desktop file, not driven from here | — | useJumpList(tasks) — the jump list's Tasks category |
| a hover toolbar | — | — | useThumbnailToolbar(buttons) — up to seven, under the preview |
| recent documents | — | — the platform has one; this backend does not use it | useRecentDocument(path) — the shell's Recent lists |
The badge
import { setBadge, useBadge } from 'react-x11';
useBadge(unread); // a count: 3 shows "3", 0 clears
await setBadge(null); // the imperative twin, for code with no component
A badge is a count, because that is the one shape both desktops agree
on: a number shows on both. A string ('•', '!') shows on macOS, where the
tile takes any label, and is a visible count of nothing on Linux, where the
protocol has no text field — pass one only where the Mac is the audience.
0, null and '' all clear it. useBadge clears on unmount; setBadge
resolves to whether a launcher was told and never rejects for anything about
the machine.
On Linux the count travels as one D-Bus signal, LauncherEntry.Update,
attributed to the app by application://<appId>.desktop — so it needs the
identity registerApplication({ appId }) establishes and
a .desktop file of that name, or the launcher has nothing to pin the
count to and setBadge resolves false. The entry stays on the bus while
a badge is shown and is released when it is cleared. A launcher that starts
after the badge was set will not see it until the next setBadge; the
protocol has no query.
Progress
import { useProgress, setProgress } from 'react-x11';
useProgress(downloaded / total); // 0…1; null or undefined clears it
await setProgress(null); // the imperative twin
A bar drawn across the app's icon, for a download, an export, a long build. Out-of-range values are clamped rather than refused — a caller dividing by a total that briefly went to zero should not get an exception.
Linux launchers only. NSDockTile has no progress bar; an app that wants
one on a Mac draws it into a custom tile view, which is app-side art rather
than a call. useDesktopCapability('launcher').features.progress is the
honest answer to branch on.
Attention
Nothing new to call: <window states={['demands_attention']}> is the
existing request, and on the cocoa backend it is
NSApp.requestUserAttention — the Dock icon bounces until the user
activates the app, and stops when the prop drops the state or the window
goes away. AppKit ignores the request while the app is already active, which
is the same outcome a window manager gives a focused window's urgency hint.
The other states names are inert on that backend until the bridge grows
zoom, miniaturize and fullscreen.
The Dock menu
useLauncherMenu([
{ label: 'New Window', onSelect: openWindow },
{ type: 'separator' },
{
label: 'Recent',
items: recent.map((r) => ({ label: r.name, onSelect: () => open(r) })),
},
]);
The same items vocabulary MenuBar and ContextMenu take (menuitem
names), so one authoring model covers the window's menu, the
panel's, the tray's and the Dock's. Installed while the component is mounted,
replaced when items changes, taken down on unmount.
NSDockTile's menu on the cocoa backend; on Linux it is the launcher
protocol's quicklist — an object path to a com.canonical.dbusmenu tree,
which is the same menu protocol the tray and the global menu already speak.
ubuntu-dock/Dash-to-Dock, Plank and the Unity-heritage launchers render it.
An earlier version of this page said the freedesktop counterpart was
Actions=in the.desktopfile, an install step rather than runtime code. That is a different feature wearing a similar hat: desktop actions are the right place for entries that must work while the app is not running, and the quicklist is the right place for a menu that follows the app's state.
Like the badge, it needs the identity
registerApplication({ appId }) establishes and a
.desktop file of that name for a launcher to attach it to.
The app's presence
Two things the Dock and the ⌘-Tab switcher show that a bare node process
gets wrong, as root options on the cocoa backend:
await createRoot({
cocoa: {
appName: 'Notes', // what the Dock, ⌘-Tab and the app menu print (was "node")
activationPolicy: 'regular', // 'accessory' for a menu-bar app: no tile, no ⌘-Tab entry
},
});
activationPolicy is fixed before the app finishes launching — a Regular
launch registers a Dock tile, and an agent app that switched afterwards would
already have flashed its icon — so it is a root option rather than a hook.
appName renames LaunchServices' record of an unbundled process; a bundle's
Info.plist wins, as it should.
The tray
import { useTray } from 'react-x11';
const { available, backend, features, error } = useTray({
icon: 'mail-unread', // a themed icon name (an SF Symbol on macOS), or PNG bytes
tooltip: 'Notifications',
menu: [
{ label: 'Open', onSelect: open },
{ type: 'separator' },
{ label: 'Quit', onSelect: quit },
],
});
An icon in the system tray for as long as the component is mounted. With
menu a click opens it — the same items vocabulary as MenuBar and the
Dock menu, so the three menus an app puts on the desktop are one authoring
model. Without one, onClick gets the button, where the click was and, where
the backend knows it, the item's screen rect — all in logical screen pixels,
the unit a <popup>'s x/y are in, and what a popover under the item
anchors to (elements.md). title shows text beside the icon or alone;
visible, tooltip, attention and the rest follow their values while
mounted, and the item is removed on unmount. null means no item.
Several trays coexist: each is its own item.
Linux (statusnotifier) | macOS (cocoa) | Windows (shellnotifyicon) | |
|---|---|---|---|
| mechanism | org.kde.StatusNotifierItem over D-Bus | NSStatusItem | Shell_NotifyIcon |
| menu | com.canonical.dbusmenu | NSMenu | a tracked popup menu |
| icon by name | the desktop's icon theme | SF Symbols | — no icon theme to name |
| icon by bytes | ARGB32 pixmap | a template image | an HICON from RGBA |
| title | text beside the icon | text beside the icon | — an icon and a tooltip |
| attention | Status = NeedsAttention | — | — |
| click rect | — | the item's frame | — |
| click modifiers | — | shift/control/option/command | — |
| scroll | Scroll(delta, orientation) | — | — |
On Linux this is StatusNotifierItem, which is D-Bus only — so it works
identically under X11, XWayland and Wayland. It needs something hosting a
tray: KDE Plasma and most panels do out of the box, and GNOME needs an
AppIndicator extension. A stock GNOME session with no extension has no tray,
available is false, and nothing is logged, because that is a
configuration rather than a fault.
A click's position on Linux is a point whose unit the protocol never
names, and hosts differ. Plasma sends device pixels, and so does GNOME in its
physical layout, the default up to GNOME 49. GNOME's logical layout — the
default from 50, and on Fedora and Debian 13 before it — sends logical
pixels, as xfce4-panel, LXQt and Cinnamon do. So the point is read both
ways: a reading that lands on no monitor is dropped, then the one the pointer
is on is kept, and otherwise the numbers are taken as sent. What that cannot
tell apart is a device-pixel host under XWayland whose tray, read as logical
pixels, still lands on a monitor — in the top-left quarter of the screen, or
with another monitor beyond it — and there the point is scale times as far
from the screen's origin as the click was.
On the cocoa backend this is NSStatusItem, the menu-bar extra. A
menu-bar app that wants no Dock tile pairs it with
createRoot({ cocoa: { activationPolicy: 'accessory' } }) above. A PNG icon
is drawn as a template — its shape in the bar's ink, so it follows light
and dark the way a symbol does; template: false keeps its colours. AppKit
is handed a new NSMenu only when the menu's items change, so a title that
ticks every second leaves the menu alone, whether menu is memoized or
written inline.
What the hook hands back
available settles. Finding a freedesktop tray takes a bus round trip, so
it is false on the first frame and true a tick later if a host is there —
render the fallback first and upgrade, never the other way round. It follows
the host for the life of the item, so a panel that exits flips it back.
settled says whether available is an answer yet. For a feature inside
a window the order above is all an app needs. For an app whose whole UI is its
tray item, the fallback is a window, and rendering it first flashes a window
up and away on every start on a desktop that has a tray. So wait for the
answer instead:
const tray = useTray({ icon: 'waveform', onClick: open });
if (!tray.settled) return null; // a bus round trip, not a timeout
if (!tray.available) return <window title="Hush">{controls}</window>;
return popover;
It is true on the first frame on macOS, where the status item is made there
and then, and for useTray(null); on the freedesktop tray it turns true once
the host has taken or refused the item, and stays so.
features is the same vocabulary feature discovery
uses, for the rung that answered. Read it instead of testing the platform:
// Right: ask about the thing you need.
{
features.clickModifiers && <ShiftClickHint />;
}
error is the fourth field because "this desktop has no tray" and "there is
a tray and it refused me" are different facts, and only one of them has a
fix. A missing tray leaves error null; a host that rejected the registration
puts the reason there.
Feature discovery
Everything on this page is a ladder, and which rung a machine lands on
changes what the app may do. available alone is not enough to branch on,
because it is true on machines that mean very different things by it — a
notification daemon with actions gives you a banner with buttons that call
back, and notify-send gives you one-way text with an urgency and an icon.
An app that posts reply buttons on the second one gets a banner with no
buttons and no callback, silently.
So the unit of discovery is a feature set, not a flag:
import { useDesktopCapability } from 'react-x11';
const notifications = useDesktopCapability('notifications');
if (notifications.features.actions) return <ReplyFromBanner />;
if (notifications.available) return <BannerThenOpenApp />;
return <InAppToastOnly />;
{ available, backend, features, settled }. backend names the mechanism —
'dbus', 'cocoa', 'statusnotifier', 'launcherentry', 'notify-send',
'osascript', 'win32', 'shellnotifyicon', 'taskbar' — and never the
platform, so that a second Linux mechanism does not need a second name for
Linux.
A backend declares its mechanism rather than having one inferred from it,
because backends install the same method names for the same rung on purpose —
that sharing is what makes useTray one hook — so there is nothing in the
methods to tell two of them apart. Branch on features; backend is for logs
and bug reports.
settled is false until the first probe answers and true from then on, so
an app can hold its fallback back rather than flash it; it is true on the
first frame for the answers that need nothing asked, the tray and the Dock tile
on macOS.
Capability names are 'notifications', 'tray' and 'launcher'.
desktopCapability(name) is the imperative twin for code with no component.
Which one to ask
A hook that does the thing already knows whether it worked, and that measurement beats any prediction:
useTray(options)— "did my icon get taken?" It tried; it knows.useDesktopCapability('tray')— "would one be taken if I asked?" This is what a settings screen needs: a "Show tray icon" checkbox has to render correctly without putting an icon in the tray to find out.
Prefer the feature hook wherever the feature is actually mounted. Both read the same probe, so they cannot disagree.
Degrading well
Three rules, in the order they matter:
- Render the fallback first. Every desktop answer takes a round trip, so the first frame has none. Upgrading a fallback is invisible; downgrading a feature the user already reached for is not.
- Never disable a feature permanently on a
false. A panel restarts, an extension is enabled, a daemon is installed. The hooks re-probe onNameOwnerChangedand nothing is cached — an app that latched the first answer would outlive every one of those fixes. - A missing desktop service is not an error. No bus, no tray host, no
notification daemon and no launcher are all ordinary states of a healthy
machine. Nothing is logged and nothing throws; the feature is simply not
there. Reserve
errorfor a service that answered and then refused.
All of it, in one app
examples/desktop.jsx (npm run examples:desktop) is a backup agent that
uses every hook on this page at once — the tray icon with its attention state
and its overlay, the badge, the progress bar, the quicklist, the urgency flag,
and a banner whose buttons are offered only where the daemon has actions —
and whose second pane renders useDesktopCapability() for each of
CAPABILITIES beside the live answers, so the prediction and the measurement
are on screen together. It is the one to read when the question is what an app
will get on a machine you do not have. The three feature-sized examples are
badge.jsx (the launcher), tray.jsx (the tray) and notify.jsx (the
notification).
Turning the desktop off
Three things react-x11 turns on for you reach the session bus, and none of them was asked for:
appearance | following the desktop's light/dark, accent, contrast and reduced motion | appearance.md |
a11y | the AT-SPI bridge — whether a screen reader can see the app | accessibility.md |
globalMenu | a MenuBar handing its menu to the panel instead of drawing it | globalmenu.md |
Each is the right default: an app that follows the desktop looks like it
belongs there, one a screen reader can read is one more person can use, and a
menu in the panel is what the panel is for. But sooner or later an embedder
owns the thing we assumed was ours, and until
#417 the only way out was
an environment variable — NO_AT_BRIDGE=1,
REACT_X11_NO_GLOBAL_MENU=1, or unsetting DBUS_SESSION_BUS_ADDRESS. That is
a seam an app cannot reach for itself: the environment is inherited, so
setting one before createRoot() sets it for every child process too, and the
D-Bus one takes the portals and the app's own exported services down with the
follower.
await createRoot({ desktop: false }); // none of the three
await createRoot({ desktop: { appearance: false } }); // just that one
With all three off, nothing dials the session bus at startup — no connection, no subprocess, no portal probe. That is the shape an embedder wants, and a kiosk, and a test that needs the same answer on every machine.
Turning appearance off means the app draws in its own palette rather than
the desktop's: useSystemAppearance() reports 'no-preference' with
source: null, and the remembered answer on disk is not read either — an
opted-out app that started in whatever colours the machine was in last time
would be the opposite of what the switch is for.
Off is process-wide, and it latches. There is one desktop, one D-Bus identity and one AT-SPI bridge per process, so the policy cannot honestly be per-root: a second root turning back on what the first turned off would be a bug rather than a feature. Nothing turns an integration back on, and a feature already started stays started — pass this on the first root.
Not blocking the first frame on the bus
Worth knowing because it is invisible when it works, and was worth ~150 ms when it did not.
On macOS the session bus address is not in the environment; D-Bus advertises
it through launchd, and finding it means running launchctl getenv.
dbus-native's entry point is synchronous, so it has to do that with
spawnSync — 120–150 ms of blocked event loop on a cold page cache, landing
in front of the X handshake, the layout engine's WebAssembly and the first
paint, all so the accessibility bridge could find out there was nothing to
connect to.
Every dial now goes through one asynchronous, once-per-process lookup instead,
and hands dbus-native a resolved unix:path=…. The same wall clock is
spent, none of it on the loop: the handshake and the layout engine run
straight through it. There is nothing to configure — it is the same address,
found the same way, off the critical path.
Linux is unaffected: $DBUS_SESSION_BUS_ADDRESS or $XDG_RUNTIME_DIR/bus
answers without a subprocess, and always did.
Password fields and password managers
There is no password-field protocol on the Linux desktop. No toolkit
publishes "this is a password field, fill it", and no manager asks. What
exists instead are four seams, none of which a widget can opt into by
declaring itself — they are things an application either supports or does
not. PasswordInput supports the two that reach a field, and this is what
they turn out to be.
1. Typing — XTEST auto-type, and the keymap race
The mechanism nearly every desktop manager uses is auto-type: KeePassXC
matches the focused window's title against its entries, then synthesises
the keystrokes. Its X11 backend is the whole of the story — SendKeyEvent()
sends XTestFakeKeyEvent(), and for a character the current layout cannot
type, RemapKeycode() writes the keysym into a spare keycode with
XkbSetMap() first, XSyncing before it types.
For us that is good news and one hazard:
- A faked key is an ordinary key. XTEST events are delivered by the
server through the normal event path, so
PasswordInput— and every other focusable node — cannot tell auto-type from a person, and nothing has to be done to support it. An auto-type sequence of{USERNAME}{TAB}{PASSWORD} {ENTER}walks a react-x11 form because Tab moves focus and Enter reachesonSubmit. - The window needs a title worth matching. Auto-type's default matching is
on the window title, so
<window title="…">is the integration surface. A window titled after the document with nothing identifying the app is one a user cannot write an auto-type rule for;appIdis worth setting too, since a manager that grew a smarter matcher would read that. - The keymap race is real. ntk refetches the mapping when the server sends
MappingNotify, which is what makes a remapped keycode decode correctly — but the refetch is a round trip, and a manager that remaps, syncs and types immediately can land its key before the reply does. The window is small and only affects characters outside the user's layout, but a password of ASCII is not the case that fails. Nothing here can close it; the fix belongs where the map lives.
2. Pasting — the clipboard, and the hint that keeps it out of history
The other path every manager offers is copy-to-clipboard, usually with a
countdown before it clears. That is ordinary CLIPBOARD interop
(clipboard.md); PasswordInput takes Ctrl+V and
Shift+Insert, and strips control characters so a manager's trailing newline
does not end up inside the secret.
If your app ever puts a secret on the clipboard — this widget never does
— offer x-kde-passwordManagerHint with the value secret beside the text.
Klipper drops such an offer from its history, KDE Connect refuses to forward
it, and wl-clipboard marks the state sensitive. It is a convention rather
than a specification, and it is the only thing standing between a copied
password and a clipboard manager's on-disk history:
clipboard.write({ UTF8_STRING: secret, 'x-kde-passwordManagerHint': 'secret' });
PasswordInput also never takes the PRIMARY selection, which a
<textinput> does on every selection: PRIMARY is pasted by a middle click in
any window on the display, and a secret does not belong in a selection that
can be spent by accident.
3. Fetching it yourself — the Secret Service
When the app has an account of its own to unlock, the seam is not a field at
all: the Secret Service D-Bus API, org.freedesktop.secrets, which
gnome-keyring, KWallet and KeePassXC all implement, and which libsecret
speaks for C applications. react-x11 has no wrapper for it and does not need
one — useSessionBus() reaches it directly (dbus.md) — and the
shape is: search by attributes, unlock the collection if it is locked, read
the secret back. An app that does this shows no password field on most
launches, which is a better outcome than any field can offer.
Inside a sandbox that name is not there. Flatpak's answer is
org.freedesktop.portal.Secret, whose RetrieveSecret hands the app a
per-application master secret down a pipe; libsecret switches to it
automatically and encrypts a local store with it.
4. Reading the field — AT-SPI, which we do not implement
The accessibility bus is the only channel through which an outside program
can see a field's contents and role rather than guess at a window title:
AT-SPI2 gives a password entry the role password text, which is how a
screen reader knows to announce "bullet" instead of the character. Some
automation leans on the same tree. react-x11 has no accessibility tree yet
(NEXT_STEPS §11.3) — role is a prop the testing queries read and nothing
else — so this seam is closed here for now, and worth reopening as one piece
with the rest of AT-SPI rather than as a password-shaped hole in it.
What a field can still do wrong
None of the above stops the field itself from leaking. What PasswordInput
does about that, and what it cannot:
- the masked value is never laid out or drawn — no glyphs of the secret reach ntk's shaping cache or the X server, and the mask's width is measured from one reference character;
- there is no copy, no selection, no undo history;
- the value is still a JavaScript string, and strings are immutable. It cannot be zeroed, it lives until the garbage collector takes it, and every intermediate value typed on the way lives alongside it. A design that needs more than that needs the secret to never enter this process.