Documentation

JavaScript SDK

The laptop.js SDK for pages your resource shows as apps, sites and Shroud messages.

laptop.js connects a page from your resource to the laptop. It receives what your Lua sends, opens apps and addresses, shows notifications, keeps state while the laptop sleeps, matches the OS's look, and makes typing work on the laptop's own screen. It is plain JavaScript with no build step and no dependencies.

Include it in every page you register with RegisterApp or RegisterSite, and in pages you show in a Shroud message:

html
<script src="https://cfx-nui-zyke_laptop/nui/sdk/laptop.js"></script>

It adds window.Laptop:

js
Laptop.ready().then((context) => {
    console.log(context.kind, context.id, context.params);
});

Laptop.on("message", (data) => renderOrders(data.orders));

document.querySelector("#buy").addEventListener("click", async () => {
    const result = await Laptop.post("buyItem", { item: "lockpick" });

    Laptop.notify({ title: "Order placed", text: result.message, icon: "package" });
});

#Members

MemberDescription
ready(callback?)Resolves with the context once the laptop answered. A page opened on its own in a browser resolves with a standalone context.
contextThe current context, null until ready resolved. Kept up to date by the events below.
post(endpoint, data?)POSTs to your resource's RegisterNUICallback(endpoint) and parses the JSON reply. A reply that isn't JSON comes back as text, an empty one as null.
mock(endpoint, handler)Answers post(endpoint) while the page runs in a normal browser. Ignored in game. See Developing in a Browser.
notify(options)Shows a desktop notification: {title, text?, icon?, duration?}, or just a title. icon is a Lucide name or an image URL, a relative path resolves against the page. duration is 1000 to 30000 ms. Clicking it opens the app, or the site's address.
close()Closes the app's window, or the site's browser tab.
setTitle(title)Sets the app's window title, or the site's tab title.
openUrl(url, {newTab?})Opens an address in the OS's browser: Ledge, Emberfox, Savanna or Veil Browser. A site navigates its own tab unless newTab is set.
pushUrl(url, {replace?})Sites only. Moves the tab to another of the site's addresses without loading the page again, like history.pushState, so back and forward step through the site's pages and the address bar shows where the player is. url resolves against the current address, such as "/units/12", and has to stay on the site. replace swaps the current address instead, like history.replaceState.
openApp(appId, params?)Opens another app and resolves with {windowId}. Rejects when it doesn't exist or doesn't run on this OS. An app the account hasn't downloaded opens the app store on its page.
saveState(state)Keeps a JSON value of up to 16 KiB for the app's window, or for the address the site's tab shows, like history.state. When the laptop wakes after it was closed, put away, the player relogged or the server restarted, the page gets it back in context.state. null forgets it. Rejects where nothing is kept, such as a page in a Shroud message.
getContext()Asks the laptop for the context again, updates context and resolves with it.
on(event, handler)Listens to an event and returns a function that stops listening.
off(event, handler)Stops listening.
resourceThe resource that owns the page, read from its cfx-nui-<resource> address. null in a normal browser.
inGameWhether the page runs in game.
embeddedWhether the page runs inside the laptop rather than on its own.
versionThe SDK's protocol version, 1, also in context.protocol.

The calls that talk to the laptop (notify, close, setTitle, openUrl, pushUrl, openApp, getContext, saveState) return promises. They reject with an error when the laptop refuses the call or doesn't answer within 10 seconds. Outside the laptop they reject too, except saveState, which only updates context.state there.

#Context

FieldDescription
kind"app" or "site". A page in a Shroud message runs as the app "chat". null on a page opened on its own.
idThe app id or site id.
windowIdThe window the page runs in.
resourceThe resource that registered the app or site.
paramsThe params the app was opened with, from OpenApp or Laptop.openApp.
os"facade", "lumen", "solace" or "veil".
toolkittrue in the Crowbar recovery toolkit's live session, whose os is "lumen".
theme"light" or "dark".
accentblue, teal, green, yellow, orange, red, pink, purple or slate.
paletteThe OS's resolved colours. See Theming.
shape{radius, radiusControl, controlHeight}, as CSS lengths.
connection{status, connected, exitIp} of the Onion connection. status is disconnected, connecting, connected or failed.
settings{language, clock24h, securityLevel, uiSounds, volume, doNotDisturb}. securityLevel is standard, safer or safest. A page with its own sounds follows volume and uiSounds.
visibleWhether the laptop's screen is open.
devicesWhat is plugged into the laptop's ports. See Devices.
display"dui" in game, where the OS runs on the laptop's own screen. Unset on a page opened on its own. Older laptops could also send "nui", so the type keeps it.
stateWhat the page last kept with saveState.
url, host, path, searchSites only: the address the tab shows.
chatPages in a Shroud message only: {messageId}, the message the page is in.
protocolThe SDK's protocol version.
standalonetrue on a page opened on its own, outside the laptop.

#Events

EventData
readyThe context, once the laptop answered.
messageWhat SendAppMessage or SendSiteMessage sent.
paramsNew params, when the app is opened again while its window is open.
theme{theme, accent, palette, os, shape} after the player changed the style or accent colour.
connection{status, connected, exitIp} when the Onion connection changes.
visibility{visible} when the player opens or closes the laptop's screen.
devicesThe new devices list when something is plugged into or pulled out of the laptop the OS runs on.
settingsThe new settings when the player changes one of them.
locationSites only: {url, host, path, search, state} when the tab moved to another of the site's addresses through pushUrl, back or forward. state is what the page saved for that address. A page that routes by its address renders the page for path here, and for context.path when it loads.
contextThe whole context, after any of the above except message changed it.

#Devices

context.devices holds one entry per filled port of the laptop the OS runs on: {port, kind, side, type, item, label}, such as {port: "usba", kind: "usba", side: "right", type: "data_stick", item: "usb_drive", label: "USB Drive"}. kind is usbc, usba, hdmi or audio, and side is "rear" on a desktop computer. type is the device type, built in or registered by a resource. The list is empty on a screen opened without a laptop prop.

A page that needs a device, such as a scanner that only works with an antenna your resource registered as a device type, checks it and listens to devices:

js
const hasAntenna = (devices) => devices.some((device) => device.type === "wifi_antenna");

Laptop.ready().then((context) => setScanner(hasAntenna(context.devices)));
Laptop.on("devices", (devices) => setScanner(hasAntenna(devices)));

The list comes from the client, so check the device again on the server with the GetSessionDevices export before anything that pays out. See Ports & Devices for the devices themselves.

#Theming

The SDK styles the page to match the OS. It sets data-laptop-theme (light or dark), data-laptop-accent and data-laptop-os on <html>, sets its color-scheme, and exposes the palette and shapes as CSS variables. They update when the player changes the style or accent.

VariableDescription
--laptop-fontThe OS's font.
--laptop-font-monoThe monospace font, Adwaita Mono.
--laptop-window-bg, --laptop-window-fgWindow background and text.
--laptop-view-bg, --laptop-view-fgContent areas, such as lists and text views.
--laptop-headerbar-bgHeader bars.
--laptop-sidebar-bgSidebars.
--laptop-card-bgCards.
--laptop-popover-bgPopovers and menus.
--laptop-bordersBorders and separators.
--laptop-dim-fgDimmed text.
--laptop-hover-bgHovered rows.
--laptop-button-bgButtons.
--laptop-entry-bgText entries.
--laptop-accentAccent-coloured text.
--laptop-accent-bg, --laptop-accent-fgAccent-coloured buttons and their text.
--laptop-success, --laptop-warning, --laptop-errorStatus colours.
--laptop-destructive-bgDestructive buttons.
--laptop-radiusCards and panels.
--laptop-radius-controlButtons and entries.
--laptop-control-heightButtons and entries.

The font and shapes follow the OS:

VariableFacadeLumenSolaceVeil
--laptop-fontSelawikUbuntu SansInterAdwaita Sans
--laptop-radius4px12px10px12px, 14px in the modern style
--laptop-radius-control4px6px6px6px
--laptop-control-height32px34px28px34px
css
body { font-family: var(--laptop-font); background: var(--laptop-window-bg); color: var(--laptop-window-fg); }
.card { border-radius: var(--laptop-radius); background: var(--laptop-card-bg); }
button { height: var(--laptop-control-height); border-radius: var(--laptop-radius-control); }
[data-laptop-os="facade"] button { font-weight: 400; }

The fonts load from the laptop's own files, and the browser only downloads the ones the page uses.

#Keyboard

Keys pressed in your page never reach the laptop on their own, so the SDK passes some on:

  • Escape closes the laptop's topmost dialog or popover, or the screen.
  • The browser's keyboard shortcuts, such as Ctrl+T, Ctrl+W, Ctrl+L, Ctrl+Tab and Alt+Left, go to the browser your site is open in. The SDK calls preventDefault() on the ones it passes on. In an app's window they do nothing.

To keep one of these keys for your page, call preventDefault() on it in your own keydown listener, on an element, on document or on window. The SDK checks this after all of them have run.

#Wheel Scrolling

The SDK eases the mouse wheel the way the laptop's own pages do: a notch glides over about 200 ms instead of jumping, and quick notches run into one motion. Everything that scrolls through CSS overflow gets it, the page itself included.

  • To keep the wheel for a handler of your own, such as a map that zooms, call preventDefault() on the wheel event.
  • An element with scroll-snap-type or scroll-behavior: smooth keeps the browser's own scrolling.
  • Ctrl with the wheel, and the wheel over a focused number field, are left to the browser.
  • A page opened on its own outside the laptop keeps its browser's scrolling.

#On the Laptop's Own Screen

The OS runs on the laptop's own screen and the camera zooms onto it. Your page runs in a browser that has no keyboard of its own: the laptop catches what the player types and hands it to the page. The mouse works, but that browser can't show a <select>'s list, has no double clicks and never starts a drag. context.display is "dui" there.

A page with the SDK gets almost everything it would get in a normal browser, with no changes:

  • Typing into inputs, text areas and contenteditable elements, with the caret keys, Backspace, Delete, Ctrl+Backspace, Enter, Tab and undo. Your keydown, keyup, beforeinput and input listeners and React's onChange see it all.
  • Ctrl+C, Ctrl+X and Ctrl+A. Ctrl+V pastes the player's real clipboard and raises a paste event you can handle.
  • Japanese, Chinese and other input methods: the text being composed shows in the field and is replaced by the final text once, with compositionstart, compositionupdate and compositionend events.
  • Ctrl, Shift and Alt on mouse events, and dblclick.
  • A <select> opens a list in the OS's colours under it. The arrow keys, Home, End, Enter and Escape work on it, and choosing an option raises input and change. A <select multiple> or one with size is a plain list box and needs no popup.
  • Dragging a range slider's thumb or a scrollbar.
  • The arrow keys on sliders, Space on check boxes, and Space or Enter on buttons and links.
  • The player sees the cursor your CSS asks for, such as the text cursor over a field or the hand over a link.

What a page has to keep in mind:

  • The keys it gets are events your script can see, not ones the browser types for: isTrusted is false on them. Don't ignore untrusted key events.
  • HTML5 drag and drop (draggable, dragstart, drop) doesn't start. Build drags from pointerdown, pointermove and pointerup, which work.
  • Date, time and colour inputs can't open their pickers. Use your own.
  • Reading the clipboard with navigator.clipboard is blocked. Handle the paste event instead.

A page without the SDK only gets the mouse: clicks, scrolling and hovering work, but nothing can be typed into it, and the cursor stays an arrow over it. Escape and the browser's shortcuts, such as Ctrl+W, still reach the laptop. Include laptop.js in any page that has a text field.

#TypeScript and React

The laptop serves two more files next to laptop.js, at https://cfx-nui-zyke_laptop/nui/sdk/. They are in the resource at nui/sdk/:

  • laptop.d.ts declares the window.Laptop global and its types in the ZykeLaptop namespace: the context, the events and their data, the notify options, the theme, palette, shape, devices and OS.
  • laptop-react.ts holds React hooks.
HookDescription
useLaptopContext()Returns the context, null before the laptop answered, and re-renders when it changes.
useLaptopEvent(event, handler)Listens to an event, always calling the newest handler.
useLaptopMessage<T>(handler)Receives what SendAppMessage or SendSiteMessage sent.
useLaptopDevices()Returns the plugged devices and re-renders when they change.
useLaptopSettings()Returns the OS settings, null before the laptop answered, and re-renders when they change.
useLaptopState<T>(key, initial)Works like useState, but keeps the value through saveState, so the page opens on the same tab after the laptop slept. Every key of the page together takes 16 KiB at most.
laptopPost<T>(endpoint, data?)A typed Laptop.post.

Copy both files into your page's source, for example src/laptop/, so your build doesn't depend on where zyke_laptop is installed. laptop.d.ts is a global declaration, so it only has to be inside your tsconfig's include, which src already covers:

json
{
    "include": ["src"]
}

A file outside include can be added by path, such as "include": ["src", "../../zyke_laptop/nui/sdk/laptop.d.ts"], or referenced with /// <reference path="./laptop.d.ts" />.

Load laptop.js with a <script> tag before your bundle in index.html, as above, so window.Laptop is there when your code runs:

tsx
import { useState } from "react";
import { laptopPost, useLaptopContext, useLaptopMessage } from "./laptop/laptop-react";

const Orders = () => {
    const context = useLaptopContext();
    const [orders, setOrders] = useState<Order[]>([]);

    useLaptopMessage<{ orders: Order[] }>((data) => setOrders(data.orders));

    if (!context) return null;

    return <OrderList orders={orders} compact={context.os === "facade"} onBuy={(id) => laptopPost<BuyResult>("buyItem", { id })} />;
};

#Developing in a Browser

A page can be worked on in a normal browser, outside the game. There Laptop.ready() resolves with a standalone context in Veil's dark style, and Laptop.post answers from the mocks you register instead of your resource. A post without a mock rejects. Calls that need the laptop, such as notify and openApp, reject, and saveState only updates context.state.

js
Laptop.mock("getOrders", () => ({ orders: [{ id: 1, item: "lockpick", amount: 2 }] }))
    .mock("buyItem", (data) => ({ message: `Bought ${data.item}` }));

Mocks are ignored in game, so they can stay in the page. The https://cfx-nui-zyke_laptop/... script address only resolves in game; while working in a browser, point the script tag at nui/sdk/laptop.js inside the laptop's folder. See Working on a Page in a Browser for how the examples do it.