Skip to content

Custom lua scripts

Bobby Comet edited this page Aug 6, 2026 · 1 revision

Custom Lua & Custom Script — Creator/Developer Guide

Audience: HUD creators, theme porters, and anyone writing or extending node behaviour in Conky Studio.
Scope: How visual.custom_lua and source.custom_script work end-to-end; canvas wiring, property panel, build/codegen, Live Preview, and runtime Lua/shell, plus how to keep them reliable.

This page expands the short Custom Lua section in the Visuals node reference. It assumes you already know the three core ideas: Sources → (optional Logic) → Visuals.


Table of contents

  1. Mental model
  2. Custom Lua (visual.custom_lua)
  3. Custom Script (source.custom_script)
  4. Wiring patterns that work
  5. Making sure it runs in Studio
  6. Legacy import path
  7. Codegen & runtime contracts
  8. Debugging checklist
  9. Recipes
  10. Related extension points

1. Mental model

Piece Category Outputs data? Draws? Runs where?
Custom Script Source Yes (one value/line) No Shell (execi or daemon cache) → SRC[id] in Lua
Logic nodes Logic Yes No Pure Lua in refresh_sources()
Custom Lua Visual No Yes Cairo draw function in render.lua

Data only flows into Custom Lua; nothing can bind from it.
If you need “Custom Lua computes X, then a Bar uses X,” you must compute X in a Logic node (or Custom Script + Logic) and feed that into both the visual and any downstream consumers. Custom Lua is a drawing sink, not a passthrough.

  [Source / Custom Script]
           │
           ▼
     [Logic chain]  ──optional──►  other visuals (Bar, Text, …)
           │
           ▼
   [Custom Lua in1…in12]  →  Cairo only

2. Custom Lua (visual.custom_lua)

2.1 What you get on the canvas

  • One node box in the Visuals → Advanced palette (Complex mode; not in Simple).
  • No output socket on the right.
  • Up to 12 input sockets on the left (in1in12), each bindable.
  • Header colour matches other visuals (violet).

Double-click / select the node → Properties dock:

Group Property Notes
Position Offset X, Offset Y Applied as cairo_translate before your code. Move the whole block without editing Lua.
Data inputs Input 1 … Input 12 Each can be a constant or a wire. Accepts percent, Celsius, number, text, category.
(code) Lua code Multiline monospace editor. Body of the draw function.

2.2 What runs at draw time

Codegen emits roughly:

local function draw_node_<id>(cr, W, H)
    cairo_save(cr)
    cairo_translate(cr, <Offset X>, <Offset Y>)

    local in1 = <resolved expr or nil>
    local in2 = ...
    -- … in12 …

    -- === your Property "Lua code" body is inserted here ===

    cairo_restore(cr)
end

Then main_draw_impl calls every visible visual’s function in ascending z-order (Layers dock order).

Already in scope inside your code (framework + injected locals):

Name Meaning
cr Cairo context for this Conky window
W, H Window width/height (after canvas settings)
in1in12 Wired values, or nil if unwired
clamp, lerp, rounded_rect, … Framework helpers
wall_clock() Monotonic real time (prefer over os.clock())
safe_parse / safe_number Safe conky_parse wrappers
load_image_cached / draw_image_fit Images under IMAGES_DIR
studio_draw_text, studio_heat_rgb Text + heat colour helpers
STATE, SRC, HIST, CACHE_KV Shared runtime tables
THEME_DIR, SCRIPTS_DIR, IMAGES_DIR, CACHE_DIR Paths

You do not need require 'cairo' — the framework already loads it.

2.3 Critical rule: unwired inputs are nil

Unbound slots are intentionally nil, not 0. That lets you distinguish “nothing wired” from a real zero reading.

Always write:

local cpu = tonumber(in1) or safe_number('${cpu}', 0)
local label = tostring(in2 or '')

Anti-patterns:

-- BAD: crashes or misbehaves when in1 is nil
local cpu = in1 + 1

-- BAD: treats "unwired" the same as "user set constant 0" in confusing ways
local cpu = tonumber(in1) or 0   -- OK only if you truly want 0 when unbound

Prefer the tonumber(inN) or safe_number(...) pattern so:

  1. Live Preview/Build with a wire uses the Studio graph.
  2. The same Lua still works if someone deletes the wire (falls back to Conky’s built-in).

2.4 One-directional data flow

Custom Lua cannot feed Logic or other visuals. If you need a shared computed value:

  1. Put the math in Logic (Math, Map Range, Conditional, Smooth, …).
  2. Wire that Logic output into Custom Lua and into any Bar/Text/Gauge that needs it.

If you need more than 12 live values into one Custom Lua node:

  • Combine upstream with Logic (e.g., Math/String Join/Enum Map), or
  • Split drawing across multiple Custom Lua nodes (each with its own 12 inputs).

2.5 Persistence across frames (STATE)

Use the framework STATE table for animation phase, previous values, eased numbers:

local key = 'my_effect_' .. tostring(W)  -- or any stable string
local st = STATE[key]
if st == nil then
  st = { phase = 0, last = wall_clock() }
  STATE[key] = st
end
local now = wall_clock()
local dt = now - st.last
if dt < 0 or dt > 1 then dt = 0 end
st.phase = st.phase + dt * 30  -- degrees per second
st.last = now

Avoid relying on module-level globals that disappear on rebuild; STATE is the supported hook (same pattern as Smooth/Vinyl Spinner generators).

2.6 Images and paths

  • Prefer assets copied into the theme’s images/ (or assets/ for legacy imports).
  • Load with:
local img = load_image_cached(IMAGES_DIR .. '/logo.png')
draw_image_fit(cr, img, 0, 0, 64, 0, 1.0)
  • Absolute paths from your machine will not exist on another user’s install. Always build through Studio so paths are rewritten/copied.

2.7 Click actions

Custom Lua can still use the node’s Interaction fields (on_click_command + click region in canvas pixels). Clicks are handled by the generated mouse_handler, independent of your Cairo code. Region is not inferred from what you draw; set X/Y/W/H explicitly in Properties.

2.8 Errors at runtime

Draw is wrapped in pcall. A Lua error in your code prints:

[conky-studio] draw error: <message>

to Conky’s log (visible in Studio’s Live Preview log). One bad Custom Lua node should not tear down the whole HUD; other nodes still draw.


3. Custom Script (source.custom_script)

3.1 Role

A source that runs your shell (or any executable) and exposes a single value into the graph. Use it when:

  • Conky has no ${…} variable for the data.
  • You are porting a legacy sensors.sh /one-liner.
  • You need structured polling with the same cache harness as GPU/Weather families.

Palette: Data Sources → Custom (Complex mode recommended).

3.2 Properties

Property Purpose
Script path Optional file on disk; copied into scripts/ on Build. Ignored if Inline script is non-empty.
Inline script Body edited in Properties (like Custom Lua). Written to scripts/ at Build. Prefer this when fixing imports; no external file is required.
Treat output as text / number / percent / celsius / category — drives socket colour and which bindable props accept the wire.
Polling mode execi (Conky ${execi N …}) or daemon (background loop + cache file).
Refresh every (sec) Interval for that mode.

Optional flags used heavily by legacy import:

  • self_caching — script already writes its own cache (e.g., sensors.cache); Studio runs it without a stdout-capture wrapper.
  • script_basename/companion conf paths — keep names Lua expects.

3.3 Output contract

Stdout, one primary line (or key=value lines for family-style caches). For a normal custom script in daemon mode, the wrapper stores:

value=<first line of stdout>

In Lua, SRC['<node_id>'] becomes that value (string or number, depending on Treat output as).

Empty/failed commands should print a safe default (0 or empty string), not spam stderr every interval.

3.4 execi vs daemon

Mode Behaviour When to use
execi Conky runs the script on its interval; a brief block is possible when the cache expires. Slow polls, simple scripts, decorative HUDs.
daemon start.sh runs a background loop; Lua only reads a cache file (zero stutter on the draw path). Network, sensors, anything that can hang; high FPS HUDs.

Live Preview: daemon families get one synchronous prime per rebuild (no long-lived poller in the preview process). Values appear after that prime; they are not continuously refreshed until you Build & Install with start.sh.

3.5 Wiring Custom Script into the Graph

  1. Drop Custom Script, set inline body or path, set Treat output as.
  2. Connect its output socket to:
    • a Visual bindable (value, trigger, …), and/or
    • Logic inputs, and/or
    • Custom Lua inN.
  3. Unused Custom Scripts are still emitted if they are daemon/self-caching (so Custom Lua that reads sensors.cache by path still works after legacy import).

3.6 Minimal inline example

Treat output as: number

#!/usr/bin/env bash
# Example: count open TCP connections (illustrative)
ss -tan 2>/dev/null | grep -c ESTAB || echo 0

Wire → Bar or Text (with String Format).


4. Wiring patterns that work

4.1 Source → Custom Lua

CPU Usage  ──►  Custom Lua in1
RAM Usage  ──►  Custom Lua in2
local cpu = tonumber(in1) or 0
local ram = tonumber(in2) or 0
-- draw using cpu, ram

4.2 Source → Logic → Custom Lua (+ other visuals)

CPU Temp  ──►  Smooth  ──►  Map Range (0–100 → 0–1)
                    │
                    ├──► Needle Gauge value
                    └──► Custom Lua in1

4.3 Custom Script → Logic → visuals

Custom Script (category tokens)  ──►  Enum Map  ──►  LED Dot/Icon swap

4.4 Text composition

Track Artist ──┐
               ├──► String Join ──► Text Label
Track Title  ──┘

Or feed the joined string into Custom Lua in3 for a custom typography layout.

4.5 What not to do

  • Wire Custom Lua to anything (impossible, no output).
  • Bind a text Custom Script into a gauge that only accepts numeric kinds (change Treat output as, or insert Logic that parses).
  • Rely on absolute /home/you/... paths in Custom Lua after Build.
  • Assume Live Preview runs your daemon loop forever (it primes once per rebuild).

5. Making sure it works in Conky Studio

5.1 Authoring loop

  1. Studio tab — add nodes, wire sockets (drag output → input).
  2. Properties — set Inline script/Lua code; set kinds and intervals.
  3. Live Preview → Start — real Conky + generated render.lua.
  4. Watch the preview log for [build], [hint], draw error, script failures.
  5. Project → Build & Install to Manager when stable; use Manager to launch full start.sh (real daemon loops).

5.2 Graph hygiene

  • Rename the HUD before treating it as finished (Manager/naming notes in Getting Started).
  • Use Layers: hide skips codegen; lock blocks accidental moves.
  • Keep Canvas (canvas.root) FPS and Sensor refresh (Hz) sensible: high FPS with low stats_hz is normal (draw chrome often, poll sensors less).

5.3 Session/hardware

Tools → Hardware & Session before blaming Lua. Wayland/GNOME overlay limits, missing lua-cairo, missing playerctl / sensors, etc., show up there and in preview log hints.

5.4 Fonts and images

  • Tools → Install Font for families used in Custom Lua studio_draw_text / cairo_select_font_face.
  • Image nodes and Custom Lua assets must be present after Build under images/ / assets/.

5.5 Plugins vs Custom Lua

Community plugins register real NodeSpec + generators (Logic/Visual). Prefer a plugin when the behaviour is reusable. Prefer Custom Lua when the drawing is one-off, or you are pasting legacy Cairo.


6. Legacy import path (beta)

Project → Import Legacy Theme… parses classic conf/Lua into a Studio project.

Typical outcomes for hand-drawn themes:

  1. Unrecognised Cairo bodies → Custom Lua nodes with pasted code.
  2. Common sensors auto-wired into early in1…inN.
  3. External scripts → Custom Script nodes (often self_caching + original basenames).
  4. Warnings list approximations; expect to reposition and re-bind.

After import:

  1. Live Preview immediately.
  2. Open each Custom Lua — confirm tonumber(inN) or … bridges where auto-patch applied.
  3. Verify Custom Script Treat output as and poll mode.
  4. Rebuild; check Manager Start uses the generated start.sh.

Semantic extraction is not pixel-perfect recreation — that is by design.


7. Codegen & runtime contracts

7.1 Custom Lua generator expectations

  • Your code is the body of a local function; do not declare function conky_… inside it unless you know you need a nested helper.
  • Prefer framework helpers over reimplementing clamp/lerp/text.
  • Heavy work every frame (file I/O, process spawn) will stutter; push polling to Custom Script/native sources and only draw in Custom Lua.

7.2 refresh_sources vs draw

  • Sources + Logic update in refresh_sources() on the stats cadence (STATS_HZ /canvas).
  • Visuals (including Custom Lua) run every draw frame (FPS).
  • Bound inN values are whatever was last written to SRC[…] on the stats tick — same as any other visual’s bound property.

7.3 Coverage assertion

Build runs assert_full_coverage(): every registered visual type must have a Lua generator. Custom Lua is a first-class registered type; your user code is data inside that generator’s template, not a separate registry entry.

7.4 Multi-window

All enabled windows share the same render.lua graph by default. Custom Lua draws in each process; the window size is that conf’s width/height. Pin monitors in the Windows dock when needed.


8. Debugging checklist

Symptom Checks
Blank Custom Lua Syntax error → preview log draw error. pcall swallowed the frame.
Always zero/empty inputs Wire missing; wrong socket; source not in used set (nothing reads it; wire it). Custom Script kind wrong.
Works in Preview, fails installed Daemon scripts need start.sh; path permissions; missing deps (curl, sensors).
Stale daemon values in Preview Expected until rebuild prime; use execi for continuous preview polling, or Build & Install.
Image missing Not copied to images/; wrong basename; SVG without RSVG build.
Nil arithmetic Use tonumber(inN) or default.
Logic order wrong Cycles are broken arbitrarily; keep DAGs.
High CPU Lower canvas FPS; move work to scripts; avoid per-frame shell.

9. Recipes

9.1 Legacy Cairo island with live CPU

  1. Import or paste into Custom Lua.
  2. Wire CPU Usagein1.
  3. Replace internal parse:
local cpu = tonumber(in1) or safe_number('${cpu}', 0)
  1. Keep Offset X/Y for layout; use Layers z-order relative to other Studio visuals.

9.2 Custom sensor → gauge

  1. Custom Script (daemon, number) reading your tool.
  2. Optional Smooth.
  3. Arc Gauge / Needle Gauge on value.
  4. Optional second wire into Custom Lua for a matching custom bezel.

9.3 Category → art without image files

  1. Weather Category or Custom Script printing tokens.
  2. Enum Map → index, or Weather Icon on category, or Custom Lua if on tostring(in1).

9.4 Now Playing Chrome

  1. Title/Artist/Progress sources (or Custom Script + playerctl).
  2. String Join → Text; Progress → Bar.
  3. Custom Lua or Vinyl Spinner for decorative disc; gate with playback status.

9.5 Clickable custom control

  1. Draw the control in Custom Lua.
  2. Set On click to, e.g., playerctl play-pause.
  3. Set the click region to the drawn hotspot in canvas coordinates (not only Offset-local).

10. Related extension points

For reusable behaviour (not one-off Cairo):

Extension Where
New logic node type NodeSpec + logic_generator registration (see built-in logic_* modules)
New visual type NodeSpec + visual_generator + import from nodes/__init__ /extensions bootstrap
Plugins Tools → Plugins; trust boundary = Conky process
Framework helpers Shared FRAMEWORK_LUA (prefer extending there only in Studio itself)

Custom Lua remains the escape hatch when a full node type is overkill.


Quick reference card

Custom Lua

  • Visual only · 12 inputs · no output · nil if unbound · Offset X/Y · framework in scope · errors → log via pcall.

Custom Script

  • Source · stdout → SRC[id] · inline or path · kind + poll mode · daemon cached for zero-stutter · still built if self-caching/unwired for pure-Lua themes.

Golden rules

  1. Compute in Source/Logic; draw in Custom Lua.
  2. tonumber(inN) or fallback every time.
  3. Validate with Live Preview log, then full start.sh via Manager.
  4. No absolute home-machine paths in shipped themes.

Document version aligned with Conky Studio’s node model: Custom Lua as visual.custom_lua, Custom Script as source.custom_script, shared render.lua framework, and builder/start.sh, daemon families.

Clone this wiki locally