-
-
Notifications
You must be signed in to change notification settings - Fork 659
Lua: Shell Integration
Conky provides many built-in variables for system data, but sometimes you need information from external commands — disk usage details, package counts, custom sensor readings, or data from web APIs.
Lua's io.popen()
function runs shell commands and captures their output. This page covers how to
use it effectively in Conky, including timed execution and parsing command
output.
io.popen() runs a
shell command and returns a file handle for reading its output:
local handle = io.popen("df -h")
local output = handle:read("*a") -- read all output as a single string
handle:close()Warning
Always call handle:close()
on file handles to avoid
resource leaks.
Commands placed directly in a draw hook run on every Conky update — which can mean every second. For anything more expensive than a trivial command, this is wasteful and can cause noticeable lag.
The solution is a timer based on Conky's
${updates} variable, which counts how
many times the window has been redrawn. Using the modulo operator, you can run
code only every N-th update:
local updates = tonumber(conky_parse("${updates}"))
local interval = 30 -- every 30th update
if updates % interval == 0 then
-- runs once every 30 updates
local handle = io.popen("df -h")
cached_output = handle:read("*a")
handle:close()
end
-- use cached_output for drawing (available every update)The result is stored in a global variable (cached_output) so it remains
available on updates where the command doesn't run. This is analogous to
${execi 30 command} in conky.text.
Caution
The interval is measured in update cycles, not seconds. It only
corresponds to seconds if
update_interval is set
to 1. With update_interval = 2, an interval of 30 means every 60 seconds.
Note
update_interval is a goal, not guaranteed to be precise. Each update cycle
includes Conky's own internal processing, so the actual interval is always
slightly longer than configured. The drift is small (sub-second) but
accumulates over time. Use time-based intervals
if precision matters.
For cases where precise real-time intervals matter (e.g. polling an API exactly
every 5 minutes), use os.time()
instead of the update counter:
last_run = last_run or 0
local interval_seconds = 300 -- 5 minutes
function conky_main()
if conky_window == nil then return end
local now = os.time()
if now - last_run >= interval_seconds then
local handle = io.popen("curl -s 'https://api.example.com/data'")
cached_response = handle:read("*a")
handle:close()
last_run = now
end
-- draw using cached_response
endThis runs the command on the first update (since last_run starts at 0) and
then at real-time intervals regardless of how fast or slow Conky's update cycle
is.
If you need to run a command once when Conky starts (e.g. to populate initial
data before the first timer fires), the preferred approach is to use the
lua_startup_hook:
-- conky.conf
conky.config = {
lua_load = "script.lua",
lua_startup_hook = "startup",
lua_draw_hook_post = "main",
}-- script.lua
function conky_startup()
local handle = io.popen("df -h")
cached_output = handle:read("*a")
handle:close()
end
function conky_main()
if conky_window == nil then return end
local updates = tonumber(conky_parse("${updates}"))
if updates % 30 == 0 then
local handle = io.popen("df -h")
cached_output = handle:read("*a")
handle:close()
end
-- draw using cached_output
endIn cases where the startup logic is tightly coupled with the draw hook (e.g. it's part of a larger function that also needs to run periodically), a flag variable can be used instead:
run_immediately = true
function conky_main()
if conky_window == nil then return end
local updates = tonumber(conky_parse("${updates}"))
if updates % 30 == 0 or run_immediately then
local handle = io.popen("df -h")
cached_output = handle:read("*a")
handle:close()
run_immediately = nil
end
-- draw using cached_output
endhandle:read("*a")
returns the entire command output as a single string. This is the simplest
approach and works well for short outputs:
local handle = io.popen("hostname")
local name = handle:read("*a"):gsub("\n$", "") -- trim trailing newline
handle:close()For multi-line output, iterating with
handle:lines()
processes one line at a time without loading everything into memory:
local handle = io.popen("df -h")
local partitions = {}
for line in handle:lines() do
local mount = string.match(line, "%%(%s+/.*)")
if mount then
table.insert(partitions, mount)
end
end
handle:close()The real power comes from combining
io.popen() with
Lua's
pattern matching to extract
structured data from command output.
Parse df -h output into a table of mount points with their usage percentages:
local handle = io.popen("df -h")
local disks = {}
for line in handle:lines() do
local used, mount = string.match(line, "(%d+)%%%s+(/.*)$")
if used and mount then
table.insert(disks, {mount = mount, percent = tonumber(used)})
end
end
handle:close()
-- disks is now:
-- { {mount = "/", percent = 28}, {mount = "/home", percent = 52}, ... }The pattern "(%d+)%%%s+(/.*)$" breaks down as:
-
(%d+)— capture one or more digits (the usage percentage) -
%%— literal%character -
%s+— one or more spaces -
(/.*)$— capture everything from/to end of line (the mount point)
local handle = io.popen("ip -br addr")
local interfaces = {}
for line in handle:lines() do
local name, state, addr = string.match(line, "(%S+)%s+(%S+)%s+(.*)")
if name and state == "UP" then
interfaces[name] = addr:match("%S+") or "no address"
end
end
handle:close()curl can be used to fetch data from web APIs or pages:
local updates = tonumber(conky_parse("${updates}"))
if updates % 300 == 0 or run_immediately then
local handle = io.popen('curl -s "https://api.example.com/data"')
cached_response = handle:read("*a")
handle:close()
run_immediately = nil
endTip
Use curl -s (silent mode) to suppress progress output. Pay attention to
quote nesting — if the Lua string uses double quotes, use single quotes for
the shell command arguments (or vice versa).
For scripts that run multiple commands on different intervals, a helper function keeps things clean:
-- Cache table: stores results and timing
local cmd_cache = {}
function run_cached(cmd, interval)
local updates = tonumber(conky_parse("${updates}"))
local entry = cmd_cache[cmd]
if not entry or updates % interval == 0 then
local handle = io.popen(cmd)
local output = handle:read("*a")
handle:close()
cmd_cache[cmd] = {output = output, updated = updates}
return output
end
return entry.output
end
-- Usage:
local disk_info = run_cached("df -h", 30)
local who_info = run_cached("who", 60)