From 3328daa9fb4976a10b92a289ed198ade546dc8b5 Mon Sep 17 00:00:00 2001 From: Joe Vivona Date: Mon, 6 Jul 2026 12:50:05 -0400 Subject: [PATCH 1/3] countupclock: add 2x (128x64) support At 2x the taller canvas is split into two equal halves: the title in one and the day/hour/minute counts in the other, each vertically centered. The title is enlarged (6x13) and word-wrapped to at most 2 lines (ellipsis if longer), and hours/minutes render as their own static lines instead of the fading marquee. 1x layout is unchanged. Adds supports2x to the manifest and regenerates both previews. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/countupclock/countupclock.star | 106 ++++++++++++++++++++++++- apps/countupclock/countupclock.webp | Bin 1102 -> 1122 bytes apps/countupclock/countupclock@2x.webp | Bin 0 -> 206 bytes apps/countupclock/manifest.yaml | 1 + 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 apps/countupclock/countupclock@2x.webp diff --git a/apps/countupclock/countupclock.star b/apps/countupclock/countupclock.star index 7a80b5c72..6238c44ce 100644 --- a/apps/countupclock/countupclock.star +++ b/apps/countupclock/countupclock.star @@ -10,7 +10,7 @@ borrowed Fade In and Out technique and the math calculations from @CubsAaron cou # 20240802 - jvivona - added in code to handle widget mode and remove animations load("math.star", "math") -load("render.star", "render") +load("render.star", "canvas", "render") load("schema.star", "schema") load("time.star", "time") @@ -24,6 +24,21 @@ HOURS_COLOR = "#888888" WIDGET_MODE = False +# 2x (128x64) has the vertical room to wrap the title across more lines and show +# hours and minutes as their own static lines instead of the fading marquee. +IS2X = canvas.is2x() +TITLE_FONT_2X = "6x13" + +# Keep the title to 2 lines. We greedily word-wrap it into at most two 21-char +# lines (6x13 is 6px wide, so 128px holds 21 chars) and render each as its own +# Text line - shrink-wrapped and vertically centered in its half with tight +# spacing (a fixed WrappedText height would top-align a single line instead). +# Longer titles get an ellipsis. +LINE_CHARS_2X = 21 +DAYS_FONT_2X = "terminus-16" +HOURS_FONT_2X = "tb-8" +HOURS_COLOR_2X = "#AAAAAA" + coloropt = [ schema.Option( display = "Red", @@ -82,10 +97,10 @@ def main(config): ) def get_render_children(config, widgetMode): - render_children = [] displayhours = config.bool("display_hours", True) displayminutes = config.bool("display_minutes", True) if displayhours else False titlebelow = config.bool("title_below", False) + is2x = IS2X and not widgetMode current_time = time.now().in_location(time.tz()) origin_time = time.parse_time(config.str("event_time", current_time.format("2006-01-02T15:04:05Z07:00"))) @@ -95,7 +110,12 @@ def get_render_children(config, widgetMode): days = math.floor(datediff.hours // 24) daystring = "{} {}".format(str(days), "Day" if days == 1 else "Days") - render_children.append(render.Text(content = daystring, font = DAYS_FONT if not (widgetMode and displayhours) else HOURS_FONT)) + if is2x: + return get_2x_children(config, datediff, days, daystring, displayhours, displayminutes, titlebelow) + + render_children = [] + days_font = HOURS_FONT if (widgetMode and displayhours) else DAYS_FONT + render_children.append(render.Text(content = daystring, font = days_font)) if displayhours: render_children.append(get_hours_minutes(datediff, days, displayminutes, widgetMode)) @@ -106,6 +126,67 @@ def get_render_children(config, widgetMode): return render_children +def title_lines_2x(text): + # Greedy word-wrap into at most two 21-char lines; ellipsis if it overflows. + lines = ["", ""] + li = 0 + truncated = False + for w in text.split(): + cand = w if lines[li] == "" else lines[li] + " " + w + if len(cand) <= LINE_CHARS_2X: + lines[li] = cand + elif li == 0: + li = 1 + lines[1] = w + else: + truncated = True + break + + # guard against a single word wider than a line + lines = [ln[:LINE_CHARS_2X] for ln in lines] + if truncated: + tail = lines[1] + if len(tail) >= LINE_CHARS_2X: + tail = tail[:LINE_CHARS_2X - 1] + lines[1] = tail + "…" + + return [ln for ln in lines if ln != ""] + +def get_2x_children(config, datediff, days, daystring, displayhours, displayminutes, titlebelow): + # Split the 128x64 canvas into two equal 32px halves - the title in one and + # the day/hour/minute counts in the other - each vertically centered in its + # half (render.Box centers its child). The larger title is clamped to 2 lines. + titlecolor = config.str("event_color", coloropt[3].value) + title_box = render.Box( + width = 128, + height = 32, + child = render.Column( + main_align = "center", + cross_align = "center", + children = [ + render.Text(content = line, font = TITLE_FONT_2X, color = titlecolor) + for line in title_lines_2x(config.str("event", "")) + ], + ), + ) + + count_lines = [render.Text(content = daystring, font = DAYS_FONT_2X)] + if displayhours: + count_lines.extend(get_hours_minutes_2x(datediff, days, displayminutes)) + count_box = render.Box( + width = 128, + height = 32, + child = render.Column( + main_align = "center", + cross_align = "center", + children = count_lines, + ), + ) + + if titlebelow: + return [count_box, title_box] + return [title_box, count_box] + def get_title(eventtitle, titlecolor, displayhours, widgetMode): if displayhours and not widgetMode: # since we are displaying hours - title needs to be marquee - text less than width will center on screen @@ -152,6 +233,25 @@ def get_hours_minutes(datediff, days, displayminutes, widgetMode): expanded = True, ) +def get_hours_minutes_2x(datediff, days, displayminutes): + # 2x static lines: no fade animation - just stack hours (and minutes) below days + hours = math.floor(datediff.hours - days * 24) + lines = [ + render.Text( + content = "{} {}".format(str(hours), "Hour" if hours == 1 else "Hours"), + font = HOURS_FONT_2X, + color = HOURS_COLOR_2X, + ), + ] + if displayminutes: + minutes = math.floor(datediff.minutes - (days * 24 * 60 + hours * 60)) + lines.append(render.Text( + content = "{} {}".format(str(minutes), "Minute" if minutes == 1 else "Minutes"), + font = HOURS_FONT_2X, + color = HOURS_COLOR_2X, + )) + return lines + def createfadelist(text, cycles): alpha_values = ["00", "33", "66", "99", "CC", "FF"] cycle_list = [] diff --git a/apps/countupclock/countupclock.webp b/apps/countupclock/countupclock.webp index 38af8c8e837703e600864c493aa8411a1b5c8742..c6f36fe820490822e2bf1d6daa0ab2d35088991e 100644 GIT binary patch literal 1122 zcmWIYbaRVhVPFV%bqWXzu!!JdU|b7tAopb z^@h&fT4%~P9+m$ckY4ntPT)HCjQ8pff;r zgX}T}*(KM&D8O$Zz1dLuv;2nu;geyXnQwB&zPs3D_Mw4Q%cj=H@^8~w}2s6!+jm@ZSW}vWXAhpfRy=<0Ck(n|)7~$E`Y|%lInI1UeNFN?Y0)n6A zopN(-U)cQF>gP<;@a|hL9J!U=x_+@|IH1I&z#RhEckamLxnn1yYsxPFXaX&3JqFJ j|F2de)^up>)5vsaI-rs1&=f%p(_v`?DcqrHgH+Q2{?t5? literal 1102 zcmWIYbaQiKVPFV%bqWXzu!!JdU|-E_e%XvVyxByM#U|`^8V31>A06G9@3j@d& z9gr<@4U7V&8X33Un2P?){MUY<^TmIb3VXGSbHBK|3VN>K71Etlblzms!hmij;>}7b z{d4dyL9;-<#O5wtn7i0mb8mY{^xMz< zZ)=nYI4lX8HdP}<#Z$?yzVScDDqf{u_Kf%XJxkR81_rtaWi`8WFS%pfo1<}M5hP~N z!%T(;5@y;kw}HcKQT>7c&JSFG@v=vL@(aITix+jMTw&`J^W2#(bu(avDAga|90%|R!I7%-ca5Apznt>E32c6gS0|ZYsV&cktK~xQ5s;g0)X}dqYxOE z>L_t(0dki*C@u{$ZhHt#c-&tCb=jZqvs+#)SZm^GxldVBOG%V#t0RwzaL}pMQw(pe zOw(#vM2fkqAD_=9$=o0u?zM!umyIn~+VfDwpNDViIp(;TuKF+hVE=s;7xvtxo~J&_ zdIk!v3|PUNv|#3hptz+yC;d!=6-8KybMaJz78OsSk3X5;)U!mg3I3hW@c*xxqE&{r zZ&2TRkFG9{R;OG6=CdtRK53=;l&zY{wNL|;!rOo@g8)dL1jal#Pv!vy!FkfuB2UG0 z;<-PcrKUf8F;(#5f6g6$=H6F3z?Y(vTCC;X9HjYXfzhH(wmq)d`U|BNoQm@NtTE|S zRHls9rPWT>rd!fW+O9KBH9D-8;$Tu8?J45LDJm+h@inL|s53)dyh4S_(Czsisc1d{w! zhJ5}5e-*fKZ7Xh`2c$1*=n)mm@Yv7$K~yNKuv}T$=J^L6r%%B^7(i|Du>Hu9S*+e$WwU+A+IAB#duND I6ap7B02KOMoB#j- literal 0 HcmV?d00001 diff --git a/apps/countupclock/manifest.yaml b/apps/countupclock/manifest.yaml index 1f1291f8e..c862209e2 100644 --- a/apps/countupclock/manifest.yaml +++ b/apps/countupclock/manifest.yaml @@ -7,6 +7,7 @@ author: jvivona fileName: countupclock.star packageName: countupclock recommendedInterval: 0 +supports2x: true category: clocks tags: - time From 2087774b12b41af9d9ec188fedfd53018edc41fa Mon Sep 17 00:00:00 2001 From: Joe Vivona Date: Mon, 6 Jul 2026 12:50:15 -0400 Subject: [PATCH 2/3] passover: add 2x (128x64) support with Star of David + Kiddush cup At 2x, all three screens (countdown, during-Passover, default) gain a gold Star of David hero beside the Hebrew word, flanked by a Kiddush wine cup. The countdown is expanded with a centered date block showing the full weekday, the month/day, and the year (e.g. "Thursday / Apr 22 / 2027") beneath the day count. 1x layout is unchanged. Adds supports2x to the manifest and both preview images (the app previously shipped none). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/passover/images/cup.png | Bin 0 -> 166 bytes apps/passover/images/star.png | Bin 0 -> 154 bytes apps/passover/manifest.yaml | 1 + apps/passover/passover.star | 95 ++++++++++++++++++++++++++++++++- apps/passover/passover.webp | Bin 0 -> 196 bytes apps/passover/passover@2x.webp | Bin 0 -> 352 bytes 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 apps/passover/images/cup.png create mode 100644 apps/passover/images/star.png create mode 100644 apps/passover/passover.webp create mode 100644 apps/passover/passover@2x.webp diff --git a/apps/passover/images/cup.png b/apps/passover/images/cup.png new file mode 100644 index 0000000000000000000000000000000000000000..56b785f7b8047173fe4cb3b775042e9c854995d0 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^oFFy_8<4DKZ~;-do-U3d6?1wg81gkJ@VLfH>mTNv zW80MeFhTBM(3{lLM4L$lG|M03v`v#DPuNTe`YeBa8QbA0N;-_^eb7Ay{A z(Bk{3IBUM0%Mrz5HHOn+Iu3fOUoE*3j_tbr$8YV@t(n{rd%t~Kald}i291qPV%H7< PEoSg^^>bP0l+XkKu>d_P literal 0 HcmV?d00001 diff --git a/apps/passover/images/star.png b/apps/passover/images/star.png new file mode 100644 index 0000000000000000000000000000000000000000..43c312eb4254752105ba8523bfde918a46523b06 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*L{AsTkcv5bCm-ZxP~c#`n0u#Q z;@H9I7uk=Qh5gRcTiS3lzcpH6-zp|YsSVjLxEvB%4VO2VNrnh%XNvYq-wA6sJBb5F7y1WZFo(2B}TE^h%>gTe~DWM4f Du_8Hm literal 0 HcmV?d00001 diff --git a/apps/passover/manifest.yaml b/apps/passover/manifest.yaml index d6a90e53d..31253f0e8 100644 --- a/apps/passover/manifest.yaml +++ b/apps/passover/manifest.yaml @@ -5,6 +5,7 @@ summary: Passover Countdown / In Progress desc: Track days until Passover show current when it is in progress. author: jvivona recommendedInterval: 5 +supports2x: true category: lifestyle tags: - time diff --git a/apps/passover/passover.star b/apps/passover/passover.star index c1d837dc4..d02b90996 100644 --- a/apps/passover/passover.star +++ b/apps/passover/passover.star @@ -5,9 +5,18 @@ Description: Shows countdown to Passover or which day of the 8-day celebration i Author: jvivona """ -load("render.star", "render") +load("images/cup.png", CUP = "file") +load("images/star.png", STAR = "file") +load("render.star", "canvas", "render") load("time.star", "time") +# 2x (128x64) adds a Star of David + Kiddush cup and an expanded date line; +# 1x (64x32) is unchanged. +IS2X = canvas.is2x() + +STAR_IMG = STAR.readall() +CUP_IMG = CUP.readall() + PASSOVER_DATES = [ {"year": 2026, "start": "2026-04-01", "end": "2026-04-08"}, {"year": 2027, "start": "2027-04-22", "end": "2027-04-29"}, @@ -24,6 +33,7 @@ PASSOVER_DATES = [ # Hebrew text for Passover PASSOVER_HEBREW = "פסח" +CHAG_SAMEACH = "חג שמח" def main(): now = time.now().in_location(time.tz()) @@ -53,6 +63,40 @@ def main(): else: return render_default() +# --- 2x sprite helpers ------------------------------------------------------- + +def top_row_2x(with_cup): + """Star of David + Hebrew word, optionally flanked by the Kiddush cup.""" + children = [ + render.Image(src = STAR_IMG), + render.Box(width = 5, height = 1), + render.Text(content = PASSOVER_HEBREW, font = "6x13", color = "#FFD700"), + ] + if with_cup: + children.append(render.Box(width = 5, height = 1)) + children.append(render.Image(src = CUP_IMG)) + return render.Row( + main_align = "center", + cross_align = "center", + children = children, + ) + +def frame_2x(children): + return render.Root( + child = render.Box( + width = 128, + height = 64, + child = render.Column( + expanded = True, + main_align = "space_evenly", + cross_align = "center", + children = children, + ), + ), + ) + +# --- During Passover --------------------------------------------------------- + def render_during_passover(passover, now, timezone): """Render display during Passover showing which day it is""" start_time = time.parse_time(passover["start"] + "T00:00:00Z").in_location(timezone) @@ -75,6 +119,22 @@ def render_during_passover(passover, now, timezone): day_name = day_names[day_of_passover - 1] + if IS2X: + return frame_2x([ + top_row_2x(True), + render.Text(content = CHAG_SAMEACH, font = "6x13", color = "#87CEEB"), + render.Text(content = day_name, font = "5x8", color = "#FFFFFF"), + render.Row( + main_align = "center", + cross_align = "center", + children = [ + render.Text(content = "Day ", font = "tb-8", color = "#98D8C8"), + render.Text(content = str(day_of_passover), font = "tb-8", color = "#FFD700"), + render.Text(content = " of 8", font = "tb-8", color = "#98D8C8"), + ], + ), + ]) + return render.Root( child = render.Box( child = render.Column( @@ -139,6 +199,8 @@ def render_during_passover(passover, now, timezone): ), ) +# --- Countdown --------------------------------------------------------------- + def render_countdown(passover, now, timezone): """Render countdown to next Passover""" start_time = time.parse_time(passover["start"] + "T00:00:00Z").in_location(timezone) @@ -147,6 +209,29 @@ def render_countdown(passover, now, timezone): time_until = start_time - now days_until = int(time_until.hours / 24) + if IS2X: + # Parse at noon UTC so the displayed calendar date never shifts a day + # under a negative UTC offset (e.g. America/New_York). + start_disp = time.parse_time(passover["start"] + "T12:00:00Z").in_location(timezone) + return frame_2x([ + top_row_2x(True), + render.Row( + cross_align = "center", + children = [ + render.Text(content = str(days_until), font = "6x13", color = "#FFD700"), + render.Text(content = " days", font = "6x13", color = "#FFFFFF"), + ], + ), + render.Column( + cross_align = "center", + children = [ + render.Text(content = start_disp.format("Monday"), font = "5x8", color = "#FFFFFF"), + render.Text(content = start_disp.format("January 2"), font = "tom-thumb", color = "#FFFFFF"), + render.Text(content = str(passover["year"]), font = "tom-thumb", color = "#98D8C8"), + ], + ), + ]) + return render.Root( child = render.Box( child = render.Column( @@ -206,8 +291,16 @@ def render_countdown(passover, now, timezone): ), ) +# --- Default ----------------------------------------------------------------- + def render_default(): """Default render if no Passover data available""" + if IS2X: + return frame_2x([ + top_row_2x(True), + render.Text(content = "Passover", font = "6x13", color = "#FFFFFF"), + ]) + return render.Root( child = render.Box( child = render.Column( diff --git a/apps/passover/passover.webp b/apps/passover/passover.webp new file mode 100644 index 0000000000000000000000000000000000000000..364f53f7005828c367fd729587ea4627bc221364 GIT binary patch literal 196 zcmV;#06YIuNk&Gz00012MM6+kP&iDm0000lKfnh7A0Q+k{9ova=!SrxXuj730tEl5 zBFuk)KdAz@4ZyYyC8bYP%1(In)E9CEMn~h}n^b`NFunASjA~ilX2odzTV-xI;801Jyy@kfw@pzE~q8K=vgS%w}bg zwX^xpnw>Yf0R9FVGZUINJ24!ZscE9Q43G!(3fT!MZFUmvflc&0NE&FG_}Zk90AjpW yB@dD$zBh?WG`kksHXg|OZ#-Y$wz;mu%rGqUyv;ms&)d4g%rFbRUEi4NO8>@JyP#13 literal 0 HcmV?d00001 From bfa0285af65d898935a4dbda8d919ea6292414f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Jul 2026 20:01:50 +0000 Subject: [PATCH 3/3] chore: auto-update manifest metadata [skip ci] --- apps/countupclock/manifest.yaml | 2 +- apps/passover/manifest.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/countupclock/manifest.yaml b/apps/countupclock/manifest.yaml index c862209e2..b024c1d80 100644 --- a/apps/countupclock/manifest.yaml +++ b/apps/countupclock/manifest.yaml @@ -14,4 +14,4 @@ tags: - utility - tracking published: '2022-11-17T20:03:31Z' -updated: '2025-12-02T19:30:22Z' +updated: '2026-07-06T16:50:05Z' diff --git a/apps/passover/manifest.yaml b/apps/passover/manifest.yaml index 31253f0e8..c2caf2141 100644 --- a/apps/passover/manifest.yaml +++ b/apps/passover/manifest.yaml @@ -11,4 +11,4 @@ tags: - time - lifestyle published: '2026-03-20T00:15:10Z' -updated: '2026-03-20T00:15:10Z' +updated: '2026-07-06T16:50:15Z'