-- Conky Lua dashboard: entry point. -- -- Drawn entirely with Cairo in lua_draw_hook_post; conky.text is empty because -- Cairo output covers it. -- Resolve requires relative to this file's directory, since Conky's working -- directory is wherever it was launched from, not the config's location. local here = debug.getinfo(1, 'S').source:match('^@(.*/)') or './' package.path = here .. '?.lua;' .. package.path require 'cairo' local card = require 'lib.card' -- === Layout =============================================================== -- Order and position are yours: edit this table, nothing else. -- col/row are grid cells, w/h span cells. Cell size is derived from the -- surface, so the same table works on a 2560x1080 and a 1920x1080 screen. local COLS, ROWS = 4, 2 local layout = { { widget = 'clock', col = 1, row = 1, w = 1, h = 2 }, } -- ========================================================================== local GAP = 16 -- gap between cards, px local MARGIN = 28 -- outer margin, px -- #rrggbb -> {r, g, b} as 0-1 floats, which is what Cairo wants. -- UDT's gen_conky emits hex6, so the conversion lives here rather than in the -- other repo's renderer. local function hex(s, fallback) local r, g, b = tostring(s or ''):match('^#?(%x%x)(%x%x)(%x%x)$') if not r then return fallback or { 1, 0, 1 } end -- magenta: visibly wrong return { tonumber(r, 16) / 255, tonumber(g, 16) / 255, tonumber(b, 16) / 255 } end -- The colours, read out of the config file itself. -- -- NOT via conky_parse('${color3}'): that returns an EMPTY STRING. Conky's -- colour variables emit renderer escape codes into conky.text, they do not -- evaluate to a hex string, and ${default_shade_color} is not a variable at all -- (conky_parse hands the literal text straight back). Verified experimentally; -- both were checked before this approach was chosen. -- -- conky.config is also not exposed to Lua (_G.conky is nil) and conky_info -- carries only cpu_count and update_interval. What Lua does get is -- conky_config, the path of the config file, so the colours are parsed out of -- the rendered file. UDT has already substituted real hex into it by then. -- -- Read once at load, not per frame: the file cannot change without a conky -- restart, since conky never rereads its config. local function config_colors() local f = io.open(conky_config, 'r') if not f then return {} end local src = f:read('*a') f:close() local c = {} -- [%w_]+ not %w+: default_color and default_shade_color carry underscores. for k, v in src:gmatch("([%w_]+)%s*=%s*'(#%x%x%x%x%x%x)'") do c[k] = v end return c end local CFG = config_colors() local function palette() return { heading = hex(CFG.color1), label = hex(CFG.color2), border = hex(CFG.color3), rule = hex(CFG.color3), value = hex(CFG.color4), highlight = hex(CFG.color5), ok = hex(CFG.color6), critical = hex(CFG.color7), body = hex(CFG.default_color), -- The card fill, from default_shade_color (@BODY_SHADE@, `mantle` in -- Macchiato). It must NOT reuse color3: that is the border, and filling and -- stroking a card in one hue makes the hairline invisible, so the cards -- read as blobs instead of the mockup's thin outlines. Every scheme already -- defines body_shade, so this needs no palette change. surface = hex(CFG.default_shade_color), } end -- Cell rect for a layout entry. 1-indexed cols and rows, as the table reads. local function rect_for(entry, sw, sh) local cw = (sw - MARGIN * 2 - GAP * (COLS - 1)) / COLS local ch = (sh - MARGIN * 2 - GAP * (ROWS - 1)) / ROWS return { x = MARGIN + (entry.col - 1) * (cw + GAP), y = MARGIN + (entry.row - 1) * (ch + GAP), w = cw * (entry.w or 1) + GAP * ((entry.w or 1) - 1), h = ch * (entry.h or 1) + GAP * ((entry.h or 1) - 1), } end -- Widget modules, loaded once and cached. A widget that fails to load must not -- take the frame down with it, so the require is wrapped. local widgets = {} local function widget(name) if widgets[name] == nil then local ok, mod = pcall(require, 'widgets.' .. name) widgets[name] = ok and mod or false if not ok then print('dashboard: cannot load widget ' .. name .. ': ' .. tostring(mod)) end end return widgets[name] or nil end -- Draw an error where the dashboard should be. -- -- Conky reports a Lua fault as a blank screen with no message on any stream, so -- without this every mistake looks identical to "nothing ran". local function draw_error(cr, msg, sw) cairo_select_font_face(cr, 'Inconsolata Nerd Font', CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD) cairo_set_font_size(cr, 16) cairo_set_source_rgba(cr, 0.93, 0.53, 0.59, 1) -- literal: the palette may be what failed local y = 40 for line in tostring(msg):gmatch('[^\n]+') do cairo_move_to(cr, 24, y) cairo_show_text(cr, line) y = y + 20 if y > 400 then break end end print('dashboard error: ' .. tostring(msg)) end local function draw(cr, sw, sh, colors) for _, entry in ipairs(layout) do local w = widget(entry.widget) if w then w.draw(cr, rect_for(entry, sw, sh), colors) else -- Name the missing widget in place, rather than leaving a blank cell. local r = rect_for(entry, sw, sh) local inner = card.card(cr, r, colors) card.label(cr, inner.x, inner.y + 16, 'missing: ' .. entry.widget, colors) end end end function conky_main() if conky_window == nil then return end local s = conky_surface() if s == nil then return end local cr = cairo_create(s) local sw, sh = conky_window.width, conky_window.height local ok, err = pcall(function() draw(cr, sw, sh, palette()) end) if not ok then draw_error(cr, err, sw) end cairo_destroy(cr) end