Skip to content

Lua: Lua Basics

Tin Švagelj edited this page Apr 17, 2026 · 1 revision

Lua Syntax and Basic Concepts

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.

Variables and Data Types

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        -- boolean

Variables 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"

Multiple Assignment

Lua allows assigning multiple variables in a single statement:

local x, y = 100, 200
local r, g, b, a = 1, 1, 1, 1

If there are fewer values than variables, the extras get nil. If there are more values than variables, the extras are discarded.

Type Conversion

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")  -- nil

type() returns the type of a value as a string:

type(42)        -- "number"
type("hello")   -- "string"
type(nil)       -- "nil"
type({})        -- "table"

Nil and Default Values

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 calls

Operators and Expressions

Lua provides standard operators for arithmetic, comparison, and logic.

Arithmetic

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).

Relational

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)  -- false

Inequality 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.

Logical

local t = true
local f = false

print(t and f)  -- false
print(t or f)   -- true
print(not t)    -- false

and 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.

String Concatenation

In Lua string concatenation is achieved with the .. operator.

local first = "Conky"
local second = "Rocks"

local result = first .. " " .. second
print(result)  -- Conky Rocks

Numbers are automatically converted to strings when concatenated:

local status = "CPU: " .. 42 .. "%"  -- "CPU: 42%"

Bitwise Operators

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

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)  -- 3

Creating and Manipulating Tables

local 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 nil

Accessing a nonexistent index returns nil (not an error):

local t = {10, 20, 30}
print(t[5])   -- nil
print(t.foo)  -- nil

Iterating Tables

-- 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")
end

Control Structures

Control structures help dynamically adapt Conky displays based on system state.

If-Else Statement

if cpu_usage > 80 then
  print("High CPU usage!")
elseif cpu_usage > 50 then
  print("Moderate CPU usage")
else
  print("CPU is normal")
end

Caution

It's elseif (one word), not else if. Using else if creates a nested if-statement which requires its own end.

For Loop

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
end

Iterator for loop using in:

local colors = {"red", "green", "blue"}
for index, color in ipairs(colors) do
  print(index .. ": " .. color)
end

See Iterating Tables for more on ipairs and pairs.

While Loop

local temp = 70
while temp > 60 do
  print("Fan cooling... Current temp: " .. temp)
  temp = temp - 1
end

Functions

Functions help structure complex display logic, like calculating average load or formatting time.

Defining and Calling Functions

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 Bob

Local Functions

Functions can be declared local to limit their scope, just like variables:

local function helper(x)
  return x * 2
end

This 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.

Multiple Return Values

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  89

Table Parameters

Functions 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 white

This 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
})

Scope and Lifetime

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 here

Variables 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.

Persisting State 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.
end

The 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 {}.

Higher-Order Functions and Closures

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 >= 85

This is useful for creating reusable, configurable functions without repeating threshold logic.

Standard Library

Lua includes a small but practical standard library. These are the modules most commonly used in Conky scripts.

string

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.

string.format()

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 sign

string.sub()

Extracts 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"

string.find() and string.match()

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
end

For simple cases, string.gmatch() is usually easier.

string.gmatch()

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"
end

string.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"}

math Library

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)            -- 42

Lua 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)   -- 0

Degrees to radians (needed for Cairo arc functions):

local function deg_to_rad(deg)
  return deg * math.pi / 180
end

os

The 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.

io

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

Clone this wiki locally