Control protocol
Last updated: 2026-10-23 · Change: symbol library panels (
window.symbolLibrary,window.symbolLibrary.other,ui.saveSymbolLibrary); previously: synthetic input budgets (#1709); bounded transport waits (#1726).
stylo --control <port> listens on
127.0.0.1:<port> (loopback only). The port must be a
number from 1 to 65535 (--control=<port> works too),
given once; otherwise the app prints an error and exits before it
starts. One JSON request per line:
{"id": 1, "method": "ui.inspect", "params": {}} →
{"id": 1, "ok": true, "result": {...}} or
{"id":1,"ok":false,"error":"..."}.
Set STYLO_CONFIG_DIR to a dedicated directory for an
isolated native test profile. Preferences, logs, recovery and user
libraries then use that root, without importing the legacy DrawCraft
profile. An unset override preserves the usual platform paths and legacy
migration; an explicitly empty override disables those paths.
STYLO_NO_PREFS continues to disable preference reads and
writes. Without the override, a portable.txt or
Stylo.portable file beside the executable (as in the
Windows portable zip) puts that root in StyloData next to
the executable; on macOS the marker and the folder sit next to
Stylo.app, for an AppImage next to the
.AppImage file, and a Flatpak install has neither.
STYLO_NO_PREFS never looks for the marker or creates the
folder.
Only requests are read. Every line must be a JSON
object with a string method (id and
params are optional; blank lines are skipped). Anything
else gets one error reply
({"ok": false, "error": "… closing the connection"}) and
the server closes the connection, so nothing sent after
it on that connection runs. That covers text that isn't JSON, a JSON
array or number, an object without method, invalid UTF-8,
and a line longer than 4 MiB. An HTTP request (for example a web page's
cross-origin fetch to 127.0.0.1:<port>)
therefore can't smuggle a command in its body: its request line is
rejected first. At most 16 connections are served at once; further ones
get an error line and are closed. Clients that get an error reply should
reconnect. The port has no authentication, so only enable it while you
use it. Idle clients have 5 minutes to start their next line; once the
first byte arrives, the complete line must arrive within an absolute 30
seconds (trickled bytes do not renew it). A reply has 60 seconds to be
written. Expiration closes the connection and returns its slot (an idle
connection is closed without a line, so nothing waits where the next
reply would be; a line that stalls gets the usual error line first);
persistent clients can send further requests within these bounds, or
reconnect after idle expiration. These transport budgets are separate
from the existing 60-second command-processing wait. Transport:
apps/stylo/src/control_server.rs.
Synthetic input budgets
ui.drag accepts at most 256
steps; ui.click and ui.drag
accept at most 16 count. They must be
non-negative JSON integers. Omitted values keep their defaults (8 steps,
one click); explicit zero still acts as one, as before. Oversized or
malformed values return an error before any move, press or release is
queued.
All synthetic methods (ui.key, ui.text,
ui.move, ui.click, ui.drag,
ui.wheel) share a 1,024-event
pending-queue limit. The whole request must fit; rejection preserves
existing input and does not append a partial gesture. Clients can retry
once earlier input has drained. At most 64 control
requests run per frame; later requests remain on the channel
and another frame is scheduled. These limits cover synthetic input
admission, separately from socket transport and command-processing
timeouts.
A request the app hasn't answered after 60 s gets
{"ok": false, "error": "timeout: …"}. If the app had not
started it (it was busy, or its window wasn't being drawn), the request
is withdrawn and never runs, and the error says "will not run": retrying
is safe. If the app was already running it, the error says "still
running": it may yet complete, so check the document before retrying. On
macOS the app keeps itself out of App Nap while the control channel is
on, so it keeps answering with its window hidden, minimized or covered,
or with the screen locked.
Confining file access
stylo --control 7979 --automation-read-root /work/project --automation-write-root /work/project/out
--automation-read-root <dir> and
--automation-write-root <dir> (or
STYLO_AUTOMATION_READ_ROOT /
STYLO_AUTOMATION_WRITE_ROOT;
--flag=<dir> works too) confine what the control
channel reads and writes, with the same flags as
stylo-cli mcp and the other NC apps. Every path a request
reads (open, place, relink, library and profile loads, plug-ins…) must
lie inside a read root, every path it writes (save, export, package,
captures, library saves…) inside a write root; anything else is an
ordinary error reply. Paths are made absolute against the app's working
directory, links are followed (for a file that doesn't exist yet, its
folder's), and the comparison is by whole folder names
(/work2 is not inside /work), ignoring case on
Windows; .. below a folder that doesn't exist, a link that
leads nowhere and Windows device names are refused. A root left out
grants none of its access. The roots must be existing folders, or the
app doesn't start; without --control they are ignored. See
docs/mcp.md › Confining file access for the details.
Each root flag may be given more than once, a path inside any of its
folders being allowed (#3546); the command line's folders of a kind
replace its environment default. Missing or empty values and another
option used as a value stop the desktop before it starts any services,
even without --control. For a folder name starting with
-, use --flag=-folder or the relative path
./-folder.
What is confined: the requests themselves, the frames that carry
input they inject (ui.click, ui.key,
ui.text…, so a menu item an agent clicks is confined too),
the background saves and exports they start, and window captures. The
person at the keyboard is not: files opened from the command line,
Finder or a file dialog, and everything done with the mouse and
keyboard, work as usual. The preferences that move folders the app reads
or writes on its own (Additional Fonts Folder, Additional Plug-ins
Folder, Data Recovery's folder, the Templates folder) can only be
pointed inside the roots by automation. The app's own folders
(preferences, Data Recovery, the library folders, Stylo's Fonts folder)
are not confined, since no agent names a path there. A path is checked,
then opened: another program that swaps in a link between the two can
still win that race.
| Method | Params | |
|---|---|---|
engine.execute |
{command, params} |
run any engine or UI command (see engine.commands). A
key of params that the command's params description doesn't
name is an error naming it and the nearest key the command takes, before
anything runs
(unknown parameter widht; did you mean width?);
see Parameter keys |
engine.commands |
every command with label, shortcut, params doc, enablement | |
document.inspect |
{depth?, childLimit?, ids?, kind?, limit?, offset?} |
layer tree, selection, the key object (key, null
without one; a key ruler guide as keyGuide), history, paint
defaults; depth / childLimit slice the layer
tree as document.node {summary: true} does;
ids, kind, limit and
offset list just some nodes as nodes (with
total) instead of the whole tree, see mcp.md. Each node reports opacity below 1
and blend when it isn't Normal; objects and compound paths
report fill, stroke and
strokeWidth, a compound path's members
compoundMember: true instead |
ui.inspect |
tool, UI state, screenMode (0 normal, 1 full screen
with menu bar, 2 full screen, 3 Presentation Mode), taskBar
(pinned, and rect [x, y, w, h]
while the Contextual Task Bar shows), freeTransformWidget
([x, y, w, h] while the Free Transform widget shows), view,
canvas rect, window size, perf, background saves and exports still
running, nativeMenuBar (true when the menus are in the
macOS menu bar rather than the window; ui.menu.list lists
the in-window menus either way), fileDialog
("open", "place", "save" or
"folder" while a native file dialog is open,
null otherwise; see below) |
|
ui.menu.list / ui.menu.invoke |
{command, params} |
the full menu tree / invoke an item |
ui.contextMenu.list |
the canvas context menu for the current selection, flattened like
ui.menu.list (path holds its submenus).
ui.click {x, y, button: "right"} on the canvas opens it,
after selecting the object there unless it is already selected. While a
menu opened over locked objects is open, it has an Unlock
submenu with one item for each locked object under the pointer, topmost
first and at most 20, labeled with the object's name, which runs
layer.setProps {ids: [id], locked: false}. A locked group
gets one item, and the locked objects inside it get none. Hidden
objects, objects on hidden, locked or template layers, and in isolation
mode objects outside the isolated group get no item |
|
ui.tool.select / ui.tool.list |
{tool} |
|
ui.pointer |
`{events:[{kind: down | drag |
ui.key / ui.text |
{key, shift?, alt?, cmd?} / {text} |
synthetic keyboard input |
ui.move |
{x, y} |
screen-point pointer move |
ui.click / ui.drag |
{x, y, button?, count?, shift?, alt?, ctrl?, cmd?} /
{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?} |
egui pointer input; see synthetic input budgets above |
ui.wheel |
{x, y, dy?, dx?, unit?: "line"|"point", shift?, alt?, cmd?} |
a mouse wheel turn over screen point (x, y): dy notches
up (+) or down, dx sideways. Over the canvas the wheel
scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the
pointer; with the zoomWithMouseWheel preference the wheel
and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down
and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused
numeric field (click it first) each notch steps its value as Up/Down do
(Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything
else in a panel the wheel scrolls it |
ui.set |
{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?, imageTracePreview?} |
imageTracePreview: the Image Trace panel's Preview
(ui.inspect shows it as
ui.image_trace_preview) |
ui.dialog.set / .confirm /
.cancel |
{field, value} |
fill and submit the open dialog |
ui.screenshot |
{path?, data?} |
capture the window (PNG), written to path;
data: true sends it back as pngBase64 too.
Needs a presented frame: with the screen locked or the window
minimized/covered it fails after ~8 s with an explanatory error |
ui.render |
{path?, scale?, artboard?, data?} |
render an artboard headlessly (PNG with the pixels of
app.export {format: "png", background: "white"} at the same
scale; artboard: its 0-based index, default the first):
written to path, else (or with data: true as
well) sent back as pngBase64 |
ui.resize / ui.focus |
||
app.open / app.save /
app.export / app.quit |
{path} / {path?} /
{path?, format?, artboard?, range?, scale?, …} |
app.open reads every format document.open
reads (see document.formats) and returns its result
({index, title, format, warnings, …}; warnings
say what didn't come in as it was, such as an EPS shown as its preview
image and why its PostScript couldn't be read), or null when a dialog
asks first or a library loads. app.export encodes through
the engine's document.export (same options; the document
keeps its path) and writes path through the host; without
path it returns {dataBase64, format, bytes},
as headless mode does. app.quit, file.close
and file.closeAll first open a saveChanges
dialog for each modified document (they return
{"pending": "saveChanges"}): ui.dialog.confirm
saves, ui.dialog.set {field: "discard", value: true} then
confirm discards, ui.dialog.cancel cancels the whole close
or quit |
app.export with useArtboards |
{path, useArtboards: true, range? | artboards?, …} |
Export As: PNG, JPEG and WebP write one file per chosen artboard
(default all; SVG does so for a range),
<path stem>-<artboard>.<ext>, and return
{path, files: [path…]}; a PDF keeps them as pages of
path. useArtboards: false covers the bounds of
the visible art. The menu's Export As… is file.exportAs:
the exportAs dialog (format,
useArtboards, all, range), then a
save dialog and the format's options: pngOptions /
jpgOptions / webpOptions,
svgOptions, or savePdf for a PDF with Use
Artboards |
ui.dialog.* on gradientStop |
{field: "color" | "opacity" | "location", value} |
double-clicking a stop on the Gradient tool's annotator opens its
popover (fields index, x, y,
tab); set color (hex), opacity or
location (percentages) and ui.dialog.confirm
to apply them to the selected stop.
gradient.selectStop {index} picks the stop the annotator,
the panels and Delete/←/→ (ui.key) act on |
engine.execute ui.savePdfDialog |
{path?, preset?, range?, …document.exportPdf options} |
opens the Save PDF dialog (file.export.pdf with no
params opens it too). Its fields are the document.exportPdf
options: set compatibility with
ui.dialog.set {field: "compatibility", value: "1.5"}, a
whole section with {field: "compression", value: {…}}, the
artboards with __allArtboards: false and
range, the visible section with __section
(General, Compression, Marks and Bleeds, Output, Advanced, Security,
Summary). ui.dialog.confirm writes path (else
asks for one), returns {path, bytes, warnings} and opens
the file when viewAfterSaving is set |
file.save / file.saveAs /
file.saveCopy / file.saveAsTemplate (via
engine.execute) |
{path?, format?, options?, svg?} |
Save writes the document's own file in its own format (a document
opened from or saved as SVG/PDF saves as that again; the result's
warnings say what the format loses). With path
the file is written at once (Save As or a Copy to an SVG path without
SVG options first opens SVG Options, below). Without one (never saved, a
converted older file, Save As, a Copy, a Template) the app shows a save
panel with one file type per save format (stylo,
template, pdf, svg,
svgz, ai; for a Template,
template and ait), then the format's options:
SVG opens the svgOptions dialog (below; returns
{"pending": "svgOptions", "path"}), PDF the
savePdf dialog (its __save field names the
save command); ui.dialog.confirm finishes the save. A name
typed in the panel without the file type's extension gets it, and a name
the panel returns with the extension of a format the panel does not list
has it replaced (in a native save, card.png and
card.ait are saved as card.stylo, and
notes.v2, whose extension names no format, is saved as
notes.v2.stylo); when a file of that name exists, which the
panel did not ask about, the save (and every export or other write
through a save panel) answers cancelled and opens a
replaceFile dialog (fields message,
detail, path): ui.dialog.confirm
writes that file, ui.dialog.cancel writes nothing. On the
web a saveOptions dialog (fields path,
format) first names the file and picks the format.
file.formatOptions {format?} reads the options a save would
use. file.revert opens a confirm dialog
({"pending": "confirm"}); ui.dialog.confirm
reloads the saved file in the same tab |
SVG Options (engine.execute of
file.export.svg, file.saveAs,
file.saveCopy) |
{} / {path?, svg?: {…}} |
file.export.svg without params, and Save As or Save a
Copy to an .svg/.svgz path without SVG
options, open the svgOptions dialog
(ui.dialog.set its fields: styling,
outlineText, images, objectIds,
decimals, minify, responsive,
fewerTspans, metadata,
preserveEditing, useArtboards,
allArtboards, range, showCode,
profile (svg11/tiny12),
encoding
(utf8/utf16/latin1),
embedFonts; ui.dialog.confirm exports or saves
and remembers the choices). With svg: {…} (the
document.formats SVG options) they run directly.
app.save {path?, svg?} takes the same options; a document
saved as SVG saves as SVG again with the same options |
engine.execute file.place |
{paths?} /
{path | name+dataBase64, …file.place params} |
with no file, picks files and opens the place dialog;
with paths, opens it for them (fields files,
link, template, replace;
__replace says whether Replace applies, __info
describes each file). ui.dialog.confirm places one file
centered in the view (or in place of the selected object) and loads the
place cursor with several (file.place.queue). In the
desktop app the dialog reads its files off the UI thread:
__info is empty until they are read,
ui.dialog.confirm returns
{background: true, status}, ui.inspect lists
the files still being read in placing, and the file is
placed (or the cursor loaded) once it is read. With a file, it runs
file.place centered in the view, at once. Files dropped on
the window with no document open open. On the web, files dropped on the
canvas are placed at the drop point (linked; Shift embeds) and files
dropped off it (the tab bar) open, as in Illustrator; desktop drops
carry no pointer position, so documents (native, SVG, PDF,
.ai, EPS, DXF, EMF/WMF) open as tabs of their own and
pictures and text are placed in the middle of the view. Swatch libraries
and color books (.vcswatches, .gpl,
.ase, .acb), graphic style libraries
(.vcstyles), Libraries panel files
(.vclibrary), presets (.vcflattener,
.vcpdfpresets, .vcprintpresets,
.vcperspective), plug-ins (.wasm) and shortcut
sets (.kys, in Import Shortcut Set) open wherever they are
dropped, as File › Open opens them. With the cursor loaded
(ui.inspect → tool: "place",
toolOptions: {count, current, name, names}),
ui.pointer clicks and drags place files (in the desktop app
each file is read off the UI thread and placed once it is read, and
ui.inspect lists it in placing until then) and
ui.key ←/→/↑/↓ and Escape cycle and discard them |
app.export raster options |
{path?, format: png | jpg | webp, ppi?, background?, antiAlias?, interlaced?, quality?} |
ppi sets the pixel size (72 = one pixel per point) and
is stored in the file; background is
transparent, white, black or
"#rrggbb"; antiAlias is none,
art or type (text snapped to pixels);
interlaced writes an Adam7 PNG. The PNG Options dialog
(pngOptions; jpgOptions /
webpOptions for the other raster formats) has the same
fields: fill them with ui.dialog.set and press OK with
ui.dialog.confirm |
app.export JPEG options |
{path?, format: jpg, quality?: 0–100, colorModel?: rgb | cmyk | gray, method?: baseline | optimized | progressive, scans?: 3–5, embedIcc?, imageMap?: none | client | server} |
cmyk writes ink amounts in the working CMYK space (CMYK
colours keep their inks; RGB ones are separated with the colour
settings); embedIcc (default true) embeds sRGB, the working
CMYK profile or a grey profile, generated by the CMS;
imageMap also writes <stem>.html
(client-side <map>) or <stem>.map
(NCSA) beside the image for the objects with a URL and an Image Map
shape (attributes.set), returned in linked.
The jpgOptions dialog has these fields too
(quality is shown 0–10; a CMYK document defaults to
cmyk) |
app.export PNG-8 and GIF |
{path?, format: png8 | gif, colors?: 2–256, reduction?: perceptual | selective | adaptive | web | blackWhite | gray, dither?: none | diffusion | pattern | noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options} |
a palette of at most colors entries (art with that many
colours or fewer keeps them exactly); pixels under half opacity become
one transparent entry unless transparency is false, partly
transparent edges are blended over matte (none
keeps their colour). png8 writes an indexed
.png (1–8 bits per pixel); Export for Screens rows take
both. The png8Options / gifOptions dialogs
have these fields |
app.export Text and WebP |
{path?, format: txt, encoding?: utf8 | utf16, lineEndings?: lf | crlf, selectionOnly?}
/ {format: webp, lossless?, quality?} |
Text writes the stories in stacking order (back to front; a thread
once; hidden objects and template layers left out), each line ended by
lineEndings; utf16 is little-endian with a
byte order mark. Export As → Text opens txtOptions (fields
path, encoding, lineEndings,
selectionOnly; OK writes the file). WebP is always
lossless: lossless: false adds a warning |
engine.execute
document.exportForOffice |
{} /
{path?, ppi?: 72 | 150 | 300, transparent?, artboard?} |
File → Save for Office Documents…: without params opens
saveForOffice (fields ppi,
transparent, artboard; OK picks the file);
with params writes the artboard as a PNG (path, else a
picked file) → {path}, and the status bar shows the first
of the export's warnings as Save As shows a save's notes.
Headless it returns
{dataBase64, width, height, warnings} |
| Native file dialogs | any command with no path that needs one
(file.open, file.saveAs,
file.place, exports…) |
on macOS (a sheet on the window) and Linux the dialog doesn't hold
the window: the call returns "cancelled" at once,
ui.inspect reports fileDialog, and when the
person at the computer picks a file the command runs again on the
document it was asked for. While it is open another dialog is refused:
that call returns "cancelled" too, and the status bar
(ui.inspect → ui.status) says
"A file dialog is open: choose there first"; on macOS
app.quit, file.close and
file.closeAll return that message and the menu bar does
nothing (#592, #867). On Windows the dialog is shown in line: the call
returns once it closes. Pass path to skip the dialog |
| Background Save / Export | app.save, app.export {path},
file.save, file.saveAs,
file.saveCopy,
file.exportForScreens {folder, …} |
with the backgroundSave / backgroundExport
preferences on (the default) the desktop app writes the file from a
snapshot on a worker thread: the call returns
{path, background: true, status} at once
(file.exportForScreens:
{background: true, status}, and the files are written into
the folder from a snapshot taken at the call), ui.inspect
lists the jobs still running in background (labels such as
"Saving a.stylo"), and the document counts as saved (as of
the snapshot) once its file is written. Quit and Close wait for them. A
job that fails reports the error in the status bar only
(ui.inspect → ui.status), a path outside
--automation-write-root included. With the preferences off
(prefs.set), saves return once written.
app.save {path?, wait: true} and
app.export {path, wait: true} wait for the jobs already
running, write the file before they reply whatever the preferences are
set to, and return its result or the error. When the save asks first
(the question before replacing a file that opened with losses, or on
macOS and Linux the file dialog for an untitled document without
path), the reply is {pending} or
"cancelled" as before, and the save that runs after the
answer uses the preferences. A wait that is not
true, false or null is refused
with "wait must be true or false", and nothing is
written |
engine.execute file.exportForScreens |
{} /
{…document.exportForScreens params} |
without params opens exportForScreens on the settings
the document last exported with (document.exportSettings).
While the Artboard tool is in use, the artboards it has selected (its
selected option, the rows the Artboards panel highlights)
are checked instead, and select is range, or
all when every artboard is selected. Fields:
tab (artboards | assets |
layers | selection; OK reads Export Layer or
Export Selection on those), layers ([layer id…] checked),
selectionMode (one | each),
name (the file name pattern of
document.exportForScreens), counterStart,
folders (none | rows |
format | item | itemFormat),
exportPreset (the preset applied last),
__savePreset (the name typed to save one),
select (all | range |
full), range ("1-3, 5"), boards
(a bool per artboard; checking them sets select and
range), includeBleed, folder
(desktop; default the Desktop, else home), openLocation,
subfolders, prefix, preset
("" | mobile | density),
formats (rows
{format, scale: "2x" | "100w" | "100h" | "72ppi", suffix, quality?, folder?}),
settings
({png | png8 | jpg | webp | gif | svg | pdf: {…options}},
the gear's Format Settings; __settings names the format
shown). ui.dialog.confirm exports (a bad range or no folder
keeps it open); on the web OK reads "Download" and the files download
(several as one .zip). With params it runs
document.exportForScreens: no folder returns
the files (or with zip: true one store-only zip) as
dataBase64; with openLocation the first file
is shown in the file manager. When warnings is not empty,
an export into a folder or a download shows the first in the status bar
as Save As shows a save's notes |
| Export for Screens' Assets tab, Asset Export panel | ui.dialog.set tab: "assets",
assets: [asset id…];
window.panel {panel: "assetExport"} |
on the Assets tab of exportForScreens the
assets field lists the checked asset ids (all of them when
it opens); ui.dialog.confirm runs
document.exportForScreens {assets} (nothing checked keeps
it open) and OK reads "Export Asset". The Asset Export panel acts
through assets.* and shares the format rows with the dialog
(assets.settings.set) |
engine.execute file.exportSelection |
{multiple?} |
File › Export Selection…: collects the selected objects as one asset
(assets.add; one per object with
multiple: true) and opens exportForScreens on
its Assets tab with just them checked → {assets}.
document.exportSelection still exports the selection in one
go, without the dialog |
| CSS Properties panel | window.panel {panel: "cssProperties"};
engine.execute css.copy /
css.exportFile |
the panel shows css.selection of the selection (after
Generate CSS, while nothing is selected, css.generate); its
menu holds the options the commands take (units,
position, dimensions, unnamed,
rasterize). css.copy {scope?, …options} copies
the CSS to the clipboard → {css, rules};
css.exportFile {path?, scope?, …options} writes
css.export to path (else a picked file; the
web downloads it) with the pictures of rasterized art next to it →
{path, rules, images, warnings}, and the status bar shows
the first of warnings as Export As shows an export's
notes |
| ui.dialog.* on importPdf |
{field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value}
| opening (app.open, file.open) a PDF or
.ai with several pages or a password, or placing one
(file.place without page), opens the Import
PDF dialog (mode: open or place;
pageCount). While __locked is true, set
password and ui.dialog.confirm to unlock it (a
one-page file then opens or is placed). Then set
allPages: false and range ("2-3, 5") to open
some pages, or page to place one, and cropTo
(bounding, art, crop,
trim, bleed, media);
ui.dialog.confirm opens or places them (placing keeps the
other file.place params, such as link,
template and at, in __place).
document.open {pages, cropTo, password} and
file.place {page, crop, password} do the same without the
dialog |
Swatch editors: engine.execute with
ui.swatchOptions {name} opens the
swatchOptions dialog for a colour swatch (fields
name, spot, global,
mode:
gray/rgb/hsb/lab/cmyk/web,
color: "#rrggbb" or a colour object,
preview), which previews on the canvas while open;
ui.dialog.confirm applies it with swatch.edit
as one undo step and ui.dialog.cancel rolls the preview
back. For a gradient swatch the dialog has name only (it
shows the gradient) and OK renames it; pattern swatches open pattern
editing instead. Dropping a gradient (or colour) on a swatch of its kind
in the Swatches panel with Alt held replaces it
(swatch.edit {name, paint}), unless that is a tint swatch.
A swatch dragged from the panel with Alt held also replaces the swatch
of its kind it is dropped on, which keeps its name and place
(swatch.edit {name, paint: {swatch}}); without Alt, or on a
tint swatch or a swatch of another kind, it moves.
Confirmations: deleting swatches from the Swatches panel opens a
confirm dialog (fields message,
detail); ui.dialog.confirm runs the command it
asks about (here swatch.delete) and
ui.dialog.cancel drops it.
New swatches and colour groups:
ui.newSwatch {spot?, global?, group?} opens the
newSwatch dialog prefilled from the active fill or stroke
(fields as in swatchOptions plus group; a
gradient or pattern only takes name) and
ui.newColorGroup {swatches?} opens
newColorGroup (fields name,
fromArtwork, toGlobal,
includeTints, swatches);
ui.dialog.confirm runs swatch.new /
swatch.newGroup.
The Color Picker is a dialog too:
engine.execute {command: "ui.colorPicker", params: {stroke?, color?}}
opens it for the fill (or stroke) proxy;
ui.dialog.set {field: "hex", value: "00FF00"} (or
color, channel, webOnly,
swatches) then ui.dialog.confirm applies the
colour through paint.setFill /
paint.setStroke.
Sample Color from Screen:
ui.sampleScreenColor {stroke?, picker?} shows the system's
color sampler (macOS, and browsers with an EyeDropper) and returns
null at once. When the person at the computer clicks a
color anywhere on the screen, it runs paint.setFill (or
paint.setStroke) with that color as one undo step, or with
picker: true sets the open Color Picker's
color; Escape cancels it and the colors stay as they were.
The sampled display sRGB color is converted into the working RGB space.
ui.inspect reports screenSampler: true until
the sampler answers. Without a sampler (Windows, Linux, other browsers)
the call returns an error. To read a color from Stylo's own window
without a person, call ui.screenshot.
Graphic styles: ui.graphicStyleOptions {name} opens the
graphicStyleOptions dialog (field name) for a
style and ui.dialog.confirm renames it with
graphicStyle.rename (a name another style has returns an
error and keeps the dialog open); without name it names a
new style made from the selection (graphicStyle.new).
ui.mergeGraphicStyles {names} opens the same dialog to name
the style ui.dialog.confirm merges from them
(graphicStyle.merge). Deleting styles from the panel asks
first with a confirm dialog
(graphicStyle.delete).
Symbols: ui.symbolOptions opens the
symbolOptions dialog (field name) that the
Symbols panel's New Symbol asks first; ui.dialog.confirm
makes the symbol from the selection with that name
(symbol.new). F8 runs ui.symbolOptions without
params.
Gradient panel: double-clicking a stop on the panel's slider opens
the same gradientStop popover, with a screen
field ([x, y], screen points) in place of
x/y. The panel's stop eyedropper selects the
Eyedropper tool with the tool option stop (the tool to
return to;
tool.setOption {key: "stop", value: "gradient"}): its next
click on art samples the colour there into the selected stop
(paint.sampleColor {color, stop}) and switches back.
Dragging a swatch, a Fill/Stroke proxy or the panel's gradient thumbnail
onto art runs paint.setFill / paint.setStroke
(the active proxy) with the paint's params and the object's
ids; a colour dropped on the panel's ramp adds or recolours
a stop. Alt-clicking a color swatch in the Swatches panel recolors the
selected stop as a drop on it does
(paint.editGradient {stops}), or the selected point of a
freeform gradient (paint.freeform.setPoint); with no stop
or point selected, it paints the inactive Fill or Stroke.
Freeform gradients: double-clicking a point with the Gradient tool
opens the same gradientStop popover on the selected point
(paint.freeform.selectPoint); its fields are
color (hex), opacity and spread
(percentages), applied by ui.dialog.confirm through
paint.freeform.setPoint. ui.key Escape ends
the line Lines mode is drawing.
Tool options: double-clicking a tool button runs
tool.options {tool}. For gradient it opens the
Gradient panel; for eyedropper it opens Eyedropper Options,
an eyedropperOptions dialog (fields sampleSize
1/3/5, pickUp and apply, the attribute trees
of eyedropper.setOptions) whose
ui.dialog.confirm runs eyedropper.setOptions
(what appearance.copyFrom copies). For hand it
fits the artboard in the window (view.fitArtboard) and for
zoom it shows 100% (view.actualSize). For
rotate, scale, reflect and
shear it opens the same dialog as Object › Transform
(dialog kind = the tool id), or fails with
nothing selected. For selection,
directSelection and groupSelection it opens
the Move dialog (kind move), with the same failure; Move
opens on the offset of the last drag or Move (dx,
dy) and with the copy, objects
and preview it was last confirmed with (its
patterns is the transformPatternTiles
preference, which its OK sets). ui.key Enter with one of
those seven tools opens its dialog too (nothing happens without a
selection), with the transform tools' origin at their
reference point. For lineSegment, arc,
rectangularGrid and polarGrid it opens the
tool's Tool Options: the dialog its click opens (kind = the tool id),
without x and y and with
__toolOptions: true, whose ui.dialog.confirm
runs tool.setOption {tool, values} and draws nothing; the
next drag with the tool and its click dialog use the values (the line's
fillLine, the grids' Default Size width,
height and reference too). Gradient tool
handles snap to anchors, edges and smart guides; Shift constrains them
to 45° steps from the constrainAngle preference.
Effect dialogs:
engine.execute {command: "effect.dialog", params: {effect, index?, item?}}
opens the effect dialog (fields: the effect's parameters,
preview). With index it edits that applied
effect of item (null: the object's effects) prefilled with
its values, and ui.dialog.confirm runs
effect.setParams; otherwise confirm runs
effect.apply. Choosing an effect that the list already has
returns {"pending": "effectExists"} and opens the
effectExists question: ui.dialog.confirm opens
the applied effect's dialog,
ui.dialog.set {field: "discard", value: true} then confirm
opens a fresh one that adds another, ui.dialog.cancel drops
it. The Transform effect's dialog
(effect.dialog {effect: "distort.transform"}) has Transform
Each's controls: sliders for scaleH, scaleV
(%) and moveH, moveV (pt), an angle dial for
rotate, then copies, reflectX,
reflectY, reference (0–8, the 9-point grid)
and random (each object varies its own way, the same on
every redraw).
Plug-in dialogs:
engine.execute {command: "plugin.dialog", params: {id}}
(Object › Plug-ins) runs an object filter plug-in at once when it takes
no parameters, else opens the plugin dialog: one field per
declared parameter (named after it), __plugin (the plug-in
id) and preview. It previews plugin.run on the
canvas while open; ui.dialog.confirm keeps the result as
one undo step and ui.dialog.cancel rolls it back.
Live-effect plug-ins open the effect dialog
(effect.dialog {effect: "plugin.<id>"}). An exporter
plug-in with parameters opens the pluginExport dialog once
Export As has picked the file: one field per parameter,
__plugin, and __export (the export's own
parameters); ui.dialog.confirm writes the file.
ui.installPlugin {path?} installs a .wasm
(without a path it asks for one); see plugins.md.
Overprint Black: the Edit → Edit Colors → Overprint Black… menu item
opens a command parameter dialog for
edit.colors.overprintBlack (fields remove,
percentage, fill, stroke,
includeCmyBlacks, includeSpotBlacks);
ui.dialog.set then ui.dialog.confirm runs it
on the selection.
Color Guide:
color.harmony {color, rule, steps?, variation?, amount?}
returns the harmony group (base first) and its variation grid; the panel
draws the same grid. ui.colorGuideOptions opens the
colorGuideOptions dialog (fields steps 1–20,
amount 0–100) and ui.dialog.confirm sets the
panel's options, which ui.inspect reports as
ui.color_guide (variation, steps,
amount). Save Colors as Swatches runs
swatch.new {colors: [...]} (one swatch per colour, one undo
step).
Color Guide Limit to Library:
ui.colorGuideLimit {library} (a swatch library id or name,
"document" for the document's swatches, "" for
none) limits the panel's colours; ui.inspect reports it as
ui.color_guide_limit, and
color.harmony {..., limitTo} answers the limited guide
(every colour snaps to the library's nearest colour, ΔE 2000). Edit or
Apply Colors runs ui.recolorDialog {colors, library?} with
the harmony colours: on the selected art it recolours as usual; with
nothing selected the colours themselves are the rows and
ui.dialog.confirm saves them as a new colour group
(swatch.newGroup, named by the groupName
field).
Color panel options: ui.colorPanel {mode?, options?}
picks the panel's mode as its panel menu does (grayscale,
rgb, hsb, cmyk or
websafe; the active color converts to the mode's model as
one undo step) and shows (options: true) or hides
(false) the options. It answers
{mode, options, shown}: mode is null until a
mode is picked, and shown is the mode the panel shows the
color in, or tint for a tint of a global or spot swatch.
Another mode, lab included, answers an error. The desktop
app keeps both settings with its preferences, and
ui.inspect reports them as ui.color_mode and
ui.color_hide_options.
Color Themes panel
(window.panel {panel: "colorThemes"}): its Create tab makes
a theme on a harmony wheel and its My Themes tab lists the saved themes;
every action runs a command (colorTheme.save,
colorTheme.delete, colorTheme.addToSwatches,
swatch.newGroup for the theme being made), and
colorTheme.list reads the library.
Edit Colors dialogs: ui.colorBalanceDialog opens Adjust
Colors (colorBalance: fields mode
gray/rgb/cmyk/
global, the channels r g
b / c m y
k / gray / tint (global mode)
from −100 to 100, convert, fill,
stroke, preview) and
ui.saturateDialog opens Saturate (saturate:
intensity −100..100, preview). Both preview on
the canvas while open; ui.dialog.confirm keeps the result
as one undo step (edit.colors.adjustBalance /
edit.colors.saturate) and ui.dialog.cancel
rolls it back. Global mode answers with an error until tints of global
and spot colours exist; the dialog then stays open.
Recolor Artwork:
ui.recolorDialog {colors?, library?, group?} opens the
recolor dialog on the selected art (colors: a
count for an n-colour job, or colours to assign as the new colours;
library: Limit to Library, a library or
"document", "" for the first library;
group: a colour group, Edit or Apply Color Group). Fields:
rows ([{from: [keys], to: key, exclude?}] as
recolor.reduce returns them), colors (null for
Auto), method, preserveWhite,
preserveBlack, preserveGrays,
limitTo, groupName, tab
(assign or edit), rule (harmony
id), linked, preview. Setting
colors, a preserve flag or limitTo reduces the
rows again (also on ui.dialog.confirm). The dialog previews
recolor.apply on the canvas; confirm keeps it as one undo
step (with a group, the group is rewritten in the same step; with no art
selected only the group changes). In the Swatches panel, double-clicking
a colour group's folder, or the options button while a group is
selected, opens it.
Library panel:
engine.execute {command: "window.swatchLibrary", params: {library}}
opens the read-only library panel on a swatch library
(ui.inspect shows it as
library_panel: {kind, id}; library: null
closes it). Clicking a swatch there runs
swatch.library.add {library, names: [name], apply} (the
active proxy, or the other one with Alt), so one undo step adds and
applies it; Shift/Cmd-clicks select swatches and colour groups for Add
to Swatches. window.swatchLibrary.other {path?} loads a
library file (or another document's swatches) and opens it there;
opening a .vcswatches, .gpl or
.ase file with app.open does the same. A file
with the same extension and bytes as a User Defined library opens as
that library. ui.saveSwatchLibrary {names?} opens the
saveSwatchLibrary dialog (fields name,
format:
vcswatches/gpl/ase/css,
user: save to the user library folder,
selectedOnly with names);
ui.dialog.confirm runs swatch.library.save (to
a file it asks for a path and suggests name with the
format's extension as the file name; the web downloads the file under
that name).
Graphic style libraries open in the same panel:
window.graphicStyleLibrary {library}
(library_panel: {kind: "graphicStyles", id};
library: null closes it). Clicking a style there runs
graphicStyle.addFromLibrary {library, name, apply: true, add}
(add with Alt), adding and applying it in one undo step;
Shift/Cmd-clicks select styles for Add to Graphic Styles.
window.graphicStyleLibrary.other {path?} loads a
.vcstyles file (or another document's graphic styles) and
opens it there; opening a .vcstyles file with
app.open does the same. The User Defined libraries are
window.userGraphicStyleLibrary1…10 in Window →
Graphic Style Libraries.
ui.saveGraphicStyleLibrary {names?} opens the
saveGraphicStyleLibrary dialog (fields name,
user: save to the user library folder,
selectedOnly with names);
ui.dialog.confirm runs
graphicStyle.saveLibrary (to a file it asks for a path and
suggests name with .vcstyles as the file name;
the web downloads the file under that name).
Symbol libraries open in the same panels:
window.symbolLibrary {library} (library_panels
kind symbols; library: null closes them).
Clicking a symbol there runs
symbol.addFromLibrary {library, name}, adding it to the
document's Symbols in one undo step; Shift/Cmd-clicks select symbols for
Add to Symbols, and a symbol dragged onto the canvas runs it with
place: true and the drop point as x,
y. window.symbolLibrary.other {path?} loads
the symbols of any document Stylo opens (a saved symbol library is a
.stylo document) and opens them there. The User Defined
libraries are window.userSymbolLibrary1…10 in
Window → Symbol Libraries. ui.saveSymbolLibrary {names?}
(Save Symbols…, the Symbols panel's Save Symbol Library…) opens the
saveSymbolLibrary dialog (fields name,
user: save to the user library folder,
selectedOnly with names);
ui.dialog.confirm runs symbol.saveLibrary (to
a file it asks for a path and suggests name with
.stylo).
Tile Edge Color: Object → Pattern → Tile Edge Color…
(ui.tileEdgeColor) opens the tileEdgeColor
dialog (field color: #rrggbb or a preset name
such as "Light Blue"); ui.dialog.confirm sets the
preference patternTileEdgeColor (prefs.set),
the colour pattern editing mode draws the tile edge and the swatch
bounds in.
Window title bar: on Windows and Linux the window has no OS
decorations and the app bar is the title bar. Its caption buttons
(Minimize, Maximize/Restore, Close) sit at the bar's right end, 46 pt
each; they are window chrome, not commands, so drive them with
ui.click if needed. Close runs app.quit (the
same saveChanges questions for modified documents), and
empty bar space and the 5 pt window edges move and resize the window.
Preferences › User Interface › System Title Bar
(systemTitleBar, read when the app starts) gives the window
its OS decorations back instead: the OS title then shows the active
document (*name — Stylo with unsaved changes) and the app
bar drops its brand mark. On macOS the window buttons are centred on the
app bar; the web build is unchanged.
Fill/Stroke chips and panel shortcuts: the Control bar's and
Properties' Fill and Stroke chips bring their proxy forward
(paint.toggleActive {fill}) and open a popover with the
Swatches panel (Shift-click: the Color panel's mixer); a swatch clicked
there runs paint.setFill / paint.setStroke.
The Properties panel has them with nothing selected too (they set up the
next object), with the Control bar's Stroke link (the Stroke panel as a
popover) and weight spinner. Panel keys (Brushes F5, Color F6, Layers
F7, Color Guide Shift+F3, Graphic Styles Shift+F5, Appearance Shift+F6,
Align Shift+F7, Transform Shift+F8, Info Cmd+F8, Gradient Cmd+F9,
Pathfinder Cmd+Shift+F9, Stroke Cmd+F10, Transparency Cmd+Shift+F10,
Attributes Cmd+F11, Symbols Cmd+Shift+F11, Character Cmd+T, Paragraph
Cmd+Alt+T, Tabs Cmd+Shift+T, OpenType Cmd+Alt+Shift+T; Cmd is Ctrl on
Windows and Linux) run window.panel {panel}, can be changed
in Edit › Keyboard Shortcuts (entry panel:<id>, every
panel) and pressed with ui.key; ui.menu.list
shows them on the Window menu's items. window.panel takes a
panel id in any case or the panel's display label
("Layers", "Color Guide").
Collapsing the dock: window.collapseDock {collapsed?}
(the » at the top of the dock; omitted toggles) hides the Properties |
Layers | Libraries group and puts its three panels as icons at the top
of the icon column, under a « that expands them again. While collapsed,
those icons and
window.panel {panel: "properties"|"layers"|"libraries"} pop
the panel out next to the column like the other icon panels
(ui.dock_collapsed, ui.open_panel in
ui.inspect); expanding with one popped out shows its tab.
The state is saved with the preferences and in user workspaces; the
built-in workspaces expand the dock.
Floating tool groups: dragging or clicking the tear-off bar down a
tool group's flyout (or releasing the long press that opened it over the
bar) floats the group as a strip of tool buttons, moved by the same bar
and put back in the toolbar by the × at its top;
window.floatTools {tool, floating?} does the same for the
group of the current layout holding tool (omitted toggles;
a tool alone in its slot is an error). While a group floats, the presses
that open its flyout raise the strip instead. The strips
(ui.floating_flyouts in ui.inspect: the
group's tools and the strip's top-left corner) are saved with the
preferences and in user workspaces; the built-in workspaces float
none.
Edit Toolbar: the ••• button at the bottom of the toolbar (or
window.editToolbar {open?}) opens the All Tools drawer
beside the toolbar; Escape or a click outside closes it. While it is
open, ui.drag a tool from it onto the toolbar to add it
(onto a button's top or bottom edge: its own slot before or after; onto
its middle: into its group), from the toolbar into it to remove it, or
within the toolbar to move it.
toolbar.addTool {tool, slot?, place?},
toolbar.removeTool {tool, group?} and
toolbar.reset do the same for the current Basic or Advanced
toolbar and return its slots; the edits (ui.toolbar_edits)
are saved with the preferences. The drawer lists the tools one per row
(icon, name, shortcut), each flyout group framed together under its
heading; its ☰ menu has Reset and the Basic and Advanced toolbars. Its
Show section (toolbar.show {control?, on?},
control one of fillStroke,
coloring, drawMode, screenMode)
shows or hides the controls under the toolbar's tools, saved with the
preferences (ui.toolbar_show). The toolbar names its groups
only with Preferences › User Interface › Show Tool Group Labels, off as
it ships (#1736).
The Contextual Task Bar: its handle (the grip at its left end) drags
it anywhere on the canvas, which always holds it whole, also when the
window shrinks. Unpinned, the bar keeps that offset as it follows the
selection, until another document becomes active. Its More Options (…)
menu has Hide Bar (window.taskBar); Pin Bar Position
(window.taskBar.pin {pinned?}, omitted toggles, checked
while pinned), which holds the bar where it is across selection changes
(the handle still moves it) and, turned off, lets it follow the
selection again from there; Reset Bar Position
(window.taskBar.reset), which unpins it and puts it back
under the selection; and Show Properties Panel. As in Illustrator,
neither the position nor the pin is saved: each launch starts with the
bar under the selection.
The Free Transform widget: while the Free Transform tool (E) is
active, a small box at the canvas's top left holds Constrain over the
tool's three modes, Free Transform, Perspective Distort and Free
Distort. The buttons set the tool's options, as
tool.setOption {key: "mode", value: "free"|"perspective"|"distort"}
and {key: "constrain", value} do: Constrain acts as Shift
held (proportional scaling, moves and rotations by 45°) and does nothing
in the distort modes, where it is greyed. The modifier keys still work
while dragging (Cmd on a corner distorts it freely, Cmd+Alt+Shift in
perspective, Cmd on a side shears).
Ruler guides are layer objects: guide.add puts a guide
on the current layer (or layer), where it shows, hides,
locks and is deleted with its layer and is listed in the Layers panel (a
click on its row selects it, a drag onto another layer's rows moves it
there); guide.setLayer {index?, indexes?, layer} moves
guides to another layer, and guide.list gives each guide's
layer, shown and editable. Guides
on a hidden or locked layer leave the selection and can't be picked,
moved or deleted, and no guide goes onto a locked layer; Duplicate
copies a layer's guides, Merge Selected and Flatten Artwork keep them on
the merged layer (Flatten discards a hidden layer's). Guides in files
saved before guides had layers go on the top visible, unlocked layer
when the document opens.
Floating panels: dragging a dock tab, a panel icon or a popped-out
panel's title out of the dock floats that panel inside the window, and
the strip right of the dock's tabs floats the whole Properties | Layers
| Libraries group. A floating group moves by its title bar (or the strip
right of its tabs); a tab dragged out of it floats on its own; dropped
on another group's title bar or tabs it stacks with that group, and
dropped on the dock (lit up while the pointer is over it) or closed with
its × its panels go back to the tabbed group or the icon column. Dropped
on the upper or lower half of another group, floating or docked (a line
shows where), a group joins it in a set right above or below it: the
groups stack top to bottom, each showing one of its panels, and move
together by the top group's title bar, whose × docks them all; a group
dragged by its tab strip (a lone panel's tab or the strip right of the
tabs) leaves the set. A double-click on a tab collapses a group to its
tabs, and another expands it. Dropped on the left edge of the icon
column or of a dock column, a set docks as a new dock column there: its
groups stack in it as in a floating set, take groups dropped on their
edges and tabs, and leave it dragged by their tab strip; the strip along
the column's top floats the whole column again, and its × puts its
panels back where they live.
window.panel.float {panel, x?, y?, onto?, below?, above?, group?, collapsed?}
floats a panel (with group, its whole group) with its
top-left corner at x, y (window points;
default cascaded), or stacks it as a tab with the floating group holding
onto, or puts its group in a set right below
or above the group holding that panel;
collapsed collapses or expands its group;
window.panel.dock {panel, group?, column?} puts it back, or
with column (true, or the place: 1 next to the
icon column, counting left) docks its group and set as a dock column;
window.panel.float on a docked panel floats its column.
panel takes the window.panel ids and labels,
and "tools" for the Tools panel, which floats by its title
bar and docks on the window's left edge. window.panel on a
floating panel shows its tab (expanding its group).
ui.floating_panels in ui.inspect lists the
groups (panel ids, the index of the tab shown, the top-left corner,
column: the set it is stacked in, top to bottom in list
order, collapsed, and docked: the dock column
it stands in) and ui.toolbar_pos the Tools panel's corner
(null: docked); both are saved with the preferences and in user
workspaces (the built-in workspaces dock everything), and a corner saved
on a bigger window is clamped into this one when drawn. Floating panels
stay inside the app window (no separate OS windows).
Flatten Transparency: ui.flattenTransparencyDialog opens
the flattenTransparency dialog for the selection (fields
preset: a preset name, setting it loads that preset's
options; the option keys of object.flattenTransparency:
balance 0–100, lineArtPpi and
gradientPpi 1–2400, textToOutlines,
strokesToOutlines, clipComplexRegions,
antiAlias, preserveAlpha,
preserveOverprints; preview, off at first).
With preview on it flattens on the canvas while open;
ui.dialog.confirm keeps the result as one undo step and
ui.dialog.cancel rolls it back. Out-of-range values answer
with an error and the dialog stays open.
Flattener presets: ui.flattenerPresetsDialog {selected?}
opens Edit → Transparency Flattener Presets (dialog
flattenerPresets). Fields: selected (a preset
name), then the selected preset's name and option keys. On
a saved preset, ui.dialog.set of an option saves it and a
new name renames it (both through
flattener.presets.save); built-in presets don't change.
New, Delete, Import… and Export… are
flattener.presets.save {preset},
flattener.presets.delete,
flattener.presets.import and
flattener.presets.export; ui.dialog.confirm
closes. Opening a .vcflattener file with
app.open imports its presets. In the Flatten Transparency
dialog, Save Preset… keeps the dialog's options as a saved preset.
PDF presets: ui.pdfPresetsDialog {selected?} opens Edit
→ PDF Presets (dialog pdfPresets, field
selected: a preset name). Built-in presets are read-only.
New… and Edit… open the preset editor, which
ui.pdfPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)}
opens too: dialog pdfPreset, the Save PDF dialog's option
fields plus name and description;
ui.dialog.confirm runs pdf.preset.save and
returns to PDF Presets (a new preset can't take a name in use). Delete,
Import… and Export… are pdf.preset.delete,
pdf.preset.import and pdf.preset.export;
ui.dialog.confirm closes. Opening a
.vcpdfpresets file with app.open imports its
presets. In the Save PDF dialog, Save Preset… shows a name field
(__savePresetAs): while it shows,
ui.dialog.confirm saves the dialog's settings as that
preset and selects it instead of writing the PDF.
Flattener Preview:
window.panel {panel: "flattenerPreview"} shows the panel;
ui.flattenerPreview {highlight?, overprints?, preset?, options?, showOptions?}
sets it (a preset loads its options, options adjust them),
shows it and refreshes its snapshot of the document, answering as
flattener.preview does. ui.inspect reports the
settings as ui.flattener_preview. The panel keeps its
snapshot until Refresh (it notes when the document or the settings
changed since); its preview greys the art and paints the highlighted
objects or areas red.
Expand: Object → Expand… (ui.expandDialog, or
object.expand without params from the menus, palette and
shortcuts) opens the expand dialog for the selection.
Fields: object, fill, stroke (all
on; __object, __fill, __stroke
say which the selection has something for, as
object.expand.info does, and the others are disabled),
gradient (objects or mesh) and
steps (1–1000, default 255), which are enabled when
fill is on and __gradient says the selection
has a gradient fill (object.expand.info's
gradient). ui.dialog.confirm runs
object.expand with them as one undo step.
Attributes panel: window.panel {panel: "attributes"}
(Cmd+F11) shows it; its controls run attributes.set,
path.reverse {reversed} and
path.setFillRule {rule}, and its Browser button runs the UI
command attributes.openUrl {url?}, which opens the URL
(default: the selection's) in the web browser and answers
{url}. It opens http:, https: and
mailto: URLs only, and answers a control request with an
error: it hands the URL to another app.
SVG Interactivity panel:
window.panel {panel: "svgInteractivity"} shows it; its
controls run svgInteractivity.setEvent,
svgInteractivity.removeEvent and
svgInteractivity.clearEvents, and its Link JavaScript
Files… button runs the UI command ui.javascriptFilesDialog,
which opens the javascriptFiles dialog (fields
file and selected). The dialog's Add and
Remove buttons run svgInteractivity.addFile {file} and
svgInteractivity.removeFile {file} at once, and
ui.dialog.confirm closes it.
Spot Colors: the Swatches panel menu's Spot Colors…
(ui.spotColors) opens the spotColors dialog
(field useLab: true shows and separates spot colours
defined in Lab from their Lab values, false from their working-CMYK
equivalents); ui.dialog.confirm runs
swatch.spotOptions {useLab} as one undo step.
Scale options: ui.menuDialog {command} opens the dialog
a command's menu item opens (object.scale →
scale, object.transformEach →
transformEach, also Move, Rotate, Reflect, Shear and the
Path dialogs). The Scale dialog (scale) and Object →
Transform → Transform Each… (dialog transformEach:
scaleH, scaleV %, moveH,
moveV pt, rotate °, reflectX,
reflectY, random, reference 0–8,
copy, preview) have the fields
corners (Scale Corners) and strokes (Scale
Strokes & Effects). They start from the preferences
scaleCorners and scaleStrokes, and
ui.dialog.confirm keeps them as the preferences
(prefs.set) before running object.scale /
object.transformEach with them. The Transform panel's
checkboxes and flyout item and the Properties panel's checkbox set the
same preferences through prefs.set.
Use Preview Bounds: the Align panel flyout's Use Preview Bounds item
shows and toggles the usePreviewBounds preference
(prefs.set); while it is on, the bounding box and the
Transform panel, Properties and Control bar fields measure visual
bounds.
Align To: the Align panel's Align To choice is saved with the
preferences, and ui.inspect reports it as
ui.align_to ("selection" or
"artboard"; Align to Key Object stores no choice). The
align buttons of the Align panel, the Control bar and the Properties
panel run object.align with to: "artboard"
when one object is selected, else with "key" while the
selection has a key object, else with that choice. The Object → Align
items send the same to while every selected object is
selected whole. With direct-selected anchor points or segments they send
no to, and the points align to their own bounds.
object.align from engine.execute or
ui.menu.invoke without to keeps the engine's
default: the key object while there is one, else the selection.
Width Point Edit: double-clicking a width point with the Width tool,
or ui.widthPointEdit {id, index}, opens the
widthPoint dialog (fields id,
index, t, side1 and
side2: the left and right widths in points,
linked, adjustAdjoining).
ui.dialog.confirm runs stroke.widthPoint.set
with them; ui.dialog.set {field: "discard", value: true}
then confirm (the Delete button) removes the point with
stroke.widthPoint.remove.
Corners: double-clicking a Live Corners widget with the Selection or
Direct Selection tool, or ui.corners {id?, corners?}, opens
the corners dialog for a path's corners (the
Direct-Selected ones, else every corner; fields id,
corners: anchor indices of the path with its corners uncut,
a rectangle's 0–3 clockwise from the top-left, kind: round,
invertedRound or chamfer, radius in points;
kind or radius is absent while the corners
differ). ui.dialog.confirm runs
object.setLiveShape with them.
Perspective plane options: double-clicking a plane widget of the
perspective grid, or ui.perspectivePlane {plane}, opens the
perspectivePlane dialog (fields plane: left,
right or ground; location: points along the plane's normal;
objects: none, move or copy).
ui.dialog.confirm runs perspective.plane.move
with them.
Units: dialog distance fields (Move's
dx/dy, shape sizes, Offset Path's
offset, Split Into Grid's gutters and totals, Artboard
Options' sizes, effects' distances, transformEach's
moveH/moveV, New Document's
width/height, the length preferences in
preferences) show in the General unit (the active
document's units, or unitsGeneral without a document).
Through ui.dialog.set a number is in points and a string
may carry its unit ("10 mm"; a bare number in a string is
points). The preferences dialog's unitsGeneral
starts as the active document's units, and OK sets them.
newDocument has units (a unit label; print
presets start in unitsGeneral, screen presets in
Pixels).
Document Setup: File → Document Setup…
(file.documentSetup, Cmd+Alt+P) opens the
documentSetup dialog. Its fields are what
document.setup reports (units,
bleed [top, bottom, left, right] in pt,
outlineImages, highlightSubstitutedFonts,
highlightSubstitutedGlyphs, gridSize,
gridColors, simulatePaper,
flattenerPreset, discardWhiteOverprint,
language, quotes,
typographersQuotes, superscript,
subscript, smallCapsSize,
exportText, backgroundContents) plus
tab (General, Transparency or
Type) and bleedLinked.
ui.dialog.confirm runs document.setup with
them as one undo step; a bad value answers with an error and the dialog
stays open.
New Document: File → New… (file.newDialog, Cmd+N) opens
the newDocument dialog. Its fields are
file.new's params (preset, name,
width and height in pt, units,
artboards, artboardLayout, bleed,
backgroundContents, colorMode,
rasterEffectsPpi, previewMode) plus
category (the tab: Recent, Saved, Mobile, Web, Print, Film
& Video, Art & Illustration, Branding, Social),
advanced (Advanced Options open), bleedLinked
and presetName (Save Preset's name). Setting
preset to a name from file.newPresets fills
the other fields from that preset; changing a field afterwards makes the
settings custom (preset becomes ""). More Settings is the
newDocumentMore dialog over the same fields (its Profile is
category). ui.dialog.confirm runs
file.new with the fields (with
previewMode: "pixel" the app turns Pixel Preview on); on an
error the dialog stays open.
Swatch Conflict: through the app, when the pasted objects' global or
spot swatches conflict with the document's (see
clipboard.conflicts) and the params have no
swatchConflict, every Paste command opens the
swatchConflict dialog instead and answers
{dialog: "swatchConflict"}. Fields: conflicts
([{name, document, clipboard, spot}]), index
(the conflict asked about), choice (merge or
add), applyToAll and choices (the
answers so far). ui.dialog.confirm records the answer and
asks about the next conflict (with applyToAll, answers the
rest the same); the last one runs the paste with
swatchConflict. ui.dialog.cancel pastes
nothing.
Paste placement: through the app, edit.paste and
edit.pasteWithoutFormatting without center,
dx or dy paste at the centre of the view. The
Paste menu items are enabled while the system clipboard holds something
to paste (SVG, PDF, text or a bitmap on Windows; text or a bitmap on
macOS; text on Linux, whose paste keys still take a bitmap; SVG on the
web, whose paste keys also take pictures and files), even with nothing
copied in the app (looked at up to four times a second;
ui.menu.list shows it). The Layers panel menu (≡ on the
dock's tab strip while Layers shows) lists the layer commands (New
Layer…, Duplicate and Delete Selection, Options for Selection…, clipping
mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect
in New Layer, Release to Layers, Reverse Order, Template,
Hide/Outline/Lock Others or Show/Preview/Unlock All Layers), Paste
Remembers Layers (layer.pasteRemembersLayers, checked when
on) and Panel Options…. Layer Options is the layerOptions
dialog (ui.layerOptions {ids?},
ui.newLayer {sublayer?}), Panel Options the
layersPanelOptions dialog
(ui.layersPanelOptions), and
ui.layersExpand {ids?, open?} opens or closes rows; see the
Layers panel section of docs/mcp.md for the row commands
(layer.setCurrent, layer.highlight,
layer.move…).
File Info: File → File Info… (file.info from the menu or
Cmd+Alt+Shift+I; ui.fileInfoDialog for agents) opens the
fileInfo dialog. Its fields are what file.info
reports (title, author,
authorTitle, description,
keywords (a list: the chips), rating,
copyrightStatus, copyrightNotice,
copyrightUrl; created and
modified are shown read-only) plus __keyword,
keywords typed but not added yet (comma-separated; OK adds them).
ui.dialog.confirm runs file.info with them as
one undo step; a bad value answers with an error and the dialog stays
open.
Document Raster Effects Settings: Effect → Document Raster Effects
Settings… (ui.rasterEffectsSettingsDialog for agents) opens
the rasterEffectsSettings dialog. Its fields are what
document.rasterEffectsSettings reports
(resolution, colorModel,
background, antiAlias,
clippingMask, addAround in pt,
preserveSpotColors); ui.dialog.confirm runs it
with them as one undo step; a bad value keeps the dialog open. Object →
Rasterize… opens a command dialog for
object.rasterize whose fields (ppi,
colorModel, background,
antiAlias, clippingMask,
addAround) start from these settings.
Missing linked files: app.open of a document whose
linked images' files can't be found (document.open's
missingLinks) opens the missingLinks dialog
for the first (fields missing:
[{name, path, ids}], modified,
applyToAll, path).
ui.dialog.set {field: "path", value} then
ui.dialog.confirm replaces it with that file
(links.relink; without path a file is picked),
and with applyToAll the others are looked for by name in
the same folder;
ui.dialog.set {field: "discard", value: true} then confirm
ignores it (the images keep their preview), with applyToAll
the rest too. The next missing file is asked about after each answer;
ui.dialog.cancel stops asking. Then, with the preference
updateLinks: "askWhenModified", modified linked files are
offered for update in a confirm dialog whose
ui.dialog.confirm runs links.update. The same
happens when the app's window comes back to the front and a linked file
of the active document changed meanwhile (asked once per change of a
file); with "automatically" the files are read again at
once, with "manually" the Links panel shows them as
modified.
Find Linked Files: in the missingLinks dialog,
ui.dialog.set {field: "searchFolder", value} then
ui.dialog.confirm is Find in Folder…: the dialog becomes
findLinks, which looks in that folder and its subfolders
for every file the dialog still asks about
(links.findFiles). Fields: document (the
document's uid), missing
([{name, path, ids}]), modified,
back, state (empty, searching,
done, stopped or failed),
search (the search's id), folder,
found (links.findFiles's links,
with the files found), searched, skipped,
unreadable, stopped, error,
picks (the file chosen for each row of
missing, or null; with each search result, a row not chosen
in the dialog gets its only same file, else its only file found),
touched (the rows whose file was chosen in the dialog;
search results leave their pick as it is), copy (Copy into
the document's folder, shown for a saved document) and
searchFolder.
ui.dialog.set {field: "searchFolder", value} then
ui.dialog.confirm searches that folder and keeps the dialog
open. The fields change as frames draw the dialog;
links.findFiles {} reports the search directly.
ui.dialog.confirm without searchFolder is
Relink: each row with a file runs
links.relink {ids, path, copy}, one undo step each
(copy for images only: a placed document is relinked where
its file is), and it returns {relinked, failed}; the status
line names the first file that failed. With back (opened
from missingLinks), the rows left without a file and those
that failed go back to that dialog, then the modified files are offered
for update. ui.dialog.cancel closes it and stops its
search. ui.findLinksInFolder {folder?, ids?} (Find in
Folder… in the Links panel menu) asks for a folder (or takes
folder), then opens the dialog for the files of
ids, else of the selected linked images and placed
documents, else the missing ones; with nothing to look for, it fails
with "No linked files to look for" and asks for no folder. On the web,
ui.findLinksInFolder is disabled,
links.findFiles can't start a search and the dialog doesn't
open.
Missing fonts: once the missing linked file questions are answered,
or at once when there are none, app.open of a document
whose type uses fonts that aren't available
(text.missingFonts) opens the missingFonts
dialog, one document at a time, and so does
links.editOriginal of a placed document. When another
dialog takes its place (Import PDF for a file opened next, for example),
the dialog opens again once that one closes. Fields:
document (the document's uid), fonts
([{family, style, status, resolved}]),
fontsNextToDocument (the Fonts folder next to
the document, or empty), state (empty,
searching, done, stopped or
failed), search (the search's
id), folder, found
(text.findFontFiles's fonts, with the files
found), searched, skipped,
unreadable, stopped, error and
chosen (the files to add; a font's first file is chosen
when the search finds it). Its buttons are Find Fonts and Close, with
Search Fonts Folder, Find in Folder… and, once files are found, Add
Fonts in the dialog.
ui.dialog.set {field: "discard", value: true} then
ui.dialog.confirm is Find Fonts: the dialog closes and Type
› Find Font opens (dialog findFont) with the first missing
font selected. ui.dialog.set {field: "searchFolder", value}
then ui.dialog.confirm searches that folder
(text.findFontFiles), keeps the dialog open and clears
searchFolder, also when the search doesn't start. The
fields change as frames draw the dialog;
text.findFontFiles {} reports the search directly.
ui.dialog.confirm without discard or
searchFolder is Add Fonts: it copies the files in
chosen (text.addFontFiles) and closes the
dialog. ui.dialog.cancel (Close) closes it and stops its
search. After Find Fonts or Close, the dialog doesn't open again for
that open. ui.missingFontsDialog {folder?} opens the dialog
for the active document, searching folder when given.
ui.findFontsInFolder {folder?} (Find in Folder… in Type ›
Find Font…, shown when the document misses fonts) asks for a folder,
then opens the dialog searching it. The web shows a status line instead
of the dialog, and both commands are disabled there.
Text Import Options: placing a .txt file through the app
(file.place with a file and no text options,
the Place dialog, a drop) opens the textImport dialog
(fields platform: windows/mac,
characterSet: unicode/ansi,
removeLineReturns, removeParagraphReturns,
replaceSpaces, spaces).
ui.dialog.confirm places the file as area type with those
options (file.place {text: {…}}).
System clipboard formats: through the desktop app,
edit.copy and edit.cut publish what
clipboard.flavours lists to the system clipboard (once they
are written, the status bar shows the first of the PNG's and PDF's
warnings, such as an image that couldn't be decoded), and
every Paste command first loads what another app copied
(clipboard.importSvg, importPdf,
importEps, importText or
importImage, centred in the view; the desktop app reads PDF
and EPS from the Windows clipboard and, with SVG, from the macOS
pasteboard, and an .eps file, or a .ai or
.ait file that holds a PDF or PostScript, copied in a file
manager anywhere; a copied .ai or .ait file
that holds a PDF loads with importPdf {editingData: true},
which reads its editing data); an import error answers the Paste with
that error and pastes nothing. Cmd+V also pastes a bitmap or PDF with no
text beside it (on the key's release, as egui sends no Paste event for
it). Screenshots paste on every platform: Windows reads
PNG, else CF_DIBV5/CF_DIB, macOS
the pasteboard's PNG or TIFF, Linux image/png, and the web
the picture the browser's paste event carries (#457).
File menu details:
file.openRecent1…file.openRecent30 open the
recent files the recentFilesCount preference lists (0 hides
them all; file.recentFiles returns the listed paths),
file.reveal shows the document's file in the system file
manager (desktop only; answers {path}), and
file.newFromTemplate without params opens a file picker in
the Templates folder (preference templatesFolder). Older
native files open as "appendConverted), so their Save asks for a new
.stylo name. Native files reopen at the view they were
saved with.
DXF Options: Export As… with the DXF format
(file.exportAs {format: "dxf"}, then the save dialog), or
ui.dxfOptionsDialog {path?, …document.exportDxf options}
for agents, opens the dxfOptions dialog. Its fields are
document.export params: version,
unit (a unit name such as Millimeters),
scale, scaleLineweights, colors
(8, 16, 256, true),
rasterFormat (png, jpeg),
preserve (appearance,
editability), selectedOnly,
alterPaths, outlineText, plus
path and the artboard choice.
ui.dialog.confirm checks them (a bad value, or Export
Selected Art Only with nothing selected, keeps the dialog open),
remembers them for the next DXF Options and writes the file (asking for
a path when there is none). Export As also lists DWG, greyed out:
setting its format to dwg shows, and on
confirm answers, the hint to export DXF instead.
DXF Import Options: opening a DXF drawing through the app
(app.open, file.open, a drop) or placing one
(file.place with a .dxf file and no
dxf options, which answers
{"dialog": "dxfImport"}) opens the dxfImport
dialog (mode: open or place;
layouts, units and version
describe the drawing). Its fields are the document.open
dxf options: layout (one of
layouts), fit, unit (a unit name
such as Millimeters) and scale (1 unit =
scale drawing units; ignored with fit),
scaleLineweights, center,
mergeLayers. ui.dialog.confirm opens or places
the drawing (placing keeps the other file.place params in
__place) and remembers fit,
scaleLineweights, center and
mergeLayers for the next drawing; an error (an unknown
layout, art too large at that scale) keeps the dialog open.
Placement Options: the Links panel's flyout (or
ui.placementOptionsDialog {ids?} for agents) opens the
placementOptions dialog for the selected images (fields
ids, preserve:
transforms/bounds/fileDimensions/
fit/fill, align:
topLeft … bottomRight, clip).
ui.dialog.confirm runs links.placementOptions
with them as one undo step. The Links panel
(window.panel {panel: "links"}) shows
links.list and the first selected image's
links.info; links.editOriginal and
links.reveal (Edit › Edit Original, Show in Folder) hand
the linked file to the system and return {path}. Edit
Original opens only a regular file whose extension is a format
document.open reads, and answers a control request with an
error unless the file is a placed document (which opens in a tab).
Package: File → Package… (ui.packageDialog) opens the
package dialog for a saved document (a document never saved
gets a confirm dialog whose OK runs
file.saveAs). Fields: folder (the location),
name (the package folder), copyLinks,
linksFolder, relink, copyFonts,
report. ui.dialog.confirm saves unsaved
changes, runs file.package and then opens a
confirm dialog whose OK runs
file.showPackage {folder}; the web downloads the zip
instead. Document Info's flyout picks a category and Save… runs
docInfo.save {path?, selectionOnly?}.
Slices: through the app, object.slice.options and
object.slice.divide with no params open their dialogs on
the selected slices. sliceOptions (fields
kind:
image/noImage/htmlText,
name, url, target,
message, alt, text,
background:
""/matte/#rrggbb,
hAlign, vAlign; __html says
whether HTML Text applies) runs object.slice.options on
ui.dialog.confirm; divideSlices (fields
divideRows, rows, rowMode:
count/size, rowHeight, and
divideColumns, columns,
columnMode, columnWidth) runs
object.slice.divide. Read slice options with
slice.list.
The Slice tools: ui.pointer drags with the
slice tool make a user slice (Shift square, Alt from the
centre; one undo step). With sliceSelection a click selects
a slice (Shift toggles), a drag moves the selected slices, a drag on a
selected user slice's handle resizes it, a double click opens
sliceOptions and ui.key Delete deletes the
selected slices. Hidden or locked slices (view.slices.hide,
view.slices.lock) can't be picked.
EPS Options: Export As… with the EPS format
(file.exportAs {format: "eps"}, then the save dialog), or
ui.epsOptionsDialog {path?, …document.exportEps options}
for agents, opens the epsOptions dialog. Its fields are
document.export params: previewFormat
(none, tiffBw, tiffColor),
transparentPreview, overprints
(preserve, discard),
flattenerPreset (a built-in or saved preset's name),
includeLinkedFiles, thumbnails,
cmykPostScript, compatibleGradients,
level (2, 3), plus
path and the artboard choice (useArtboards,
range). ui.dialog.confirm checks them (a bad
value or an unknown preset keeps the dialog open), remembers them for
the next EPS Options and writes the file(s), asking for a path when
there is none. With Use Artboards and two or more artboards, it writes a
central file of the whole document under the chosen name and one
{stem}_{artboard}.eps per artboard.
Data Recovery: at launch, copies left behind by a crash open the
recovery dialog (never copies another running instance
keeps, so instances started side by side for screenshots don't get it).
Fields: copies ([{file, title, saved}], from
file.recovery.list) and discard.
ui.dialog.confirm restores them all
(file.recovery.restore each, opening "discard: true then confirm deletes them,
ui.dialog.cancel keeps them for the next launch. While
copies are written in the background ui.inspect lists them
under background ("Saving recovery data for
Native save options: File → Save As to a native or .ai
file opens the saveOptions dialog after the save panel
(heading "Stylo Options" or "PDF-compatible .ai Options"). Fields:
separateArtboards, all (true: every artboard;
false: range, such as "1-3, 5"),
includeLinked, embedProfiles,
pdfCompatible, compress, and for native files
version and preview. A bad range keeps the
dialog open; ui.dialog.confirm writes the file and the
artboards' files. A .ai file's aiVersion comes
first: "pdf" (the default) or "7" (legacy,
heading "Legacy .ai Options"), which takes only
transparentImages ("flatten" or
"omit"), hiddenObjects ("layer",
"omit" or "visible", the Hidden Objects menu),
outlineText (the Text menu: false Editable, true Outlines)
and, with several artboards, the artboard picker: boards,
one bool per artboard (default: those last saved, else all), saved as
range, one file per checked artboard. OK is disabled with
none checked.
EMF and WMF: Export As… lists both formats
(file.exportAs {format: "emf"} or "wmf"); they
have no options dialog, so ui.dialog.confirm asks for the
path and writes the file (one per artboard with Use Artboards). Open and
Place read .emf and .wmf; a paste of
image/emf from a system clipboard service runs
clipboard.importEmf.
TIFF, BMP and Targa Options: Export As… with the TIFF, BMP or Targa
format (file.exportAs {format: "tiff"}, then the save
dialog) opens tiffOptions, bmpOptions or
tgaOptions: the raster rows of PNG Options
(ppi, background, antiAlias),
then TIFF's colorModel (rgb,
cmyk, gray; a CMYK document starts on
cmyk), byteOrder (little,
big), lzw and embedIcc; BMP's
colorModel (rgb, gray),
fileFormat (windows, os2),
depth (1, 4, 8, 16, 24, 32), reduction and
dither (4 and 8 bits), rle and
flipRows; Targa's depth (16, 24, 32). The BMP
dialog keeps its choices writable together (OS/2 falls back to 24 bits
and turns RLE and flipped rows off; RLE turns flipped rows off).
ui.dialog.set the fields and ui.dialog.confirm
writes the file(s) with document.export.
Save for Web: File → Export → Save for Web (Legacy)…
(file.saveForWeb, Ctrl/Cmd+Alt+Shift+S) opens the
saveForWeb dialog on the remembered settings
(webExport.settings). Its fields are the
document.exportForWeb settings (format,
colors, reduction, dither,
quality, colorTable, width,
percent, output…; set preset to
load a preset) plus __view (original,
optimized, 2up, 4up),
__zoom (fit or a percentage),
__kbps and __color (the colour table's
selected colour). ui.dialog.confirm remembers the settings
and saves (a save panel for the image, or the page with HTML output; the
web downloads the files); with discard: true it is Done: it
remembers the settings and closes. file.saveForWeb with
settings saves without the dialog, and
file.saveForWeb.browser writes the HTML page to a temporary
folder and opens it in the default browser (desktop). Both return the
export's warnings, and the status bar shows the first one
as Save As shows a save's notes.
PSD Options: Export As… with the PSD format
(file.exportAs {format: "psd"}, then the save dialog) opens
psdOptions: the raster rows of PNG Options
(ppi, background, antiAlias),
then colorModel (rgb, cmyk,
gray; a CMYK document starts on cmyk),
layers (true: Write Layers; false: Flat Image, whose
background list starts at White), maxEditability and
hiddenLayers (with layers) and embedIcc.
ui.dialog.set the fields and ui.dialog.confirm
writes the file(s) with document.export.
Print: File → Print… (file.print with no params,
Ctrl/Cmd+P) opens the print dialog on the document's print
settings. Its fields are the print.setup settings
(copies, artboards, range,
media, orientation, placement,
scaling, marks, bleed,
output, graphics, color,
margin…; a section is an object, as
{field: "marks", value: {"trim": true}}) plus
printer (a name from print.printers,
"" the system's default) and toFile (save the
job as a PDF), and the UI-only __section (General, Marks
and Bleed, Output, Graphics, Color Management, Advanced, Summary) and
__sheet (the page the preview shows, 0-based).
ui.dialog.confirm (Print) keeps the settings with the
document (print.setup, one undo step) and prints them: to
the printer through the system's print queue (the web: the browser's
print dialog), else, with toFile or no printing available,
a save panel for the PDF. With discard: true it is Done: it
only keeps the settings. Settings that can't print (a missing artboard,
a range of no tiles) answer with an error and keep the dialog open.
file.print {settings?, printer?, toFile?, path?} prints
without the dialog; print.printers lists the printers and
print.printerSetup {printer?} opens their system settings
(Setup…). Desktop submission returns
{background: true, printed: false, printer, status} while
the spooler runs on a worker. ui.inspect lists it under
background; completion or refusal appears in
ui.status. Quit waits for submissions. PDF/PostScript
export and the browser's print dialog retain their existing command
results.
Agents still get
{printers: [{name, default}], service, setup} from
print.printers; discovery failure yields an empty list.
Print replies preserve pages, bytes,
format and warnings alongside the pending
fields above. Validation/start failures use the existing control reply
{ok: false, error: "…"}; later spooler failures appear in
ui.inspect → ui.status, with no second reply
or structured backend error object. Headless MCP keeps its engine-only
file.print result (path or
dataBase64, plus the same print metadata); no spooler
fields are added there.
Print presets: in the print dialog, preset
names the print preset whose settings were loaded last; setting it
(ui.dialog.set {field: "preset", value: "Posters"}) loads
that preset's settings, and the list reads [Custom] once
they differ. Save Preset… shows a name field
(__savePresetAs): while it shows,
ui.dialog.confirm saves the dialog's settings as that
preset (print.presets.save) and selects it instead of
printing. ui.printPresetsDialog {selected?} opens Edit →
Print Presets (dialog printPresets, field
selected); [Default] is protected. New… and
Edit… open the preset editor, which
ui.printPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)}
opens too: dialog printPreset, the Print dialog's settings
fields plus name; ui.dialog.confirm runs
print.presets.save and returns to Print Presets (a new
preset can't take a name in use). Delete, Import… and Export… are
print.presets.delete, print.presets.import and
print.presets.export; ui.dialog.confirm
closes. Opening a .vcprintpresets file with
app.open imports its presets.
Modifiers on synthetic input: ui.key,
ui.click, ui.drag and ui.wheel
take shift, alt, ctrl and
cmd (Command on macOS, Ctrl elsewhere), and the app holds
them for the frames the input spans, as if the keys were down: a key's
press, its text and its release; a click's press and
release; a drag from the press through every move to the release. Every
handler sees them (Shift+arrow nudges by the big increment, Alt+arrow
nudges a copy, Shift-clicking a Layers selection square adds to the
selection); the keyboard's own modifiers apply again afterwards.
Cutting tools: Mirror & Cut (mirrorCut), Line Cut
(lineCut) and Rectangle Cut (rectCut) cut the
selected paths and compound paths. ui.tool.select one, then
drag with ui.pointer: the cut previews live and the release
keeps it as one undo step (ui.key Escape cancels). They run
path.mirrorCut, path.lineCut and
path.rectCut, which an agent can call directly. Mirror
& Cut's tool options (tool.setOption, shown in the
Control bar) are axis (free |
vertical | horizontal; a constrained axis
follows the pointer and a click places it) and keep
(left | right | top |
bottom); Alt on release keeps the other side.
Puppet Warp (puppetWarp): with art selected, the tool
shows pins at once (automatic ones at the centre and the end of each
limb until pins are placed). ui.pointer clicks on the art
add pins (each new pin is selected; Shift-click adds a pin to the
selection or takes it out), dragging a pin moves every selected pin and
warps the art, and Alt-dragging near (not on) a selected pin turns the
art around it. A click off the mesh adds nothing: the cursor shows
not-allowed there and the canvas says so. ui.key Delete or
Backspace removes the selected pins. Adding, deleting and each drag are
one undo step each, running
object.puppetWarp {rest: true, …}; the pins live in the
document, so Undo/Redo take them back with the art, they stay across
tool switches while the selection is unchanged, and another selection
starts afresh. object.puppetWarp.pins reads them. Its tool
options (tool.setOption, shown in the Control bar) are
expand (Expand Mesh, points, 0 allowed),
showMesh (on by default) and selectAllPins
(true selects every pin, false none);
ui.inspect → toolOptions lists the
pins the tool last showed and the selected
ones.
Colour adjustment effects (Effect → Color Adjustments) open the
effect dialog like the other effects
(effect.dialog {effect: "adjust.hueSaturation"}): its
fields are the effect's parameters (sliders for the amounts,
channel for Curves and Levels, points for
Curves, color for Shift to Color) and they preview live;
set them with ui.dialog.set and
ui.dialog.confirm to apply (Curves also has a graph: drag
its points). Object → Vector Halftone…
(engine.execute {command: "ui.menuDialog", params: {command: "object.vectorHalftone"}})
opens the vectorHalftone dialog (fields shape,
frequency, angle, mode,
color, invert, clip,
keepOriginal, preview): it previews live and
ui.dialog.confirm keeps the halftone as one undo step.
Tool options last: tool.setOption {key, value} or
{values: {…}} (and tool to set another tool's)
sets them, and the options a tool keeps (Liquify brush and tool options,
Mirror & Cut, Puppet Warp, Symbolism, drawing tools) stay when you
switch tools or documents and are saved with the preferences. The
Liquify tools share their Global Brush Dimensions (width,
height, angle, intensity).
Perspective grid: View → Perspective Grid → Define Grid…
(ui.perspectiveGridDialog) opens the
perspectiveGrid dialog prefilled from the grid. Its fields
are those of perspective.grid.define (kind,
units, scale, gridline,
angle, distance, horizonHeight,
thirdVp, leftColor, rightColor,
groundColor, opacity; lengths in
units, so setting units converts them).
ui.dialog.confirm runs
perspective.grid.define; a refused value keeps the dialog
open.
Object → Envelope Distort's dialogs open through their menu items or
ui.menuDialog {command}. Make with Warp…
(object.envelope.makeWithWarp) opens
envelopeWarp (fields style,
horizontal, bend, h,
v, preview, and reset); while an
envelope is selected the menu shows Reset with Warp… instead, and the
same item, shortcut or object.envelope.resetWithWarp opens
it with reset: true and the envelope's values. Make with
Mesh… / Reset with Mesh… open envelopeMesh
(rows, cols, reset, and
maintainShape when resetting). Envelope Options…
(object.envelope.options) opens
envelopeOptions (antiAlias,
preserveShape, fidelity,
distortAppearance, distortLinearGradients,
distortPatternFills) with the selected envelope's values,
or with nothing selected the defaults for new envelopes. They preview
live while an envelope is involved, and ui.dialog.confirm
keeps the result as one undo step.
Liquify Tool Options: double-clicking a Liquify tool
(tool.options {tool: "twirl"}) opens a
liquifyOptions dialog. Its fields are tool,
the Global Brush Dimensions width, height
(pt), angle, intensity (%) and
usePressure, then the tool's options detail,
simplify and simplifyOn (Warp, Twirl, Pucker,
Bloat), rate (Twirl), complexity,
affectAnchors, affectIn and
affectOut (Scallop, Crystallize, Wrinkle),
horizontal and vertical (Wrinkle, %), and
showBrush. ui.dialog.confirm runs
tool.setOption {tool, values}. ui.pointer
events take pressure (0..1, default 1): it is the Liquify
intensity while Use Pressure Pen is on.
Freehand Tool Options: double-clicking the Pencil, Paintbrush,
Smooth, Blob Brush or Eraser tool
(tool.options {tool: "pencil"}) opens a
freehandOptions dialog. Its fields are tool
and the options the tool keeps: fidelity (pt; Pencil,
Paintbrush, Smooth), fill (Pencil, Paintbrush),
keepSelected (Pencil, Paintbrush, Blob Brush; on by default
for the Pencil and Paintbrush and off for the Blob Brush, and while it
is off the path a stroke draws, continues or merges into is not selected
afterwards), mergeOnlySelection (Blob Brush; off by
default, and while it is on a stroke merges only with selected blobs),
closeWithin and editWithin (screen pixels, 0
turns it off; Pencil, Paintbrush) and size (pt; Blob Brush,
Eraser). ui.dialog.confirm runs
tool.setOption {tool, values}.
ui.pointer events also take holdMs
(0..60000): the pointer then holds still that long, button down, before
the next event. Twirl, Pucker and Bloat keep applying while held (a
repeat of the last point every 0.1 s), exactly as for the same time held
with the mouse. Liquify leaves type, symbols, images, graphs, meshes,
envelopes, repeats and blends as they are and says so in the status
bar.
Perspective grid presets: in the perspectiveGrid dialog,
name is the preset whose fields are loaded (setting the
fields of a preset, as the Preset menu does, keeps its name; other
changes clear it, which reads [Custom]). Save Preset… sets
__mode: "save" and __from: "define" with
name the new preset's name: ui.dialog.confirm
then saves the fields as that preset
(perspective.presets.save) and returns to Define Grid on
it. View → Perspective Grid → Save Grid as Preset…
(ui.savePerspectivePreset) opens the same dialog in
save mode; OK saves and closes. Edit → Perspective Grid
Presets… (ui.perspectivePresetsDialog {selected?}) opens
dialog perspectiveGridPresets (field
selected): New… and Edit… open the preset editor
(perspectiveGrid with __mode: "edit",
__original the preset edited), whose OK runs
perspective.presets.save and comes back; Delete, Import…
and Export… are perspective.presets.delete,
import and export. View → Perspective Grid →
One/Two/Three Point Perspective list that type's built-in views and the
saved presets
(ui.perspectiveUserPreset<type>.<n>, hidden
while empty). Opening a .vcperspective file with
app.open imports its presets.
Blend Options (Object › Blend › Blend Options…,
ui.blendOptions, the Blend tool's double-click, Alt-click
and toolbar button) opens the blendOptions dialog on the
selected blend's options: spacing (smooth |
steps | distance), steps,
distance (pt), orientation (page
| path) and preview; it previews live and
ui.dialog.confirm keeps the change as one undo step. With
no blend selected (__target: "defaults") OK sets what new
blends start with. The Blend tool
(ui.tool.select {tool: "blend"}): ui.pointer
down on an object, then on another, blends them; down on an anchor point
blends from that point; each further object clicked joins the blend. A
selected blend shows its spine. With Direct Selection
(directSelection) a ui.pointer drag from a
spine point moves it (object.blend.spine.moveAnchor, one
undo step) and, once a point is clicked, from one of its handles
reshapes the curve, and a drag on a curved spine segment bends it
(object.blend.spine.reshapeSegment); the Anchor Point tool
(anchorPoint) bends any segment of a selected blend's
spine, straight ones too. Direct and Group Selection pick a blend's key
objects. With the Pen tool a click on a selected blend's spine adds a
point (on a point no key sits on, deletes it).
Perspective grid view options: View → Perspective Grid lists
Show/Hide Grid, Show/Hide Rulers, Snap to Grid (checked), Lock/Unlock
Grid and Lock Station Point (checked); ui.menu.list reports
the current labels. They run perspective.grid.show,
rulers, snap, lock and
lockStation.
Perspective grid widgets: the Plane Switching Widget is fixed in a
corner of the canvas (ui.pointer with
space: "screen" reaches it at the same pixels whatever the
zoom); a press on it with any tool picks the plane while the grid shows.
ui.key with key
"1"…"4" picks the left, horizontal, right or
no plane. Double-clicking the Perspective Grid tool
(tool.options {tool: "perspectiveGrid"}) opens
perspectiveGridOptions (fields show,
position: topLeft, topRight,
bottomLeft, bottomRight);
ui.dialog.confirm runs
perspective.widget.options.
Envelopes on the canvas: pointer_gesture with the Mesh
tool (mesh) or Direct Selection on a selected mesh envelope
drags its points (a press on a point focuses it, and a press on one of
the focused point's handle ends drags that handle); a Mesh tool click
inside a mesh envelope adds a row and a column. While an envelope or its
content is selected the Control bar shows its controls (Edit Envelope /
Edit Contents, warp style, orientation, bend and distortions or mesh
rows and columns, Reset, Envelope Options); each runs an
object.envelope.* command.
Shared panel docking
Panel arrangements support tab grouping and reordering, horizontal and vertical splits, and movable, resizable floating groups inside the application window. Drag a panel tab, or use its context menu. Floating groups remain within the application viewport.
The shared layout begins with the existing workspace on the first docking operation. Workspace saves retain its groups, active tabs, split sizes, floating geometry, and the positions of hidden panels. Resetting a built-in workspace restores its default dock.
window.panel.move {panel, anchor, zone?, before?}moves a panel into the target group.zoneistab(default),center,left,right,top, orbottom;beforereorders tabs by panel ID. The destination must already be visible.window.panel.float {panel, x?, y?, width?, height?}detaches a panel.window.panel.dock {panel, anchor?, zone?, before?}returns a panel to its previous dock placement when possible, or to the supplied destination.window.panel.activate {panel}reveals and selects a panel, restoring its saved position when possible.window.panel.close {panel}hides it.window.panel.collapse {panel, collapsed?}shows the dock group holding a panel as its tabs only, as a double-click on a tab does (withoutcollapsedit toggles), and makes the panel the group's shown one; showing another of its panels expands the group. Only a group that is the whole dock or sits above or below another collapses (one beside another is refused). Floating groups collapse withwindow.panel.float {panel, collapsed}. Returns{panel, collapsed}.
Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and invalid destinations return an error without changing the arrangement.
window.panel.layout {action} also accepts the shared,
externally tagged action schema. For example,
{"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}}
updates a floating group's position and size;
{"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}}
resizes the root divider (false/true path
entries select first/second children). Move with
placement: {"Tab":{"before":"swatches"}} reorders tabs, and
SetStackOpen {panel,open} /
ResizeStack {panel,height} control accordion entries. The
UI dispatches these same commands. Unknown panel IDs, invalid geometry,
and stale split paths return an error without changing the
workspace.
Synthetic input hardening checklist
Transport hardening checklist
In the browser:
window.stylo
The web build has no TCP server (a page can't listen on a socket), so
it offers the same methods to the page itself, as promises on
window.stylo (apps/stylo-web/src/api.rs). An
agent driving Stylo in a browser tab, or a host app showing it in an
<iframe>, uses the commands it would send to the
desktop app:
| Call | |
|---|---|
stylo.request(method, params) |
any control method of this document → its result |
stylo.execute(command, params) |
engine.execute |
stylo.commands() / stylo.inspect() |
engine.commands / the inspect method |
stylo.screenshot(params) |
ui.screenshot (no path: the PNG comes back
inline) |
stylo.openFile(name, bytes) |
opens the bytes the way a dropped file opens (returns
true once queued) |
stylo.captureDownloads(on) |
while on, a save or export is kept in the page instead of downloaded |
stylo.files() / stylo.readFile(name) |
the kept files ({path, size}) / one of them as a
Uint8Array |
Errors reject with an Error carrying the method's
message. Requests run on the UI's next frame, so a hidden tab (whose
frames the browser pauses) answers when it is shown again.
Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-23 | minor | Documented the symbol library panel commands
(window.symbolLibrary, .other,
ui.saveSymbolLibrary,
window.userSymbolLibrary1…10). |
| 2026-10-13 | minor | Documented count, pending-event and per-frame request budgets (#1709). |
| 2026-10-13 | minor | Documented transport deadlines and connection-slot recovery (#1726). |
| 2026-10-18 | minor | Documented window.stylo, the web build's control
channel. |