Skip to content

Commit b2a9dc5

Browse files
Fumunchuclaude
andcommitted
Add radial/text widgets, netmap pages, Cyberdeck art page
- radial: arc gauge from 12 o'clock, with own track or tracing ring art from a background image; text: free-form value text for art pages - type: netmap pages embed the netmap app in the carousel rotation (page keys override display.yaml netmap config; header gains page dots) - shipped CYBERDECK page: live gauges (cpu/ram/temp/load/disk) traced over the stock Cyberdeck theme art, plus k3s/fleet values in its boxes - time_hm local value Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b78e7ec commit b2a9dc5

5 files changed

Lines changed: 86 additions & 20 deletions

File tree

DISPLAY.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ Home Assistant, kubectl, etc. is one small class in `apps/common.py`
3939

4040
## Page schema
4141

42-
See the comment block at the top of `pages.yaml` — widgets (`bar`, `metric`),
43-
format strings over a value namespace (local psutil values + per-page query
44-
results + derived expressions), threshold/conditional color specs, optional
45-
`background:` art from any stock theme.
42+
See the comment block at the top of `pages.yaml` — widgets (`bar`, `metric`,
43+
`radial`, `text`), format strings over a value namespace (local psutil values
44+
+ per-page query results + derived expressions), threshold/conditional color
45+
specs, optional `background:` art from any stock theme (the shipped CYBERDECK
46+
page traces live radial gauges over the stock Cyberdeck theme art), and
47+
`type: netmap` pages that embed the netmap app in the rotation.
4648

4749
## Preview without a screen
4850

apps/carousel.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ def page_values(page, sources):
3939
return vals
4040

4141

42-
def render_page(page, idx, npages, sources):
42+
def render_page(page, idx, npages, sources, display_cfg=None):
43+
if page.get("type") == "netmap": # embed the netmap app as a page
44+
from apps import netmap
45+
nm = dict((display_cfg or {}).get("netmap") or {})
46+
nm.update({k: v for k, v in page.items() if k != "type"})
47+
src = sources.get(nm.get("source", "prom"))
48+
return netmap.render(netmap.gather_hosts(src, nm), nm, idx, npages)
4349
img = Image.new("RGB", (c.W, c.H), c.BG)
4450
bg = page.get("background")
4551
if bg:
@@ -77,7 +83,7 @@ def run(lcd, display_cfg):
7783
pages = cfg.get("pages") or []
7884
for k, page in enumerate(pages):
7985
p = f"/tmp/carousel_{k}.png"
80-
render_page(page, k, len(pages), sources).save(p)
86+
render_page(page, k, len(pages), sources, display_cfg).save(p)
8187
print("wrote", p)
8288
return
8389

@@ -102,7 +108,7 @@ def run(lcd, display_cfg):
102108
dwell = float(cfg.get("dwell", 8))
103109
refresh = float(cfg.get("refresh", 2))
104110

105-
frame = render_page(page, idx, len(pages), sources)
111+
frame = render_page(page, idx, len(pages), sources, display_cfg)
106112
c.push_frame(lcd, frame, prev)
107113
prev = frame
108114
t0 = time.monotonic()
@@ -113,7 +119,7 @@ def run(lcd, display_cfg):
113119
time.sleep(min(refresh, remaining))
114120
if dwell - (time.monotonic() - t0) <= 0.05:
115121
break
116-
frame = render_page(page, idx, len(pages), sources)
122+
frame = render_page(page, idx, len(pages), sources, display_cfg)
117123
c.push_frame(lcd, frame, prev)
118124
prev = frame
119125
idx += 1

apps/common.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def local_values():
126126
"temp": host_temp(),
127127
"load1": os.getloadavg()[0],
128128
"uptime": fmt_uptime(),
129+
"time_hm": time.strftime("%H:%M"),
129130
}
130131

131132

@@ -203,7 +204,35 @@ def draw_metric(d, w, vals):
203204
fill=resolve_color(w.get("color"), vals), anchor="la")
204205

205206

206-
WIDGETS = {"bar": draw_bar, "metric": draw_metric}
207+
def draw_radial(d, w, vals):
208+
"""Arc gauge: fills clockwise from 12 o'clock. With `track: true` draws its
209+
own ring; without, it traces ring art from a background image."""
210+
x, y, r = w["x"], w["y"], w.get("r", 40)
211+
pct = vals.get(w.get("pct")) or 0
212+
width = w.get("width", 8)
213+
color = resolve_color(w["color"], vals) if w.get("color") else threshold_color(pct)
214+
if w.get("track"):
215+
d.arc([x - r, y - r, x + r, y + r], 0, 360, fill=TRACK, width=width)
216+
if pct > 0:
217+
d.arc([x - r, y - r, x + r, y + r], -90, -90 + min(pct, 100) * 3.6,
218+
fill=color, width=width)
219+
if w.get("text"):
220+
d.text((x, y), fmt(w["text"], vals), font=font(w.get("size", 20), "Bold"),
221+
fill=COLORS["fg"], anchor="mm")
222+
if w.get("label"):
223+
d.text((x, y + r + 4), str(w["label"]), font=font(12, "Medium"),
224+
fill=COLORS["dim"], anchor="ma")
225+
226+
227+
def draw_text(d, w, vals):
228+
"""Free-form text at (x, y) -- for value boxes on background art pages."""
229+
d.text((w["x"], w["y"]), fmt(w.get("text", ""), vals),
230+
font=font(w.get("size", 16), w.get("weight", "Medium"),
231+
w.get("family", "roboto")),
232+
fill=resolve_color(w.get("color"), vals), anchor=w.get("anchor", "mm"))
233+
234+
235+
WIDGETS = {"bar": draw_bar, "metric": draw_metric, "radial": draw_radial, "text": draw_text}
207236

208237

209238
def push_frame(lcd, new, old=None, band=20):

apps/netmap.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ def gather_hosts(src, nm_cfg):
3131
return sorted(hosts.items())
3232

3333

34-
def render(hosts, nm_cfg):
34+
def render(hosts, nm_cfg, idx=None, npages=None):
3535
img = Image.new("RGB", (c.W, c.H), c.BG)
3636
d = ImageDraw.Draw(img)
3737
n_up = sum(1 for _, v in hosts if v >= 1)
38-
c.header(d, "NETWORK MAP")
39-
d.text((c.W - 16, 22), f"{n_up}/{len(hosts)} up",
38+
c.header(d, nm_cfg.get("title", "NETWORK MAP"), idx, npages)
39+
count_x = c.W - 16 - (18 * npages + 16 if npages else 0) # clear the page dots
40+
d.text((count_x, 22), f"{n_up}/{len(hosts)} up",
4041
font=c.font(16, "B", "mono"),
4142
fill=c.COLORS["ok"] if n_up == len(hosts) else c.COLORS["bad"], anchor="rm")
4243

pages.yaml

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
#
88
# Widget types:
99
# bar: {type: bar, x, y, w, label, pct: <value-name>, text: "<format>"}
10-
# metric: {type: metric, x, y, label, text: "<format>", color: <spec>, bw: <region width>}
10+
# metric: {type: metric, x, y, label, text: "<format>", color: <spec>}
11+
# radial: {type: radial, x, y, r, width, pct: <value-name>, text, size, label, track: true}
12+
# arc gauge from 12 o'clock; omit `track` to trace background ring art
13+
# text: {type: text, x, y, size, weight, family, anchor, text: "<format>", color: <spec>}
14+
#
15+
# Page types:
16+
# (default) widget page as below
17+
# type: netmap embeds the netmap app as a page; keys here override
18+
# display.yaml's `netmap:` section (title, up_query, ...)
1119
#
1220
# Page options:
1321
# background: res/themes/<name>/background.png draw widgets over theme art
@@ -71,10 +79,30 @@ pages:
7179
- {type: metric, x: 16, y: 170, label: CPU, text: "{cpu:.0f}%", color: "auto:cpu"}
7280
- {type: metric, x: 176, y: 170, label: MEM, text: "{mem:.0f}%", color: "auto:mem"}
7381

74-
# Example: widgets over existing theme art (try it, then tune positions):
75-
# - title: CYBERDECK
76-
# background: res/themes/Cyberdeck/background.png
77-
# header: false
78-
# footer: false
79-
# widgets:
80-
# - {type: metric, x: 40, y: 80, label: CPU, text: "{cpu_pct:.0f}%", color: accent}
82+
# The netmap app embedded as a page (settings inherit from display.yaml `netmap:`)
83+
- type: netmap
84+
85+
# Live values over the stock Cyberdeck theme art: radials trace the ring art,
86+
# text widgets sit in its value boxes. GPU ring shows disk until the GPU lands.
87+
- title: CYBERDECK
88+
background: res/themes/Cyberdeck/background.png
89+
header: false
90+
footer: false
91+
queries:
92+
hosts_up: sum(up{job=~".*node.*"})
93+
hosts_total: count(node_uname_info)
94+
pods_run: sum(kube_pod_status_phase{phase="Running"})
95+
derived:
96+
temp_pct: "(temp or 0)/90*100"
97+
load_pct: "(load1 or 0)/8*100"
98+
widgets:
99+
- {type: radial, x: 96, y: 79, r: 37, width: 9, pct: cpu_pct, text: "{cpu_pct:.0f}%"}
100+
- {type: radial, x: 107, y: 206, r: 35, width: 9, pct: ram_pct, text: "{ram_pct:.0f}%"}
101+
- {type: radial, x: 164, y: 136, r: 27, width: 7, pct: temp_pct, text: "{temp:.0f}°", size: 16}
102+
- {type: radial, x: 232, y: 69, r: 31, width: 8, pct: load_pct, text: "{load1:.1f}", size: 16}
103+
- {type: radial, x: 388, y: 165, r: 50, width: 10, pct: disk_pct, text: "{disk_pct:.0f}%"}
104+
- {type: text, x: 375, y: 47, size: 12, text: "{ram_used_g:.1f} / {ram_total_g:.0f} G"}
105+
- {type: text, x: 72, y: 137, size: 14, text: "{uptime}"}
106+
- {type: text, x: 266, y: 160, size: 14, text: "{time_hm}"}
107+
- {type: text, x: 252, y: 250, size: 22, weight: Bold, text: "{pods_run:.0f} pods"}
108+
- {type: text, x: 398, y: 292, size: 12, text: "{hosts_up:.0f}/{hosts_total:.0f} up", color: "ok_if:hosts_up>=hosts_total"}

0 commit comments

Comments
 (0)