Skip to content
Scripted Decorations

Scripted Decorations

A Scripted Decoration exposes a small set of objects and functions rather than arbitrary Qt, QML, Plasma, or TaskManSync objects.

Unlike a Static Decoration, it runs Lua to compute its presentation. Its taskbar output is called the Taskbar Decoration; an optional separate Script-Created Window belongs to the same script. Neither surface lets Lua manipulate other applications’ task items. See Getting Started to create and place a definition.

The main object is decoration:

decoration.text = "CPU: {bld}18%{}"
decoration.backgroundColor = "#202020"
decoration.widthMode = "Content"

The commonly used properties are:

PropertyPurposeValues / syntax
decoration.textMain Decoration textTDML string
decoration.backgroundColorBackground of the whole Decoration surfaceColor string, such as #RRGGBB or #RRGGBBAA; empty string removes the override
decoration.widthModeRequested mode; effective mode can fall back"Content", "Fixed", "Flexible"
decoration.fixedWidthRequested extent along the taskbar flow when using Fixed modeInteger 1..4096 logical pixels; assignment selects Fixed
decoration.geometryRealized surface sizeRead-only table; nil until a valid surface exists
decoration.intervalRequests periodic update opportunitiesInteger 50..2147483647 ms; set during top-level initialization only

Content, Fixed, and Flexible are Scripted Decoration requested layout choices. They do not determine the Decoration’s position in the task order. Content measures parsed whole-Decoration text only; positioned text and graphics are excluded. Visual-only output falls back to effective Flexible with a warning, while genuinely empty output remains Content.

Flexible Decorations share the remaining taskbar layout space equally with other participating Flexible Decorations. A Fixed extent follows the taskbar flow, so it is the height on a vertical taskbar. Your drawing still uses the realized surface’s width and height, not the requested flow extent.

There is a precedence to the scripted width controls:

  • decoration.fixedWidth = value selects Fixed mode automatically. You do not need to assign decoration.widthMode = "Fixed" separately.
  • A later decoration.widthMode = "Flexible" changes the requested mode while retaining the previously assigned fixed width for a later return to Fixed.
  • Content is the default when the script does not assign widthMode.
  • decoration.widthMode accepts Content, Fixed, and Flexible; unsupported values raise a script diagnostic and leave the previous requested mode unchanged.

decoration.widthMode describes the requested mode. The immutable presentation snapshot carries the effective mode consumed by layout; they match except for the visual-only Content fallback described above. Positioned or animated graphics bounds are not measured.

decoration.geometry reports the realized Decoration surface, not the requested width or the size of the taskbar. Its values are logical pixels:

local geometry = decoration.geometry
if geometry then
    log.info(geometry.width .. " x " .. geometry.height)
end

The geometry table is a copy; changing it cannot resize the surface. Defer geometry-dependent calculations while it is nil. Later surface-size changes request a coalesced onTick() opportunity, folded into the next periodic tick when periodic updates are enabled. Initial geometry does not itself trigger an initial tick.

decoration.text uses TDML, not HTML. Plain text is valid, but dynamically inserted literal text containing TDML delimiters must use the TDML escape rules. Absolute size/sz values use logical pixels; szr is a positive factor of the internal Decoration height, not percentage syntax. Root-level posh and posv position the whole text presentation inside the surface; they do not move the taskbar item. offseth/osh and offsetv/osv offset a logical line.

A non-empty decoration.backgroundColor overrides the whole surface. Clearing it restores TDML dcolor/dc, then the definition and Theme fallbacks. TDML bgc colors only its text run. Successful background values read back as canonical #RRGGBBAA; invalid non-empty colors raise a script diagnostic and preserve the previous value. These runtime changes do not save new definition settings.

Updating a Decoration

For an immediate additional update opportunity, use:

requestUpdate()

This is coalesced: multiple requests can result in one pending onTick() opportunity. It does not recursively call onTick() and does not promise immediate rendering.

A periodic update can be requested during top-level initialization. This clock publishes immediately and refreshes once per requested second:

decoration.interval = 1000

local function updateClock()
    local fields = time.localDateTime(time.now())
    decoration.text = string.format("%02d:%02d:%02d", fields.hour, fields.minute, fields.second)
end

function onLoad()
    updateClock()
end

function onTick()
    updateClock()
end

The value is a requested interval, not a Lua-owned timer. The applet that runs the script schedules serialized, coalesced opportunities; other applets render its shared output. onTick(frameTimeSeconds) receives elapsed seconds for periodic or animated Window frame opportunities, but an explicit requestUpdate() supplies no argument. Check for nil before using the argument for animation. Missed ticks are not queued for catch-up, and callbacks should do bounded work rather than loops that wait for time to pass.

Pending work is cancelled when the runtime is stopped, reloaded, removed, faulted, or handed off to another applet. A reload starts fresh Lua variables and handles; use persistent storage for values that must survive it. The interval is initialization-only, and onLoad() is already too late to assign it. See Scheduling for one-shot deadlines and cancellation.

Click Modifiers

Task-item click callbacks receive a modifiers table:

FieldMeaning
modifiers.shiftShift was held
modifiers.ctrlCtrl was held
modifiers.altAlt was held
modifiers.metaMeta/Super was held

All fields are boolean. There is no global keyboard-state API and no key-down/key-up callback for the Decoration’s task item.

For example, let an ordinary click open a Window and Shift-click reset a stored counter by branching on modifiers.shift. Right-click remains TaskManSync’s Decoration context menu; there is no onTaskRightClick or script-owned menu API. Window keyboard and action events are separate from task-item clicks.

Positioned Text

A Scripted Decoration can create independently positioned text elements:

local status = decoration:createText("status")

status.x = 12
status.y = 4
status.text = "{bld}Ready{}"

The key must be non-empty and unique among the live elements in the current runtime. Elements retain creation order; later elements are drawn above earlier elements.

Positioned text is useful when ordinary multiline TDML is not enough. Its coordinates are relative to the realized Decoration surface and are clipped by that surface.

Coordinates are finite fractional logical pixels with a top-left origin; x increases rightward and y downward. Negative and off-surface coordinates are allowed. The elements are content-sized and never enlarge the taskbar allocation or contribute to Content sizing. status.horizontalFlip = true mirrors an element horizontally; status:remove() removes it and invalidates its handle. A removed key can be reused for a new element, but every old handle becomes invalid on reload.

Each element parses its TDML independently. Run styles and line offsets work, but whole-Decoration directives posh, posv, dcolor, and dc are invalid here; use x/y and the parent’s surface background instead. Alignment has no visible effect in the initial content-sized element box. Element changes within one callback publish together; assignment is not an immediate paint. Elements draw above whole-Decoration text, with later-created elements above earlier ones.

Graphics

Scripted Decoration graphics use two retained collections:

  • decoration.graphics.lines
  • decoration.graphics.paths

For example:

decoration.graphics = {
    lines = {
        { x1 = 0, y1 = 0, x2 = 0.9, y2 = 0.5, color = "#39FF72", width = 2 },
    },
    paths = {
        {
            x = 12, y = 4, width = 24, height = 16,
            fill = "#39FF72",
            operations = {
                { op = "move", x = 0.1, y = 0.5 },
                { op = "cubic", c1x = 0.2, c1y = 0, c2x = 0.8, c2y = 1, x = 0.9, y = 0.5 },
                { op = "close" },
            },
        },
    },
}

Line endpoints are normalized from 0.0 through 1.0 relative to the surface, with a top-left origin and y increasing downward. Path rectangles use logical pixels; path operation coordinates are normalized within each rectangle.

Line width is an integer from 1 through 256, defaulting to 1; at most 2,048 lines are accepted. For a path, x/y range from -4,096 through 4,096 and required width/height from 1 through 4,096. Optional fill and stroke select paint, and strokeWidth is a finite logical-pixel value from 0 through 4,096, defaulting to 1. Missing/empty fill or stroke means no such paint. Paths are mapped to the rectangle without preserving aspect ratio, so use its dimensions to control stretching.

move starts a subpath; line and cubic need an existing one; close closes it. Open paths and multiple subpaths are allowed. Graphics accept at most 2,048 paths, 32,768 total operations, and 4 MiB of copied path data. Arrays must be contiguous; malformed records, unknown fields, invalid colors, and out-of-range values reject the assignment and retain the previous graphics.

Assigning decoration.graphics replaces both collections as one copied value; omitted collections become empty. To animate, keep the data in Lua, change it, and assign the complete value again from onTick(). { lines = {}, paths = {} } clears graphics. The surface clips all primitives. Lines draw before paths, and positioned text draws above them. Graphics do not change taskbar width allocation and have no pointer hit-testing API.

These are the supported scripted graphics primitives. They are not an SVG, PNG, shader, arbitrary native-object, or general scene-graph API.