REF remove submodule for core and move all lua files to common subdir

This commit is contained in:
Nathan Dwarshuis 2022-07-20 00:11:03 -04:00
parent 5549d8c95d
commit f6e3ff9574
52 changed files with 3300 additions and 148 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "core"]
path = core
url = gitolite:conky/core.git

View File

@ -49,6 +49,9 @@ properties:
required: [show_temp, show_clock, show_gpu_util, show_mem_util, show_vid_util]
additionalProperties: false
properties:
dev_power:
description: the sysfs path to the graphics card power indicator
type: string
show_temp:
description: show the GPU temp
type: boolean

View File

@ -3,16 +3,14 @@
local conky_dir = debug.getinfo(1).source:match("@?(.*/)")
local subdirs = {
'?.lua',
'drawing/?.lua',
'schema/?.lua',
'core/?.lua',
'core/widget/?.lua',
'core/widget/arc/?.lua',
'core/widget/text/?.lua',
'core/widget/timeseries/?.lua',
'core/widget/rect/?.lua',
'core/widget/line/?.lua',
'src/?.lua',
'src/modules/?.lua',
'src/widget/?.lua',
'src/widget/arc/?.lua',
'src/widget/text/?.lua',
'src/widget/timeseries/?.lua',
'src/widget/rect/?.lua',
'src/widget/line/?.lua',
'lib/share/lua/5.4/?.lua',
'lib/share/lua/5.4/?/init.lua',
}
@ -41,7 +39,7 @@ if i_o.exe_exists('yajsv') then
end
else
validate_config = function(_)
print('WARNING: could not validate config')
i_o.warnf('could not validate config')
return true
end
end
@ -52,16 +50,16 @@ local find_valid_config = function(paths)
local r = i_o.read_file(path)
if r ~= nil then
if validate_config(path) then
i_o.printf('INFO: Using config at %s', path)
i_o.infof('Using config at %s', path)
return path, yaml.load(r)
else
i_o.printf('WARNING: %s did not pass; trying next', path)
i_o.warnf('%s did not pass; trying next', path)
end
else
i_o.printf('INFO: could not find %s; trying next', path)
i_o.infof('could not find %s; trying next', path)
end
end
assert(false, 'ERROR: could not load valid config')
i_o.assertf(false, 'ERROR: could not load valid config')
end
local get_config_dir = function()

1
core

@ -1 +0,0 @@
Subproject commit ac2604709f5344125519913849e9013e1dfea717

View File

@ -1,20 +0,0 @@
local M = {}
M.LEFT_X = 32
M.SECTION_WIDTH = 436
M.CENTER_PAD = 20
M.PANEL_HORZ_SPACING = 10
M.PANEL_MARGIN_X = 20
M.PANEL_MARGIN_Y = 10
M.TOP_Y = 21
M.SIDE_HEIGHT = 1020
M.CENTER_HEIGHT = 220
local margin_width = M.PANEL_MARGIN_X * 2 + M.PANEL_HORZ_SPACING
M.CENTER_LEFT_X = M.LEFT_X + M.SECTION_WIDTH + margin_width
M.CENTER_RIGHT_X = M.CENTER_LEFT_X + M.SECTION_WIDTH + M.CENTER_PAD
M.CENTER_WIDTH = M.SECTION_WIDTH * 2 + M.CENTER_PAD
M.RIGHT_X = M.CENTER_LEFT_X + M.CENTER_WIDTH + margin_width
return M

View File

@ -1,64 +0,0 @@
local M = {}
local color = require 'color'
M.FONT = 'Neuropolitical'
-- text colors
M.HEADER_FG = color.rgb(0xefefef)
M.PRIMARY_FG = color.rgb(0xbfe1ff)
M.CRITICAL_FG = color.rgb(0xff8282)
M.INACTIVE_TEXT_FG = color.rgb(0xc8c8c8)
M.MID_GREY = color.rgb(0xd6d6d6)
M.BORDER_FG = color.rgb(0x888888)
M.PLOT_GRID_FG = color.rgb(0x666666)
M.PLOT_OUTLINE_FG = color.rgb(0x777777)
-- arc bg colors
local GREY2 = 0xbfbfbf
local GREY5 = 0x565656
M.INDICATOR_BG = color.gradient_rgb{
[0.0] = GREY5,
[0.5] = GREY2,
[1.0] = GREY5
}
-- arc/bar fg colors
local PRIMARY1 = 0x99CEFF
local PRIMARY3 = 0x316BA6
M.INDICATOR_FG_PRIMARY = color.gradient_rgb{
[0.0] = PRIMARY3,
[0.5] = PRIMARY1,
[1.0] = PRIMARY3
}
local CRITICAL1 = 0xFF3333
local CRITICAL3 = 0xFFB8B8
M.INDICATOR_FG_CRITICAL = color.gradient_rgb{
[0.0] = CRITICAL1,
[0.5] = CRITICAL3,
[1.0] = CRITICAL1
}
-- plot colors
local PLOT_PRIMARY1 = 0x003f7c
local PLOT_PRIMARY2 = 0x1e90ff
local PLOT_PRIMARY3 = 0x316ece
local PLOT_PRIMARY4 = 0x8cc7ff
M.PLOT_FILL_BORDER_PRIMARY = color.gradient_rgb{
[0.0] = PLOT_PRIMARY1,
[1.0] = PLOT_PRIMARY2
}
M.PLOT_FILL_BG_PRIMARY = color.gradient_rgba{
[0.2] = {PLOT_PRIMARY3, 0.5},
[1.0] = {PLOT_PRIMARY4, 1.0}
}
-- panel pattern
M.PANEL_BG = color.rgba(0x121212, 0.7)
return M

87
src/color.lua Normal file
View File

@ -0,0 +1,87 @@
local err = require 'err'
--------------------------------------------------------------------------------
-- colors
--
-- these are tables like {red :: Int, green :: Int, blue :: Int, alpha :: Int}
local rgba = function(hex, alpha)
local obj = err.safe_table(
{
r = ((hex / 0x10000) % 0x100) / 255.,
g = ((hex / 0x100) % 0x100) / 255.,
b = (hex % 0x100) / 255.,
a = alpha,
}
)
return err.set_type(obj, "color")
end
local rgb = function(hex)
return rgba(hex, 1.0)
end
--------------------------------------------------------------------------------
-- Gradients
--
-- these are tables like {[stop] :: color} where stop is a float between 0 and 1
-- and color is a color as defined above
local _make_gradient = function(colorstops, f)
local c = {}
for stop, spec in pairs(colorstops) do
assert(
stop <= 1 and stop >= 0,
"ERROR: color stop must be between 0 and 1; got " .. stop
)
c[stop] = f(spec)
end
return err.set_type(err.safe_table(c), "gradient")
end
-- {[stop] :: hex} -> Gradient
local gradient_rgb = function(colorstops)
return _make_gradient(colorstops, rgb)
end
-- {[stop] :: {hex, alpha}} -> Gradient
local gradient_rgba = function(colorstops)
return _make_gradient(
colorstops,
function(spec) return rgba(spec[1], spec[2]) end
)
end
local compile_patterns
compile_patterns = function(patterns)
local r = {}
for k, v in pairs(patterns) do
if type(v) == "number" then
r[k] = rgb(v)
elseif v.color ~= nil then
r[k] = rgba(v.color, v.alpha)
elseif v.gradient ~= nil then
local p = {}
local g = v.gradient
for i = 1, #g do
local _g = g[i]
p[_g.stop] = _g.color
end
r[k] = gradient_rgb(p)
elseif v.gradient_alpha ~= nil then
local p = {}
local g = v.gradient_alpha
for i = 1, #g do
local _g = g[i]
p[_g.stop] = {_g.color, _g.alpha}
end
r[k] = gradient_rgba(p)
else
r[k] = compile_patterns(v)
end
end
return r
end
return compile_patterns

52
src/err.lua Normal file
View File

@ -0,0 +1,52 @@
local M = {}
local i_o = require 'i_o'
M.assert_trace = function(test, msg)
if not test then
i_o.errorf(msg)
print(debug.traceback())
os.exit(1)
end
end
M.safe_table = function(tbl)
local ck_key = function(_, key)
local v = rawget(tbl, key)
M.assert_trace(v ~= nil, "key doesn't exist: "..key)
return v
end
return setmetatable(tbl, {__index = ck_key})
end
local TYPE_KEY = '__type'
M.set_type = function(tbl, _type)
local mt = getmetatable(tbl)
mt[TYPE_KEY] = _type
return setmetatable(tbl, mt)
end
M.get_type = function(x)
local ltype = type(x)
if ltype == "table" then
local mt = getmetatable(x)
if mt == nil then
return ltype
else
return mt[TYPE_KEY]
end
else
return ltype
end
end
M.check_type = function(x, _type)
local xtype = nil
if x ~= nil then
xtype = M.get_type(x)
end
i_o.assertf(xtype == _type, "type must be '%s' got '%s' instead", _type, xtype)
end
return M

43
src/format.lua Normal file
View File

@ -0,0 +1,43 @@
local M = {}
local __tostring = tostring
local __math_floor = math.floor
local __math_ceil = math.ceil
local __string_format = string.format
M.round = function(x, places)
local m = 10 ^ (places or 0)
if x >= 0 then
return __math_floor(x * m + 0.5) / m
else
return __math_ceil(x * m - 0.5) / m
end
end
M.round_to_string = function(x, places)
places = places or 0
if places >= 0 then
return __string_format('%.'..places..'f', x)
else
return __tostring(M.round(x, 0))
end
end
M.precision_round_to_string = function(x, sig_fig)
sig_fig = sig_fig or 4
if x < 10 then return M.round_to_string(x, sig_fig - 1)
elseif x < 100 then return M.round_to_string(x, sig_fig - 2)
elseif x < 1000 then return M.round_to_string(x, sig_fig - 3)
else return M.round_to_string(x, sig_fig - 4)
end
end
M.convert_data_val = function(x)
if x < 1024 then return '', x
elseif x < 1048576 then return 'Ki', x / 1024
elseif x < 1073741824 then return 'Mi', x / 1048576
else return 'Gi', x / 1073741824
end
end
return M

107
src/i_o.lua Normal file
View File

@ -0,0 +1,107 @@
local M = {}
local __string_format = string.format
local __tonumber = tonumber
local __os_execute = os.execute
local __io_popen = io.popen
local __io_open = io.open
local __string_match = string.match
local __conky_parse = conky_parse
--------------------------------------------------------------------------------
-- logging/printing
M.printf = function(fmt, ...)
print(__string_format(fmt, ...))
end
M.logf = function(level, fmt, ...)
M.printf(level..': '..fmt, ...)
end
M.errorf = function(fmt, ...)
M.logf('ERROR', fmt, ...)
end
M.warnf = function(fmt, ...)
M.logf('WARN', fmt, ...)
end
M.infof = function(fmt, ...)
M.logf('INFO', fmt, ...)
end
M.assertf = function(test, fmt, ...)
-- NOTE use exit here because the assert command will only break one loop
-- in the conky update cycle rather than quit the entire program
if not test then
M.errorf(fmt, ...)
print(debug.traceback())
os.exit(1)
end
end
--------------------------------------------------------------------------------
-- reading files/command output
--[[
available modes per lua docs
*n: number (actually returns a number)
*a: entire file (default here)
*l: reads one line and strips \n (default for read cmd)
*L; reads one line and keeps \n
N: reads number of lines (where N is a number)
--]]
local read_entire_file = function(file, regex, mode)
if not file then return end
local str = file:read(mode or '*a')
file:close()
if not str then return end
if regex then return __string_match(str, regex) or '' else return str end
end
M.read_file = function(path, regex, mode)
return read_entire_file(__io_open(path, 'rb'), regex, mode)
end
M.execute_cmd = function(cmd, regex, mode)
return read_entire_file(__io_popen(cmd), regex, mode)
end
--------------------------------------------------------------------------------
-- boolean tests
M.exit_code_cmd = function(cmd)
local _, _, rc = __os_execute(cmd)
return rc
end
M.exe_exists = function(exe)
return M.exit_code_cmd('command -v '..exe..' > /dev/null') == 0
end
M.assert_exe_exists = function(exe)
M.assertf(M.exe_exists(exe), 'executable %s not found', exe)
end
M.file_exists = function(path)
return M.exit_code_cmd('stat '..path..' > /dev/null 2>&1') == 0
end
M.assert_file_exists = function(path)
M.assertf(M.file_exists(path), '%s does not exist', path)
end
--------------------------------------------------------------------------------
-- conky object execution
M.conky = function(expr, regex)
local ans = __conky_parse(expr)
if regex then return __string_match(ans, regex) or '' else return ans end
end
M.conky_numeric = function(expr, regex)
return __tonumber(M.conky(expr, regex)) or 0
end
return M

28
src/impure.lua Normal file
View File

@ -0,0 +1,28 @@
local M = {}
M.sequence = function(...)
local fs = {...}
for i = 1, #fs do
fs[i]()
end
end
M.each = function(f, seq, ...)
for i = 1, #seq do
f(seq[i], ...)
end
end
M.ieach = function(f, seq, ...)
for i = 1, #seq do
f(i, seq[i], ...)
end
end
M.each2 = function(f, seq1, seq2, ...)
for i = 1, #seq1 do
f(seq1[i], seq2[i], ...)
end
end
return M

194
src/json.lua Normal file
View File

@ -0,0 +1,194 @@
local M = {}
local __string_gsub = string.gsub
local __string_char = string.char
local __string_find = string.find
local __string_sub = string.sub
local __table_concat = table.concat
local __math_floor = math.floor
local __pairs = pairs
local __tonumber = tonumber
local decode -- to ref this before definition
local decode_scan_whitespace = function(s, start_pos)
local whitespace = " \n\r\t"
local string_len = #s
while (__string_find(whitespace, __string_sub(s, start_pos, start_pos), 1, true) and
start_pos <= string_len) do
start_pos = start_pos + 1
end
return start_pos
end
local decode_scan_array = function(s, start_pos)
local array = {}
local string_len = #s
start_pos = start_pos + 1
repeat
start_pos = decode_scan_whitespace(s, start_pos)
local cur_char = __string_sub(s,start_pos,start_pos)
if (cur_char == ']') then
return array, start_pos + 1
end
if (cur_char == ',') then
start_pos = decode_scan_whitespace(s, start_pos + 1)
end
object, start_pos = decode(s, start_pos)
array[#array + 1] = object
until false
end
local decode_scan_comment = function(s, start_pos)
local end_pos = __string_find(s, '*/', start_pos + 2)
return end_pos + 2
end
local decode_scan_constant = function(s, start_pos)
local consts = {["true"] = true, ["false"] = false, ["null"] = nil}
local const_names = {"true", "false", "null"}
for _, k in __pairs(const_names) do
if __string_sub(s, start_pos, start_pos + #k - 1 ) == k then
return consts[k], start_pos + #k
end
end
end
local decode_scan_number = function(s, start_pos)
local end_pos = start_pos + 1
local string_len = #s
local acceptable_chars = "+-0123456789.e"
while (__string_find(acceptable_chars, __string_sub(s, end_pos, end_pos), 1, true)
and end_pos <= string_len) do
end_pos = end_pos + 1
end
local number_string = __string_gsub(__string_sub(s, start_pos, end_pos - 1), '+', '')
return __tonumber(number_string), end_pos
end
local decode_scan_object = function(s, start_pos)
local object = {}
local string_len = #s
local key, value
start_pos = start_pos + 1
repeat
start_pos = decode_scan_whitespace(s, start_pos)
local cur_char = __string_sub(s, start_pos, start_pos)
if (cur_char == '}') then
return object, start_pos + 1
end
if (cur_char == ',') then
start_pos = decode_scan_whitespace(s, start_pos + 1)
end
-- Scan the key
key, start_pos = decode(s, start_pos)
start_pos = decode_scan_whitespace(s, start_pos)
start_pos = decode_scan_whitespace(s, start_pos + 1)
value, start_pos = decode(s, start_pos)
object[key] = value
until false
end
local escape_sequences = {
["\\t"] = "\t",
["\\f"] = "\f",
["\\r"] = "\r",
["\\n"] = "\n",
["\\b"] = "\b"
}
setmetatable(escape_sequences, {__index = function(t, k) return __string_sub(k, 2) end})--skip "\"
local decode_scan_string = function (s, start_pos)
local start_char = __string_sub(s, start_pos, start_pos)
local t = {}
local i, j = start_pos, start_pos
while __string_find(s, start_char, j + 1) ~= j + 1 do
local oldj = j
local x, y = __string_find(s, start_char, oldj + 1)
i, j = __string_find(s, "\\.", j + 1)
if not i or x < i then i, j = x, y - 1 end
t[#t + 1] = __string_sub(s, oldj + 1, i - 1)
if __string_sub(s, i, j) == "\\u" then
local a = __string_sub(s, j + 1, j + 4)
local n = __tonumber(a, 16)
local x
j = j + 4
if n < 0x80 then
x = __string_char(n % 0x80)
elseif n < 0x800 then
x = __string_char(0xC0 + (__math_floor(n / 64) % 0x20), 0x80 + (n % 0x40))
else
x = __string_char(0xE0 + (__math_floor(n / 4096) % 0x10), 0x80 +
(__math_floor(n / 64) % 0x40), 0x80 + (n % 0x40))
end
t[#t + 1] = x
else
t[#t + 1] = escape_sequences[__string_sub(s, i, j)]
end
end
t[#t + 1] = __string_sub(j, j + 1)
return __table_concat(t, ""), j + 2
end
decode = function(s, start_pos)
start_pos = start_pos or 1
start_pos = decode_scan_whitespace(s, start_pos)
local cur_char = __string_sub(s, start_pos, start_pos)
if cur_char == '{' then
return decode_scan_object(s, start_pos)
end
if cur_char == '[' then
return decode_scan_array(s, start_pos)
end
if __string_find("+-0123456789.e", cur_char, 1, true) then
return decode_scan_number(s, start_pos)
end
if cur_char == [["]] or cur_char == [[']] then
return decode_scan_string(s, start_pos)
end
if __string_sub(s, start_pos, start_pos + 1) == '/*' then
return decode(s, decode_scan_comment(s, start_pos))
end
return decode_scan_constant(s, start_pos)
end
M.decode = decode
return M

View File

@ -19,43 +19,10 @@ local style = require 'style'
local source = require 'source'
local pure = require 'pure'
local compile_patterns
-- TODO move to color module
compile_patterns = function(patterns)
local r = {}
for k, v in pairs(patterns) do
if type(v) == "number" then
r[k] = color.rgb(v)
elseif v.color ~= nil then
r[k] = color.rgba(v.color, v.alpha)
elseif v.gradient ~= nil then
local p = {}
local g = v.gradient
for i = 1, #g do
local _g = g[i]
p[_g.stop] = _g.color
end
r[k] = color.gradient_rgb(p)
elseif v.gradient_alpha ~= nil then
local p = {}
local g = v.gradient_alpha
for i = 1, #g do
local _g = g[i]
p[_g.stop] = {_g.color, _g.alpha}
end
r[k] = color.gradient_rgba(p)
else
r[k] = compile_patterns(v)
end
end
return r
end
return function(config)
local M = {}
local patterns = compile_patterns(config.theme.patterns)
local patterns = color(config.theme.patterns)
local font = config.theme.font
local font_sizes = font.sizes
local font_family = font.family

View File

@ -10,7 +10,7 @@ return function(config, main_state, common, width, point)
-----------------------------------------------------------------------------
-- smartd
i_o.exe_assert('pidof')
i_o.assert_exe_exists('pidof')
local mk_smart = function(y)
local obj = common.make_text_row(point.x, y, width, 'SMART Daemon')
@ -35,6 +35,7 @@ return function(config, main_state, common, width, point)
local mk_bars = function(y)
local paths = pure.map_keys('path', config.fs_paths)
local names = pure.map_keys('name', config.fs_paths)
impure.each(i_o.assert_file_exists, paths)
local CONKY_CMDS = pure.map(
pure.partial(string.format, '${fs_used_perc %s}', true),
paths

View File

@ -14,7 +14,8 @@ return function(update_freq, config, common, width, point)
-----------------------------------------------------------------------------
-- nvidia state
i_o.exe_assert(NVIDIA_EXE)
i_o.assert_exe_exists(NVIDIA_EXE)
i_o.assert_file_exists(config.dev_power)
-- vars to process the nv settings glob
--
@ -38,8 +39,6 @@ return function(update_freq, config, common, width, point)
'(%d+),(%d+)\n'..
'graphics=(%d+), memory=%d+, video=(%d+), PCIe=%d+\n'
local GPU_BUS_CTRL = '/sys/bus/pci/devices/0000:01:00.0/power/control'
local mod_state = {
error = false,
used_memory = 0,
@ -52,7 +51,7 @@ return function(update_freq, config, common, width, point)
}
local update_state = function()
if i_o.read_file(GPU_BUS_CTRL, nil, '*l') == 'on' then
if i_o.read_file(config.dev_power, nil, '*l') == 'on' then
local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY)
if nvidia_settings_glob == '' then
mod_state.error = 'Error'

View File

@ -39,12 +39,10 @@ return function(update_freq, config, main_state, common, width, point)
if math.fmod(ncores, config.core_rows) == 0 then
show_cores = true
else
print(
string.format(
'WARNING: could not evenly distribute %i cores over %i rows',
ncores,
config.core_rows
)
i_o.warnf(
'could not evenly distribute %i cores over %i rows; disabling',
ncores,
config.core_rows
)
end
end

View File

@ -1,6 +1,8 @@
local format = require 'format'
local pure = require 'pure'
local sys = require 'sys'
local i_o = require 'i_o'
local impure = require 'impure'
return function(update_freq, config, common, width, point)
local PLOT_SEC_BREAK = 20
@ -9,6 +11,8 @@ return function(update_freq, config, common, width, point)
local mod_state = {read = 0, write = 0}
local device_paths = sys.get_disk_paths(config.devices)
impure.each(i_o.assert_file_exists, device_paths)
local update_state = function()
mod_state.read, mod_state.write = sys.get_total_disk_io(device_paths)
end

259
src/pure.lua Normal file
View File

@ -0,0 +1,259 @@
local M = {}
local err = require 'err'
local __math_floor = math.floor
--------------------------------------------------------------------------------
-- zippy functions
-- TODO generalize to arbitrary number of sequences
M.zip_with = function(f, seq1, seq2)
local r = {}
for i = 1, #seq1 do
r[i] = f(seq1[i], seq2[i])
end
return r
end
M.zip = function(...)
local seqs = {...}
local imax = math.min(table.unpack(M.map(function(t) return #t end, seqs)))
local jmax = #seqs
local r = {}
for i = 1, imax do
r[i] = {}
for j = 1, jmax do
r[i][j] = seqs[j][i]
end
end
return r
end
M.unzip = function(seqs)
return M.zip(table.unpack(seqs))
end
--------------------------------------------------------------------------------
-- reductions
M.reduce = function(f, init, seq)
if seq == nil then
return init
else
local r = init
for i = 1, #seq do
r = f(r, seq[i])
end
return r
end
end
--------------------------------------------------------------------------------
-- mappy functions
M.map = function(f, seq, ...)
local r = {}
for i = 1, #seq do
r[i] = f(seq[i], ...)
end
return r
end
M.map_n = function(f, n, ...)
local r = {}
for i = 1, n do
r[i] = f(i, ...)
end
return r
end
M.imap = function(f, seq)
local r = {}
for i = 1, #seq do
r[i] = f(i, seq[i])
end
return r
end
M.map_keys = function(key, tbls)
local r = {}
for i = 1, #tbls do
r[i] = tbls[i][key]
end
return r
end
M.map_at = function(key, f, tbl)
local r = {}
for k, v in pairs(tbl) do
if k == key then
r[k] = f(v)
else
r[k] = v
end
end
return r
end
--------------------------------------------------------------------------------
-- generations
M.seq = function(n, start)
start = start or 1
local r = {}
for i = 1, n do
r[i] = i + start - 1
end
return r
end
M.rep = function(n, x)
local r = {}
for i = 1, n do
r[i] = x
end
return r
end
--------------------------------------------------------------------------------
-- random list things
M.set = function(tbl, key, value)
local r = {}
for k, v in pairs(tbl) do
if k == key then
r[k] = value
else
r[k] = v
end
end
return r
end
M.reverse = function(xs)
local j = 1
local r = {}
for i = #xs, 1, -1 do
r[j] = xs[i]
j = j + 1
end
return r
end
M.filter = function(f, seq)
local r = {}
local j = 1
for i = 1, #seq do
if f(seq[i]) == true then
r[j] = seq[i]
j = j + 1
end
end
return r
end
M.flatten = function(xs)
local r = {}
for i = 1, #xs do
for j = 1, #xs[i] do
table.insert(r, xs[i][j])
end
end
return r
end
M.concat = function(...)
return M.flatten({...})
end
M.table_array = function(tbl)
local r = {}
for i = 1, #tbl do
r[i] = tbl[i]
end
return r
end
--------------------------------------------------------------------------------
-- functional functions
local get_arity = function(f, args)
local i = #args
while args[i] == true do
i = i - 1
end
if i < #args then
return table.move(args, 1, #args - i, 1, {}), #args
else
local arity = debug.getinfo(f, "u")["nparams"]
err.assert_trace(arity > #args, 'too many arguments for partial')
return args, arity
end
end
-- poor man's Lisp macro :)
M.partial = function(f, ...)
local args, arity = get_arity(f, {...})
local format_args = function(fmt, n, start)
return table.concat(
M.map(function(i) return string.format(fmt, i) end, M.seq(n, start)),
','
)
end
local partial_args = format_args('args[%i]', #args, 1)
local rem_args = format_args('x%i', arity - #args, #args + 1)
local src = string.format(
'return function(%s) return f(%s,%s) end',
rem_args,
partial_args,
rem_args
)
return load(src, 'partial_apply', 't', {f = f, args = args})()
end
M.compose = function(f, ...)
if #{...} == 0 then
return f
else
local g = M.compose(...)
return function(x) return f(g(x)) end
end
end
-- TODO is there a way to do this without nesting a zillion function calls?
M.sequence = function(...)
local fs = {...}
return function(x)
for i = 1, #fs do
fs[i](x)
end
end
end
M.memoize = function(f)
local mem = {} -- memoizing table
setmetatable(mem, {__mode = "kv"}) -- make it weak
return function (x, ...)
local r = mem[x]
if not r then
r = f(x, ...)
mem[x] = r
end
return r
end
end
M.maybe = function(def, f, x)
if x == nil then
return def
else
return f(x)
end
end
-- round to whole numbers since I don't need more granularity and extra values
-- will lead to cache misses
M.round_percent = __math_floor
return M

332
src/sys.lua Normal file
View File

@ -0,0 +1,332 @@
local M = {}
local i_o = require 'i_o'
local pure = require 'pure'
local __string_match = string.match
local __string_gmatch = string.gmatch
local __string_format = string.format
local __tonumber = tonumber
local dirname = function(s)
return __string_match(s, '(.*)/name')
end
local read_micro = function(path)
return i_o.read_file(path, nil, '*n') * 0.000001
end
--------------------------------------------------------------------------------
-- memory
local MEMINFO_PATH = '/proc/meminfo'
local fmt_mem_field = function(field)
return field..':%s+(%d+)'
end
local meminfo_regex = function(read_swap)
-- ASSUME the order of the meminfo file will never change, but some options
-- (like swap) might not exist
local free_fields = {
'MemFree',
'Buffers',
'Cached'
}
local swap_field = 'SwapFree'
local slab_fields = {
'Shmem',
'SReclaimable'
}
local all_fields = read_swap == true
and {free_fields, {swap_field}, slab_fields}
or {free_fields, slab_fields}
local patterns = pure.map(fmt_mem_field, pure.flatten(all_fields))
return table.concat(patterns, '.+\n')
end
M.meminfo_updater_swap = function(mem_state, swap_state)
local regex = meminfo_regex(true)
return function()
mem_state.memfree,
mem_state.buffers,
mem_state.cached,
swap_state.free,
mem_state.shmem,
mem_state.sreclaimable
= __string_match(i_o.read_file(MEMINFO_PATH), regex)
end
end
M.meminfo_updater_noswap = function(mem_state)
local regex = meminfo_regex(false)
return function()
mem_state.memfree,
mem_state.buffers,
mem_state.cached,
mem_state.shmem,
mem_state.sreclaimable
= __string_match(i_o.read_file(MEMINFO_PATH), regex)
end
end
M.meminfo_field_reader = function(field)
local pattern = fmt_mem_field(field)
return function()
return tonumber(i_o.read_file(MEMINFO_PATH, pattern))
end
end
--------------------------------------------------------------------------------
-- intel powercap
local SYSFS_RAPL = '/sys/class/powercap'
M.intel_powercap_reader = function(dev)
local uj = __string_format('%s/%s/energy_uj', SYSFS_RAPL, dev)
i_o.assert_file_exists(uj)
return function()
return read_micro(uj)
end
end
--------------------------------------------------------------------------------
-- battery
local SYSFS_POWER = '/sys/class/power_supply'
local format_power_path = function(battery, property)
local p = __string_format('%s/%s/%s', SYSFS_POWER, battery, property)
i_o.assert_file_exists(p)
return p
end
M.battery_power_reader = function(battery)
local current = format_power_path(battery, 'current_now')
local voltage = format_power_path(battery, 'voltage_now')
return function()
return read_micro(current) * read_micro(voltage)
end
end
M.battery_status_reader = function(battery)
local status = format_power_path(battery, 'status')
return function()
return i_o.read_file(status, nil, '*l') ~= 'Discharging'
end
end
--------------------------------------------------------------------------------
-- disk io
M.get_disk_paths = function(devs)
return pure.map(pure.partial(string.format, '/sys/block/%s/stat', true), devs)
end
-- fields 3 and 7 (sectors read and written)
local RW_REGEX = '%s+%d+%s+%d+%s+(%d+)%s+%d+%s+%d+%s+%d+%s+(%d+)'
-- the sector size of any block device in linux is 512 bytes
-- see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/types.h?id=v4.4-rc6#n121
local BLOCK_SIZE_BYTES = 512
M.get_disk_io = function(path)
local r, w = __string_match(i_o.read_file(path), RW_REGEX)
return __tonumber(r) * BLOCK_SIZE_BYTES, __tonumber(w) * BLOCK_SIZE_BYTES
end
M.get_total_disk_io = function(paths)
local r = 0
local w = 0
for i = 1, #paths do
local _r, _w = M.get_disk_io(paths[i])
r = r + _r
w = w + _w
end
return r, w
end
--------------------------------------------------------------------------------
-- network
-- ASSUME realpath exists (part of coreutils)
local get_interfaces = function()
local s = i_o.execute_cmd('realpath /sys/class/net/* | grep -v virtual')
local interfaces = {}
for iface in __string_gmatch(s, '/([^/\n]+)\n') do
interfaces[#interfaces + 1] = iface
end
return interfaces
end
M.get_net_interface_paths = function()
local is = get_interfaces()
return pure.map(
function(s)
local dir = string.format('/sys/class/net/%s/statistics/', s)
return {rx = dir..'rx_bytes', tx = dir..'tx_bytes'}
end,
is
)
end
--------------------------------------------------------------------------------
-- cpu
-- ASSUME nproc and lscpu will always be available
M.get_core_number = function()
return tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)'))
end
M.get_cpu_number = function()
return tonumber(i_o.execute_cmd('nproc', nil, '*n'))
end
-- TODO what if this fails?
local get_coretemp_dir = function()
i_o.assert_exe_exists('grep')
local s = i_o.execute_cmd('grep -l \'^coretemp$\' /sys/class/hwmon/*/name')
return dirname(s)
end
-- map cores to integer values starting at 1; this is necessary since some cpus
-- don't report their core id's as a sequence of integers starting at 0
local get_core_id_mapper = function()
local s = i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort | uniq')
local m = {}
local i = 1
for core_id in string.gmatch(s, '(%d+)') do
m[tonumber(core_id)] = i
i = i + 1
end
return m
end
local get_core_mappings = function()
local ncpus = M.get_cpu_number()
local ncores = M.get_core_number()
local nthreads = ncpus / ncores
local core_id_mapper = get_core_id_mapper()
local conky_thread_ids = pure.rep(ncores, nthreads)
local core_mappings = {}
local s = i_o.execute_cmd('lscpu -p=cpu,CORE | tail -n+5')
for cpu_id, core_id in string.gmatch(s, '(%d+),(%d+)') do
local conky_core_id = core_id_mapper[tonumber(core_id)]
local conky_cpu_id = tonumber(cpu_id) + 1
core_mappings[conky_cpu_id] = {
conky_core_id = conky_core_id,
conky_thread_id = conky_thread_ids[conky_core_id],
}
conky_thread_ids[conky_core_id] = conky_thread_ids[conky_core_id] - 1
end
return core_mappings
end
M.get_coretemp_paths = function()
local d = get_coretemp_dir()
i_o.assert_exe_exists('grep')
local s = i_o.execute_cmd(string.format('grep Core %s/temp*_label', d))
local ps = {}
local core_id_mapper = get_core_id_mapper()
for temp_name, core_id in string.gmatch(s, '/([^/\n]+)_label:Core (%d+)\n') do
ps[core_id_mapper[tonumber(core_id)]] = string.format('%s/%s_input', d, temp_name)
end
return ps
end
M.read_freq = function()
-- NOTE: Using the builtin conky functions for getting cpu freq seems to make
-- the entire loop jittery due to high variance latency. Querying
-- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu
-- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery
-- problem.
local c = i_o.execute_cmd('lscpu -p=MHZ')
local f = 0
local n = 0
for s in __string_gmatch(c, '(%d+%.%d+)') do
f = f + __tonumber(s)
n = n + 1
end
return __string_format('%.0f Mhz', f / n)
end
M.get_hwp_paths = function()
return pure.map_n(
function(i)
return '/sys/devices/system/cpu/cpu'
.. (i - 1)
.. '/cpufreq/energy_performance_preference'
end,
M.get_cpu_number()
)
end
M.read_hwp = function(hwp_paths)
-- read HWP of first cpu, then test all others to see if they match
local hwp_pref = i_o.read_file(hwp_paths[1], nil, "*l")
local mixed = false
local i = 2
while not mixed and i <= #hwp_paths do
if hwp_pref ~= i_o.read_file(hwp_paths[i], nil, '*l') then
mixed = true
end
i = i + 1
end
if mixed then
return 'Mixed'
elseif hwp_pref == 'power' then
return 'Power'
elseif hwp_pref == 'balance_power' then
return 'Bal. Power'
elseif hwp_pref == 'balance_performance' then
return 'Bal. Performance'
elseif hwp_pref == 'performance' then
return 'Performance'
elseif hwp_pref == 'default' then
return 'Default'
else
return 'Unknown'
end
end
M.init_cpu_loads = function()
local m = get_core_mappings()
local cpu_loads = {}
for cpu_id, core in pairs(m) do
cpu_loads[cpu_id] = {
active_prev = 0,
total_prev = 0,
percent_active = 0,
conky_core_id = core.conky_core_id,
conky_thread_id = core.conky_thread_id,
}
end
return cpu_loads
end
M.read_cpu_loads = function(cpu_loads)
local ncpus = #cpu_loads
local i = 1
local iter = io.lines('/proc/stat')
iter() -- ignore first line
for ln in iter do
if i > ncpus then break end
local user, system, idle = __string_match(ln, '(%d+) %d+ (%d+) (%d+)', 5)
local active = user + system
local total = active + idle
local c = cpu_loads[i]
if total > c.total_prev then -- guard against 1/0 errors
c.percent_active = (active - c.active_prev) / (total - c.total_prev)
c.active_prev = active
c.total_prev = total
end
i = i + 1
end
return cpu_loads
end
return M

29
src/widget/arc/arc.lua Normal file
View File

@ -0,0 +1,29 @@
local M = {}
local geom = require 'geom'
local circle = require 'circle'
local path = require 'path'
local shape = require 'shape'
--------------------------------------------------------------------------------
-- pure
M.make_shape = function(arc, thickness, pattern)
return shape.shape(
path.create_arc_from_geom(geom.CR_DUMMY, arc),
circle.make_source(pattern, arc.center, arc.radius, thickness)
)
end
M.make = function(arc, config)
return circle._make(arc, config, M.make_shape)
end
--------------------------------------------------------------------------------
-- impure
M.config = circle.config
M.draw = circle.draw
return M

55
src/widget/arc/circle.lua Normal file
View File

@ -0,0 +1,55 @@
local M = {}
local geom = require 'geom'
local err = require 'err'
local source = require 'source'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
--------------------------------------------------------------------------------
-- pure
M.make_pattern_radii = function(r, t)
return r - t * 0.5, r + t * 0.5
end
M.make_source = function(pattern, center, radius, thickness)
local r1, r2 = M.make_pattern_radii(radius, thickness)
return source.radial_pattern(pattern, center, r1, r2)
end
M.make_shape = function(circle, thickness, pattern)
return shape.shape(
path.create_circle_from_geom(geom.CR_DUMMY, circle),
M.make_source(pattern, circle.center, circle.radius, thickness)
)
end
M._make = function(_geom, config, make_shape_fun)
return shape.styled_shape(
config.style,
make_shape_fun,
_geom,
config.style.thickness,
config.pattern
)
end
M.make = function(circle, config)
return M._make(circle, config, M.make_shape)
end
M.config = function(_style, pattern)
return err.safe_table({style = _style, pattern = pattern})
end
--------------------------------------------------------------------------------
-- impure
M.draw = function(obj, cr)
style.set_line_style(obj.style, cr)
shape.draw_shape(obj.shape, cr)
end
return M

View File

@ -0,0 +1,60 @@
local M = {}
local arc = require 'arc'
local dial = require 'dial'
local pure = require 'pure'
local impure = require 'impure'
local style = require 'style'
local shape = require 'shape'
local dynamic = require 'dynamic'
--------------------------------------------------------------------------------
-- pure
M.make = function(_arc, bg_config, fg_threshold_config, inner_radius, num_dials)
local t = bg_config.style.thickness
local spacing = (t * num_dials - _arc.radius + inner_radius)
/ (1 - num_dials)
assert(spacing >= 0, "ERROR: compound dial spacing is negative")
local arcs = pure.map_n(
function(i)
return pure.set(
_arc,
"radius",
inner_radius + t * 0.5 + (i - 1) * (spacing + t)
)
end,
num_dials
)
local setters = pure.map(dial.make_setter, arcs, t, fg_threshold_config)
return dynamic.compound(
{
shapes = pure.map(arc.make_shape, arcs, t, bg_config.pattern),
style = bg_config.style,
},
setters,
0
)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, i, percent)
obj.var[i] = obj.setters[i](percent)
end
M.draw_static = function(obj, cr)
local static = obj.static
style.set_line_style(static.style, cr)
impure.each(shape.draw_shape, static.shapes, cr)
end
M.draw_dynamic = function(obj, cr)
style.set_line_style(obj.static.style, cr)
impure.each(shape.draw_shape, obj.var, cr)
end
return M

75
src/widget/arc/dial.lua Normal file
View File

@ -0,0 +1,75 @@
local M = {}
local geom = require 'geom'
local pure = require 'pure'
local arc = require 'arc'
local circle = require 'circle'
local source = require 'source'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
local dynamic = require 'dynamic'
--------------------------------------------------------------------------------
-- pure
M.make_setter = function(_arc, thickness, threshold_config)
local c = _arc.center
local r = _arc.radius
local t1 = _arc.theta0
local t2 = _arc.theta1
local r1, r2 = circle.make_pattern_radii(r, thickness)
local source_chooser = source.radial_pattern_chooser(
threshold_config.low_pattern,
threshold_config.high_pattern,
threshold_config.threshold,
c,
r1,
r2
)
local f = pure.memoize(
function(percent)
return shape.shape(
path.create_arc(
geom.CR_DUMMY,
c.x,
c.y,
r,
t1,
t1 + (percent / 100) * (t2 - t1)
),
source_chooser(percent)
)
end
)
return function(percent)
return f(pure.round_percent(percent))
end
end
M.make = function(_arc, bg_config, fg_threshold_config)
local setter = M.make_setter(
_arc,
bg_config.style.thickness,
fg_threshold_config
)
return dynamic.single(arc.make(_arc, bg_config), setter, 0)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, percent)
obj.var = obj.setter(percent)
end
M.draw_static = function(obj, cr)
arc.draw(obj.static, cr)
end
M.draw_dynamic = function(obj, cr)
style.set_line_style(obj.static.style, cr)
shape.draw_shape(obj.var, cr)
end
return M

45
src/widget/dynamic.lua Normal file
View File

@ -0,0 +1,45 @@
local M = {}
local err = require 'err'
local pure = require 'pure'
-- TODO generalize these
-- types of dynamic mappers
-- 1 -> 1
-- 1 -> 1 with recursion
-- 1 -> many
-- 1 -> many (nested in many)
-- many -> many
M.single = function(static, setter, init)
return err.safe_table(
{
static = static,
setter = setter,
var = setter(init)
}
)
end
-- TODO think of a better more mathy name
M.multi = function(static, setter, inits)
return err.safe_table(
{
static = static,
setter = setter,
var = pure.map(setter, inits)
}
)
end
M.compound = function(static, setters, init)
return err.safe_table(
{
static = static,
setters = setters,
var = pure.map(function(setter) return setter(init) end, setters),
}
)
end
return M

60
src/widget/geom.lua Normal file
View File

@ -0,0 +1,60 @@
local M = {}
local err = require 'err'
-- TODO this is weird to have here
-- dummy drawing surface
local cs = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1366, 768)
M.CR_DUMMY = cairo_create(cs)
cairo_surface_destroy(cs)
cs = nil
M.make_point = function(x, y)
return err.safe_table({x = x, y = y})
end
M.make_box_at_point = function(p, w, h)
return err.safe_table(
{
corner = p,
width = w,
height = h,
-- TODO these might be unnecessary
right_x = w + p.x,
bottom_y = h + p.y
}
)
end
M.make_box = function(x, y, w, h)
return M.make_box_at_point(M.make_point(x, y), w, h)
end
M.make_arc_at_point = function(p, r, t1, t2)
return err.safe_table(
{
center = p,
radius = r,
theta0 = t1,
theta1 = t2,
}
)
end
M.make_arc = function(x, y, r, t0, t1)
return M.make_arc_at_point(M.make_point(x, y), r, t0, t1)
end
M.make_circle_at_point = function(p, r)
return err.safe_table({center = p, radius = r})
end
M.make_circle = function(x, y, r)
return M.make_circle_at_point(M.make_point(x, y), r)
end
M.make_line = function(p1, p2)
return err.safe_table({p1 = p1, p2 = p2})
end
return M

View File

@ -0,0 +1,31 @@
local M = {}
local __imlib_load_image = imlib_load_image
local __imlib_context_set_image = imlib_context_set_image
local __imlib_render_image_on_drawable = imlib_render_image_on_drawable
local __imlib_free_image = imlib_free_image
local __imlib_image_get_width = imlib_image_get_width
local __imlib_image_get_height = imlib_image_get_height
local set = function(obj, path)
local img = __imlib_load_image(path)
__imlib_context_set_image(img)
obj.width = __imlib_image_get_width()
obj.height = __imlib_image_get_height()
obj.path = path
__imlib_free_image()
end
local draw = function(obj)
local img = __imlib_load_image(obj.path)
__imlib_context_set_image(img)
__imlib_render_image_on_drawable(obj.x, obj.y)
__imlib_free_image()
end
M.set = set
M.draw = draw
return M

View File

@ -0,0 +1,31 @@
local M = {}
local __imlib_load_image = imlib_load_image
local __imlib_context_set_image = imlib_context_set_image
local __imlib_render_image_on_drawable_at_size = imlib_render_image_on_drawable_at_size
local __imlib_free_image = imlib_free_image
local __imlib_image_get_width = imlib_image_get_width
local __imlib_image_get_height = imlib_image_get_height
local set = function(obj, path)
local img = __imlib_load_image(path)
__imlib_context_set_image(img)
obj.img_width = __imlib_image_get_width()
obj.img_height = __imlib_image_get_height()
obj.path = path
__imlib_free_image()
end
local draw = function(obj)
local img = __imlib_load_image(obj.path)
__imlib_context_set_image(img)
__imlib_render_image_on_drawable_at_size(obj.x, obj.y, obj.width, obj.height)
__imlib_free_image()
end
M.set = set
M.draw = draw
return M

61
src/widget/line/bar.lua Normal file
View File

@ -0,0 +1,61 @@
local M = {}
local geom = require 'geom'
local line = require 'line'
local source = require 'source'
local pure = require 'pure'
local path = require 'path'
local shape = require 'shape'
local dynamic = require 'dynamic'
--------------------------------------------------------------------------------
-- pure
M.make_setter = function(_line, config, threshold_config)
local p1 = _line.p1
local p2 = _line.p2
local _p1, _p2 = line.get_wide_pattern_points(_line, config.style.thickness, config.is_wide_pattern)
local source_chooser = source.linear_pattern_chooser(
threshold_config.low_pattern,
threshold_config.high_pattern,
threshold_config.threshold,
_p1,
_p2
)
local f = pure.memoize(
function(percent)
local frac = percent / 100
local mp = geom.make_point(
(p2.x - p1.x) * frac + p1.x,
(p2.y - p1.y) * frac + p1.y
)
return shape.shape(
path.create_line(geom.CR_DUMMY, p1, mp),
source_chooser(percent)
)
end
)
return function(percent)
return f(pure.round_percent(percent))
end
end
M.make = function(p1, p2, bg_config, fg_threshold_config)
local setter = M.make_setter(geom.make_line(p1, p2), bg_config, fg_threshold_config)
return dynamic.single(line.make(p1, p2, bg_config), setter, 0)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, percent)
obj.var = obj.setter(percent)
end
M.draw = function(obj, cr)
line.draw(obj.static, cr)
shape.draw_shape(obj.var, cr)
end
return M

View File

@ -0,0 +1,67 @@
local M = {}
local geom = require 'geom'
local bar = require 'bar'
local dynamic = require 'dynamic'
local line = require 'line'
local style = require 'style'
local pure = require 'pure'
local impure = require 'impure'
local shape = require 'shape'
--------------------------------------------------------------------------------
-- pure
-- TODO make this handle vertical bars
M.make = function(point, length, bg_config, fg_threshold_config, spacing,
num_bars, is_vertical)
local lines = pure.map_n(
function(i)
local y = (i - 1) * spacing + point.y
local p1 = geom.make_point(point.x, y)
local p2 = geom.make_point(point.x + length, y)
return geom.make_line(p1, p2)
end,
num_bars
)
local setters = pure.map(
bar.make_setter,
lines,
bg_config,
fg_threshold_config
)
local bs = bg_config.style
local static = {
shapes = pure.map(
line.make_shape,
lines,
bg_config.pattern,
bs.thickness,
bg_config.is_wide_pattern
),
style = bs,
}
return dynamic.compound(static, setters, 0)
end
--------------------------------------------------------------------------------
-- pure
M.set = function(obj, i, percent)
obj.var[i] = obj.setters[i](percent)
end
M.draw_static = function(obj, cr)
local static = obj.static
style.set_line_style(static.style, cr)
impure.each(shape.draw_shape, static.shapes, cr)
end
M.draw_dynamic = function(obj, cr)
style.set_line_style(obj.static.style, cr)
impure.each(shape.draw_shape, obj.var, cr)
end
return M

74
src/widget/line/line.lua Normal file
View File

@ -0,0 +1,74 @@
local M = {}
local geom = require 'geom'
local err = require 'err'
local source = require 'source'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
local __math_atan2 = math.atan2
local __math_sin = math.sin
local __math_cos = math.cos
--------------------------------------------------------------------------------
-- pure
M.config = function(_style, pattern, is_wide_pattern)
return err.safe_table(
{
style = _style,
pattern = pattern,
is_wide_pattern = is_wide_pattern or false,
}
)
end
M.get_wide_pattern_points = function(line, thickness, is_wide)
local p1 = line.p1
local p2 = line.p2
if is_wide then
local theta = __math_atan2(p2.y - p1.y, p2.x - p1.x)
local delta_x = 0.5 * thickness * __math_sin(theta) --and yes, these are actually flipped
local delta_y = 0.5 * thickness * __math_cos(theta)
local _p1 = geom.make_point(p1.x + delta_x, p1.y + delta_y)
local _p2 = geom.make_point(p1.x - delta_x, p1.y - delta_y)
return _p1, _p2
else
return p1, p2
end
end
M.make_source = function(line, thickness, pattern, is_wide)
local p1, p2 = M.get_wide_pattern_points(line, thickness, is_wide)
return source.linear_pattern(pattern, p1, p2)
end
M.make_shape = function(line, pattern, thickness, is_wide)
return shape.shape(
path.create_line_from_geom(geom.CR_DUMMY, line),
M.make_source(line, thickness, pattern, is_wide)
)
end
M.make = function(line, config)
local _style = config.style
return shape.styled_shape(
_style,
M.make_shape,
line,
config.pattern,
_style.thickness,
config.is_wide_pattern
)
end
--------------------------------------------------------------------------------
-- impure
M.draw = function(obj, cr)
style.set_line_style(obj.style, cr)
shape.draw_shape(obj.shape, cr)
end
return M

73
src/widget/path.lua Normal file
View File

@ -0,0 +1,73 @@
local M = {}
local impure = require 'impure'
local __cairo_new_path = cairo_new_path
local __cairo_line_to = cairo_line_to
local __cairo_copy_path = cairo_copy_path
local __cairo_close_path = cairo_close_path
local __cairo_arc = cairo_arc
local __cairo_rectangle = cairo_rectangle
local __math_rad = math.rad
local line_to = function(p, cr)
__cairo_line_to(cr, p.x, p.y)
end
local create_poly = function(cr, points)
__cairo_new_path(cr)
impure.each(line_to, points, cr)
end
local copy_path = function(cr)
local path = __cairo_copy_path(cr)
__cairo_new_path(cr) -- clear path to keep it from reappearing
return path
end
M.create_open_poly = function(cr, points)
create_poly(cr, points)
return copy_path(cr)
end
M.create_closed_poly = function(cr, points)
create_poly(cr, points)
__cairo_close_path(cr)
return copy_path(cr)
end
M.create_line = function(cr, p1, p2)
return M.create_open_poly(cr, {p1, p2})
end
M.create_line_from_geom = function(cr, line)
return M.create_line(cr, line.p1, line.p2)
end
M.create_arc = function(cr, x, y, radius, theta1, theta2)
__cairo_new_path(cr)
__cairo_arc(cr, x, y, radius, __math_rad(theta1), __math_rad(theta2))
return copy_path(cr)
end
M.create_arc_from_geom = function(cr, arc)
local c = arc.center
return M.create_arc(cr, c.x, c.y, arc.radius, arc.theta0, arc.theta1)
end
M.create_circle = function(cr, x, y, radius)
return M.create_arc(cr, x, y, radius, 0, 360)
end
M.create_circle_from_geom = function(cr, circle)
local c = circle.center
return M.create_circle(cr, c.x, c.y, circle.radius)
end
M.create_rect = function(cr, x, y, w, h)
__cairo_new_path(cr)
__cairo_rectangle(cr, x, y, w, h)
return copy_path(cr)
end
return M

View File

@ -0,0 +1,34 @@
local M = {}
local geom = require 'geom'
local source = require 'source'
local path = require 'path'
local style = require 'style'
local shape = require 'shape'
--------------------------------------------------------------------------------
-- pure
local make_shape = function(box, line_pattern, fill_pattern)
local p1 = box.corner
local p2 = geom.make_point(p1.x, p1.y + box.height)
return shape.filled_shape(
path.create_rect(geom.CR_DUMMY, p1.x, p1.y, box.width, box.height),
source.linear_pattern(line_pattern, p1, p2),
source.linear_pattern(fill_pattern, p1, p2)
)
end
M.make = function(box, config, fill_pattern)
return shape.styled_shape(config.style, make_shape, box, config.pattern, fill_pattern)
end
--------------------------------------------------------------------------------
-- impure
M.draw = function(obj, cr)
style.set_closed_poly_style(obj.style, cr)
shape.draw_filled_shape(obj.shape, cr)
end
return M

40
src/widget/rect/rect.lua Normal file
View File

@ -0,0 +1,40 @@
local M = {}
local geom = require 'geom'
local err = require 'err'
local source = require 'source'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
M.config = function(closed_poly_style, pattern)
return err.safe_table(
{
style = closed_poly_style,
pattern = pattern,
}
)
end
local make_shape = function(box, pattern)
local p = box.corner
return shape.shape(
path.create_rect(geom.CR_DUMMY, p.x, p.y, box.width, box.height),
source.linear_pattern(
pattern,
p,
geom.make_point(p.x, p.y + box.height)
)
)
end
M.make = function(box, config)
return shape.styled_shape(config.style, make_shape, box, config.pattern)
end
M.draw = function(obj, cr)
style.set_closed_poly_style(obj.style, cr)
shape.draw_shape(obj.shape, cr)
end
return M

70
src/widget/shape.lua Normal file
View File

@ -0,0 +1,70 @@
local M = {}
local impure = require 'impure'
local err = require 'err'
local __cairo_append_path = cairo_append_path
local __cairo_fill_preserve = cairo_fill_preserve
local __cairo_set_source = cairo_set_source
local __cairo_stroke = cairo_stroke
--------------------------------------------------------------------------------
-- pure
M.shape = function(path, source)
return {path = path, source = source}
end
M.filled_shape = function(path, line_source, fill_source)
return {path = path, line_source = line_source, fill_source = fill_source}
end
M.shapes = function(paths, source)
return {paths = paths, source = source}
end
M.styled_shape = function(style, make_shape_fun, geom, ...)
return err.safe_table(
{
style = style,
shape = make_shape_fun(geom, ...),
}
)
end
--------------------------------------------------------------------------------
-- impure
M.draw_path_with_source = function(path, cr, source)
__cairo_append_path(cr, path)
__cairo_set_source(cr, source)
__cairo_stroke(cr)
end
M.draw_shape = function(shape, cr)
M.draw_path_with_source(shape.path, cr, shape.source)
end
M.draw_filled_shape = function(shape, cr)
__cairo_append_path(cr, shape.path)
__cairo_set_source(cr, shape.fill_source)
__cairo_fill_preserve(cr)
__cairo_set_source(cr, shape.line_source)
__cairo_stroke(cr)
end
local append_path = function(p, cr)
__cairo_append_path(cr, p)
end
M.draw_paths_with_source = function(paths, cr, source)
impure.each(append_path, paths, cr)
__cairo_set_source(cr, source)
__cairo_stroke(cr)
end
M.draw_shapes = function(shapes, cr)
M.draw_paths_with_source(shapes.paths, cr, shapes.source)
end
return M

117
src/widget/source.lua Normal file
View File

@ -0,0 +1,117 @@
local M = {}
local err = require 'err'
local __cairo_pattern_create_rgba = cairo_pattern_create_rgba
local __cairo_pattern_create_radial = cairo_pattern_create_radial
local __cairo_pattern_create_linear = cairo_pattern_create_linear
local __cairo_pattern_add_color_stop_rgba = cairo_pattern_add_color_stop_rgba
M.threshold_config = function(low_pattern, high_pattern, threshold)
return err.safe_table(
{
low_pattern = low_pattern,
high_pattern = high_pattern,
threshold = threshold,
}
)
end
local set_color_stops = function(pattern, colorstops)
for stop, color in pairs(colorstops) do
__cairo_pattern_add_color_stop_rgba(
pattern,
stop,
color.r,
color.g,
color.b,
color.a
)
end
end
local _solid_color = function(color)
return __cairo_pattern_create_rgba(color.r, color.g, color.b, color.a)
end
local _linear_gradient = function(gradient, p1, p2)
local pattern = __cairo_pattern_create_linear(p1.x, p1.y, p2.x, p2.y)
set_color_stops(pattern, gradient)
return pattern
end
local _radial_gradient = function(gradient, p, r1, r2)
local pattern = __cairo_pattern_create_radial(p.x, p.y, r1, p.x, p.y, r2)
set_color_stops(pattern, gradient)
return pattern
end
local _is_gradient = function(spec)
return err.get_type(spec) == "gradient"
end
local _create_critical_function = function(limit)
if limit then
return function(n) return (n > limit) end
else
return function(_) return nil end
end
end
M.solid_color = function(spec)
err.check_type(spec, "color")
return _solid_color(spec)
end
M.linear_pattern = function(spec, p1, p2)
if _is_gradient(spec) then
return _linear_gradient(spec, p1, p2)
else
return _solid_color(spec)
end
end
M.radial_pattern = function(spec, p, r1, r2)
if _is_gradient(spec) then
return _radial_gradient(spec, p, r1, r2)
else
return _solid_color(spec)
end
end
local source_chooser = function(low_source, high_source, limit)
local f = _create_critical_function(limit)
return function(value)
if f(value) then
return high_source
else
return low_source
end
end
end
M.solid_color_chooser = function(low_color, high_color, limit)
return source_chooser(
M.solid_color(low_color),
M.solid_color(high_color),
limit
)
end
M.linear_pattern_chooser = function(low_pattern, high_pattern, limit, p1, p2)
return source_chooser(
M.linear_pattern(low_pattern, p1, p2),
M.linear_pattern(high_pattern, p1, p2),
limit
)
end
M.radial_pattern_chooser = function(low_pattern, high_pattern, limit, p, r1, r2)
return source_chooser(
M.radial_pattern(low_pattern, p, r1, r2),
M.radial_pattern(high_pattern, p, r1, r2),
limit
)
end
return M

55
src/widget/style.lua Normal file
View File

@ -0,0 +1,55 @@
local M = {}
local err = require 'err'
local __cairo_set_line_width = cairo_set_line_width
local __cairo_set_line_cap = cairo_set_line_cap
local __cairo_set_line_join = cairo_set_line_join
--------------------------------------------------------------------------------
-- pure
-- circle: a closed path with no corners
M.circle = function(thickness)
return err.safe_table({thickness = thickness})
end
-- line: an open path with no corners
M.line = function(thickness, cap)
return err.safe_table({thickness = thickness, cap = cap})
end
-- open poly: an open path with corners
M.open_poly = function(thickness, cap, join)
return err.safe_table({thickness = thickness, cap = cap, join = join})
end
-- closed poly: an closed path with corners
M.closed_poly = function(thickness, join)
return err.safe_table({thickness = thickness, join = join})
end
--------------------------------------------------------------------------------
-- impure
M.set_circle_style = function(style, cr)
__cairo_set_line_width(cr, style.thickness)
end
M.set_line_style = function(style, cr)
__cairo_set_line_width(cr, style.thickness)
__cairo_set_line_cap(cr, style.cap)
end
M.set_open_poly_style = function(style, cr)
__cairo_set_line_width(cr, style.thickness)
__cairo_set_line_cap(cr, style.cap)
__cairo_set_line_join(cr, style.join)
end
M.set_closed_poly_style = function(style, cr)
__cairo_set_line_width(cr, style.thickness)
__cairo_set_line_join(cr, style.join)
end
return M

58
src/widget/text/text.lua Normal file
View File

@ -0,0 +1,58 @@
local M = {}
local err = require 'err'
local dynamic = require 'dynamic'
local ti = require 'text_internal'
local source = require 'source'
local pure = require 'pure'
--------------------------------------------------------------------------------
-- pure
M.config = function(font_spec, color, x_align, y_align)
return err.safe_table(
{
font_spec = font_spec,
color = color,
x_align = x_align,
y_align = y_align,
}
)
end
local _make = function(point, chars, config, format)
local font = ti.make_font(config.font_spec)
local get_delta_x = ti.x_align_function(config.x_align, font)
local format_chars = ti.make_format_function(format)
local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x)
local setter = pure.memoize(pure.compose(make_vtext, format_chars))
local static = {
y = point.y + ti.get_delta_y(config.y_align, font),
font = font,
source = source.solid_color(config.color),
}
return dynamic.single(static, setter, chars)
end
M.make_formatted = function(point, text, config, format)
return _make(point, (text or ti.NULL_TEXT_STRING), config, format)
end
M.make_plain = function(point, text, config)
return M.make_formatted(point, text, config, nil)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, text)
obj.var = obj.setter(text)
end
M.draw = function(obj, cr)
local st = obj.static
ti.set_font_spec(cr, st.font, st.source)
ti.draw_vtext_at_y(obj.var, st.y, cr)
end
return M

View File

@ -0,0 +1,49 @@
local M = {}
local source = require 'source'
local ti = require 'text_internal'
local pure = require 'pure'
local impure = require 'impure'
local dynamic = require 'dynamic'
--------------------------------------------------------------------------------
-- pure
M.make = function(point, texts, config, format, spacing)
local font = ti.make_font(config.font_spec)
local get_delta_x = ti.x_align_function(config.x_align, font)
local format_chars = ti.make_format_function(format)
local delta_y = ti.get_delta_y(config.y_align, font)
local ys = pure.map_n(
function(i) return point.y + spacing * (i - 1) + delta_y end,
#texts
)
local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x)
local setter = pure.memoize(pure.compose(make_vtext, format_chars))
local static = {
y_positions = ys,
font = font,
source = source.solid_color(config.color),
}
return dynamic.multi(static, setter, texts)
end
M.make_n = function(point, num_rows, config, format, spacing, init_text)
local dummy = pure.rep(num_rows, init_text or ti.NULL_TEXT_STRING)
return M.make(point, dummy, config, format, spacing)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, row_num, text)
obj.var[row_num] = obj.setter(text)
end
M.draw = function(obj, cr)
local static = obj.static
ti.set_font_spec(cr, static.font, static.source)
impure.each2(ti.draw_vtext_at_y, obj.var, static.y_positions, cr)
end
return M

View File

@ -0,0 +1,165 @@
local M = {}
local err = require 'err'
local geom = require 'geom'
local __string_sub = string.sub
local __cairo_toy_font_face_create = cairo_toy_font_face_create
local __cairo_font_extents = cairo_font_extents
local __cairo_set_font_face = cairo_set_font_face
local __cairo_set_font_size = cairo_set_font_size
local __cairo_text_extents = cairo_text_extents
local __cairo_set_source = cairo_set_source
local __cairo_move_to = cairo_move_to
local __cairo_show_text = cairo_show_text
M.NULL_TEXT_STRING = '<null>'
--------------------------------------------------------------------------------
-- pure
local trim_to_length = function(text, len)
if #text > len then
return __string_sub(text, 1, len)..'...'
else
return text
end
end
M.make_format_function = function(format)
if type(format) == "function" then
return format
elseif type(format) == "number" and format > 0 then
return function(_text) return trim_to_length(_text, format) end
elseif type(format) == "string" then
return function(_text) return string.format(format, _text) end
elseif format == nil or format == false then
return function(_text) return _text end
else
local msg = "format must be a printf string, positive int, or function: got "
local t = type(format)
if t == "number" or t == "string" then
msg = msg..format
else
msg = msg.."a "..t
end
err.assert_trace(nil, msg)
end
end
M.make_font_face = function(font_spec)
return __cairo_toy_font_face_create(
font_spec.family,
font_spec.slant,
font_spec.weight
)
end
M.make_font = function(font_spec)
return {
face = M.make_font_face(font_spec),
size = font_spec.size,
}
end
M.make_text = function(x, y, chars)
return err.safe_table({x = x, y = y, chars = chars})
end
M.make_htext = function(y, delta_x, chars)
return err.safe_table({y = y, delta_x = delta_x, chars = chars})
end
M.make_vtext = function(x, delta_x_fun, chars)
return err.safe_table({x = x + delta_x_fun(chars), chars = chars})
end
--------------------------------------------------------------------------------
-- impure
local dummy_text_extents = cairo_text_extents_t:create()
tolua.takeownership(dummy_text_extents)
local dummy_font_extents = cairo_font_extents_t:create()
tolua.takeownership(dummy_font_extents)
local set_font_extents = function(font)
__cairo_set_font_size(geom.CR_DUMMY, font.size)
__cairo_set_font_face(geom.CR_DUMMY, font.face)
__cairo_font_extents(geom.CR_DUMMY, dummy_font_extents)
return dummy_font_extents
end
local set_text_extents = function(chars, font)
__cairo_set_font_size(geom.CR_DUMMY, font.size)
__cairo_set_font_face(geom.CR_DUMMY, font.face)
__cairo_text_extents(geom.CR_DUMMY, chars, dummy_text_extents)
return dummy_text_extents
end
M.get_width = function(chars, font)
return set_text_extents(chars, font).width
end
M.font_height = function(font)
return set_font_extents(font).height
end
M.x_align_function = function(x_align, font)
if x_align == 'left' then
return function(text)
local te = set_text_extents(text, font)
return -te.x_bearing
end
elseif x_align == 'center' then
return function(text)
local te = set_text_extents(text, font)
return -(te.x_bearing + te.width * 0.5)
end
elseif x_align == 'right' then
return function(text)
local te = set_text_extents(text, font)
return -(te.x_bearing + te.width)
end
else
err.assert_trace(nil, "invalid x_align")
end
end
M.get_delta_y = function(y_align, font)
local fe = set_font_extents(font)
if y_align == 'bottom' then
return -fe.descent
elseif y_align == 'top' then
return fe.height
elseif y_align == 'center' then
return 0.92 * fe.height * 0.5 - fe.descent
else
err.assert_trace(nil, "invalid y_align")
end
end
M.set_font_spec = function(cr, font, source)
__cairo_set_font_face(cr, font.face)
__cairo_set_font_size(cr, font.size)
__cairo_set_source(cr, source)
end
local draw_text_at = function(cr, x, y, chars)
__cairo_move_to(cr, x, y)
__cairo_show_text(cr, chars)
end
M.draw_text = function(obj, cr)
draw_text_at(cr, obj.x, obj.y, obj.chars)
end
M.draw_htext_at_x = function(obj, x, cr)
draw_text_at(cr, obj.delta_x + x, obj.y, obj.chars)
end
M.draw_vtext_at_y = function(obj, y, cr)
draw_text_at(cr, obj.x, y, obj.chars)
end
return M

View File

@ -0,0 +1,183 @@
local M = {}
local source = require 'source'
local rect = require 'rect'
local err = require 'err'
local ti = require 'text_internal'
local geom = require 'geom'
local pure = require 'pure'
local impure = require 'impure'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
--------------------------------------------------------------------------------
-- pure
M.config = function(border_config, sep_config, header_config, body_config, padding)
return err.safe_table(
{
border_config = border_config,
sep_config = sep_config,
header_config = header_config,
body_config = body_config,
padding = padding,
}
)
end
M.body_config = function(font_spec, color, columns)
return err.safe_table(
{
font_spec = font_spec,
color = color,
columns = columns,
}
)
end
M.column_config = function(header, format)
return err.safe_table({header = header, format = format})
end
M.header_config = function(font_spec, color, bottom_padding)
return err.safe_table(
{
font_spec = font_spec,
color = color,
bottom_padding = bottom_padding,
}
)
end
M.padding = function(l, t, r, b)
return err.safe_table(
{
left = l,
right = r,
top = t,
bottom = b,
}
)
end
-- TODO this is basically the same as that from ylabels
local make_header_text = function(x, y, chars, font)
return ti.make_text(
x + ti.x_align_function('center', font)(chars),
y + ti.get_delta_y('center', font),
chars
)
end
-- M.make = function(box, num_rows, columns, table_config)
M.make = function(box, num_rows, table_config)
local bc = table_config.body_config
local hs = table_config.header_config
local columns = bc.columns
local header_font = ti.make_font(hs.font_spec)
local body_font = ti.make_font(bc.font_spec)
local p = table_config.padding
local tbl_width = box.width - p.left - p.right
local tbl_height = box.height - p.top - p.bottom
local tbl_x = box.corner.x + p.left
local tbl_y = box.corner.y + p.top
local body_delta_y = ti.get_delta_y('center', body_font)
local body_y = tbl_y + hs.bottom_padding + body_delta_y
local column_width = tbl_width / #columns
local spacing = (tbl_height - hs.bottom_padding) / (num_rows - 1)
local headers = pure.imap(
function (i, conf)
local x = tbl_x + column_width * (i - 0.5)
return make_header_text(x, tbl_y, conf.header, header_font)
end,
columns
)
local sep_paths = pure.map_n(
function(i)
local x = tbl_x + column_width * i
return path.create_line(
geom.CR_DUMMY,
geom.make_point(x, tbl_y),
geom.make_point(x, tbl_y + tbl_height)
)
end,
#columns - 1
)
local get_delta_x = ti.x_align_function('center', body_font)
local make_setter = function(i, conf)
local column_x = tbl_x + column_width * (i - 0.5)
return pure.memoize(
pure.compose(
pure.partial(ti.make_vtext, column_x, get_delta_x),
ti.make_format_function(conf.format)
)
)
end
local setters = pure.imap(make_setter, columns)
return {
static = {
header = {
font = header_font,
source = source.solid_color(hs.color),
texts = headers,
},
body = {
font = body_font,
source = source.solid_color(bc.color),
y_positions = pure.map_n(
function(i) return body_y + spacing * (i - 1) end,
num_rows
),
},
separators = {
style = table_config.sep_config.style,
shapes = {
source = source.solid_color(table_config.sep_config.pattern),
paths = sep_paths,
},
},
border = rect.make(box, table_config.border_config),
},
setters = setters,
var = pure.map_n(
function(c) return pure.rep(num_rows, setters[c]("NULL")) end,
#columns
)
}
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, col_num, row_num, text)
obj.var[col_num][row_num] = obj.setters[col_num](text)
end
M.draw_static = function(obj, cr)
local static = obj.static
local seps = static.separators
local header = static.header
rect.draw(static.border, cr)
style.set_line_style(seps.style, cr)
shape.draw_shapes(seps.shapes, cr)
ti.set_font_spec(cr, header.font, header.source)
impure.each(ti.draw_text, header.texts, cr)
end
local draw_column = function(rows, ys, cr)
impure.each2(ti.draw_vtext_at_y, rows, ys, cr)
end
M.draw_dynamic = function(obj, cr)
local body = obj.static.body
ti.set_font_spec(cr, body.font, body.source)
impure.each(draw_column, obj.var, body.y_positions, cr)
end
return M

View File

@ -0,0 +1,79 @@
local M = {}
local err = require 'err'
local pure = require 'pure'
local source = require 'source'
local ti = require 'text_internal'
local dynamic = require 'dynamic'
local __tonumber = tonumber
--------------------------------------------------------------------------------
-- pure
local _make = function(point, chars, config, threshold_config, format)
local font = ti.make_font(config.font_spec)
local get_delta_x = ti.x_align_function(config.x_align, font)
local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x)
local format_chars = ti.make_format_function(format)
local source_chooser = source.solid_color_chooser(
config.color,
threshold_config.high_color,
threshold_config.threshold
)
local _setter = pure.memoize(
function(cs)
local _cs = cs or 0
return {
text = make_vtext(format_chars(_cs)),
source = source_chooser(_cs),
}
end
)
local f = threshold_config.pre_function
local setter
if f then
setter = function(x) return _setter(f(x)) end
else
setter = _setter
end
local static = {
y = point.y + ti.get_delta_y(config.y_align, font),
font = font,
}
return dynamic.single(static, setter, chars or 0)
end
M.config = function(high_color, threshold, pre_function)
return err.safe_table(
{
high_color = high_color,
threshold = threshold,
pre_function = pre_function,
}
)
end
M.make_formatted = function(point, text, config, format, threshold_config)
return _make(point, text, config, threshold_config, format)
end
M.make_plain = function(point, text, config, threshold_config)
return M.make_formatted(point, text, config, threshold_config, nil)
end
--------------------------------------------------------------------------------
-- impure
M.set = function(obj, x)
obj.var = obj.setter(x)
end
M.draw = function(obj, cr)
local st = obj.static
local var = obj.var
ti.set_font_spec(cr, st.font, var.source)
ti.draw_vtext_at_y(var.text, st.y, cr)
end
return M

View File

@ -0,0 +1,269 @@
local M = {}
local timeseries = require 'timeseries'
local tsi = require 'timeseries_internal'
local ti = require 'text_internal'
local err = require 'err'
local ylabels = require 'ylabels'
local xlabels = require 'xlabels'
local geom = require 'geom'
local source = require 'source'
local pure = require 'pure'
local impure = require 'impure'
local __table_remove = table.remove
local __math_ceil = math.ceil
local __math_log = math.log
--------------------------------------------------------------------------------
-- pure
local choose_scale_factor = function(timers, new_sf)
if #timers == 0 then
return new_sf
else
local cur_sf = timers[1].factor
if new_sf < cur_sf then
return new_sf
else
return cur_sf
end
end
end
M.scaling_parameters = function(base, min_domain, threshold)
return err.safe_table(
{
base = base,
min_domain = min_domain,
threshold = threshold,
}
)
end
local tick_timer = function(timer)
timer.remaining = timer.remaining - 1
end
-- ASSUME
-- 1. this is a FIFO queue
-- 2. timers will be sorted from highest scale to lowest scale going from
-- back to front
-- 3. no timers will share a time slot
-- 4. no timers will have the same scale factor
-- 5. the table will always be a sequence
-- NOTE: scale factor is inversely related to scale, so higher scale -> lower
-- factor (hence the inequalities)
local update_timers = function(timers, prev_sf, new_sf, init_timer)
if timers[1] and timers[1].remaining == 0 then
__table_remove(timers, 1)
end
impure.each(tick_timer, timers)
local n = #timers
if new_sf < prev_sf then
while n > 0 and timers[n].factor >= new_sf do
timers[n] = nil
n = n - 1
end
elseif new_sf > prev_sf and (n == 0 or prev_sf > timers[1].factor) then
timers[n + 1] = init_timer(prev_sf)
end
return timers
end
local scale_point = function(value, y, h, new_factor, old_factor)
return y + h * (1 - (1 - (value - y) / h) * (new_factor / old_factor))
end
-- local debug_timers = function(timers, sf, value)
-- print('----------------------------------------------------------------------')
-- print('value', value, 'scale_factor', sf)
-- for i, v in pairs(timers) do
-- print('timers', 'i', i, 't', v.remaining, 'f', v.factor)
-- end
-- print('length', #timers)
-- end
M.make = function(box, samplefreq, plot_config, label_config, scaling_params)
local t = scaling_params.threshold
local m = scaling_params.min_domain
local b = scaling_params.base
local get_scale_factor = function(x)
local domain = m
if x > 0 then
domain = __math_ceil(__math_log(x / t) / __math_log(b))
end
if domain < m then domain = m end
return b ^ -domain
end
local x = box.corner.x
local gconf = plot_config.grid_config
local x_label_format = timeseries.make_format_timecourse_x_label(plot_config.num_points, samplefreq)
local label_font = ti.make_font(label_config.font_spec)
local x_label_data = xlabels.make(box.bottom_y, gconf.num_x + 1, x_label_format, label_font)
local axis_x = box.corner.x
local right_x = axis_x + box.width
local plot_y = box.corner.y
local total_width = box.width
local n_y_labels = gconf.num_y + 1
local plot_height = box.height - xlabels.get_x_axis_height(label_font)
local make_y_labels = pure.memoize(
function(scale_factor)
return ylabels.make(
box.corner,
plot_height,
n_y_labels,
label_font,
label_config.y_format,
scale_factor
)
end
)
local make_grid_paths = pure.memoize(
function(y_axis_width)
return tsi.make_plotarea_paths(
geom.CR_DUMMY,
right_x,
plot_y,
total_width - y_axis_width,
plot_height,
gconf.num_x,
gconf.num_y
)
end
)
local get_x_label_positions = pure.memoize(
function(y_axis_width)
local w = total_width - y_axis_width
return xlabels.get_x_label_positions(right_x, w, x_label_data)
end
)
local init_timer = function(scale_factor)
return {
factor = scale_factor,
remaining = plot_config.num_points
}
end
local scale_data_points = function(series, old_factor, new_factor)
if old_factor == new_factor then
return series
else
return pure.map(scale_point, series, plot_y, plot_height, new_factor, old_factor)
end
end
local insert_data_point = function(series, value, scale_factor)
return tsi.insert_data_point(plot_y, plot_height, plot_config.num_points, series, value * scale_factor)
end
local inner_setter = function(value, series, cur_sf, prev_sf, timers)
local new_sf = get_scale_factor(value)
local new_timers = update_timers(timers, prev_sf, new_sf, init_timer)
local new_cur_sf = choose_scale_factor(new_timers, new_sf)
local y_labels = make_y_labels(1 / new_cur_sf)
local width = total_width - y_labels.width
-- debug_timers(new_timers, new_sf, value)
return {
axis = {
x = {
positions = get_x_label_positions(y_labels.width),
},
y = {
labels = y_labels,
}
},
current_scale_factor = new_cur_sf,
prev_scale_factor = new_sf,
plotarea = {
dx = width / plot_config.num_points,
paths = make_grid_paths(y_labels.width)
},
timers = new_timers,
series = insert_data_point(
scale_data_points(series, cur_sf, new_cur_sf),
value,
new_cur_sf
)
}
end
local setter = function(value, var)
return inner_setter(
value,
var.series,
var.current_scale_factor,
var.prev_scale_factor,
var.timers
)
end
return err.safe_table(
{
static = {
box = box,
axis = {
font = label_font,
source = source.solid_color(label_config.color),
x = {
label_data = x_label_data,
}
},
plotarea = {
bottom_y = plot_height + plot_y,
sources = tsi.make_sources(x, plot_y, total_width, plot_config),
},
},
setter = setter,
var = inner_setter(0, {}, 1, 1, {}),
}
)
end
--------------------------------------------------------------------------------
-- impure
M.update = function(obj, value)
obj.var = obj.setter(value, obj.var)
end
-- nothing here is "static" because we cannot assume that
-- any object will remain the same shape (can shift in both x and y)
M.draw_static = function(_, _)
-- stub
end
M.draw_dynamic = function(obj, cr)
local var = obj.var
local static = obj.static
local box = static.box
local saxis = static.axis
local vaxis = var.axis
local vplotarea = var.plotarea
local splotarea = static.plotarea
local sources = splotarea.sources
local paths = vplotarea.paths
local right_x = box.right_x
timeseries.draw_labels(
cr,
saxis.font,
saxis.source,
vaxis.x.positions,
saxis.x.label_data,
vaxis.y.labels
)
tsi.draw_grid(cr, paths.grid, sources.grid)
tsi.draw_series(cr, right_x, splotarea.bottom_y, vplotarea.dx, var.series, sources.series)
tsi.draw_outline(cr, paths.outline, sources.outline)
end
return M

View File

@ -0,0 +1,163 @@
local M = {}
local err = require 'err'
local xlabels = require 'xlabels'
local ylabels = require 'ylabels'
local source = require 'source'
local ti = require 'text_internal'
local tsi = require 'timeseries_internal'
local geom = require 'geom'
local impure = require 'impure'
local __string_format = string.format
--------------------------------------------------------------------------------
-- pure
-- TODO group these better
M.config = function(num_points, outline_color, data_line_pattern,
data_fill_pattern, grid_config)
return err.safe_table(
{
num_points = num_points,
outline_color = outline_color,
data_line_pattern = data_line_pattern,
data_fill_pattern = data_fill_pattern,
grid_config = grid_config,
}
)
end
M.grid_config = function(num_x, num_y, color)
return err.safe_table(
{
num_x = num_x,
num_y = num_y,
pattern = color,
}
)
end
M.label_config = function(color, font_spec, y_format)
return err.safe_table(
{
color = color,
font_spec = font_spec,
y_format = y_format
}
)
end
M.make_format_timecourse_x_label = function(n, freq)
return function(x) return __string_format('%.0fs', (1 - x) * n / freq) end
end
M.make = function(box, samplefreq, config, label_config)
local x = box.corner.x
local y = box.corner.y
local w = box.width
local h = box.height
local right_x = x + w
local bottom_y = y + h
local gconf = config.grid_config
local label_font = ti.make_font(label_config.font_spec)
local plot_height = box.height - xlabels.get_x_axis_height(label_font)
local y_labels = ylabels.make(
box.corner,
plot_height,
gconf.num_y + 1,
label_font,
label_config.y_format,
1
)
local plot_width = w - y_labels.width
local x_label_format = M.make_format_timecourse_x_label(config.num_points, samplefreq)
local x_labels = xlabels.make(bottom_y, gconf.num_x + 1, x_label_format, label_font)
local setter = function(value, series)
return tsi.insert_data_point(y, plot_height, config.num_points, series, value)
end
return err.safe_table(
{
static = {
box = box,
axis = {
font = label_font,
source = source.solid_color(label_config.color),
x = {
label_data = x_labels,
positions = xlabels.get_x_label_positions(right_x, plot_width, x_labels),
},
y = {
labels = y_labels,
}
},
plotarea = {
dx = plot_width / config.num_points,
bottom_y = y + plot_height,
paths = tsi.make_plotarea_paths(
geom.CR_DUMMY,
right_x,
y,
plot_width,
plot_height,
gconf.num_x,
gconf.num_y
),
sources = tsi.make_sources(x, y, w, config),
},
},
setter = setter,
var = setter(0, {}),
}
)
end
--------------------------------------------------------------------------------
-- impure
M.update = function(obj, value)
obj.var = obj.setter(value, obj.var)
end
M.draw_labels = function(cr, font, _source, x_positions, x_label_data, y_labels)
ti.set_font_spec(cr, font, _source)
impure.each2(ti.draw_htext_at_x, x_label_data, x_positions, cr)
impure.each(ti.draw_text, y_labels, cr)
end
M.draw_static = function(obj, cr)
local static = obj.static
local axis = static.axis
local ax = axis.x
local plotarea = static.plotarea
local paths = plotarea.paths
local sources = plotarea.sources
M.draw_labels(
cr,
axis.font,
axis.source,
ax.positions,
ax.label_data,
axis.y.labels
)
tsi.draw_grid(cr, paths.grid, sources.grid)
end
M.draw_dynamic = function(obj, cr)
local static = obj.static
local box = static.box
local plotarea = static.plotarea
local right_x = box.right_x
local sources = plotarea.sources
tsi.draw_series(cr, right_x, plotarea.bottom_y, plotarea.dx, obj.var, sources.series)
tsi.draw_outline(cr, plotarea.paths.outline, sources.outline)
end
return M

View File

@ -0,0 +1,132 @@
local M = {}
local source = require 'source'
local geom = require 'geom'
local pure = require 'pure'
local style = require 'style'
local path = require 'path'
local shape = require 'shape'
local __cairo_move_to = cairo_move_to
local __cairo_line_to = cairo_line_to
local __cairo_set_source = cairo_set_source
local __cairo_fill_preserve = cairo_fill_preserve
local __cairo_stroke = cairo_stroke
local __table_insert = table.insert
local DATA_STYLE = style.open_poly(1, CAIRO_LINE_CAP_BUTT, CAIRO_LINE_JOIN_MITER)
local GRID_CONFIG = style.line(1, CAIRO_LINE_CAP_BUTT)
local OUTLINE_STYLE = style.open_poly(2, CAIRO_LINE_CAP_BUTT, CAIRO_LINE_JOIN_MITER)
--------------------------------------------------------------------------------
-- pure
local make_x_grid = function(cr, x, y, w, h, n)
local y1 = y - 0.5
local y2 = y1 + h + 0.5
local grid_line_spacing = w / n
local f = function(i)
local x1 = x - w + grid_line_spacing * i - 0.5
local p1 = geom.make_point(x1, y1)
local p2 = geom.make_point(x1, y2)
return path.create_line(cr, p1, p2)
end
return pure.map_n(f, n)
end
local make_y_grid = function(cr, x, y, w, h, n)
local x1 = x
local x2 = x - w
local grid_line_spacing = h / n
local f = function(i)
local y1 = y + (i - 1) * grid_line_spacing - 0.5
local p1 = geom.make_point(x1, y1)
local p2 = geom.make_point(x2, y1)
return path.create_line(cr, p1, p2)
end
return pure.map_n(f, n)
end
local make_grid_paths = function(cr, x, y, w, h, nx, ny)
return {
x = make_x_grid(cr, x, y, w, h, nx),
y = make_y_grid(cr, x, y, w, h, ny),
}
end
local make_outline = function(cr, right_x, y, w, h)
local x1 = right_x - w
local y1 = y - 0.5
local x2 = right_x + 0.5
local y2 = y + h + 1.0
local p1 = geom.make_point(x1, y1)
local p2 = geom.make_point(x1, y2)
local p3 = geom.make_point(x2, y2)
return path.create_open_poly(cr, {p1, p2, p3})
end
M.make_plotarea_paths = function(cr, right_x, y, w, h, num_x, num_y)
return {
grid = make_grid_paths(cr, right_x, y, w, h, num_x, num_y),
outline = make_outline(cr, right_x, y, w, h),
}
end
M.make_sources = function(x, y, width, config)
local p1 = geom.make_point(x, y)
local p2 = geom.make_point(x + width, y)
return {
grid = source.solid_color(config.grid_config.pattern),
outline = source.solid_color(config.outline_color),
series = {
line = source.linear_pattern(config.data_line_pattern, p1, p2),
fill = source.linear_pattern(config.data_fill_pattern, p1, p2),
}
}
end
--------------------------------------------------------------------------------
-- impure
M.insert_data_point = function(y, h, n, series, value)
__table_insert(series, 1, y + h * (1 - value))
if #series == n + 2 then
series[#series] = nil
end
return series
end
M.draw_grid = function(cr, grid, _source)
style.set_line_style(GRID_CONFIG, cr)
-- TODO this sets the same source twice and strokes twice
shape.draw_paths_with_source(grid.x, cr, _source)
shape.draw_paths_with_source(grid.y, cr, _source)
end
M.draw_outline = function(cr, _path, _source)
style.set_open_poly_style(OUTLINE_STYLE, cr)
shape.draw_path_with_source(_path, cr, _source)
end
M.draw_series = function(cr, right_x, bottom_y, dx, series, sources)
style.set_open_poly_style(DATA_STYLE, cr)
local n = #series
-- TODO this accounts for up to 20% of the execution time once the series
-- reaches its max length
__cairo_move_to(cr, right_x, series[1])
for j = 2, n do
__cairo_line_to(cr, right_x - (j - 1) * dx, series[j])
end
__cairo_line_to(cr, right_x - (n - 1) * dx, bottom_y)
__cairo_line_to(cr, right_x, bottom_y)
__cairo_set_source(cr, sources.fill)
__cairo_fill_preserve(cr)
__cairo_set_source(cr, sources.line)
__cairo_stroke(cr)
end
return M

View File

@ -0,0 +1,46 @@
local M = {}
local ti = require 'text_internal'
local pure = require 'pure'
--------------------------------------------------------------------------------
-- pure
local make_x_label_text = function(y, chars, font)
return ti.make_htext(
y + ti.get_delta_y('bottom', font),
ti.x_align_function('center', font)(chars),
chars
)
end
local X_LABEL_PAD = 8
M.get_x_axis_height = function(font)
return ti.font_height(font) + X_LABEL_PAD
end
M.make = function(y, n, format_fun, font)
local f = function(i)
return make_x_label_text(y, format_fun((i - 1) / (n - 1)), font)
end
return pure.map_n(f, n)
end
M.get_x_label_positions = function(right_x, w, x_labels)
local n = #x_labels
local f = function(i)
return right_x - w * (1 - (i - 1) / (n - 1))
end
return pure.map_n(f, n)
end
--------------------------------------------------------------------------------
-- impure
M.draw = function(obj, cr, positions)
ti.set_font_spec(cr, obj.font, obj.source)
pure.each2(ti.draw_htext_at, obj.labels, positions, cr)
end
return M

View File

@ -0,0 +1,47 @@
local M = {}
local geom = require 'geom'
local text = require 'text'
local ti = require 'text_internal'
local Y_LABEL_PAD = 5
--------------------------------------------------------------------------------
-- pure
local make_y_label_text = function(point, chars, font)
return ti.make_text(
point.x,
point.y + ti.get_delta_y('center', font),
chars
)
end
-- TODO this function smells funny
M.make = function(point, h, n, font, y_format, scale_factor)
local y_labels = {width = 0}
local f = y_format(scale_factor)
for i = 1, n do
local z = (i - 1) / (n - 1)
local l = make_y_label_text(
geom.make_point(point.x, point.y + z * h),
f((1 - z) * scale_factor),
font
)
local w = ti.get_width(l.chars, font)
if w > y_labels.width then
y_labels.width = w
end
y_labels[i] = l
end
y_labels.width = y_labels.width + Y_LABEL_PAD
return y_labels
end
--------------------------------------------------------------------------------
-- impure
M.draw = text.draw
return M