Desktop, folders, apps, games, launchers, and clipboard
Reference for dd.desktop items, dd.folders listings and icons, dd.apps launching, dd.games, the installed game library, dd.launchers, the items the user drops on a widget, and dd.clipboard, the history of what the user copies.
Four namespaces share one design: the host enumerates, the widget gets opaque ids, and opening something means handing an id back. A widget never passes a path, so it can only ever open what the host really listed.
They also share the change-ping pattern: onChange events carry no data,
they just tell you to list again.
dd.desktop
Needs the desktop permission.
dd.desktop.list()
dd.desktop.list(): Promise<{ items: DesktopItem[] }>
interface DesktopItem {
/** Opaque host-issued id: the only handle open() accepts. */
id: string;
name: string;
kind: "file" | "folder" | "shortcut" | "virtual";
/** Filesystem path; null for virtual items (Recycle Bin, This PC, ...). */
path: string | null;
/** Icon PNG data URI (up to 96 px), or null when the host couldn't extract one. */
icon: string | null;
}Everything currently on the user's desktop (user and public folders plus visible virtual items), sorted by name. Served from a host cache the desktop watcher keeps fresh, so it is cheap to call.
dd.desktop.open()
dd.desktop.open(id: string): Promise<void>Opens the item like a desktop double-click; a folder item opens in the file
manager the user chose in Settings > General, when one is set (app 0.3.7 or
newer). Accepts only ids from list; a
stale id (the item disappeared since your last list) rejects NOT_FOUND, so
re-list and retry. 5 opens per rolling 10 seconds, shared with
folders.open.
dd.desktop.onChange()
dd.desktop.onChange(cb: () => void): () => voidData-free ping when the desktop's item set changes (add, remove, rename,
icon refresh): call list again. Changes are debounced around 400 ms.
dd.folders
Needs the folders permission. All methods take settingKey, the name of
one of this widget's filePath settings; the host resolves the folder the
user pointed it at. A blank setting value resolves to the desktop source,
which additionally requires the desktop grant.
dd.folders.list()
dd.folders.list(settingKey: string): Promise<{
items: DesktopItem[];
total: number;
truncated: boolean;
error?: string;
}>One non-recursive listing of the granted folder. Items arrive with
icon: null here; fetch icons for what you actually render via icons (the
desktop source inlines them instead). Listings cap at 500 items
(truncated: true, total is the real count). An unreadable folder
(unplugged drive, dead share) answers the last good items plus
error: "unavailable" rather than rejecting. 30 calls per rolling 10
seconds.
dd.folders.icons()
dd.folders.icons(settingKey: string, ids: string[]): Promise<{
icons: Record<string, string>;
}>Icon PNG data URIs (up to 96 px) for up to 128 listed ids, extracted on demand and cached host-side. Unknown or extraction-failed ids are simply absent from the result, so keep a glyph fallback. 60 calls per rolling 10 seconds.
dd.folders.open()
dd.folders.open(settingKey: string, id: string): Promise<void>Opens a listed item like a double-click in Explorer; a folder item opens in
the file manager the user chose in Settings > General, when one is set (app
0.3.7 or newer). Stale ids reject
NOT_FOUND (re-list). Shares desktop.open's 5 per 10 seconds launch
bucket, whichever source the id lives in.
dd.folders.onChange()
dd.folders.onChange(cb: () => void): () => voidData-free ping when a folder one of your filePath settings points at
changes (the desktop, for blank settings): re-list. Delivered only to the
instances whose folder actually changed. Folders that cannot be watched
(network shares generally) re-enumerate on every list instead of pretending.
dd.apps
Needs the systemApps permission.
dd.apps.list()
dd.apps.list(): Promise<{ apps: SystemApp[] }>
interface SystemApp {
/** Catalog key: the only handle launch() accepts. */
id: string;
name: string;
/** Icon PNG data URI (up to 96 px), or null (render your own glyph). */
icon: string | null;
}The host's fixed catalog of Windows system apps (Settings, File Explorer, Recycle Bin, Task Manager, Terminal, Control Panel, Calculator, Notepad, Snipping Tool), with real icons where the host could extract them. Icons are cached host-side after the first call. 30 calls per rolling 10 seconds.
dd.apps.launch()
dd.apps.launch(id: string): Promise<void>Launches a catalog app by key. There is no path, URI, or executable surface
here; ids from list are the whole vocabulary. A shell-folder entry (the
Recycle Bin) opens in the file manager the user chose in Settings > General,
when one is set, handed the folder's shell id; the File Explorer entry always
launches explorer.exe (app 0.3.7 or newer). Each app carries host-owned
fallback launch targets; if all fail the call rejects INTERNAL naming the
app, and unknown ids reject NOT_FOUND. 5 calls per rolling 10 seconds.
dd.games
Needs the games permission. App 0.3.2 and newer.
dd.games.list()
dd.games.list(): Promise<{ games: InstalledGame[]; total: number }>
interface InstalledGame {
/** Opaque host-issued id, the only handle launch and art accept. */
id: string;
name: string;
store: "steam" | "gog";
/** Tagged host-side, because a library is often a third demos. */
kind: "game" | "demo" | "playtest";
/** Epoch ms, or null when the store records no play history. */
lastPlayedMs: number | null;
/** Total minutes played, or null when the store records none. */
playtimeMin: number | null;
sizeBytes: number | null;
installedMs: number | null;
}Every game installed on this PC that a supported launcher vouches for, most recently played first and then alphabetical.
The sources are the launchers' own records, read off disk and the registry: Steam's per-game manifests plus its local play-time config, and the GOG registry keys that both Galaxy and the offline installers write. There is no folder scan anywhere in this, by design, so nothing else the user keeps on a games drive can ever appear here.
The appid, the registry id and the install path never cross the bridge. A
widget sees the fields above and nothing more. GOG records no play history
that the host can read, so its games always report lastPlayedMs: null and
sort below anything played.
The scan is cached host-side for 60 seconds, so calling this every time a pane opens is cheap. 20 calls per rolling 10 seconds.
dd.games.art()
dd.games.art(
ids: string[],
kind?: "portrait" | "header" | "icon",
): Promise<{ art: Record<string, string> }>Cover art for up to 32 listed ids, as PNG or JPEG data URIs.
It comes from the launcher's own on-disk cache, never the network. kind
picks the shape you lay out for: portrait (the default, a 2:3 library
capsule) or header (the wide banner). Whichever you ask for, the other
fills in when a title cached only that one, so a tile is rarely empty.
icon asks for the game's square icon instead, so it suits a launcher
tile more than a cover wall. For Steam it is the icon the client writes for
the game's shortcuts, as a PNG up to 96 px, or the 32 px one its library
list shows when the client has none. It never falls back to a cover; a game
with no icon is absent from the result (app 0.4.5+).
Ids with no cached art are simply absent from the result, so keep a placeholder. A GOG entry answers its executable's icon for every kind, since GOG caches no art. 30 calls per rolling 10 seconds.
dd.games.launch()
dd.games.launch(id: string): Promise<void>Starts a listed game through its own launcher, by the target the host holds
for that id. As with apps.launch, ids are the whole vocabulary: there is no
path or command-line surface. An id from a stale list rejects NOT_FOUND
rather than starting anything. 5 calls per rolling 10 seconds.
dd.launchers
Needs the launchers permission. Grid widgets only. App 0.3.9 and newer.
Drop-to-launch: the user drags files, folders, shortcuts or exes from Explorer or the desktop onto your widget, and the host turns each one into a launch item. The drop is the consent. Nothing prompts, and nothing your widget says can add an item.
The store is per widget id, not per instance: any instance of your widget
may open any item dropped on any instance of it. Keep your own membership
list per instance (an id list in dd.storage), the way the drawer keeps
members. Ids not in the store stay latent, never wrong.
No path ever crosses the bridge. A widget sees the fields below and nothing more; the host resolves and opens each item itself.
dd.launchers.list()
dd.launchers.list(): Promise<{ items: LauncherItem[] }>
interface LauncherItem {
/** Opaque host-issued id, the only handle open and forget accept. */
id: string;
/** Frozen at drop time. */
name: string;
/** app = an exe or a shortcut to one; file and folder open like a double-click. */
kind: "app" | "file" | "folder";
/** Always null here: fetch via icons. */
icon: string | null;
}Everything the user dropped on your widget type, in drop order. An exe or a
shortcut resolves through the same ladder a taskbar pin uses, so its id
equals the pin of the same app; a file or folder hashes its full name, so a
desktop file keeps the id dd.desktop.list gives it. The same target
dropped twice dedupes.
Cap 256 items per widget; further drops are refused with a log line. 30 calls per rolling 10 seconds.
dd.launchers.icons()
dd.launchers.icons(ids: string[]): Promise<{ icons: Record<string, string> }>PNG data URIs (up to 96 px) for up to 128 ids of your store, extracted at drop time and cached host-side for the session. Unknown or failed ids are simply absent, so keep a glyph fallback. 60 calls per rolling 10 seconds.
dd.launchers.open()
dd.launchers.open(id: string): Promise<void>Opens an item like a double-click: an app through the shortcut or exe it was
dropped as (a dropped shortcut keeps its arguments), a file by name, a folder
in the file manager the user chose in Settings, when one is set. Accepts only
ids in your widget's store. A target that is gone rejects with NOT_FOUND.
No gesture gate: the user consented per item by dropping it. Shares the
desktop and folders launch quota of 5 per rolling 10 seconds.
dd.launchers.forget()
dd.launchers.forget(id: string): Promise<void>Removes the item from the store of every instance of your widget. A widget
with several instances edits its own membership list instead and never calls
this. The host drops the whole store when the widget is uninstalled. Unknown
id: NOT_FOUND.
dd.launchers.onDrag()
dd.launchers.onDrag(cb: (drag: { inside: boolean; count: number; kinds: string[] }) => void): () => voidAn OS drag entered (inside: true) or left your drop rect. count is the
number of paths in the drag; kinds are extension hints only (app for
.exe, shortcut for .lnk and .url, item otherwise), since the host
never touches the filesystem during a hover. Paint your drop ring from this:
the native dragover never reaches the page.
dd.launchers.onDrop()
dd.launchers.onDrop(cb: (drop: { items: LauncherItem[] }) => void): () => voidThe drop landed on you. The items are already in the store; an item that was
there already is announced again so this instance can adopt it. Add the ids
to your membership, re-render, and fetch icons via icons. Drops are refused
outright in edit mode and while widgets are peeked away.
dd.launchers.onChange()
dd.launchers.onChange(cb: () => void): () => voidThe store changed: a drop on any instance of your widget, a forget, or an
uninstall. Call list again.
dd.clipboard
Reading needs the clipboard permission. Writing needs clipboardWrite. Grid
widgets only. App 0.5.2 and newer.
The host keeps the history, in memory only. Your widget gets opaque ids, previews and thumbnails, and hands an id back to paste an item. A restart clears the history, and so does the last granted instance being hidden, disabled or removed, so it starts empty. All widgets with the grant share one history, because Windows has one clipboard.
A manifest cannot declare clipboard together with http, links or feeds.
The app and the store both refuse it: a widget that reads the clipboard must
have no way to send it off the machine. clipboardWrite reads nothing, so a
copy button can sit beside a network grant.
Limits. 50 items. Text up to 1 MB, images up to 32 MB encoded, 64 MB of images in all with the oldest removed first. Text wins when a copy carries both text and an image. Files and rich formats are not captured, so a rich copy restores as plain text.
Private copies. The host skips a copy that asks to stay out of clipboard
monitors (ExcludeClipboardContentFromMonitorProcessing, Clipboard Viewer Ignore, or CanIncludeInClipboardHistory set to 0), and a copy from a browser
extension. This promises nothing: other copies of a password still get through.
Treat everything you read as secret, do not show it unmasked by default, do not
save it unless that is what your widget is for, and never tell the user that
passwords stay out.
The click gate. restore, setPaused, writeText and writeImage reject
PERMISSION_DENIED unless a primary button release landed on your widget in the
last 1.5 seconds and the cursor is still inside. Call them from a click handler.
A keyboard press does not count, a widget covered by another one gets no click,
and none of them work in edit mode.
dd.clipboard.list()
dd.clipboard.list(): Promise<{ items: ClipboardItem[]; paused: boolean }>
type ClipboardItem =
| { id: string; kind: "text"; preview: string; bytes: number; copiedMs: number }
| {
id: string;
kind: "image";
width: number;
height: number;
/** PNG data URI, at most 256 px. */
thumbnail: string;
bytes: number;
copiedMs: number;
};Newest first. preview holds at most 2000 characters, and bytes is the whole
item. Rate limit 30 per 10 seconds.
dd.clipboard.get()
dd.clipboard.get(id: string): Promise<
| { kind: "text"; text: string }
| { kind: "image"; dataUrl: string; width: number; height: number }
>One item in full. An image above 8 MB rejects PAYLOAD_TOO_LARGE: show its
thumbnail, and use restore to paste it. An unknown id rejects NOT_FOUND.
Rate limit 30 per 10 seconds.
dd.clipboard.restore()
dd.clipboard.restore(id: string): Promise<void>Puts an item back on the clipboard and moves it to the top, without a second copy. An item of any size works, because the host holds the bytes. The click gate. Rate limit 10 per 10 seconds.
dd.clipboard.delete() and dd.clipboard.clear()
dd.clipboard.delete(id: string): Promise<void>
dd.clipboard.clear(): Promise<void>Remove one item, or every item, from the shared history. Rate limits 30 and 5 per 10 seconds.
dd.clipboard.setPaused()
dd.clipboard.setPaused(paused: boolean): Promise<void>Stops or restarts the capture for all widgets. It stays after a restart. While paused, the host does not open the clipboard at all. The click gate. Rate limit 5 per 10 seconds.
dd.clipboard.writeText()
dd.clipboard.writeText(text: string): Promise<void>Needs clipboardWrite. The bridge params cap holds text to 256 KB. The click
gate. Rate limit 10 per 10 seconds, shared with writeImage.
dd.clipboard.writeImage()
dd.clipboard.writeImage(dataUrl: string): Promise<void>Needs clipboardWrite. Takes a data:image/png;base64, URI, and the host checks
it by decode. The bridge params cap holds it to 256 KB. The click gate.
dd.clipboard.onChange()
dd.clipboard.onChange(cb: () => void): () => voidThe history or the pause changed. Carries no data: call list again.
Media and audio
Reference for dd.media now-playing status and transport controls, and the dd.audio spectrum stream.
Bar widgets
Reference for the bar-only surface: the taskbar store and the strip, the open windows, the pinned apps, the preview flyout, the system tray mirror, the start-menu tap and the per-app context menu, gated by the taskbar permission.