-
-
Notifications
You must be signed in to change notification settings - Fork 659
Lua: Lua Basics
Lua is often used in Conky to create powerful, dynamic widgets and displays. Here's a breakdown of core Lua concepts with examples relevant to system monitoring.
For comprehensive details, refer to the Lua Reference Manual.
In Lua, variables are dynamically typed, i.e. you don't need to declare a type explicitly:
local cpu_cores = 7 -- number
local cpu_usage = 37.5 -- number (percentage)
local hostname = "my-computer" -- string
local is_charging = true -- booleanVariables declared with local are scoped to the enclosing block (function,
loop, or file). Variables without local are global. Prefer local by default
— see Scope and Lifetime for details and when globals are
useful.
You can also reassign differently typed values to existing variables:
is_charging = "absolutely"Lua allows assigning multiple variables in a single statement:
local x, y = 100, 200
local r, g, b, a = 1, 1, 1, 1If there are fewer values than variables, the extras get nil. If there are
more values than variables, the extras are discarded.
conky_parse() always returns strings, so you'll frequently need to convert
between types:
local cpu_str = conky_parse("${cpu}") -- "42"
local cpu_num = tonumber(cpu_str) -- 42
local label = tostring(cpu_num) -- "42"tonumber() returns
nil if conversion fails, so it doubles as validation:
local value = tonumber("not a number") -- niltype() returns the
type of a value as a string:
type(42) -- "number"
type("hello") -- "string"
type(nil) -- "nil"
type({}) -- "table"nil represents the absence of a value. Uninitialized variables are nil, and
assigning nil to a variable effectively deletes it.
The or operator is commonly used to provide defaults, because nil and
false are the only values that are "falsy" in Lua:
local name = user_name or "unknown"
local threshold = config.threshold or 80
local items = items or {} -- initialize once, reuse on later callsLua provides standard operators for arithmetic, comparison, and logic.
local a = 10
local b = 3
local sum = a + b -- 13
local difference = a - b -- 7
local product = a * b -- 30
local division = a / b -- 3.333...
local integral_division = a // b -- 3 (floor division)
local mod = a % b -- 1 (modulo)
local exponentiation = a ^ b -- 1000 (10^3)Most operators listed here are pretty common and perform math operations you'd
expect. It might be worth elaborating integral division operator // though: it
rounds the result towards -∞ (3.7 result becomes 3, -20.2 result
becomes -21).
local x = 5
local y = 10
print(x == y) -- false (equality)
print(x ~= y) -- true (inequality)
print(x < y) -- true
print(x > y) -- false
print(x <= y) -- true
print(x >= y) -- falseInequality operator isn't != like in most other programming/scripting
languages, but other than that all operators behave as you'd expect: if left and
right side satisfy relation, the whole expression is evaluated as true, if
not, it's false.
local t = true
local f = false
print(t and f) -- false
print(t or f) -- true
print(not t) -- falseand and or return one of their operands rather than just true/false,
which is what makes the default-value idiom value or default work — if value
is nil or false, the or expression returns default.
In Lua string concatenation is achieved with the .. operator.
local first = "Conky"
local second = "Rocks"
local result = first .. " " .. second
print(result) -- Conky RocksNumbers are automatically converted to strings when concatenated:
local status = "CPU: " .. 42 .. "%" -- "CPU: 42%"local a = 6 -- 110 in binary
local b = 3 -- 011 in binary
print(a & b) -- 2 (bitwise AND)
print(a | b) -- 7 (bitwise OR)
print(a ~ b) -- 5 (bitwise XOR)
print(~a) -- -7 (bitwise NOT)
print(a << 1) -- 12 (shift left)
print(a >> 1) -- 3 (shift right)Tables are Lua's only data structure — they serve as arrays, dictionaries,
objects, and modules. A table is an associative array: keys can be any value
except nil.
When keys are consecutive integers starting from 1, the table behaves as an
array. The # operator returns the length of the array portion:
local drives = {"sda", "sdb", "sdc"}
print(#drives) -- 3local drives = {"sda", "sdb"}
local user_info = {name = "john", uptime = "2h 15m"}
-- Array operations
table.insert(drives, "sdc") -- append to end
table.insert(drives, 1, "nvme0") -- insert at position 1
table.remove(drives, 2) -- remove element at position 2
print(#drives) -- current length
-- Dictionary operations
user_info.shell = "bash" -- dot notation
user_info["desktop"] = "sway" -- bracket notation (equivalent)
print(user_info.name) -- "john"Dot notation (table.key) is shorthand for bracket notation
(table["key"]). Use brackets when keys are dynamic or contain special
characters.
You can also write directly to specific indices:
local values = {}
values[1] = 42
values[2] = 78
values[5] = 100 -- indices 3 and 4 will be nilAccessing a nonexistent index returns nil (not an error):
local t = {10, 20, 30}
print(t[5]) -- nil
print(t.foo) -- nil-- Array-style: ipairs iterates integer keys 1, 2, 3, ...
local interfaces = {"eth0", "wlan0", "lo"}
for i, name in ipairs(interfaces) do
print(i .. ": " .. name)
end
-- Dictionary-style: pairs iterates all keys (in undefined order)
local sensors = {CPU = 60, GPU = 70, HDD = 45}
for key, value in pairs(sensors) do
print(key .. ": " .. value .. "°C")
endControl structures help dynamically adapt Conky displays based on system state.
if cpu_usage > 80 then
print("High CPU usage!")
elseif cpu_usage > 50 then
print("Moderate CPU usage")
else
print("CPU is normal")
endCaution
It's elseif (one word), not else if. Using else if creates a nested
if-statement which requires its own end.
Numeric for loop with start, end, and optional step:
for core = 1, 4 do
print("Core " .. core .. ": " .. conky_parse("${cpu cpu" .. core .. "}") .. "%")
end
-- With step: count by 2
for i = 0, 10, 2 do
print(i) -- 0, 2, 4, 6, 8, 10
endIterator for loop using in:
local colors = {"red", "green", "blue"}
for index, color in ipairs(colors) do
print(index .. ": " .. color)
endSee Iterating Tables for more on ipairs and pairs.
local temp = 70
while temp > 60 do
print("Fan cooling... Current temp: " .. temp)
temp = temp - 1
endFunctions help structure complex display logic, like calculating average load or formatting time.
function display_greeting(user)
print("Welcome back, " .. user)
end
display_greeting("Alice")Functions can take any number of parameters. If fewer arguments are passed than
parameters, the extras are nil. If more are passed, the extras are discarded:
function greet(name, title)
title = title or "User" -- default value if not provided
print("Hello, " .. title .. " " .. name)
end
greet("Alice", "Dr.") -- Hello, Dr. Alice
greet("Bob") -- Hello, User BobFunctions can be declared local to limit their scope, just like variables:
local function helper(x)
return x * 2
endThis is generally preferred inside scripts to avoid polluting the global
namespace. The exception is hook functions like conky_main which must be global
so Conky can find them.
Functions can return multiple values:
function min_max(values)
local min, max = values[1], values[1]
for i = 2, #values do
if values[i] < min then min = values[i] end
if values[i] > max then max = values[i] end
end
return min, max
end
local lo, hi = min_max({23, 67, 12, 89, 45})
print(lo, hi) -- 12 89Functions that take many settings are easier to work with when you pass a single table instead of many positional arguments. Extract fields inside the function and provide defaults for any that might be missing:
function draw_indicator(settings)
local x = settings.x or 0
local y = settings.y or 0
local width = settings.width or 20
local height = settings.height or 100
local color = settings.color or {1, 1, 1, 1}
-- draw using these values...
end
-- Call with only the fields you care about:
draw_indicator({x = 50, y = 10, height = 200})
-- width defaults to 20, color defaults to whiteThis is the dominant configuration pattern in Conky Lua scripts — it keeps function calls readable and makes it easy to add optional settings without breaking existing callers.
Tip
When a function call has many arguments, you can split it across multiple lines and add comments to each:
draw_indicator({
x = 750, -- horizontal position
y = 400, -- vertical position
width = 30, -- bar width
height = 100, -- bar height
})Variables declared with local exist only within their enclosing block —
a function body, loop, if-branch, or file. Once the block ends, the variable
is gone:
function convert_to_gb(kb)
local gb = kb / 1048576 -- only exists inside this function
return string.format("%.2f GB", gb)
end
print(gb) -- nil, gb doesn't exist hereVariables without local are global — they persist for the entire lifetime
of the Lua state, which in Conky means until Conky exits. This has an important
practical consequence: because draw hooks are called repeatedly on every update,
global variables retain their values between calls.
Use local by default. It's faster, avoids accidental name collisions, and
makes code easier to follow. Reserve globals for state that intentionally needs
to survive across updates.
Global variables are how you track data over time in Conky. A common pattern is building a history table that accumulates values across draw hook calls:
-- Global: persists across updates, initialized once on first run
cpu_history = cpu_history or {}
local max_entries = 60
function conky_main()
if conky_window == nil then return end
local cpu = tonumber(conky_parse("${cpu}"))
table.insert(cpu_history, cpu)
-- Keep only the last max_entries values
if #cpu_history > max_entries then
table.remove(cpu_history, 1)
end
-- cpu_history now contains up to 60 recent CPU readings
-- which can be used for drawing graphs, calculating averages, etc.
endThe cpu_history = cpu_history or {} idiom initializes the table on the first
call and reuses the existing one on subsequent calls. This works because global
variables default to nil, and nil or {} evaluates to {}.
Functions in Lua are values — they can be stored in variables, passed as arguments, and returned from other functions. A closure is a function that captures variables from its enclosing scope:
function make_threshold_checker(limit)
return function(value)
return value >= limit
end
end
local is_overheating = make_threshold_checker(85)
local cpu_temp = tonumber(conky_parse("${hwmon 1 temp 1}"))
print(is_overheating(cpu_temp)) -- true if temp >= 85This is useful for creating reusable, configurable functions without repeating threshold logic.
Lua includes a small but practical standard library. These are the modules most commonly used in Conky scripts.
Beyond the .. operator, Lua provides a
string library for
formatting, searching, and extracting text.
Note
String functions can be called as methods on string values using the colon
syntax: str:sub(1, 5) is equivalent to string.sub(str, 1, 5). This
applies to all string.* functions.
Works like C's printf — format placeholders in a template string:
local text = string.format("CPU: %d%% | RAM: %.1f GB", 42, 7.832)
-- "CPU: 42% | RAM: 7.8 GB"
-- Common format specifiers:
-- %d integer
-- %f float (%.1f for 1 decimal, %.2f for 2, etc.)
-- %s string
-- %02d zero-padded integer (e.g. "07")
-- %% literal percent signExtracts a substring by position:
local path = "/home/user/.config/conky"
local last_five = string.sub(path, -5) -- "conky"
local first_five = string.sub(path, 1, 5) -- "/home"Search for patterns in strings. Lua patterns are similar to regex but simpler:
-- string.find returns start and end positions (or nil)
local s, e = string.find("CPU: 42%", "%d+")
print(s, e) -- 6 7
-- string.match returns the matched text (or nil)
local number = string.match("CPU: 42%", "%d+")
print(number) -- "42"
-- Captures with parentheses
local key, value = string.match("temp=85", "(%w+)=(%d+)")
print(key, value) -- "temp" "85"See the pattern reference
for the full list of character classes (%d, %a, %s, etc.), quantifiers
(+, *, -, ?), anchors (^, $), bracket classes ([...]), and
escaping rules.
Tip
string.find accepts an optional start position as a third argument. You can
chain calls to walk through all matches in a string by passing the previous
end position + 1:
local text = "12 °C, 45 °C, 78 °C"
local pos = 1
while true do
local s, e = string.find(text, "%d+", pos)
if not s then break end
print(string.sub(text, s, e)) -- "12", "45", "78"
pos = e + 1
endFor simple cases, string.gmatch() is usually easier.
Iterates over all matches in a string:
local line = "eth0 wlan0 lo"
for iface in string.gmatch(line, "%S+") do
print(iface) -- "eth0", "wlan0", "lo"
endstring.gmatch also works as a string splitter — match everything that isn't
the delimiter:
local csv = "one,two,three"
local parts = {}
for field in string.gmatch(csv, "[^,]+") do
table.insert(parts, field)
end
-- parts = {"one", "two", "three"}The math library provides
standard mathematical functions. The most relevant ones for Conky drawing are:
math.pi -- 3.14159... (π constant)
math.sin(angle) -- sine (angle in radians)
math.cos(angle) -- cosine
math.floor(3.7) -- 3 (round down)
math.ceil(3.2) -- 4 (round up)
math.max(10, 20, 5) -- 20
math.min(10, 20, 5) -- 5
math.abs(-42) -- 42Lua doesn't have a built-in clamp, but math.min and math.max compose into
one:
local function clamp(value, lo, hi)
return math.max(lo, math.min(hi, value))
end
clamp(150, 0, 100) -- 100
clamp(-5, 0, 100) -- 0Degrees to radians (needed for Cairo arc functions):
local function deg_to_rad(deg)
return deg * math.pi / 180
endThe os library provides
date/time functions:
-- Current time components using format strings
local hours_24 = os.date("%H") -- "14" (24-hour: 00–23)
local hours_12 = os.date("%I") -- "02" (12-hour: 01–12)
local mins = os.date("%M") -- "30"
local secs = os.date("%S") -- "45"
local weekday = os.date("%A") -- "Monday"
local date = os.date("%Y-%m-%d") -- "2025-03-15"
-- Timestamp (seconds since epoch)
local now = os.time()See the C strftime documentation for all available format specifiers.
The io library can read
files and capture command output. io.popen() runs a shell command and returns
a file handle:
-- Run a command and read its output
local handle = io.popen("whoami")
local result = handle:read("*a") -- read all output
handle:close()
print(result) -- "username\n"
-- Read output line by line
local handle = io.popen("df -h /")
for line in handle:lines() do
print(line)
end
handle:close()Warning
Always call handle:close() on file handles to avoid
resource leaks. Commands run
via io.popen() execute on every Conky update if placed inside a draw hook,
which can be expensive. Consider using a timer pattern with the
${updates} variable to run commands
less frequently:
local updates = tonumber(conky_parse("${updates}"))
if updates % 30 == 0 then -- every 30th update
cached_result = io.popen("expensive-command"):read("*a")
end