<< All versions

Skill v1.0.0

currentAutomated scan100/100
freefallingsnow/snowdesktop/snowdesktop-lua-widget
──Details
PublishedNovember 1, 2026 at 02:31 AM
Content Hashsha256:13e6602544a4d105...
Git SHA81e4e4f81d1d
──Files
Files (1 file, 23.2 KB)
SKILL.md23.2 KBactive
SKILL.md · 442 lines · 23.2 KB

name: snowdesktop-lua-widget description: Create, modify, debug, visually review, validate, package, and publish SnowDesktop API v2 Lua desktop widget packages. Use for folders containing widget.json and main.lua, including callable performance evaluation, responsive layout, host-owned materials, foreground themes, localization, resources, settings, data, interactions, previews, and the snowwidget CLI. metadata: version: "1.0.0"


SnowDesktop Lua Widget

Build one validated package directory, never a loose Lua file and manifest:

text
my-widget/
├── widget.json
├── main.lua
├── assets/
├── modules/
├── tests/
└── LICENSE

Only add optional directories when the widget needs them. Built-in widgets live under the executable widgets directory. Installed and development packages live under data\widgets\installed and data\widgets\dev. Layouts retain the immutable package UUID, so never change an existing package ID.

Confirm the development workspace

For ordinary user-authored widget work, confirm before writing that the target package is inside the development widget directory the user opened from SnowDesktop. A random current directory, downloaded package, installed package or application directory is not an authorized development workspace merely because it contains widget.json.

If the current workspace is not the development widget directory, or that relationship cannot be established, do not create or modify the package there. Tell the user to open SnowDesktop Settings → Widgets → Component Development Tools → Open Development Widget Directory (设置 → 组件 → 组件开发工具 → 打开开发组件目录), then add or open that directory in the coding client and authorize it for the workspace. Resume after the development directory is available. Never edit data\widgets\installed as a substitute.

An explicit SnowDesktop source-repository maintenance task is the exception: built-in packages and this distributed Skill may be edited in an already authorized repository workspace.

Start with the tool contract

Resolve this Skill directory and run:

powershell
bin\snowwidget.exe capabilities

Treat its JSON as the available authoring contract. Only versions listed under executableSchemaVersions and executableApiVersions can run. Use the bundled CLI beside this Skill; a copied CLI may need --host <SnowDesktop.exe> or SNOWDESKTOP_HOST for previews.

Do not load the complete API manual by default. Select the smallest relevant source of truth:

NeedUse
Callable Lua functions and permissionssnowwidget api-contract
Declarative nodes, properties, events and limitssnowwidget view-contract
Data topics, tasks and their typed argumentssnowwidget system-contract
Function signatures for the editorlibrary/snowdesktop-v2.lua
Detailed behavior for one selected featureSearch the matching heading in references/api-v2.md
Visual ownership, themes and preview reviewreferences/visual-authoring.md
Evaluate component CPU, allocations, timers, rendering or idle costreferences/performance-evaluation.md; check snowwidget capabilities first
Steam Workshop planning and publishingreferences/workshop-publishing.md
Suspected host/API defect or contract mismatchreferences/api-troubleshooting.md, then the matching implementation in the official source

Classify before creating

For a new widget, infer a compact internal brief before writing code:

  • the user's primary task and the information that must be visible first;
  • recommended starting spans, and whether the content has a real functional

reason to restrict minimum or maximum spans;

  • ready, empty, loading, error and permission-denied states that are relevant;
  • the smallest useful interaction set and any trusted-gesture requirements;
  • data sources, permissions, persistent settings and supported locales;
  • a recognizable visual direction that still respects the host surface.

Choose one rendering path:

  • Prefer a declarative view for text, controls, lists, forms, ordinary layout,

accessibility and host-rendered interaction states.

  • Use immediate render for clocks, custom charts, freeform geometry and other

visuals whose main value depends on a canvas.

  • Do not mix the two desktop rendering paths. A descriptor must expose exactly

one local view or render function.

Choose the sizing model before writing geometry:

  • Proportional visual model — the default for visually composed widgets.

Use it for artwork, media players, clocks, instruments, custom controls and other components whose parts form one visual composition. Read layout.contentWidth() and layout.contentHeight(), and derive every visible length from width, height, vw, vh or vmin. Choose structural variants from the aspect ratio only when the content relationship benefits from a different arrangement. A vertically composed instrument may keep one structure at every ratio, while a spectrum may declare a horizontal-only grid range. Do not invent horizontal or vertical branches merely because the widget is resizable. If two surfaces have the same aspect ratio and one is scaled by k, every geometric metric should also scale by k. Avoid sizeClass, grid-span breakpoints, absolute minima and scale caps in this model. Recalculate and center the group of children that are actually visible when optional content disappears.

  • **Proportional information model — the default for desktop information

tools.** Use ui.metrics().layoutRowHeight as the shared semantic row unit. It is the visible content height of a search field, input, ordinary button or single-line item; exterior insets and gaps are not included. Use 1x for those controls and items. Derive component-owned fonts, spacing, icons, radii and strokes as stable proportions of that row height, then add the derived gaps separately. Multi-line body items use an explicit multiple of the same row unit. The host keeps the row unit at the page baseline for one- and two-row widgets, then grows it slowly with vertical span. All component-derived metrics grow with it. Horizontal span never changes them, so components with the same row span stay aligned. Use layout.rpxX() and layout.rpxY() only for component-specific axis geometry, and never derive vertical controls or rows from width. Require ui.semanticMetrics.rowUnit and use layoutRowHeight. Use layout.rpx() or vmin for isotropic decoration.

  • **Grid-density model — only for content that explicitly aligns to host grid

metrics.** Use layout.cu() and layout.fontCu() for a system-status card matrix or another component whose information units intentionally track the host grid. Grid metrics and size classes may drive those card boundaries.

  • Mix the models only for an intentional fixed-density overlay. cu and

fontCu do not grow linearly with the whole widget, so using them for a proportional component's main geometry breaks same-ratio rendering.

For a proportional plan, prefer layout.rpxX(value) and layout.rpxY(value) when axis-specific values come from a design at the manifest default span. Use layout.rpx(value) for isotropic artwork. Homogeneous formulas such as short * 0.08, width * 0.3 and math.min(width * 0.4, height * 0.6) are also appropriate. Constants used only as ratios are fine. Output clamps such as math.max(layout.cu(40), ...), fixed pixel floors, or a capped global scale make two equal-aspect surfaces render differently and should be avoided.

In the proportional information model, use ui.metrics().layoutRowHeight as the only host-owned visual scale. Derive ordinary body text, for example, as row * 12 / 28, captions as row * 10 / 28, and component spacing from the same ratio. A component font preference is a percentage multiplier over the derived font. The row already includes page and accessibility scaling; do not apply a second span scale. In the proportional visual model, derive text and geometry from rpx, the surface axes or vmin so equal-aspect surfaces remain fully proportional. Read surface-dependent layout metrics in view or render; do not cache them in setup, because later resizes can differ from the initial load-time surface.

Choose text and icon alignment according to each element's role and the intended composition. Declarative text defaults to textAlign="start" and verticalAlign="center"; do not assume a control label is horizontally centered. Set alignment explicitly when the intended placement differs from the default, and check the rendered label together with its padding and any icon. This is an alignment reminder, not a requirement to center every control.

Create a package

  1. For a new widget, use the bundled deterministic initializer rather than

rewriting the starter by hand:

``powershell & scripts\init_widget.ps1 -Destination <directory> -Slug <lowercase-hyphenated-slug> -Name <name> -Description <description> -Author <author> ``

It refuses an existing destination, generates a new UUID, replaces the localization prefix and retains one honest en-US starter locale. Add only translations you can supply reliably. Do not use it when editing an existing package; preserve that package's UUID.

  1. Replace all starter content, preview copy and example identity that remain.
  2. Keep schemaVersion and apiVersion at 2. Use SemVer for version and a

positive dataVersion.

  1. Replace starter content with a coherent first product slice. Do not leave

example names, placeholder copy, empty starter cards or unused settings.

  1. Keep only locales the package actually supports with reliable translations.

Put every visible string behind a literal l10n.tr("key"), and include every used key in every declared locale. Component text belongs in widget.json, never in the host lang/ directory.

  1. Add only features and permissions actually used. Put degradable features and

permissions in the optional arrays and probe before use.

  1. Declare package images and fonts under resources. Create handles at entry

scope and pass handles to v2 APIs. Load modules only with module.require("modules/name.lua") while the entry script loads.

  1. Use state for VM-lifetime JSON-like values and storage for persistent

values. Write persistent state only when it changes and never from a render or view callback.

  1. When a host settings change affects an active subscription or a

descriptor-level visual such as backgroundLayer.blurRadius, require settings.changeEvent and reconcile it from event.kind == "settings.changed". The event covers ordinary values, host appearance, passwords, filesystem handles and entity references, and supplies sorted affected keys. preview == true identifies an uncommitted live preview; immediate writes and preview commit or cancellation emit the current persisted or restored state with preview == false. Calls to storage.set from the component do not emit this host-settings event. Keep render and view callbacks free of subscription creation or descriptor mutation.

Surface ownership and theme contract

The SnowDesktop host owns the outer shape, material, border and selected state. By default:

  • Let the host draw the component material, rounded shape, outer border,

gradient, glass or acrylic through widget.define style fields.

  • Set useCustomStyle = true when supplying fallback style values and set

followPersonalizationDefault = true unless the user's concept explicitly requires an independent material.

  • Use backgroundLayer={render=...} with required feature

widget.backgroundLayer when a component deliberately needs images, color blocks, gradients or paths above the host material tint and below foreground content. The callback is decorative, desktop-only and cannot register interactions or native marquees. Full-surface drawing is expected inside this callback.

  • Do not draw a full-surface draw.rect, draw.gradientRect, image or view

background in the foreground callback to imitate the host material. Draw only content and intentional internal surfaces. Full-canvas foreground artwork remains an exception for a canvas-led widget such as a clock face; document it with -- snowwidget: allow-full-surface-content.

  • Treat material/background and foreground theme as independent inputs. Never

infer foreground colors from background RGB, luminance, wallpaper, alpha or a material name.

  • In declarative views, require view.theme.tokens and use semantic foreground

tokens such as textPrimary, textSecondary, textDisabled and border.

  • In immediate drawing, derive the foreground palette from

widget.theme().contentTheme: 0 selects light/white foregrounds and 1 selects dark/black foregrounds. widget.context().theme.mode == "dark" likewise means light foregrounds, while "light" means dark foregrounds.

  • Preserve the host preset defaults exactly: dark, glass-dark,

glass-light and acrylic-dark resolve to light/white foregrounds; light and acrylic-light resolve to dark/black foregrounds. In particular, the default glass-light pairing still uses light text. Do not infer this mapping from the appearance name suffix; consume the resolved theme.

  • Keep ellipsis for ordinary labels. For a short timer, amount or counter that

must stay complete, set overflowText="clip", use width="auto" with a calculated minWidth, and set flexShrink=0 on it and any shrinking parent column. Reallocate the finite surface width from adjacent content; minWidth cannot resize the widget window.

  • For rounded bitmap content in immediate mode, declare and probe

draw.imageFit.roundedClip, then pass cornerRadius after originY. Drawing a rounded border alone does not clip the bitmap corners.

  • If the widget offers a custom foreground setting, use it only when the widget

is not following personalization. A component appearance preset may declare __contentTheme = 0 (light/white foreground) or 1 (dark/black foreground) in its values so host titles and handles use the same foreground. Never read or persist this host-owned key from component Lua.

Read references/visual-authoring.md before implementing custom materials, foreground choices, internal surfaces or final preview images.

Required entry shape

This immediate-mode example deliberately lets the host own the outer surface and resolves content colors independently:

lua
local function foregroundPalette()
local theme = widget.theme()
local darkForeground = theme and theme.contentTheme == 1
return {
primary = darkForeground and 0x111827 or 0xFFFFFF,
secondary = darkForeground and 0x4B5563 or 0xD1D5DB,
}
end
local function render(_context, _model)
local colors = foregroundPalette()
local width = layout.contentWidth()
local height = layout.contentHeight()
local vertical = height > width * 1.05
local padding = math.min(width * 0.06, height * 0.10)
local fontSize = math.min(width * (vertical and 0.10 or 0.06),
height * (vertical and 0.06 or 0.12))
draw.text(padding, padding,
l10n.tr("lua_widget.my_widget.hello"),
fontSize, colors.primary, width - padding * 2)
end
return widget.define({
name = l10n.tr("lua_widget.my_widget.name"),
useCustomStyle = true,
followPersonalizationDefault = true,
bg = 0x18202A,
border = 0xFFFFFF,
alpha = 0.42,
borderAlpha = 0.18,
gradientEndA = 0.28,
render = render,
})

Do not define removed globals such as top-level render, onClick, getContextMenu, imguiRender or onHttpResponse. Optional setup(context) returns the model passed to view/render, event and dispose. Keep rendering callbacks hot-path safe: do not create resources, load modules, start future queries, write storage or rebuild invariant data on every frame.

Manifest boundaries

  • id is an immutable UUID; version is SemVer; dataVersion is positive.
  • name and description are English fallbacks. Localized metadata uses

nameKey, descriptionKey and manifest locales.

  • previewData.variants may offer up to four user-visible starting sizes in

the add-component preview. Its storage remains isolated preview data and is never copied into a new instance, so do not use it to imply a persistent mode or setting that the added component will not receive.

  • Keep explicit default, preview and finite boundary dimensions from 1 through
  1. Omit minSize to use the host's 1 × 1 default. Omit maxSize, or use a

schema-permitted maximum dimension of 0, when the widget has no functional upper bound. Recommended preview variants do not need to restrict resizing.

  • Keep permissions empty unless a documented guarded API needs one. Reserved

vocabulary does not make an API available.

  • Native helpers are limited to declared, hash-matched .exe files with

native.execute permission and a required task.native.request (one request) or task.native.watch (resident stream) feature. Provide all four localized consent explanations and require the host's strong user authorization before execution. Lua remains sandboxed; use only the fixed helper protocol described under "Native EXE helpers" in references/api-v2.md. A supporting host must be published before a component requires this feature.

  • Native protocol output must use explicit UTF-8 bytes and flush each frame.

Keep logs, configuration and caches outside the package (for example in LocalAppData or the system temporary directory); any package write can stop a watch and invalidate authorization. Read the language examples and optional task.complete.diagnostic metadata in references/api-v2.md.

  • Never include component DLLs or undeclared executables. Do not use shell

commands or scripts as native helpers.

  • Never include absolute paths, parent traversal, symlinks, junctions, reparse

points or files outside the package.

Build, preview and verify

Use the lowest-cost checks first:

  1. Run snowwidget lint <directory> and resolve every error. Review warnings;

do not suppress them without understanding the resulting behavior.

  1. Run snowwidget permissions <directory> when the package declares a

permission, data topic or task.

  1. Run snowwidget test <directory> only when the package contains meaningful

pure Lua tests. Tests are for behavior and logic, not formatting or copied implementation details.

  1. Run snowwidget preview <directory> <output.png> at the default span and at

every materially different aspect ratio or density state. For proportional visual widgets, also preview two different sizes with the same aspect ratio and verify that all geometry scales by the same factor. Exercise relevant DPI, locale, appearance, foreground theme, data state and storage values.

  1. Open and visually inspect every PNG. A JSON result with ok=true proves only

that rendering completed. It does not prove readable contrast, correct hierarchy, unclipped content or a useful design. Fix defects, render again and repeat until the images pass.

  1. Generate the final catalog image inside the package, then declare its

relative path as manifest preview.

  1. Run snowwidget quality <directory>. It combines package validation and

source lint and fails on unresolved warnings, including a missing preview, hard-coded UI text and suspicious full-surface background drawing.

  1. Run snowwidget pack <directory> <name.snowwidget>, then validate the packed

archive as well.

The first meaningful preview must already show the widget's purpose, intended visual direction, representative content and primary affordance. Reject an untouched starter, placeholder copy, blank surface or loading-only handoff.

For an ordinary component that follows personalization, preview the supported host appearances with followPersonalization=1. Include glass-light with its default light foreground and acrylic-light with its default dark foreground; they intentionally differ. If the component exposes an independent foreground selector, also verify both foreground choices with personalization disabled:

powershell
bin\snowwidget.exe preview <directory> <light-fg.png> `
--appearance acrylic-light `
--storage followPersonalization=0 `
--storage __contentTheme=0
bin\snowwidget.exe preview <directory> <dark-fg.png> `
--appearance acrylic-light `
--storage followPersonalization=0 `
--storage __contentTheme=1

Repeat the corresponding cases for dark material when the widget supports it. Check primary, secondary, disabled, icon, stroke and control text. Use --data-state for relevant ready, empty, loading, error, stale and permission-denied envelopes. For a square Workshop image, preserve the real component span and use --canvas-size 512 --padding 48 rather than changing the widget to a square layout.

In the SnowDesktop repository, a Lua-package-only change uses component-level lint, tests, validation, preview and packaging. Do not build the native host unless the change crosses the package boundary. Use scripts\widget-dev.bat <directory> when an observable desktop hot-reload check is required.

Do not claim pointer interaction, context menus, declarative events, multi-monitor DPI or permission UX is verified from lint, validation or build alone. Those require an observable runtime check or explicit user validation.

Evaluate performance when requested

Use snowwidget profile <directory> <new-output-directory> for a user-requested performance review or a demonstrated performance concern. Read references/performance-evaluation.md for workload selection, artifacts and metric boundaries. This runs the real isolated preview engine and native Perfetto; it does not require a performance control in Settings. Check the performanceProfile capability and the returned report quality before drawing conclusions. Non-default module filters or input sampling also require the corresponding capabilities and confirmation in the actual result. Ordinary previews remain static and do not start tracing.

Diagnose suspected API problems

The official SnowDesktop source at <https://github.com/FreeFallingSnow/SnowDesktop> is an implementation reference. Use it when the installed CLI contracts, bundled API documentation and observed host behavior appear inconsistent. Match the source version to the user's installed SnowDesktop version when possible; the default branch may describe a newer implementation.

Read references/api-troubleshooting.md before inventing a workaround. If a minimal reproduction shows that the documented, advertised and authorized API still fails, tell the user that the evidence points to a SnowDesktop API issue and prompt them to report it at <https://github.com/FreeFallingSnow/SnowDesktop/issues>. Provide the version, capability output, minimal package or code, reproduction steps, expected and actual behavior, and relevant snowwidget/host errors. Do not silently replace the API with undocumented globals, filesystem access or a misleading mock.

Publish only on request

Package creation and external publishing are separate. When the user asks to publish or update a Steam Workshop item, read references/workshop-publishing.md, generate a plan first, show the exact plan, and obtain approval immediately before the external mutation.

All versions