Status: active clean-break design
Surface: desktop@0.10, published by @velarscript/desktop
This is the reference for the six modules the Desktop target owns:
velar/desktop, velar/window, velar/service, velar/notification,
velar/secure-storage, and velar/desktop-test. Each section below lists every
name that module publishes, the grant in velar.json its calls require, the
failures it raises, and one compiled example.
The rest of a Desktop application is documented where it is owned.
web-api.md is the whole renderer vocabulary — components, JSX,
reactivity, Look, resources, actions, velar/web-test — because Desktop
composes @velarscript/web unchanged. standard-library.md
is Core plus the permission-scoped velar/fs, velar/path, velar/process,
velar/http, and velar/env a Desktop project shares with Node.
ai-skill-desktop.md is the same target contract written
as a brief for a model; this document is written for a person with the
repository open.
One VelarScript project behind one system WebView. There is no renderer/main
split, no local server, no port, and no general IPC surface: the module that
declares a component may call the host directly through the capabilities its
manifest grants. velar create --template desktop
writes that project — a velar.json with "extensions": ["@velarscript/desktop"]
and a desktop block, a Web-shaped src/main.vel that ends in
mount(<App />, "#app"), a .test.vel unit test, and a .browser.test.vel
browser test.
The renderer is the source graph you write: Web components rendered by the
system WebView, one document generation per window, no shared JavaScript context
between windows. Checked host requests go to their capability owner: the native
shell owns windows and system integration, while a least-privilege Node
capability worker handles scoped filesystem, process, network, and environment
operations. The bundle carries the worker's Node.js executable. velar package
produces the renderer, worker, and native shell;
packages/desktop/README.md documents the
packaging, signing, and service-supervision contracts around them.
The desktop manifest block is the whole declaration of authority:
{
"formatVersion": 2,
"entry": "src/main.vel",
"extensions": ["@velarscript/desktop"],
"desktop": {
"productName": "Example",
"identifier": "com.example.app",
"windows": {
"main": { "width": 1280, "height": 820 },
"note-preview": { "style": "panel", "frame": false, "width": 512, "height": 320 }
},
"services": {
"core": { "payload": "dist/service-core", "entry": "main.js", "restart": "always" }
},
"permissions": {
"files": ["project", "app-data", "dropped"],
"processes": ["git"],
"network": ["https://api.example.com"],
"environment": [],
"secrets": [],
"links": ["https", "mailto"],
"notifications": true,
"secureStorage": ["CLOUD_SESSION"]
}
}
}Seven modules are refused at project validation when the manifest grants them nothing at all, because a module whose every export would fail is a manifest mistake rather than a program to run. The check reads the project's imports and reports one failure per module:
| Import | Required declaration | Reported as |
|---|---|---|
velar/fs |
desktop.permissions.files is non-empty |
Desktop source imports 'velar/fs' but desktop.permissions.files grants no file root |
velar/process |
desktop.permissions.processes is non-empty |
… but desktop.permissions.processes grants no executable |
velar/http |
desktop.permissions.network is non-empty |
… but desktop.permissions.network grants no origin |
velar/env |
desktop.permissions.environment is non-empty |
… but desktop.permissions.environment grants no variable |
velar/notification |
desktop.permissions.notifications is true |
… but desktop.permissions.notifications is not true |
velar/secure-storage |
desktop.permissions.secureStorage is non-empty |
… but desktop.permissions.secureStorage grants no name |
velar/service |
desktop.services declares a service |
… but desktop.services declares no service |
velar/desktop, velar/window, and velar/desktop-test have no import-level
requirement. Everything else is checked at the call: an individual capability
the manifest does declare and the author reaches wrongly — an undeclared
window kind, an ungranted link scheme, a credential slot outside the allowlist —
fails where it is used, naming the manifest field that would grant it. Never
broaden a grant to silence a failure.
None of the six declares an error class. Argument validation, capability
refusals, and host failures use ordinary Error, TypeError, or RangeError.
Grant refusals name the relevant manifest field; host failures may instead need
an operating-system permission, an available service, or another operational
recovery. Display error messages rather than using their text as a protocol.
Use the permission enums and service-state events for state-dependent behavior.
The permission-scoped
velar/fs and velar/http a Desktop project also uses keep their own classes
(FileNotFoundError, PermissionError, and the rest); those are documented in
standard-library.md.
Two shapes recur in every section. An owned handle — Window,
ServiceConnection — is released by close(), so using performs the release
and a second release is not an error. An event source is a bounded pull
stream with next() and close() and no callback registry: next() answers
null when the stream has drained, only one pull may be active at a time, and a
queues stay bounded. Their overflow policy belongs to the particular stream:
window state can coalesce transitions, while a service channel can reject an
overflow. async for consumes these pull streams.
The host itself: which platform this is, where the application's directories are, what screens are attached, and the system event sources that belong to no other module.
| Export | Signature | Meaning |
|---|---|---|
DesktopPlatform |
enum macos, test |
The host this build is running on. test is the deterministic browser-test host; macos is the native host that ships today. |
Display |
{id, bounds, workArea, scale, primary} |
One attached screen: two {x, y, width, height} rectangles, a device scale, and whether it is the primary display. |
DroppedFiles |
{paths: List<string>} |
The real paths one drag gesture brought in, in gesture order. |
DroppedFilesStream |
next() -> Promise<DroppedFiles?>, close() -> Promise<null> |
The pull stream watchDroppedFiles() opens. |
PermissionStatus |
enum granted, denied, undetermined |
What the operating system currently answers for a system permission. |
PowerState |
enum suspended, resumed |
A sleep/wake transition. |
PowerStream |
next() -> Promise<PowerState?>, close() -> Promise<null> |
The pull stream watchPower() opens. |
SystemPermission |
enum screenRecording, accessibility, microphone |
The read-only system probes permissionStatus answers. |
platform() |
-> DesktopPlatform |
Synchronous. Which host answers this application's calls. |
packaged() |
-> bool |
Synchronous. Whether this build is running from a packaged application rather than from velar dev. |
homeDirectory() |
-> Promise<string> |
The current user's home directory, as one validated absolute path. |
appDataDirectory() |
-> Promise<string> |
This application's own data directory, created if absent. The same path the host gives a service in VELAR_SERVICE_APP_DATA. |
projectDirectory() |
-> Promise<string> |
The project root the project file grant is scoped to. |
selectedProjectDirectory() |
-> Promise<string?> |
Reads what the user has already chosen; null when nothing is chosen. A query, not a dialog. |
selectProjectDirectory() |
-> Promise<string?> |
Opens the native picker; null when the user cancels. Selection may replace the current grant. |
openExternal(url) |
-> Promise<null> |
Hands one absolute URL to the system default handler. |
applyUpdate(archivePath) |
-> Promise<null> |
Replaces this installed application with a downloaded archive of itself and relaunches. |
displays() |
-> Promise<List<Display>> |
Every attached screen, at most 64. |
permissionStatus(kind) |
-> Promise<PermissionStatus> |
Reads one system permission. There is no request function. |
watchPower() |
-> Promise<PowerStream> |
Opens the sleep/wake stream. |
watchDroppedFiles() |
-> Promise<DroppedFilesStream> |
Opens the drag-gesture path stream. |
Grants. The module needs none to be imported. openExternal needs the URL's
scheme in desktop.permissions.links, a closed set of http, https, and
mailto; any other scheme is refused at the call, naming the granted schemes,
and refused again by the host. watchDroppedFiles needs the dropped root in
desktop.permissions.files — the gesture is the grant, and it lasts for the
session — and the refusal names that field. Everything else here is ungated.
Bounds and semantics. Every path this module returns is absolute, NUL-free,
and at most 4,096 code units; a host answer outside that is a TypeError rather
than a value. Display.bounds and Display.workArea are the same structural
record velar/window publishes as WindowBounds, so a window's own display()
and displays() answer one shape rather than two that look alike.
watchPower() carries transitions only: a machine that is already awake
publishes nothing on waking. watchDroppedFiles() reports the real paths behind
a drag gesture in gesture order and caps a batch at 4,096 paths and 2,097,152
UTF-16 code units of path text; the page
still receives its ordinary DOM drop event, and the two are the same gesture.
applyUpdate is the mechanism and nothing else — no feed, no channel, no
version check, no download, no delta format. The host expands the archive
elsewhere, requires the application inside it to carry this application's bundle
identifier and signing Team ID, and only then replaces the install atomically.
Every failure leaves the current install untouched, and a development build is
ad-hoc-signed, so it has no Team ID and applyUpdate refuses it by name.
import {DesktopPlatform, PermissionStatus, PowerState, SystemPermission, appDataDirectory, applyUpdate, displays, homeDirectory, openExternal, packaged, permissionStatus, platform, projectDirectory, selectProjectDirectory, selectedProjectDirectory, watchDroppedFiles, watchPower} from "velar/desktop"
export def hostSummary() -> string:
if platform() == DesktopPlatform.macos: return f"macos|{packaged()}"
return f"test|{packaged()}"
export async def roots() -> string:
const home = await homeDirectory()
const data = await appDataDirectory()
const project = await selectedProjectDirectory() ?? await projectDirectory()
return f"{home}|{data}|{project}"
export async def chooseProject() -> string: return await selectProjectDirectory() ?? "the picker was cancelled"
export async def readyToRecord() -> bool:
return await permissionStatus(SystemPermission.screenRecording) == PermissionStatus.granted
export async def openGuide() -> number:
await openExternal("https://example.com/guide")
return (await displays()).size
export async def firstDropAfterWake() -> string:
using power = await watchPower()
using drops = await watchDroppedFiles()
async for state in power:
if state == PowerState.resumed: break
const batch = await drops.next()
if batch == null: return "no drop"
return f"{batch.paths.size} files"
export async def install(archivePath: string) -> string:
try:
await applyUpdate(archivePath)
return "replaced; relaunching"
catch error: return f"refused: {error.message}"
The window kinds desktop.windows declares, opened by kind and optional
instance key. Nothing here invents a kind.
| Export | Signature | Meaning |
|---|---|---|
Window |
focus(), close(), bounds(), setBounds(bounds), display(), watchState() |
An owned window handle. Every member answers a Promise; close() is the release using performs and is idempotent. |
WindowBounds |
{x: number, y: number, width: number, height: number} |
One screen rectangle. Coordinates are finite and within 1,000,000 points; sizes are at least 1 point. |
WindowState |
enum moved, resized, focused, blurred, closed |
The closed vocabulary a window's state stream publishes. |
WindowStateStream |
next() -> Promise<WindowState?>, close() -> Promise<null> |
The pull stream Window.watchState() opens. It drains after closed, and coalesces repeated moved/resized for a slow consumer. |
currentWindowKind() |
-> string |
Synchronous. Which declared kind this document is rendering, so one source graph can decide what it is. |
currentWindow() |
-> Window |
Synchronous. This window rather than one you opened; hold it in a const. |
openWindow(kind, options) |
-> Promise<Window> |
Opens a declared kind at options.route, with optional key and bounds. The same kind and key focuses the window that already exists. |
windows() |
-> Promise<List<{kind: string, key: string?, focused: bool}>> |
Every open window, at most 256. |
Window members:
| Member | Result |
|---|---|
focus() |
Promise<null> |
close() |
Promise<null> |
bounds() |
Promise<WindowBounds> |
setBounds(bounds: WindowBounds) |
Promise<null> |
display() |
Promise<Display> — the record from velar/desktop |
watchState() |
Promise<WindowStateStream> |
Grants. The module needs none to be imported. openWindow requires the kind
to appear in desktop.windows; an undeclared kind is refused at the call,
naming that field and listing the declared kinds, and refused again by the host.
main is required, opens at launch, and is the kind currentWindowKind()
answers there. Kind names are lowercase words joined by single hyphens, at most
32 per application. An instance key is at most 128 characters of letters,
digits, ., _, : or -. openWindow's route must start with / and stay
inside this application.
Two host rules have no knob: closing main closes every other window and
quits, and closing the last window quits. Every window loads the same
application at the route it was opened with, and windows share no JavaScript
context — do not try to share state between them through the language.
import {WindowBounds, WindowState, currentWindow, currentWindowKind, openWindow, windows} from "velar/window"
export def describe(bounds: WindowBounds) -> string: return f"{bounds.width}x{bounds.height}+{bounds.x}+{bounds.y}"
export async def previewNote(note: string) -> string:
if currentWindowKind() != "main": return "only the main window opens previews"
using preview = await openWindow("note-preview", {route: f"/preview?note={note}", key: f"note-{note}"})
await preview.focus()
const bounds = await preview.bounds()
await preview.setBounds({x: bounds.x, y: bounds.y, width: 512, height: 320})
using states = await preview.watchState()
async for state in states:
if state == WindowState.closed: break
return describe(bounds)
export async def openCount() -> number: return (await windows()).size
export async def thisWindowScale() -> number: return (await currentWindow().display()).scale
The channel to the long-running processes desktop.services declares. The
language starts them, supervises them, converges them on quit, and hands the
renderer one authenticated loopback channel to each. It owns nothing inside a
service: declaring one makes it auditable, not confined.
| Export | Signature | Meaning |
|---|---|---|
ServiceClose |
{code: number, reason: string} |
Why a channel closed. |
ServiceConnection |
state(), send(message), next(), closeInfo(), close(code=1000, reason="") |
An owned channel. state() answers "open" or "closed"; close() is the release using performs. |
ServiceState |
enum starting, ready, restarting, failed, stopped |
The supervisor's transitions. |
ServiceStateEvent |
{name: string, state: ServiceState, detail: string?} |
One transition. detail is carried by failed and restarting and is null for every other state. |
ServiceStateStream |
next() -> Promise<ServiceStateEvent?>, close() -> Promise<null> |
The pull stream watchServices() opens. |
connect(name) |
-> Promise<ServiceConnection> |
Opens the authenticated channel to one declared service. |
watchServices() |
-> Promise<ServiceStateStream> |
Opens the supervision stream for every declared service. |
ServiceConnection members:
| Member | Result |
|---|---|
state() |
Promise<string> — "open" or "closed" |
send(message: string) |
Promise<null> |
next() |
Promise<string?> |
closeInfo() |
Promise<ServiceClose> — waits for closure |
close(code: number = 1000, reason: string = "") |
Promise<null> |
Grants. Importing the module requires at least one entry under
desktop.services; watchServices() says the same thing again at the call. An
undeclared name is refused by connect, naming desktop.services and listing
what is declared. A service that is not ready is refused by state — services
start before the renderer loads and are not awaited, so read watchServices()
rather than assuming one is up.
The channel. ServiceConnection keeps the velar/websocket client's
discipline under its own identity: send is backpressured and settles when the
frame has left, next is a bounded pull that permits one active pull and
answers null after release, and the release is what using performs. It
carries text, up to 8 MiB per frame; a close code is 1000 through 4999 and a
reason at most 123 UTF-8 bytes. Application code never holds the token that
authenticates the channel — the host spends it itself on the first frame — so
there is no credential to leak through this API.
detail is the only thing the language says about the inside of a service:
up to the last 4 KiB the process wrote to its own standard error, truncated on a
character boundary. Show it to a person; never parse or match on it. The whole
of a service's output is a rotating log file under the app-data directory, which
packages/desktop/README.md names.
import {ServiceClose, ServiceState, ServiceStateEvent, connect, watchServices} from "velar/service"
export def label(event: ServiceStateEvent) -> string:
if event.state != ServiceState.failed: return event.name
const detail = event.detail ?? "no detail"
return f"{event.name} failed: {detail}"
export async def firstState() -> string:
using states = await watchServices()
const event = await states.next()
if event == null: return "no event"
return label(event)
export async def indexNote(id: string) -> string:
using channel = await connect("core")
await channel.send(f"put {id}")
const reply = await channel.next() ?? ""
const open = await channel.state()
await channel.close(1000, "done")
const closed: ServiceClose = await channel.closeInfo()
return f"{open}|{reply}|{closed.code}|{closed.reason}"
System notification delivery and the activations it produces.
| Export | Signature | Meaning |
|---|---|---|
NotificationActivation |
{tag?: string?} |
One click on a delivered notification, reporting the tag it carried. |
NotificationActivationStream |
next() -> Promise<NotificationActivation?>, close() -> Promise<null> |
The pull stream activations() opens. |
NotificationPermission |
enum granted, denied, undetermined |
The operating system's own answer. |
requestPermission() |
-> Promise<NotificationPermission> |
Asks the operating system and answers what it said. |
show(notification) |
-> Promise<null> |
Delivers one {title: string, body: string, tag?: string?}. |
activations() |
-> Promise<NotificationActivationStream> |
Opens the activation stream. |
Grants. There are two gates and they are different. The manifest's
desktop.permissions.notifications: true is the application's declaration that
it may notify at all; without it requestPermission, show, and activations
each fail at the call and name that line — which is also why the import itself
is refused when the flag is absent. The operating system's own answer is the
second gate: ask for it with requestPermission(). show on an unauthorized
application fails; it never quietly delivers nothing.
Bounds. title is at most 256 UTF-16 code units, body 1024, and tag 128.
The required title and body must be non-empty and NUL-free; an omitted or null
tag is allowed, and a provided string must be non-empty and NUL-free. A tag is
the notification's identity — a
second notification carrying it replaces the first, and an activation reports it
back. Two clicks on one notification are one activation, and the host brings the
application forward with it, opening main when no window is left.
import {NotificationActivation, NotificationPermission, activations, requestPermission, show} from "velar/notification"
export def tagOf(activation: NotificationActivation) -> string: return activation.tag ?? "untagged"
export async def announce(packages: number) -> string:
if await requestPermission() != NotificationPermission.granted: return "not notified"
using clicks = await activations()
await show({title: "Build finished", body: f"{packages} packages", tag: "build"})
const click = await clicks.next()
if click == null: return "no activation"
return tagOf(click)
The named credential slots desktop.permissions.secureStorage declares, held as
macOS keychain generic passwords under the application's bundle identifier.
| Export | Signature | Meaning |
|---|---|---|
set(name, value) |
-> Promise<null> |
Writes one declared slot. |
get(name) |
-> Promise<string?> |
Reads one declared slot; null when it holds nothing. |
remove(name) |
-> Promise<null> |
Clears one declared slot. Removing what is absent is not an error. |
Grants. Importing the module requires a non-empty
desktop.permissions.secureStorage. A name outside that allowlist fails at the
call, naming the field and listing the declared names. Slot names follow the
secrets spelling rule — [A-Z_][A-Z0-9_]{0,127} — and one name may appear in
only one of the two lists, because they are different authorities: a secrets
entry is an opaque value the environment injects and only a permitted transport
resolves, while a secureStorage entry is a slot the application itself writes
and reads.
get returns the credential to the caller. Values are capped at 8 KiB of
UTF-8 data. Validation errors describe a rejected value's size, not its content,
and the host's diagnostics and test seam do not expose stored values:
velar/desktop-test reports which names the fake keychain holds, not their
contents. The application must keep returned credentials out of its own logs
and UI; report whether a credential is present, not what it is.
import {get, remove, set} from "velar/secure-storage"
export async def rotate(token: string) -> bool:
await set("CLOUD_SESSION", token)
const stored = await get("CLOUD_SESSION")
await remove("CLOUD_SESSION")
await remove("CLOUD_SESSION")
return stored != null
The test-process door into the Desktop host, the way velar/web-test is the
door into the running page. It is the one place a Desktop capability is a call a
test controls rather than a request to a machine.
What velar test --browser drives. The command builds a real CSP production
site from the project, starts an isolated local host, and creates a fresh
browser context for each *.browser.test.vel test. For a Desktop project it
also installs a deterministic fake host in the page, which answers every module
above — velar/desktop, velar/window, velar/service,
velar/notification, velar/secure-storage, and the permission-scoped
velar/fs — from the project's own manifest, above the granted roots. This
module is the test process's handle on that fake host: it produces the events a
real system would and reads back what the application did. Its runtime exists
only under that command; calls without the browser-test runtime are refused with
velar/desktop-test requires 'velar test --browser'. Put these tests in
*.browser.test.vel; ordinary .test.vel files exercise target-independent
logic without this host seam.
Two choices are made before the first browser.open() and are sealed by it —
setPlatform and setWindowKind. Start serveService before opening the page
too, so its initial service registry contains that ready server. Service
controls (serveService, pushService, serviceRejectsWrongToken, and
stopService) run in the test process; the remaining helpers operate on the
running page's fake host. In particular, call setSigningTeam after
browser.open().
| Export | Signature | Meaning |
|---|---|---|
setPlatform(value) |
-> Promise<null> |
Selects the DesktopPlatform the page will report. Before the first browser.open(). |
setWindowKind(kind) |
-> Promise<null> |
Selects the declared kind the page renders as. Before the first browser.open(). |
appDataDirectory() |
-> Promise<string> |
The fake host's app-data root, as the test process sees it. |
projectDirectory() |
-> Promise<string> |
The fake host's project root, as the test process sees it. |
makeDirectory(path) |
-> Promise<null> |
Creates a fixture directory under a granted root. |
readText(path, maxBytes) |
-> Promise<string> |
Reads a fixture file under an explicit budget of 1 byte to 16 MiB. |
writeText(path, text) |
-> Promise<null> |
Writes a fixture file. |
removeFile(path) |
-> Promise<null> |
Removes a fixture file. |
openWindows() |
-> Promise<List<{kind: string, key: string?, focused: bool}>> |
The fake window registry's contents, at most 256. |
focusWindow(kind, key=null) |
-> Promise<null> |
Produces the host focus event for one window. |
moveWindow(kind, key, bounds) |
-> Promise<null> |
Produces the host move/resize event with one WindowBounds. |
closeWindow(kind, key=null) |
-> Promise<null> |
Produces the host close event for one window. |
setNotificationPermission(permission) |
-> Promise<null> |
The answer the operating system's dialog would give. |
shownNotifications() |
-> Promise<List<{title: string, body: string, tag: string?}>> |
The fake notification centre's inbox, at most 256. |
activateNotification(tag=null) |
-> Promise<null> |
The click on a delivered notification. |
setServiceState(name, state, detail=null) |
-> Promise<null> |
Injects a supervisor transition. A detail is accepted only for failed and restarting, and is capped at 4 KiB. |
serveService(name, handler) |
-> Promise<null> |
Runs a real loopback WebSocket server for one service in the test process; handler is (string) -> Promise<string> and stays in VelarScript. |
pushService(name, message) |
-> Promise<number> |
Emits an unrequested frame to every open connection, at most 8 MiB, and answers how many took it. |
serviceRejectsWrongToken(name) |
-> Promise<bool> |
Whether the service side refuses a connection opened with a token that is not the host's. |
stopService(name) |
-> Promise<null> |
Releases a served service: the server closes, both pumps settle, the application sees it stopped. |
secureStorageNames() |
-> Promise<List<string>> |
The names the fake keychain holds, at most 64. Never the values. |
publishPower(state) |
-> Promise<null> |
Produces one PowerState transition. |
dropFiles(paths) |
-> Promise<null> |
Produces one drag gesture of 1 to 4,096 absolute paths. |
setSystemPermission(kind, status) |
-> Promise<null> |
Sets what permissionStatus will answer for one SystemPermission. |
openedLinks() |
-> Promise<List<string>> |
What was handed to the system link handler, at most 256. |
setSigningTeam(team=null) |
-> Promise<null> |
What Team ID this install carries; null is the ad-hoc-signed development build. |
stageUpdate(archivePath, bundleIdentifier, team=null) |
-> Promise<null> |
What an archive on disk claims to be, so applyUpdate's four refusals are reachable. |
appliedUpdates() |
-> Promise<List<string>> |
Which archives the host actually applied, at most 64. |
Grants. The module has no import-level grant. Its filesystem helpers still
require the matching app-data or project entry in
desktop.permissions.files, and reject paths outside those granted roots as
the application's own velar/fs calls do.
Two rules the seam keeps. The fake keychain reports names and never values:
a seam that handed a credential back would be the exception that ends that rule.
And the service fake is not a stub — serveService starts a real loopback
WebSocket server, so a message leaves the application, crosses a socket, reaches
the handler, and comes back; serviceRejectsWrongToken lets a test watch the
same server refuse a connection whose token is not the host's. A test owns the
work it starts, so stopService is how it hands the server back.
import {DesktopPlatform, PermissionStatus, PowerState, SystemPermission} from "velar/desktop"
import {NotificationPermission} from "velar/notification"
import {ServiceState} from "velar/service"
import {expect} from "velar/test"
import {activateNotification, appDataDirectory, appliedUpdates, closeWindow, dropFiles, focusWindow, makeDirectory, moveWindow, openWindows, openedLinks, projectDirectory, publishPower, pushService, readText, removeFile, secureStorageNames, serveService, serviceRejectsWrongToken, setNotificationPermission, setPlatform, setServiceState, setSigningTeam, setSystemPermission, setWindowKind, shownNotifications, stageUpdate, stopService, writeText} from "velar/desktop-test"
import {browser} from "velar/web-test"
test "the fixture filesystem writes, reads back, and removes":
await setPlatform(DesktopPlatform.macos)
await setWindowKind("main")
await browser.open()
const folder = f"{await appDataDirectory()}/fixtures"
const file = f"{folder}/note.txt"
await makeDirectory(folder)
await writeText(file, "written by the test")
expect(await readText(file, 4096)).toBe("written by the test")
await removeFile(file)
expect((await projectDirectory()).startsWith("/")).toBe(true)
test "the fake host produces the events a real system would":
await browser.open()
await focusWindow("main", null)
await moveWindow("main", null, {x: 40, y: 60, width: 512, height: 320})
await setNotificationPermission(NotificationPermission.granted)
await activateNotification("build")
expect(await shownNotifications()).toHaveLength(0)
await publishPower(PowerState.resumed)
await dropFiles(["/tmp/one.txt", "/tmp/two.txt"])
await setSystemPermission(SystemPermission.screenRecording, PermissionStatus.granted)
expect(await openedLinks()).toHaveLength(0)
expect(await secureStorageNames()).toHaveLength(0)
await closeWindow("main", null)
expect(await openWindows()).toHaveLength(0)
test "a served service authenticates clients and accepts supervisor events":
await serveService("core", async (request: string) => request)
try:
await browser.open()
expect(await serviceRejectsWrongToken("core")).toBe(true)
expect(await pushService("core", "nobody is listening yet")).toBe(0)
await setServiceState("core", ServiceState.restarting, "the process exited")
finally: await stopService("core")
test "the update identity a running application cannot read about itself":
await browser.open()
await setSigningTeam("ABCDE12345")
await stageUpdate("/tmp/Example.zip", "com.example.app", "ABCDE12345")
expect(await appliedUpdates()).toHaveLength(0)
Desktop surface 0.10 publishes no user main process, renderer project, local
server, port, or general IPC surface, and no request function for a system
permission — asking the user belongs to the product flow that consumes the
answer. Language servers, semantic project transactions, product task runners,
terminals, editors, database engines, deployment providers, and other product
features are not Desktop language capabilities: they belong to the product that
owns their policy, or to an independently versioned integration built on the
public contracts above. A short-lived process is velar/process with a
processes grant, which is a different model on purpose; desktop.services is
only for the long-running processes the product owns.
Diagnostics and the checked manifest vocabulary outrank this document if they
disagree. Runnable Desktop examples live in examples/tour/desktop/.