Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions apps/countupclock/countupclock.star
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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",
Expand Down Expand Up @@ -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")))
Expand All @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
Binary file modified apps/countupclock/countupclock.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/countupclock/countupclock@2x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/countupclock/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ author: jvivona
fileName: countupclock.star
packageName: countupclock
recommendedInterval: 0
supports2x: true
category: clocks
tags:
- time
- utility
- tracking
published: '2022-11-17T20:03:31Z'
updated: '2025-12-02T19:30:22Z'
updated: '2026-07-06T16:50:05Z'
Binary file added apps/passover/images/cup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/passover/images/star.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/passover/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ 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
- lifestyle
published: '2026-03-20T00:15:10Z'
updated: '2026-03-20T00:15:10Z'
updated: '2026-07-06T16:50:15Z'
95 changes: 94 additions & 1 deletion apps/passover/passover.star
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -24,6 +33,7 @@ PASSOVER_DATES = [

# Hebrew text for Passover
PASSOVER_HEBREW = "פסח"
CHAG_SAMEACH = "חג שמח"

def main():
now = time.now().in_location(time.tz())
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Binary file added apps/passover/passover.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/passover/passover@2x.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.