| name | snowdesktop-lua-widget |
| description | Create, modify, debug, and package Lua desktop widgets for SnowDesktop. Use when writing a new `.lua` widget and matching `.widget.json` manifest, adding drawing or mouse behavior, using widget storage and settings UI, querying desktop items, declaring permissions, or diagnosing a SnowDesktop Lua widget that does not load or render. |
SnowDesktop Lua Widget
Create widgets against SnowDesktop's built-in sandboxed Lua API. Keep the script and manifest names paired:
widgets/
├── my_widget.lua
└── my_widget.widget.json
Place runnable .lua files directly in the executable's widgets directory. SnowDesktop only discovers widgets\*.lua; it does not scan subdirectories.
Workflow
- Copy assets/widget-template.lua and assets/widget-template.widget.json into the root
widgets directory.
- Rename both files to the same stem, using ASCII
snake_case for predictable paths.
- Implement
render() first using local widget coordinates starting at (0, 0).
- Add only the callbacks required by the behavior.
- Declare every privileged API in the manifest. Keep unused permissions out.
- Store persistent values as strings and parse them with
tonumber or explicit boolean conversion.
- Test at multiple widget spans. Derive layout from
layout.width() and layout.height() instead of assuming pixels.
- Check hot reload after saving. If a render error invalidates the widget, refresh/re-add it after correcting the script.
Read references/api.md whenever using callbacks, permissions, drawing arguments, desktop integration, settings controls, or troubleshooting.
Required structure
Every script should define:
name = "组件名称"
function render()
local w = layout.width()
local pad = layout.cu(12)
draw.text(pad, pad, "Hello", layout.cu(14), 0xFFFFFF, w - pad * 2)
end
Use these optional top-level flags and appearance globals:
useCustomStyle = true: enable Lua-controlled background appearance and the
host's unified 外观 settings panel for this widget.
showTitle = true: show the host title and enable host rename actions. When
false or omitted, the host hides 重命名 and ignores F2 for the widget.
bottomBarHover = false: keep the bottom bar from using the default hover-only behavior.
bg, border: 0xRRGGBB.
alpha, borderAlpha, gradientEndA, shadowAlpha, highlightAlpha,
noiseAlpha: decimal values from 0.0 to 1.0.
shadowBlur, shadowOffsetY: design-unit values converted through the host
component scale.
For useCustomStyle widgets, prefer declarative settings.presets for visual
presets and settings.fields for behavior. Presets should stay appearance-only:
put colors, alpha, shadow, highlight, noise, and followPersonalization there;
keep data sources, intervals, toggles, durations, and other behavior in fields.
Set followPersonalization = true in the default preset when the widget should
initially follow global personalization. Keep it false for widgets that should
own their style by default.
Manifest
Create a matching manifest even when no permission is needed:
{
"name": "组件名称",
"version": "1.0.0",
"description": "一句话说明组件用途。",
"defaultSize": { "columns": 1, "rows": 1 },
"minSize": { "columns": 1, "rows": 1 },
"maxSize": { "columns": 4, "rows": 3 },
"permissions": []
}
minSize and maxSize are optional. When omitted, the widget has no declared
size restriction beyond the desktop grid itself (effective minimum 1 x 1).
Each maxSize dimension may also be 0 to mean unrestricted.
Valid permissions:
ui.input: expose imgui and support settings-editor controls.
ui.contextMenu: enable getContextMenu() and onMenu(id).
desktop.read: enable desktop queries and draw.icon.
desktop.action: enable open, reveal, and desktop refresh actions.
system.read: enable cached CPU, memory, battery, and network snapshots.
media.read: read the current Windows media session.
media.action: play/pause, skip next, and skip previous.
network.http: enable asynchronous HTTP requests to networkDomains.
Keep defaultSize.columns and defaultSize.rows between 1 and 8.
Implementation rules
- SnowDesktop uses a design unit system where
layout.cu(14) converts grid-cell-relative
design values to DPI-scaled pixels. Prefer layout.cu() over hardcoded pixel values so widgets
scale correctly across monitors and DPI settings. See references/api.md for the full layout API.
- Treat
render() as a hot path. Do not write storage or perform desktop queries repeatedly unless necessary.
- Use
storage.set only when a value changes; it persists immediately to disk.
- Use
draw.measureText for centering or fitting text.
- Use
maxWidth with singleLine = true to get single-line ellipsis.
- Treat the bottom 24px as a host-reserved move/resize area. Do not place clickable
controls there, even when the bottom bar is visually hidden or hover-only.
- Pass image paths relative to the root
widgets directory. Absolute paths are rejected.
- Pass desktop item tables directly to
draw.icon, desktop.open, or desktop.reveal.
- Add
ui.input before defining imguiRender; otherwise imgui is absent from the sandbox.
- Add
ui.contextMenu before defining custom menu callbacks; otherwise the host ignores them.
- Context-menu items may set
icon to a Font Awesome 6 Free Solid glyph, for
example { id = 1, label = "刷新", icon = "" }. Leave it out for no icon.
- To unlock the debug page, open 设置 → 关于 and click the version number
five times. Then open 调试 → Font Awesome 图标字符; clicking an icon copies
it to the clipboard.
- Use
imguiRender() for the host 详细设置 panel.
- Prefer declarative manifest
settings for simple text, bool, integer, float,
select, and color fields; keep imguiRender() for custom editors.
- The host wraps
imguiRender() in a scrollable editor area. For
useCustomStyle widgets, 恢复默认主题 only restores appearance keys, while
恢复默认设置 restores declarative fields.
- Lua scripts may also declare
settings = { presets = {...}, fields = {...} }
directly. The host merges manifest and script declarations into the same
settings panel.
- Do not expose
cornerRadius or barHeight as Lua settings or preset values.
They are host-owned layout settings; read them through widget.theme() and
layout.barHeight() only when alignment requires it.
- Use
widget.setTimer() instead of frame-count timing. Stop unnecessary timers
in onHidden() and restart them in onVisible().
- Use
ui.button, ui.toggle, ui.textInput, ui.progress, ui.scrollArea,
and ui.virtualList when host-managed interaction or scrolling is sufficient.
ui.textInput is Direct2D-rendered and transparent like the desktop file
search field; do not layer a native text editor over it.
- Use
onSelected() when a widget should react as soon as the desktop selects
it. For search-oriented widgets, call ui.focusInput(id) there so the
host-rendered input is ready for typing immediately.
- Never call
http.request() unconditionally from render(). Start requests
from lifecycle, timer, menu, or UI callbacks and consume them in
onHttpResponse. Redirect targets must also be declared in networkDomains.
- Do not use
io, os, require, package, load, or arbitrary filesystem/process APIs. They are not exposed.
- Keep colors in
0xRRGGBB; pass opacity separately where supported.
- Log recoverable diagnostics with
widget.log("info"|"warn"|"error"|"debug", message).
Verification
For repository development:
- Save the files under the source
widgets directory.
- Run
build.bat; CMake copies the complete directory recursively to .build\Release\widgets.
- In SnowDesktop, right-click the desktop and choose 添加组件, then select the manifest display name.
- Exercise click, double-click, wheel, editor, and context-menu behavior as applicable.
- Build Release before delivery. The release process copies the complete built
widgets tree, including this skill and its resources, into release\widgets.
Before finishing, verify:
- Script and manifest stems match.
- The script sits directly under
widgets.
- JSON is valid UTF-8.
defaultSize falls within any declared minSize / maxSize.
- Every used privileged API has its permission.
- Every HTTP hostname is present in
networkDomains.
- Timers and HTTP requests are not started repeatedly from
render().
render() works at the manifest's default span and at a resized span.
- No storage write occurs unconditionally on every frame.