-
-
Notifications
You must be signed in to change notification settings - Fork 659
Lua: Drawing
Conky exposes Cairo drawing functions through
Lua bindings. These bindings are nearly identical to the
Cairo C API — the only difference is
how the drawing surface is obtained (conky_surface() instead of a
platform-specific surface constructor). Other than that, any Cairo
documentation, tutorial, or example translates directly to Conky's Lua
environment.
Tip
Because the bindings mirror the C API so closely, you can use the Cairo manual as the authoritative reference. This page focuses on the drawing model, Conky-specific setup, and practical examples — not on repeating what the Cairo docs already cover.
The examples on this page assume a working Conky + Lua setup. If you haven't set that up yet, see How to Load and Use Lua Scripts.
Conky is sized based on requirements of conky.text. If it's blank you'll need
to specify minimum window dimensions:
conky.config = {
minimum_height = 200,
minimum_width = 200,
-- ... your other settings ...
}
conky.text = [[]]Warning
If you don't specify minimum_height and minimum_width and your
conky.text is empty, conky window will have dimensions of 1×1 px.
Cairo uses a standard 2D coordinate system where:
- The origin (0, 0) is at the top-left corner of the Conky window
- X increases to the right
- Y increases downward
- Units are in pixels by default (1 unit = 1 pixel, unless transforms are applied)
Coordinates can be fractional — see Anti-Aliasing for how this affects rendering.
Anything drawn outside the window bounds is silently clipped — it won't cause errors, but it won't be visible either.
Cairo uses a path-based drawing model similar to PostScript or HTML5 Canvas. Drawing is always a two-step process:
- Build a path — move to points, add lines, arcs, curves, or rectangles
- Apply an operation — stroke (outline) or fill the path
-- Step 1: build a path
cairo_move_to(cr, 10, 10)
cairo_line_to(cr, 190, 10)
cairo_line_to(cr, 190, 190)
cairo_close_path(cr)
-- Step 2: apply
cairo_stroke(cr) -- draws the outlineAfter
cairo_stroke()
or
cairo_fill(),
the current path is consumed (cleared). Use
cairo_stroke_preserve()
/
cairo_fill_preserve()
to keep the path for further operations — for example, filling a shape with
color and then stroking its outline:
cairo_rectangle(cr, 20, 20, 160, 160)
cairo_set_source_rgba(cr, 0.2, 0.4, 0.8, 0.5)
cairo_fill_preserve(cr)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
cairo_set_line_width(cr, 2)
cairo_stroke(cr)Key differences between stroke and fill:
-
cairo_fill()automatically closes open paths before filling — you don't needcairo_close_path()before it. -
cairo_set_line_width(),cairo_set_line_cap(), andcairo_set_line_join()only affectcairo_stroke(). They have no effect oncairo_fill().
Cairo maintains a drawing state that affects all subsequent operations. This
includes the current color, line width, font, transformation matrix, and
clipping region. Use
cairo_save()
and
cairo_restore()
to push/pop state:
cairo_save(cr)
cairo_set_source_rgba(cr, 1, 0, 0, 1)
cairo_set_line_width(cr, 4)
-- draw something red and thick
cairo_restore(cr)
-- back to previous color and line widthOperations are painted in order. Later draws cover earlier ones, like layers. To draw a shape with a background, draw the background first:
-- Background (drawn first, appears behind)
cairo_rectangle(cr, 0, 0, 200, 30)
cairo_set_source_rgba(cr, 0.1, 0.1, 0.1, 0.8)
cairo_fill(cr)
-- Foreground (drawn second, appears on top)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
cairo_move_to(cr, 10, 20)
cairo_show_text(cr, "System Status")Colors are set with
cairo_set_source_rgba(cr, r, g, b, a)
where each component is a float from 0.0 to 1.0.
cairo_set_source_rgba(cr, 1, 1, 1, 1) -- white, fully opaque
cairo_set_source_rgba(cr, 1, 0, 0, 0.5) -- red, 50% transparent
cairo_set_source_rgba(cr, 0, 0, 0, 0) -- fully transparentTo convert standard 0–255 RGB values:
-- RGB(246, 155, 11)
cairo_set_source_rgba(cr, 246/255, 155/255, 11/255, 1)A helper for hex colors:
function hex_color(hex, alpha)
return tonumber(hex:sub(1, 2), 16) / 255,
tonumber(hex:sub(3, 4), 16) / 255,
tonumber(hex:sub(5, 6), 16) / 255,
alpha or 1
end
cairo_set_source_rgba(cr, hex_color("F69B0B"))The color (source) stays active until you change it. The same applies to line width, font, and other state properties.
Lines are built from
cairo_move_to()
and
cairo_line_to(),
then rendered with cairo_stroke().
cairo_set_line_width(cr, 2)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
cairo_move_to(cr, 10, 50)
cairo_line_to(cr, 190, 50)
cairo_stroke(cr)Tip
cairo_rel_move_to()
and
cairo_rel_line_to()
work relative to the current point instead of using absolute coordinates.
This is useful for drawing shapes at variable positions without recalculating
every coordinate — move to the starting position once, then use relative
offsets for the rest.
cairo_set_line_width()
sets the stroke thickness. The line is centered on the path — a 4px wide line
extends 2px on each side.
Caution
Because the stroke is centered on the path, it encroaches inward on filled shapes. If you stroke a rectangle with a 4px border, 2px of that border overlaps the fill area. To compensate, inset the fill rectangle by half the border width:
local border = 4
local half = border / 2
-- Stroke border at the intended position
cairo_set_line_width(cr, border)
cairo_rectangle(cr, 20, 20, 100, 60)
cairo_stroke(cr)
-- Fill inset by half the border width
cairo_rectangle(cr, 20 + half, 20 + half, 100 - border, 60 - border)
cairo_fill(cr)cairo_set_line_cap()
controls how line endpoints are drawn:
-
CAIRO_LINE_CAP_BUTT— ends exactly at the endpoint (default) -
CAIRO_LINE_CAP_ROUND— semicircle at each end -
CAIRO_LINE_CAP_SQUARE— square extending half the line width past the endpoint
cairo_set_line_join()
controls how corners are drawn when lines meet:
-
CAIRO_LINE_JOIN_MITER— sharp corners (default) -
CAIRO_LINE_JOIN_ROUND— rounded corners -
CAIRO_LINE_JOIN_BEVEL— flattened corners
cairo_set_dash()
creates dashed or dotted lines:
cairo_set_dash(cr, {10, 5}, 2, 0) -- 10px dash, 5px gap
cairo_move_to(cr, 10, 100)
cairo_line_to(cr, 190, 100)
cairo_stroke(cr)cairo_rectangle(cr, x, y, width, height)
adds a rectangle to the current path. The path is already closed, so
cairo_close_path() is not needed.
-- Filled rectangle
cairo_rectangle(cr, 20, 20, 100, 60)
cairo_set_source_rgba(cr, 0.3, 0.6, 0.9, 1)
cairo_fill(cr)
-- Outlined rectangle
cairo_rectangle(cr, 20, 20, 100, 60)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
cairo_set_line_width(cr, 2)
cairo_stroke(cr)
-- Rounded corners via line join
cairo_rectangle(cr, 20, 20, 100, 60)
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND)
cairo_set_line_width(cr, 8)
cairo_stroke(cr)Tip
cairo_rectangle() accepts negative width or height, which flips the drawing
direction. This is a convenient way to draw bars that fill upward from a
baseline:
-- Draw a bar from bottom (y=150) upward
cairo_rectangle(cr, 20, 150, 20, -fill_h)
cairo_fill(cr)cairo_arc(cr, xc, yc, radius, angle1, angle2)
draws an arc. Angles are in radians, measured clockwise from the positive
X-axis (3 o'clock position):
- 0 = right (3 o'clock)
- π/2 = bottom (6 o'clock)
- π = left (9 o'clock)
- 3π/2 = top (12 o'clock)
If you prefer to think of 0° as the top of the circle (12 o'clock), subtract 90° before converting:
-- 0° at top, 90° at right, 180° at bottom, 270° at left
local function top_deg_to_rad(deg)
return (deg - 90) * math.pi / 180
endA full circle is an arc from 0 to 2π:
-- Filled circle
cairo_arc(cr, 100, 100, 50, 0, 2 * math.pi)
cairo_fill(cr)
-- Outlined circle
cairo_arc(cr, 100, 100, 50, 0, 2 * math.pi)
cairo_stroke(cr)cairo_arc_negative()
draws in the counter-clockwise direction. With the same start and end angles,
cairo_arc and cairo_arc_negative produce opposite halves of the circle:
-- Top half (clockwise from 270° to 90°)
cairo_arc(cr, 100, 100, 50, deg_to_rad(270), deg_to_rad(90))
cairo_stroke(cr)
-- Bottom half (counter-clockwise from 270° to 90°)
cairo_arc_negative(cr, 100, 100, 50, deg_to_rad(270), deg_to_rad(90))
cairo_stroke(cr)To convert degrees to radians:
function deg_to_rad(deg)
return deg * math.pi / 180
endTo place elements (tick marks, labels, icons) at a specific angle around a
circle, compute the X/Y coordinates using math.sin and math.cos:
-- angle in radians, center at (cx, cy)
local x = cx + radius * math.sin(angle)
local y = cy - radius * math.cos(angle)math.sin gives the horizontal offset and math.cos gives the vertical
offset. The cos is negated because Cairo's Y-axis points downward. With this
convention, angle 0 is at the top (12 o'clock), and angles increase clockwise —
matching how most people think about clock positions.
-- Draw 12 hour marks around a clock face
for i = 1, 12 do
local angle = i * math.pi / 6 -- 30° per hour
local inner = radius - 10
local outer = radius - 2
cairo_move_to(cr, cx + inner * math.sin(angle),
cy - inner * math.cos(angle))
cairo_line_to(cr, cx + outer * math.sin(angle),
cy - outer * math.cos(angle))
cairo_stroke(cr)
endNote
Line width on arcs works the same as on straight lines — it extends equally inward and outward from the path. A circle with radius 50 and line width 20 has its inner edge at 40px from center and its outer edge at 60px.
cairo_curve_to(cr, x1, y1, x2, y2, x3, y3)
draws a cubic
Bezier curve from the
current point to an endpoint (x3, y3). The shape of the curve is determined
by two control points — (x1, y1) and (x2, y2) — which act as
"magnets" that pull the curve toward them without the curve necessarily passing
through them:
- The curve starts at the current point and heads toward
(x1, y1) - It then bends toward
(x2, y2)as it approaches the endpoint - It arrives at
(x3, y3)coming from the direction of(x2, y2)
cairo_move_to(cr, 20, 100) -- start point
cairo_curve_to(cr,
20, 20, -- control point 1 (pulls curve up-left)
180, 20, -- control point 2 (pulls curve up-right)
180, 100) -- end point
cairo_stroke(cr)There is also
cairo_rel_curve_to()
which takes offsets relative to the current point rather than absolute
coordinates.
cairo_close_path()
connects the current point back to the start of the path with a straight line.
This is important for proper line joins at the start of closed shapes — without
it, the first and last segments won't have a join.
Text rendering uses Cairo's
text functions. The
text cursor position set with cairo_move_to() refers to the text
baseline — characters
render above and to the right of that point.
cairo_select_font_face(cr, "Mono",
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL)
cairo_set_font_size(cr, 14)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
cairo_move_to(cr, 10, 30)
cairo_show_text(cr, "Hello, Conky")
cairo_stroke(cr)Font slant: CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_SLANT_ITALIC,
CAIRO_FONT_SLANT_OBLIQUE
Font weight: CAIRO_FONT_WEIGHT_NORMAL, CAIRO_FONT_WEIGHT_BOLD
cairo_select_font_face() is Cairo's
"toy" font API
and only supports normal/bold weight and normal/italic/oblique slant. For
variable font weights (light, thin, semibold, etc.), use fontconfig syntax in
the family name instead — Cairo's toy API passes the family string to the
system font matcher:
-- These rely on fontconfig matching the closest available weight
cairo_select_font_face(cr, "Noto Sans Light",
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL)
cairo_select_font_face(cr, "Roboto Thin",
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL)Alternatively, Conky provides a high-performance text function that uses FreeType directly and supports full fontconfig font matching:
require("cairo_text_helper")
-- font name is matched by fontconfig, so weight variants work naturally
cairo_text_hp_show(cr, 10, 30, "Hello", "Roboto Light", 14)
cairo_text_hp_show(cr, 10, 60, "World", "Noto Sans:weight=200", 14)cairo_text_extents()
returns dimensions of a text string, which is useful for centering or
right-aligning text:
local extents = cairo_text_extents_t:create()
cairo_text_extents(cr, "centered text", extents)
local text_width = extents.width
-- Center text horizontally in a 200px wide window
local x = (200 - text_width) / 2
cairo_move_to(cr, x, 100)
cairo_show_text(cr, "centered text")cairo_text_path()
adds text outlines to the current path instead of rendering them directly. This
allows you to stroke text outlines, fill with gradients, or use text as a
clipping mask.
Any Conky variable can be used from Lua through
conky_parse(). The result is always a string — use tonumber() when you need
numeric values for calculations or drawing:
local cpu = tonumber(conky_parse("${cpu}"))
local ram = tonumber(conky_parse("${memperc}"))This is the basis for data-driven visualizations — bars, gauges, graphs, and color indicators that respond to live system data.
Cairo supports linear and radial gradients through pattern functions.
local gradient = cairo_pattern_create_linear(0, 0, 200, 0) -- left to right
cairo_pattern_add_color_stop_rgba(gradient, 0, 0.2, 0.6, 1, 1) -- blue at start
cairo_pattern_add_color_stop_rgba(gradient, 0.5, 1, 0.9, 0.2, 1) -- yellow at middle
cairo_pattern_add_color_stop_rgba(gradient, 1, 0.9, 0.2, 0.2, 1) -- red at end
cairo_rectangle(cr, 0, 0, 200, 30)
cairo_set_source(cr, gradient)
cairo_fill(cr)
cairo_pattern_destroy(gradient)local gradient = cairo_pattern_create_radial(100, 100, 10, 100, 100, 80)
cairo_pattern_add_color_stop_rgba(gradient, 0, 1, 1, 1, 1) -- white center
cairo_pattern_add_color_stop_rgba(gradient, 0.6, 0.4, 0.4, 0.5, 1) -- muted midtone
cairo_pattern_add_color_stop_rgba(gradient, 1, 0.1, 0.1, 0.1, 1) -- dark edge
cairo_arc(cr, 100, 100, 80, 0, 2 * math.pi)
cairo_set_source(cr, gradient)
cairo_fill(cr)
cairo_pattern_destroy(gradient)Warning
Patterns must be destroyed with
cairo_pattern_destroy()
when no longer needed, just like surfaces and contexts. Since draw hooks run
on every update, failing to destroy patterns causes a
resource leak — Conky will
consume more and more memory over time until it's killed by the system or it freezes your desktop if your cgroups aren't configured to limit it.
Cairo supports affine transformations on the coordinate system. These are useful for rotating elements, scaling drawings, or positioning complex groups of shapes.
cairo_save(cr)
cairo_translate(cr, 100, 100) -- move origin to center
cairo_rotate(cr, math.pi / 4) -- rotate 45 degrees
cairo_rectangle(cr, -25, -25, 50, 50) -- centered at new origin
cairo_stroke(cr)
cairo_restore(cr)Key functions:
-
cairo_translate(cr, tx, ty)— shift the origin -
cairo_rotate(cr, angle)— rotate (radians) -
cairo_scale(cr, sx, sy)— scale axes -
cairo_identity_matrix(cr)— reset to default
For more complex transformations, you can apply an arbitrary transformation matrix directly:
local matrix = cairo_matrix_t:create()
cairo_matrix_init(matrix, sx, shy, shx, sy, tx, ty) -- full affine matrix
cairo_transform(cr, matrix) -- multiply with current transform
-- or
cairo_set_matrix(cr, matrix) -- replace current transform entirelyAlways wrap transforms in cairo_save()/cairo_restore() to avoid affecting
subsequent drawing code. It's much simpler and less error-prone to save and
restore state than to try and manually undo a sequence of transforms.
Clipping
restricts drawing to a region. Build a path and call cairo_clip() — all
subsequent drawing is masked to that area:
cairo_save(cr)
cairo_arc(cr, 100, 100, 60, 0, 2 * math.pi)
cairo_clip(cr)
-- everything drawn here is clipped to the circle
cairo_paint(cr) -- fills the clipped area with current source
cairo_restore(cr)These examples combine the concepts above with live Conky data to create practical widgets. They're meant as starting points — adapt them to your setup.
Tip
When developing visual widgets, temporarily replace conky_parse() calls
with hardcoded values so you can test specific states without waiting for
real system conditions:
-- local cpu = tonumber(conky_parse("${cpu}"))
local cpu = 85 -- test the "red" thresholdA vertical bar that fills based on CPU usage, with color changing at thresholds:
local cpu = tonumber(conky_parse("${cpu}"))
local bar_x, bar_y = 20, 20
local bar_w, bar_h = 20, 150
local fill_h = bar_h * cpu / 100
-- Background
cairo_rectangle(cr, bar_x, bar_y, bar_w, bar_h)
cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.8)
cairo_fill(cr)
-- Filled portion (draws from bottom up)
-- Check highest threshold first: if cpu > 50 came first, values
-- like 90 would match it and never reach the > 80 check.
if cpu > 80 then
cairo_set_source_rgba(cr, 1, 0.2, 0.2, 1) -- red
elseif cpu > 50 then
cairo_set_source_rgba(cr, 1, 0.8, 0.2, 1) -- yellow
else
cairo_set_source_rgba(cr, 0.2, 0.8, 0.4, 1) -- green
end
cairo_rectangle(cr, bar_x, bar_y + bar_h - fill_h, bar_w, fill_h)
cairo_fill(cr)
-- Border
cairo_rectangle(cr, bar_x, bar_y, bar_w, bar_h)
cairo_set_source_rgba(cr, 1, 1, 1, 0.3)
cairo_set_line_width(cr, 1)
cairo_stroke(cr)The scaling formula fill_h = bar_h * value / max_value works for any range,
not just percentages. For example, a temperature bar from 0–100°C:
local temp = tonumber(conky_parse("${hwmon 1 temp 1}"))
local max_temp = 100
local fill_h = bar_h * temp / max_tempAn arc-based gauge that shows memory usage:
local mem = tonumber(conky_parse("${memperc}"))
local cx, cy, radius = 100, 120, 50
local start_angle = math.pi * 0.75 -- 7 o'clock
local end_angle = math.pi * 2.25 -- 5 o'clock
local sweep = end_angle - start_angle
local value_angle = start_angle + sweep * mem / 100
-- Background arc
cairo_set_line_width(cr, 8)
cairo_set_source_rgba(cr, 0.3, 0.3, 0.3, 0.6)
cairo_arc(cr, cx, cy, radius, start_angle, end_angle)
cairo_stroke(cr)
-- Value arc
cairo_set_source_rgba(cr, 0.3, 0.7, 1, 1)
cairo_arc(cr, cx, cy, radius, start_angle, value_angle)
cairo_stroke(cr)
-- Label
cairo_select_font_face(cr, "Mono",
CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD)
cairo_set_font_size(cr, 18)
cairo_set_source_rgba(cr, 1, 1, 1, 1)
local label = mem .. "%"
local extents = cairo_text_extents_t:create()
cairo_text_extents(cr, label, extents)
cairo_move_to(cr, cx - extents.width / 2, cy + extents.height / 2)
cairo_show_text(cr, label)
cairo_stroke(cr)Uses transforms to draw clock hands at the correct angles:
local cx, cy, radius = 100, 100, 80
-- Clock face
cairo_set_source_rgba(cr, 0.15, 0.15, 0.15, 0.9)
cairo_arc(cr, cx, cy, radius, 0, 2 * math.pi)
cairo_fill(cr)
-- Hour marks
cairo_set_source_rgba(cr, 1, 1, 1, 0.6)
cairo_set_line_width(cr, 2)
for i = 1, 12 do
local angle = i * math.pi / 6 -- 30 degrees per hour
cairo_move_to(cr, cx + (radius - 10) * math.sin(angle),
cy - (radius - 10) * math.cos(angle))
cairo_line_to(cr, cx + (radius - 2) * math.sin(angle),
cy - (radius - 2) * math.cos(angle))
cairo_stroke(cr)
end
-- Hands (using current time)
local hours = tonumber(os.date("%I"))
local mins = tonumber(os.date("%M"))
local secs = tonumber(os.date("%S"))
local function draw_hand(angle, length, width)
cairo_save(cr)
cairo_translate(cr, cx, cy)
cairo_rotate(cr, angle)
cairo_set_line_width(cr, width)
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND)
cairo_move_to(cr, 0, 0)
cairo_line_to(cr, 0, -length)
cairo_stroke(cr)
cairo_restore(cr)
end
-- Hour hand
cairo_set_source_rgba(cr, 1, 1, 1, 0.9)
draw_hand((hours + mins / 60) * math.pi / 6, radius * 0.5, 4)
-- Minute hand
cairo_set_source_rgba(cr, 1, 1, 1, 0.8)
draw_hand((mins + secs / 60) * math.pi / 30, radius * 0.7, 2)
-- Second hand
cairo_set_source_rgba(cr, 1, 0.3, 0.3, 0.8)
draw_hand(secs * math.pi / 30, radius * 0.75, 1)Using a loop to draw a bar for each CPU core:
local num_cores = 8
local bar_w, bar_h = 12, 100
local spacing = 4
local start_x, start_y = 20, 30
for i = 1, num_cores do
local usage = tonumber(conky_parse("${cpu cpu" .. i .. "}"))
local x = start_x + (i - 1) * (bar_w + spacing)
local fill_h = bar_h * usage / 100
-- Background
cairo_rectangle(cr, x, start_y, bar_w, bar_h)
cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.6)
cairo_fill(cr)
-- Fill
cairo_set_source_rgba(cr, 0.3, 0.7, 1, 0.9)
cairo_rectangle(cr, x, start_y + bar_h - fill_h, bar_w, fill_h)
cairo_fill(cr)
endCairo anti-aliases by default, which smooths edges but can cause lines to appear blurry or shapes to overlap by a fraction of a pixel.
For straight lines, the most common fix is to place coordinates at pixel centers by offsetting by 0.5. Integer coordinates land on pixel edges, causing a 1px line to smear across two pixels:
-- Blurry: lands on pixel edge
cairo_move_to(cr, 10, 50)
cairo_line_to(cr, 190, 50)
-- Sharp: lands on pixel center
cairo_move_to(cr, 10.5, 50.5)
cairo_line_to(cr, 190.5, 50.5)For curves and complex shapes, the 0.5 offset isn't practical. If anti-aliasing causes unwanted visual artifacts, disable it entirely:
cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE)Drawing 100 rectangles, then 100 circles (each followed by a stroke or fill) is faster than interleaving one of each 100 times. Batching similar operations allows the GPU to fill shapes while the CPU calculates the next batch.
- Cairo Manual — full API reference
- Cairo Tutorial — official getting started guide
- Cairo Cookbook — recipes and patterns
- Conky Variables — data you can feed into your drawings