Skip to content

Modernize Timely → TimelyNG (Core Devices SDK, complications, adaptive layout) - #2

Merged
eldios merged 77 commits into
watchfacefrom
feat/modernize-for-modern-stack
Jun 5, 2026
Merged

Modernize Timely → TimelyNG (Core Devices SDK, complications, adaptive layout)#2
eldios merged 77 commits into
watchfacefrom
feat/modernize-for-modern-stack

Conversation

@eldios

@eldios eldios commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Modernizes the classic Timely / PebbleTimely watchface into TimelyNG:
rebuilt for the current Core Devices Pebble SDK and the 2026 hardware, with a
configurable‑complications system, a theme engine, an adaptive layout, and a
proper test/CI setup. Freely inspired by, and an homage to, the original Timely
and the Andrew129260/Timely‑2 and alan‑johnson/PebbleTimely forks.

Single PR for the whole feat/modernize-for-modern-stack line of work.

Highlights

Build & platforms

  • Builds with the modern SDK; targets emery (Pebble Time 2), flint
    (Pebble 2 Duo), basalt, diorite. aplite dropped (24 KB RAM); round
    chalk/gabbro noted as future.
  • Cloud‑build fixes: quoted project includes (src/c/ layout) and
    const‑correct time_font_key (-Werror=discarded-qualifiers).

Complications system

  • Three configurable rows — status bar, above the time, above the calendar —
    each 1 (centred) or 2 (halves), drawing from one shared, alphabetical
    menu
    (date, day, month, week, day‑of‑year/left, seconds, AM/PM, timezone,
    2nd time zone, sunrise, sunset, moon phase, location, watch/phone battery,
    Bluetooth).
  • Adaptive layout that reflows live on config save: empty a row and the
    clock/calendar grow; the clock font scales to the band.
  • Battery styles: filling bar with % inside, text, icon+text, bar+icon.

Weather / theme / config

  • Open‑Meteo over HTTPS (no API key), per‑platform sizing.
  • Palette‑driven theme engine; Auto mode uses real sunrise/sunset.
  • Fully offline configuration (no server/CDN); grouped settings.

Architecture & quality

  • State encapsulated behind accessors; calendar/weather/splash/theme/vibes/
    layout extracted into testable modules.
  • Host unit tests (18) + tools/test.sh (host tests + build + strict
    per‑platform compile mirroring the cloud -Werror flags); warnings at zero.
  • GitHub Actions CI (host tests + PebbleKit JS syntax).

Release

  • v0.0.1; README.md modernized (badges, platform table, "why" section,
    homage); PUBLISHING.md (appstore copy) and TODO.md (incl. inherited TODOs)
    added; per‑platform store screenshots under marketing/.

Testing

  • tools/test.sh green: 18 host unit tests pass; strict per‑platform compile
    (basalt/diorite/emery/flint) clean with no warnings under the cloud flag
    set.
  • CI workflow run locally with act — job succeeded (host tests + JS check).
  • Verified on the emulator across emery/basalt/diorite/flint: default layout,
    each battery style, single‑vs‑two complications, status‑bar off, weather
    alignment, calendar header.
  • The offline config round‑trip (save → apply) confirmed earlier on a real
    device.

eldios added 30 commits June 4, 2026 22:55
- wscript: switch to pbl_build and call the SDK's
  pbl_suppress_newer_gcc_warnings so the legacy code compiles under the
  modern arm-none-eabi gcc -Werror (truncation/fallthrough/etc).
- extract pure date/timezone formatting into src/timefmt.{c,h} (no Pebble
  deps); fix the days-left and timezone snprintf truncation via input clamps.
- tests: utest.h-based host unit tests for timefmt, run via make / just /
  nix flake check.
- flake.nix: add just+make to the pebble dev shell, a devShells.test with a
  host cc, and checks.default that builds and runs the unit tests; reuse
  pebble.nix's pinned nixpkgs.

aplite + basalt build clean and produce a .pbw.
…tches

- appinfo.json: new UUID (separate app for republish), name TimelyColor,
  version 3.0, author Emanuele 'Lele' Calo; targetPlatforms now aplite,
  basalt, diorite, emery (Pebble Time 2, color) and flint (Pebble 2 Duo /
  Core 2 Duo, B&W). Round platforms (chalk/gabbro) deferred: the calendar
  UI is rectangular-centric.
- src/Timely.c: size date_string to 64 to remove a real truncation on the
  localized date string.
- flake.nix: default emulator emery (color); downgrade legacy warnings to
  non-fatal via shellHook CFLAGS=-Wno-error since the new flint platform
  does not pick up the SDK's in-tree warning suppressions.

All five target platforms build a .pbw.
Add setTodayColors(): on color hardware (basalt/emery) today's calendar cell
uses an accent fill (GColorJaegerGreen, tunable) with white text; on B&W
(aplite/diorite/flint) it falls back byte-for-byte to the classic
white-on-black inversion via PBL_IF_COLOR_ELSE. First step of a color theme
with graceful monochrome fallback.
…ribution

State the modernization changes (GPLv3 sec.5), keep the original Martin
Norland / William Heaton / Ben Johnson copyrights, and add the 2026
Emanuele 'Lele' Calo copyright for this fork. Document the Nix dev/build/test
flow and the targeted platforms.
src/layout.{c,h}: pure (Pebble-free) layout_compute(w,h) -> TimelyLayout that
derives statusbar/slot/clock/calendar rects from the runtime screen size.
Host tests verify it matches the historical 144x168 geometry and fills emery
200x228. Refactor the test suite into one binary (tests/test_main.c +
per-module tests/test_*.c) so make / just / nix flake check run all modules.

Not yet wired into the renderer (Timely.c still uses the legacy #defines);
that integration is the next step.
Convert the geometry #defines (DEVICE_*/LAYOUT_*/REL_CLOCK_*/CAL_WIDTH/HEIGHT
plus right-aligned STAT_BATT_LEFT/STAT_CHRG_ICON_LEFT) to file globals that
compute_layout() fills from the tested layout_compute() at window_load. The
renderer now fills the screen on every platform: emery (200x228) uses the full
width/height with a wider calendar; classic 144x168 is byte-identical (host
tests assert the historical geometry). Reference sites are unchanged.
cal_cell_h = slot_bot.h / 4 so the classic 144x168 keeps its historical 18px
rows (was regressing to 24px); emery gets 25px rows. Tests updated.
Pure, host-tested calendar_build() replaces the inline grid computation in
calendar_layer_update_callback (rendering stays). 3 tests assert the grid
against the verified June 2026 emulator output and the start-of-week offset
paths. Behavior unchanged (emery render identical).
period_contains() (host-tested: single-day, midnight-wrap, equal start/stop)
replaces the inline window check in period_check(); the Pebble wrapper keeps
reading currentTime and the debug log. Behavior unchanged.
weather.c owns the weather_data (file-static); the global is gone and all 23
field accesses go through weather_state(). First step of the state-encapsulation
redesign (no shared globals).
settings.c owns the persist / persist_adv_settings instances (file-static);
the globals are gone and all ~99 field accesses + the persist read/write go
through settings_get() / adv_settings_get(). Types moved to settings.h.
locale.c owns the general/months/days language tables (file-static); globals
gone, all ~38 accesses + persist read/write go through lang_gen_get() /
lang_months_get() / lang_days_get(). Types moved to locale.h.
debug.c owns the persist_debug flags (file-static); the global is gone and all
48 accesses + persist read/write go through debug_get(). Completes the
state-encapsulation pass: weather, settings, adv_settings, localization and
debug are now module-owned behind accessors (no shared global state).
setColors/setInvColors/setTodayColors move to theme.c (the home for the
upcoming color-theme work). Add ui.h for shared UI handles; the window handle
is now exposed there (un-static) so view modules can be split out.
weather_layer_update_callback now lives with the weather state it draws;
weather.c owns state + render. climacons exposed via ui.h.
weather.c now owns s_weather_layer (private) with a static render proc, exposed
via weather_create/destroy/set_frame/set_hidden/mark_dirty. window_load composes
it instead of creating the layer directly. First component of the
component-ownership view rearchitecture.
- calendar.c stays the pure host-tested model; the Pebble render + layer move
  to calendar_view.c (calendar_create/destroy/set_hidden/mark_dirty).
- splash.c: new component, rebranded 'TimelyColor 3.0', centered via layout.
- toggle_slot_bottom rewritten to switch components by id (no raw layer ptrs).
- layout module exposes the computed layout via layout_get(); view modules
  render proportionally from it. Shared read resources (currentTime, calendar
  fonts) exposed via ui.h. Build green on 5 platforms; 13 host tests pass.
theme.c defines palettes for Mono/Functional/Minimal/Vibrant, each with a
light and a dark variant; theme_palette() resolves the active palette from
settings.theme + settings.theme_mode (AUTO = night, currently a time-based
placeholder pending sunset/sunrise). setColors/setInvColors/setTodayColors are
now palette-driven. Default Functional/Dark reproduces the prior look; B&W
platforms degrade to mono. Theme selection UI comes with the options milestone.
Uploading to cloudpebble.repebble.com under the TimelyNG name (Timely, Next
Generation). Updates appinfo displayName, README, splash text and flake
description; UUID/version unchanged.
Build/run-on-emulator/screenshot/logs/kill/sdk plus the host test targets, so
common dev commands aren't retyped. Parameterized by EMU (platform) and SHOT
(screenshot path). Host-test recipes run in nix develop .#test; pebble recipes
in nix develop.
Replaces GNU unifont (bitmap) for the localized-text path (date/day/connection/
calendar on Cyrillic etc.): cleaner letterforms + a real bold (unifont had
none). Same character subset (Latin + Cyrillic + Latin-1). Pebble renders text
1-bit (no AA on any platform), so this is a letterform/coverage change, not a
smoothness one; Latin languages keep using the crisp system Gothic font.
Big time uses the system FONT_KEY_ROBOTO_BOLD_SUBSET_49 (crisp, modern, fills
more) instead of the grainy custom futura TTF. Remove the now-unused
futura_condensed and unifont_latest .ttf resources (the unicode path is DejaVu),
freeing resource budget.
Pebble top-aligns text in its box, so numbers sat squished to the top of each
cell (visible on the taller emery cells). Center them using the measured text
height (graphics_text_layout_get_content_size).
content_size height includes the font's line leading, biasing the glyph low;
compensate so numbers sit centered in the cell.
Replace the fixed -3px leading correction with ts.h/6 so it scales with the
actual text height (normal vs bold, and future per-theme fonts).
Weekend day labels + numbers use palette.weekend (Sat/Sun), distinct from
weekday fg; today keeps the accent. Mono theme: weekend == fg (no distinction).
When battery <= 20% the statusbar battery outline uses palette.warn (red on
the functional theme; unchanged on mono).
New pure, host-tested suntimes module (solar declination + hour angle, using
math.c's trig) computes sunrise/sunset from the configured weather lat/lon +
timezone; theme AUTO mode is dark between sunset and sunrise. Verified against
Milan/June. Falls back to a 19:00-06:59 window when no location is set, and on
aplite (whose 24KB budget can't afford the float trig). Expose timezone_offset
via ui.h; wire suntimes+math into the host test suite.
TextLayers (time/date/day/week/ampm/connection/battery%) used a fixed white,
invisible on light palettes. apply_palette() sets them to palette.fg and runs
each minute tick (and at load) so light/dark + Auto day/night transitions are
reflected. The legacy full-screen invert overlay stays hidden by default; the
palette now drives light/dark.
Status icons (bluetooth/charging/hourvibe/dnd) were fixed black, invisible on
colored/dark backgrounds. tint_icon() recolors their opaque pixels to
palette.fg (keeping antialiased alpha) on color platforms, refreshed each tick;
bitmap layers use GCompOpSet so the alpha composites over the themed background.
B&W platforms keep the 1-bit icons.
eldios added 27 commits June 4, 2026 22:55
- Saving now merges the changed keys into the stored settings instead of
  overwriting localStorage with just the delta. The config page kept only the
  last-changed key, so reopening showed every other setting at its default —
  making it look like changing one complication wiped the others. Merging keeps
  the full picture for all settings.
- Reduce the above-calendar complications from three to two (left/right), each
  given half the width so longer values (e.g. a location) are no longer cut.
  The middle slot is retired (hidden); status-bar and above-time complication
  rows will come next.
PebbleKit JS now sends the weather coordinates (weather_lat/weather_lon) to the
watch, which stores and persists them. Two new complications, Sunrise and
Sunset, compute local times from those coords via the existing suntimes module
(skipped on aplite, whose RAM budget can't afford the float trig — shows
--:--). The same coordinates make the Auto theme use the real sunset/sunrise
instead of the fixed 19:00-07:00 fallback.
Add a Moon phase complication: a date-only Conway approximation (no coords, no
float trig, so it works on every platform) mapped to eight phase labels (New,
Wax cres, 1st qtr, Wax gib, Full, Wan gib, Last qtr, Wan cres). Verified
against known new/full moons.
Add a configurable second time zone (clock2_tz UTC offset, whole hours) and a
'2nd time zone' complication showing that zone's clock. Uses the device's
timezone offset to convert; shows --:-- until the timezone is known.
Add small PNG icons (16x16) labelling the two battery readouts: a phone
icon by the phone-battery percentage (left) and a watch icon by the watch
battery (right). The connection text drops the "Ph " prefix and the
Linked/NOLINK word since the icons now convey what each readout is.

The watch icon shares the slot used by the charging/DND/hourly-vibe icon,
so it is hidden whenever that slot is active to avoid overlap.
The cloud build (cloudpebble.repebble.com) relocates sources into the
modern src/c/ layout and moves headers alongside. Angle-bracket includes
only search the -I paths, so <Timely.h> was not found there. Quoted
includes resolve relative to the including file, matching every other
project header in this file; the local flat-src build is unaffected.
On 144px screens the Roboto 49 clock needed almost the full width and
collided with the weather to its left. Make the clock font width-aware
(Roboto 49 on wide screens like emery, condensed LECO 38 on 144px) and
shrink the weather glyph (Climacons 28) and temperature (Gothic 18) on
narrow screens so the column is compact. Wide screens are unchanged.

Drop aplite from targetPlatforms: the modernized feature set no longer
fits its 24KB RAM (code alone ~19KB), so it faulted on launch. basalt,
diorite, emery and flint remain; aplite is a future code-diet ToDo.
Extend the unified complication menu (update_slot_text + config SLOTS)
with the system-info readouts so they can appear in any slot: Watch
battery (15), Phone battery (16), Bluetooth connection (17). They share
the same TextLayer renderer as the existing complications.
The status bar is now two configurable complication slots (left/right)
instead of a fixed connection + battery display. Each slot picks any
content from the shared menu; battery/Bluetooth additionally show a 16px
icon on the outer edge, other contents render as text. Defaults preserve
the previous look (left = phone battery, right = watch battery).

Adds slot_stat_l/slot_stat_r settings + message keys, a 16px Bluetooth
glyph, and refreshes the slots on battery/connection/tick/config changes.
The graphical battery meter and the fixed BT icon are retired.
Extract the date formatter (format_current_date) from update_date_text so
the configured date can render in any complication slot, and add Date (18)
to the unified menu. Foundation for placing the date in the CENTER row.
…l rows

The band above the time is now two configurable slots (slot_ctr_l/r,
default left = Date) drawing from the unified menu, like the status bar
and above-calendar rows. A shared layout_two_slots() gives every row the
same rule: one slot set renders full-width centered, both set renders two
halves, none hides the row. The date is just the default CENTER-left
content now; its old fixed positioning is routed through the row layout.
Make the vertical layout adaptive: each optional row (TOP status bar,
CENTER above-time, BOTTOM above-calendar) only reserves height when in
use, so disabling a row grows the clock and calendar. The clock font
scales to the resulting time-band height. layout_compute_rows() takes the
enabled rows; compute_layout() derives them from settings.

Seat the weather on the time band and top-align it with the clock so it
no longer floats above or spills into the calendar.

Config: gather all six complication slots into one "Complications" group
(was split across Clock and Status bar).
…r slots

The hourly-vibration schedule lost its pattern picker — vibe_hour had
been reduced to a 0/1 on-off flag. Add a "Vibration pattern" dropdown to
the schedule and have augment() store the chosen VIBES pattern in
vibe_hour (0 when the mode is Off). Also remove the status-bar slot
selectors left duplicated in the Status bar section (they now live only
in the grouped Complications section).
Battery complications (watch/phone) now honour a batt_style setting:
0 = battery outline with the % centred inside (battery_layer draws the
box), 1 = plain "NN%", 2 = icon + "NN%" (default, prior look). Added the
batt_style key (118) and a "Battery style" picker in the Complications
group.
…above outline

Two review findings: (1) with the status bar fully disabled (showStatus 0)
the reclaimed strip is zero-height, so reparenting the date into it clipped
it away — keep the date in the center row in that case (only use the strip
when it is still reserved, i.e. when-battery-low mode). (2) toggle_statusbar
re-raised battery_layer above the slot text; re-add the slot value layers on
top so the bar-style outline never buries the percentage.
…ring cloud

time_font_key() returned const char* but set_layer_attr_sfont takes char*,
which the cloud build rejects with -Werror=discarded-qualifiers (the local
build hides it via pbl_suppress_newer_gcc_warnings). Return char* — the
FONT_KEY_* values are string literals.

Add tools/strict-check.sh: compiles every source with the exact cloud flag
set (-Werror + the same -Wno-error= relaxations, -fsyntax-only) per platform,
so this class of error is caught locally instead of in the cloud. Verified it
reproduces the discarded-qualifiers error and passes once fixed.
…er test gate

- Weather: load a 40px Climacons on wide screens and centre the icon+temp
  block in the time band so on emery it fills the column and lines up with
  the clock instead of sitting tiny in the corner (48px exceeds Pebble glyph
  limit, 40 is the max that renders).
- Defaults: ship with all three complication rows active (status batteries,
  Date above the time, Week + AM/PM above the calendar) so the feature is
  visible out of the box.
- Warnings: remove the dead connection_text_buf, size the small snprintf
  buffers to 16, and clamp sun_time hh/mm — local builds are now warning-free
  under the cloud flag set.
- Tests: strict-check.sh now does a FULL per-platform compile with the cloud
  -Werror flags and fails on any error OR warning (catches the class of issue
  the unit tests cannot). Add tools/test.sh (unit + build + strict-check) as
  the one gate to run before handoff, and extract clock_font_for() into the
  pure layout module with unit tests + more adaptive-layout coverage.
- Battery "Bar" style now FILLS proportionally: an invert effect per status
  battery slot, raised above the % text so the value stays readable over the
  filled part (light-on-dark) and the empty part (dark-on-light).
- Status bar row is dynamic like the others: a single status complication is
  centred (icon+value group / centred text); two stay as halves. (Bar style
  keeps its side boxes.)
- Config: relabel the bar option "Bar (fills) with %".
…+icon, Clocks & Date

- Sort the complication menu alphabetically (— pinned first); values keep
  their mapping so new entries can be appended in any order.
- Battery bar fill now spans the full interior height of the box (was a short
  band that left empty strips and made the % hard to read).
- New battery style "Bar + icon": the filling bar plus the watch/phone glyph
  on the outer edge (box geometry shifts to make room).
- Rename the "Clock" settings group to "Clocks & Date".
…s its icon

Nudge the bar-style percentage up 2px so descenders are not clipped. Draw the
left slot battery with its nib on the left so it meets the adjacent icon and
the two read as a single unit (the right slot keeps its nib on the outer edge,
toward its icon).
…oday column

Raise the Su..Sa header so it no longer rests on the calendar grid line. Also
fix the column after today (e.g. Saturday when today is Friday) sitting lower:
the special-day branch reset the vertical offset to 0 instead of the header
default, so any column drawn after today's kept the old position.
…g date font

cal_bold is the 18px date-cell font; using it for today's column header made it
taller than the others so it overflowed onto the grid. Use GOTHIC_14_BOLD (same
size as the rest, just bold) for the Latin header; RU keeps its unicode font +
double-strike.
Recompute the adaptive layout and reposition every band immediately when
settings arrive, so enabling/disabling a complication row (or toggling the
status bar) redistributes the freed space and rescales the clock font without
needing the watchface to reload. Adds calendar_set_frame()/splash_set_frame()
and a relayout() that mirrors window_load's geometry (validated: calling it at
startup reproduces the same layout).
…e cleanup

- Version 0.0.1 (package.json).
- README -> README.md (so GitHub renders it); state that TimelyNG is freely
  inspired by and an homage to the original Timely/PebbleTimely and the
  Andrew129260/Timely-2 and alan-johnson/PebbleTimely forks; refresh the
  feature list and drop aplite from the platform list.
- Add TODO.md (re-add aplite, round platforms, remove the retired day_layer,
  feature ideas).
- Remove dead code/resources: the always-hidden fixed BT icon
  (bmp_connection_layer + the 20x20 bluetooth_*.png + IMAGE_BT_LINKED/NOLINK,
  Bluetooth is a slot icon now) and the unused process_show_* helpers.
- README.md: badges (CI status, version, platforms, C/JS/SDK), a 'Why TimelyNG'
  section spelling out what it adds over the original Timely and its forks, a
  platform table, and pointers to the build/test gate and publishing copy.
- CI: .github/workflows/ci.yml runs the host unit tests + PebbleKit JS syntax
  check on every push/PR (validated with actionlint). Full SDK build stays in
  tools/test.sh (TODO: containerised SDK build in CI).
- PUBLISHING.md: appstore listing copy (description, v0.0.1 release notes,
  category, keywords) and per-platform asset specs, sourced from the Rebble
  publishing guides.
- TODO.md: add LICENSE, CI full build, marketing assets.
…amed)

Default watchface captured on emery (200x228), basalt, diorite and flint
(144x168) for the appstore listing — unframed, as the store requires. CI
workflow validated locally with act (host tests + JS check pass).
…rited TODOs

- README platform table now shows every Pebble platform with a status: the four
  supported ones plus aplite (dropped), chalk (180x180 round) and gabbro
  (260x260 round) as not-supported/future (round layout needed).
- TODO.md: add an 'Inherited TODO (from the original Timely)' section gathering
  the still-relevant stubbed features (idle reminder, per-slot inversion,
  show_date, per-weekday hourly vibration, real bold unicode font).
@eldios eldios self-assigned this Jun 5, 2026
@eldios
eldios merged commit e09af6b into watchface Jun 5, 2026
2 checks passed
@eldios
eldios deleted the feat/modernize-for-modern-stack branch June 5, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant