1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
-- 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 = 16, 12
local layout = {
{ widget = 'clock', col = 1, row = 1, w = 1.5, h = 5 },
-- Under the clock, same column.
{ widget = 'weather', col = 1, row = 6, w = 3, h = 5 },
{ widget = 'system', col = 4, row = 1, w = 3, h = 6 },
{ widget = 'gpu', col = 7, row = 1, w = 3, h = 3 },
{ widget = 'disks', col = 7, row = 4, w = 5, h = 4 },
{ widget = 'cache', col = 12, row = 4, w = 3, h = 4 },
}
-- ==========================================================================
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),
warning = hex(CFG.color8),
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
-- Exported for test/test_layout.lua. The grid's whole claim is that one layout
-- table works on differently shaped screens, which is worth checking.
conky_dashboard_internal = { rect_for = rect_for, COLS = COLS, ROWS = ROWS,
GAP = GAP, MARGIN = MARGIN, layout = layout }
-- 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
|