Skip to content

Custom Plugins

Bobby Comet edited this page Aug 2, 2026 · 3 revisions

How to make plugins

This page is the authoring companion to Plugins. A plugin is never executable Python: it is a manifest entry (metadata + Lua template strings) that Conky Studio registers into the node registry and code generator.

Mental model

plugins.json  →  loader.validate + register  →  NodeSpec in palette
                                          ↘
                                            lua_gen: logic_expr OR draw_node_* body
                                          ↘
                                            Conky runs the generated render.lua
  • logic → substituted lua_expr becomes the value assigned to SRC['node_id'] in dependency order.
  • visual → substituted lua_draw_body is the body of local function draw_node_<id>(cr, W, H).
  • Optional lua_helpers → emitted once per plugin type the first time that type is drawn in a build.

Substitution is plain string replace of {property_key}; not str.format(), not Python eval.

Manifest layout

Top level:

{
  "api_version": "1.1",
  "updated_at": "2026-08-02",
  "plugins": [ /* PluginNode objects */ ]
}

One plugin entry (shape)

{
  "id": "logic.example_clamp",
  "category": "logic",
  "label": "Example Clamp",
  "author": "you",
  "version": "1.0.0",
  "description": "Clamp a number into [lo, hi].",
  "color": "#5f8fd6",
  "subcategory": "Plugins",
  "output_kind": "number",
  "tags": ["math", "clamp"],
  "simple_mode": true,
  "homepage": "",
  "license": "",
  "properties": [
    {
      "key": "value",
      "label": "Value",
      "kind": "float",
      "default": 50,
      "minimum": -1e9,
      "maximum": 1e9,
      "bindable": true,
      "accepts": ["number", "percent", "celsius"],
      "group": "Input",
      "help": "Input to clamp"
    },
    {
      "key": "lo",
      "label": "Min",
      "kind": "float",
      "default": 0,
      "group": "Range"
    },
    {
      "key": "hi",
      "label": "Max",
      "kind": "float",
      "default": 100,
      "group": "Range"
    }
  ],
  "lua_expr": "math.min({hi}, math.max({lo}, {value}))"
}

Visual sketch:

{
  "id": "visual.plugin.example_box",
  "category": "visual",
  "label": "Example Box",
  "author": "you",
  "version": "1.0.0",
  "description": "Filled rectangle.",
  "color": "#8a5fd6",
  "subcategory": "Plugins",
  "simple_mode": true,
  "tags": ["shape"],
  "properties": [
    { "key": "x", "label": "X", "kind": "float", "default": 20, "group": "Position" },
    { "key": "y", "label": "Y", "kind": "float", "default": 20, "group": "Position" },
    { "key": "width", "label": "Width", "kind": "float", "default": 80, "group": "Shape" },
    { "key": "height", "label": "Height", "kind": "float", "default": 40, "group": "Shape" },
    { "key": "color", "label": "Colour", "kind": "color", "default": "#4fd1c5", "group": "Style" },
    { "key": "opacity", "label": "Opacity", "kind": "float", "default": 1, "minimum": 0, "maximum": 1, "step": 0.05, "group": "Style" }
  ],
  "lua_draw_body": "local r, g, b = {color}\ncairo_set_source_rgba(cr, r, g, b, {opacity})\ncairo_rectangle(cr, {x}, {y}, {width}, {height})\ncairo_fill(cr)"
}

Critical rules

Rule Detail
id Must match ^(logic|visual)(\.[a-z][a-z0-9_]*)+$ — e.g. logic.clamp, visual.plugin.ring
category Only logic or visual
Placeholders Only {property_key}. Every {name} in lua_expr / lua_draw_body / lua_helpers must be a declared property key (hard error otherwise)
cr, W, H Bare names in draw bodies; not {cr}, {W}, {H}
color kind Substituted as r, g, b float literals for Cairo (e.g. 0.31, 0.82, 0.77), not hex
bool Substituted as Lua true / false via the resolved expression path (bindable uses SRC[...])
output_kind (logic) percent · celsius · number · text · category · boolean
Property kinds float · int · bool · color · string · enum · font · path · code
enum Requires non-empty choices (optional choice_labels)
Property key ^[a-z][a-z0-9_]*$, unique within the plugin
Collision Cannot register an id that already belongs to a built-in (or non-plugin) type. Re-install of a previously loaded plugin id is allowed

Property field reference

Field Meaning
key Placeholder name and storage key
label Property panel label
kind Editor widget + substitution style
default Used when unbound
minimum / maximum / step Numeric editors
choices / choice_labels Enum values/display labels
bindable May accept a wire
accepts Allowed source/logic KIND_* values for wires
help Tooltip
group Section heading in the property panel

Writing lua_expr (logic)

  • Must evaluate to a single expression (or an IIFE (function() ... end)() if you need locals).
  • Use {key} for every input; the loader replaces with a Lua expression (literal or SRC['...']).
  • Prefer defensive tonumber(...) or 0 when accepting mixed kinds.
  • For persistent state across frames, use a slot property + a table in lua_helpers (see official logic.smooth), not globals that collide between instances.

Example pattern (helpers once):

"lua_helpers": "local _cs_plugin_smooth = _cs_plugin_smooth or {}\n",
"lua_expr": "(function()\n local k = {slot}\n local v, a = {value}, {alpha}\n ...\nend)()"

Writing lua_draw_body (visual)

  • Statements only (no wrapping function); the generator adds local function draw_node_...(cr, W, H).
  • cr is the Cairo context; W / H are window size.
  • Framework helpers from the runtime are available: wall_clock, clamp, lerp, rounded_rect, safe_number, studio_draw_text, studio_heat_rgb, etc.
  • Create patterns carefully; destroy gradient patterns if you create them.
  • Prefer tonumber({value}) or 0 for bindable numbers.

Recommended JSON / Lua layout style

  1. One plugin per concern — don’t ship a mega-node with twenty unrelated modes unless the UI stays clear.
  2. Group propertiesPosition, Shape, Data, Style, Animation, Drive.
  3. Id namespacevisual.plugin.* for third-party visuals avoids clashing with future built-ins.
  4. Indent Lua with spaces inside the JSON string; keep lines readable (multi-line strings are fine).
  5. Tags — short tokens for search (gauge, animation, network).
  6. simple_mode: true only if the node is useful with defaults and few knobs.
  7. Version semver; bump when property keys or behaviour change incompatibly.

Validate without registering

from conkystudio.plugins.loader import load_manifest_file, validate_only

m = load_manifest_file("my-pack.json")
errors = validate_only(m)
print(errors)  # [] = OK

Or install into a test session via Tools → Plugins and check the palette + Build.

Local pack checklist

  1. Write my-pack.json with api_version 1.1 and a plugins array.
  2. Run validate_only (or open in the dialog and read errors).
  3. Copy to ~/.config/conky-studio/plugins/ or Install from the dialog (persists to installed-plugins.json).
  4. Create a tiny project: one source → your logic (if any) → your visual → Live Preview.
  5. Document required ids when sharing the .json project.

What the loader does not do

  • Does not eval or exec Python from the pack.
  • Does not auto-install the entire remote catalogue on startup.
  • Does not support source.* or canvas.* plugins.
  • Does not sandbox Conky’s Lua privileges.

Clone this wiki locally