Here's a quick Lua script I wrote for the system monitor Conky to display Bitcoin network/mining status, account balance, transaction history and MtGox ticker data. It's a little messy but it works.
Requires
Json4Lua and
LuaSocket, talks to an instance of bitcoin[d] on the local system.
--[[ Bitcoin Lua script for Conky, by Rena. Build #92704.
Usage: Add a "lua_load" line to your .conkyrc to load this script,
then add variables to your config:
${lua bitcoin_balance [account name]}
(You can also use lua_bar, lua_graph, etc. A graph might be interesting for
those who perform a lot of transactions!)
${lua bitcoin_info format string} (or lua_parse)
Format string is any string (can contain line breaks), in which any instance of
%foo% is replaced with the value of the variable foo output by
"bitcoin getinfo". Cannot contain multiple spaces; use ${tab} for alignment.
(This is a limitation of how Conky passes strings to Lua.)
${lua bitcoin_transactions account num format} (or lua_parse)
List up to 'num' recent transactions (most recent first) on an account. Account
name must be quoted if it contains a space, can be * for all accounts. Format
works same as bitcoin_info.
${if_empty ${lua bitcoin_running}}...${else}...${endif}
To display text only if Bitcoin is/isn't running. bitcoin_running returns an
empty string when Bitcoin running and '0' when it isn't, so that if_empty can
be used like a normal 'if'.
${lua mtgox_ticker format string} (or lua_parse)
Works same as bitcoin_info. Field names are those under 'ticker', so e.g.
write %high%, not %ticker.high%.
Send bug reports, suggestions, flames, cat photos and whatever else
to ('moc.liamg\064rekcahrepyh'):reverse().
Send spare change to 15R7dpxfWuDrQY9CtUzMsg6A7sKMdWbTd3 why not.
Version history:
-92704:
-Added mtgox_ticker() and ability to fetch JSON URLs. (Adds dependency on
LuaSocket.)
-Changed keypoololdest to a formatted date. Raw timestamp is keypoololdest_raw.
-92432: Initial release. ]]
local json = require('json') --http://json.luaforge.net/
local http = require('socket.http') --http://luaforge.net/projects/luasocket/
--options
local UpdateFreq = 5*60 --only update once every this many seconds
local SignPlus = '${color3}+' --prefix for positive numbers
local SignMinus = '${color2}-' --prefix for negative numbers
local DateFmt = '%H:%M:%S %y/%m/%d' --strftime date format
local FeeEmpty = '-.--' --string to display for %fee% when empty
--state
local Account = {} --indexed by account name
local LastQuery = 0 --last time bitcoin was called
local QueryCache = {}
local URLCache = {}
--[[ Send a query to bitcoin.
This function caches queries so that bitcoin is only called once per UpdateFreq
to avoid creating a lot of needless overhead by spawning processes over and
over. ]]
local function bitcoin_query(query)
local now = os.time()
LastQuery = now
local cache = QueryCache[query]
if cache then
if os.difftime(now, cache.Time) < UpdateFreq then
return cache.Result
end
else
QueryCache[query] = {}
cache = QueryCache[query]
end
--hack: redirect stderr to stdout and look for either an error message or
--what looks like JSON data.
local data, found = '', false
local app = io.popen('bitcoin ' .. query .. ' 2>&1')
for line in app:lines() do
if line:sub(1,6) == 'error:' then
if line == "error: no response from server"
or line == "error: couldn't connect to server" then
Running = false
return nil, "Bitcoin not running"
else
Running = false --presumably...
io.stderr:write("Bitcoin: " .. line .. "\n")
return nil, line
end
elseif found then data = data .. line
elseif line:sub(1,1) == '{' or line:sub(1,1) == '[' then
found = true
data = data .. line
end
end
app:close()
Running = true --well it must be, we just talked to it.
cache.Time, cache.Result = now, json.decode(data)
return cache.Result
end
--[[ Fetch JSON data from a HTTP URL.
This function caches queries in URLCache so that the same URL isn't accessed
more than once per UpdateFreq. This is a simple naive cache that only works if
the URL is exactly the same each time.
On success, returns JSON data as table.
On failure, returns nil and error message. ]]
local function get_json_url(url)
if not URLCache[url] then URLCache[url] = {LastQuery=0} end
local now = os.time()
if (now - URLCache[url].LastQuery) < UpdateFreq then
return URLCache[url].LastResult
end
URLCache[url].LastQuery = now
local text, status = http.request(url)
if (not text) or (status < 200) or (status >= 300) then
URLCache[url].LastResult = 'Error: ' .. tostring(status)
return nil, URLCache[url].LastResult
end
local err
URLCache[url].LastResult, err = json.decode(text)
if URLCache[url].LastResult then return URLCache[url].LastResult
else return nil, err
end
end
--[[ Conky splits args at space, regardless of quotes. So if arg begins with a
quote, look for end quote and piece the string together.
note that this fails if there are multiple spaces in a row since Conky
compresses them into one. ]]
local function readquotedarg(args, idx)
idx = idx or 1
local str = ''
if args[idx] and args[idx]:sub(1,1) == '"' then
if args[idx]:sub(-1) == '"' then
return table.remove(args, idx):sub(1, -2)
end
str = table.remove(args, idx):sub(2)
while args[idx] and args[idx]:sub(-1) ~= '"' do
str = str .. ' ' .. table.remove(args, idx)
end
return str .. ' ' .. table.remove(args, idx):sub(1, -2)
elseif args[idx] then return table.remove(args, idx)
else return nil
end
end
--[[ Check if Bitcoin is running. ]]
function conky_bitcoin_running(...)
local now = os.time()
if Running == nil --don't know
or os.difftime(now, LastQuery) >= UpdateFreq then
bitcoin_query('getinfo') --make some random query to find out
end
return Running and '' or '0' --for Conky if_empty
end
--[[ Balance for particular account. ]]
function conky_bitcoin_balance(...)
local args = {...}
local account = readquotedarg(args) or "Your Address" --default account name
--create status table for this account if not done already.
if not Account[account] then Account[account] = {
LastUpdate = 0, LastBalance = 0
} end
--Query bitcoin to find account balance.
local info = bitcoin_query('listaccounts')
--display negative balance to signal error, since some Conky functions
--expect a number.
if not info then Account[account].LastBalance = -1 end
Account[account].LastUpdate = now
return tonumber(Account[account].LastBalance)
end
--[[ Recent transaction list. ]]
function conky_bitcoin_transactions(...)
local args = {...}
local account = readquotedarg(args) or "Your Address"
local numshow = tonumber(args[1]) or 5
local fmt = table.concat(args, ' ', 2) --format string
local info = bitcoin_query(('listtransactions "%s" %d'):
format(account, numshow))
--add some convenient fields to info
for i = 1,#info do
if not info[i].sign then --cache means these may already be set
info[i].sign = (info[i].amount > 0) and
SignPlus or SignMinus
info[i].absamount = math.abs(info[i].amount)
info[i].absfee = info[i].fee and math.abs(info[i].fee) or
FeeEmpty
info[i].rawtime = info[i].time
info[i].time = os.date(DateFmt, tonumber(info[i].rawtime))
info[i].fee = info[i].fee or FeeEmpty
end
end
--transactions seem to be listed oldest first, so reverse iterate.
local res = ''
for i = math.min(numshow,#info), 1, -1 do
res = res .. fmt:gsub('%%([%w_]+)%%', info[i]) .. '\n'
end
return res
end
--[[ General info. ]]
function conky_bitcoin_info(...)
local args, info = {...}, {}
local fmt = table.concat(args, ' ') --format string
--query Bitcoin and parse results.
local info = bitcoin_query('getinfo')
if not info.keypoololdest_raw then
info.keypoololdest_raw = info.keypoololdest
info.keypoololdest = os.date(DateFmt, tonumber(info.keypoololdest))
end
--generate and return output string.
LastInfoStr = fmt:gsub('%%([%w_]+)%%', info)
LastInfoUpdate = now
return LastInfoStr
end
--[[ MtGox info. ]]
function conky_mtgox_ticker(...)
local args, info = {...}, {}
local fmt = table.concat(args, ' ') --format string
local data, err = get_json_url('http://mtgox.com/code/data/ticker.php')
if data then MtGoxLastResult = fmt:gsub('%%([%w_]+)%%', data.ticker)
else MtGoxLastResult = tostring(err)
end
return MtGoxLastResult
end
Example config:
${color1}BTC${hr}
${if_empty ${lua bitcoin_running}}${lua_parse bitcoin_info Balance:${tab 40}${color0}%balance%${color1}
Tx Fee:${tab 40}${color0}%paytxfee%${color1}
Connections:${tab 10}${color0}%connections%${color1}
Blocks:${tab 40}${color0}%blocks%${color1}
Oldest:${tab 40}${color0}%keypoololdest%${color1}
Difficulty:${tab 20}${color0}%difficulty%${color1}
Version:${tab 40}${color0}%version%${color1}}
${lua_parse bitcoin_transactions * 5 %sign%%absamount% %confirmations%${tab 16}%absfee%${tab 16}%time%}
${else}${color0}NO DATA${endif}
(Ugly ain't it?
But the result looks quite nice! Screenshot
here as it's too big to attach to the post. :p)