Skip to content

Repository files navigation

Presentation Clock

A sectioned talk timer for Windows. Enter your total presentation time and a running order, press Start, and the clock counts down the current section — then hands over to the next one automatically, with no input from you. When the total time runs out it keeps counting, upward, in overtime.

▶ Use it right now in your browser: https://robbiegal.github.io/PresentationClock/

Or download the desktop app: a single portable PresentationClock.exe with no installer, no runtime to set up and no admin rights — copy it anywhere and double-click it. Get it from Releases.

Same app either way. The desktop build adds a native window, a keep-on-top control, and saves your running order to %APPDATA%; the web version saves it in the browser instead.

Setting up — total time and the running order

The setup screen: a total presentation time of 45 minutes and four sections, with a meter showing 40:00 allocated of 45:00 and 5:00 of buffer

Mid-talk — the timer running

The running timer: section "Introduction" with 3:38 remaining, "The problem" up next, 1:22 elapsed and 43:38 remaining of the whole talk


Using it

Setup. Type the total presentation time, then list your sections. Durations accept whatever you'd naturally type:

You type Means
20 20 minutes
5:30 5 min 30 s
1:05:30 1 h 5 min 30 s
90s 90 seconds
1h30m 90 minutes

The meter under the list shows how your sections fill the total. Leftover time becomes a Buffer segment at the end — real time you own, which is usually where Q&A goes. If the sections overrun the total, the meter turns red and offers two one-click fixes: extend the total, or scale the sections to fit.

Drag the handle at the left of a row to reorder. Press Enter in a section name to add another below it. Your running order is saved automatically and restored next time you open the app.

The setup screen never scrolls. Shrink the window and the fields shrink with it — horizontally and vertically — so the Start button stays put instead of sliding off the bottom. Only once everything has shrunk as far as it usefully can does the section list alone begin to scroll, keeping the total, the meter and Start on screen at any size.

Running. The current section's remaining time is the only thing on screen large enough to read from across a room. Everything else — what's next, total elapsed, total remaining, and a segmented timeline of the whole talk — sits quietly at the edges. The controls fade out while you talk and reappear on any mouse move or keypress.

Keyboard

Key Action
Space Start, then Pause / Resume
N / Next section — starts now with its full planned time
P / Previous section
R Restart the current section
+ / - Give the current section one more / one less minute
F Fullscreen
T Keep the window on top of your slides
Esc Back to the running order

How time is handled

Two rules, and they don't bend:

The total is the deadline. The sections are the route. Overtime is derived from one comparison — elapsed >= totalSeconds — and the total is fixed the moment you press Start. Skipping, jumping, restarting and ±1 minute all re-plan the sections; none of them move the overtime line. Finish a section early and the time you saved becomes buffer at the end rather than padding the next section.

Sections hand over on schedule, by themselves. When a section's time is up the next one begins immediately — that is the whole point of the app. Next is there for when you finish early, not for when a section ends.

Elapsed time is always derived from timestamps, never accumulated:

elapsed = (now - startedAt - pausedTotal) / 1000

Nothing anywhere does elapsed += 1. An hour-long talk stays exact to the frame, a minimised window catches up instantly, and a closed laptop lid resumes on the correct section with the correct time remaining. The clock also refuses to run backwards if the system clock is stepped by NTP or a timezone change.

Escalation

The display escalates as a section burns down, using thresholds that scale with the section's own length but are clamped so they stay sensible at both extremes — a 2-minute section shouldn't warn 30 seconds in, and an hour-long one shouldn't warn 15 minutes out.

State Trigger Treatment
Normal White numerals. Colour is absent, so its arrival is the alarm.
Warning remaining ≤ 25% of the section, clamped to 20 s–2 min Amber numerals, heavier weight, amber fuse
Critical remaining ≤ 10% of the section, clamped to 8–30 s Red numerals, heavier still, field darkens red
Overtime elapsed ≥ total Field inverts and breathes, +MM:SS counts up, hatched overflow tail on the timeline

Building from source

Requires Python 3.10+ on Windows.

.\build.ps1            # creates the venv, generates the icon, builds the exe
.\build.ps1 -Clean     # same, after wiping build/ and dist/

The result is dist\PresentationClock.exe (~13 MB).

node tests\engine.test.js   # 40 timer-engine tests, no browser needed

Layout

Path Purpose
src/ui.html The entire UI and timer engine — one self-contained document
src/app.py Native window host; creates the WebView2 surface and saves the plan
tests/engine.test.js Extracts the engine block from ui.html and tests it in a bare V8 context
build_tools/make_icon.py Renders app.ico analytically — no Pillow, no image assets
PresentationClock.spec PyInstaller build definition
.github/workflows/pages.yml Publishes the web version to GitHub Pages

The website is the same src/ui.html, copied to index.html at deploy time rather than committed a second time at the repo root — one source of truth, and nothing to keep in sync. There is no build step: the document is already self-contained, with no external assets and no network requests.

Because it runs in two hosts, storage has two backends. In the exe it goes through the Python bridge; on the web it uses localStorage, which the exe cannot use (see the notes below). Keep-on-top is hidden on the web, since a page cannot float above other applications, and fullscreen falls back to the Fullscreen API.

The timer engine is fenced inside ui.html by /* ==== ENGINE:BEGIN ==== */ and /* ==== ENGINE:END ==== */. It is pure and DOM-free, so the test harness can pull that block straight out of the shipped HTML and exercise the highest-risk logic in the app without launching a GUI. There is one source of truth and no build step for the UI.


Notes for anyone hacking on this

Three things about this stack cost real debugging time and are not obvious:

localStorage does not work. pywebview hands the document to WebView2 via NavigateToString, which produces an opaque origin, and Chromium answers any storage access there with SecurityError: Access is denied for this document. The running order is persisted through the Python bridge instead (%APPDATA%\PresentationClock\plan.json, written atomically).

Every public attribute on the js_api object gets reflected into JS. pywebview recurses into non-callable members, so holding the window handle in a public self.window sends it walking window.native.* until it dies, printing "maximum recursion depth exceeded" for hundreds of attributes. Underscore-prefix anything that isn't an intended API method.

PyInstaller's bundled pywebview hook is not sufficient. webview/platforms/edgechromium.py loops over ('win-arm64', 'win-x64', 'win-x86') at import time and does not catch the FileNotFoundError when a folder is missing. The stock hook collects those DLLs with collect_dynamic_libs(), which flattens them to the bundle root, so an x64 machine crashes with FileNotFoundError: Cannot find win-arm64. PresentationClock.spec re-declares the whole webview/lib tree with its structure intact.

Fonts. The clock uses Segoe UI Variable Display with font-variant-numeric: tabular-nums. Both halves matter, and both were measured rather than assumed: without tabular-nums the digits vary by up to 186 px at display size and the clock visibly jitters every second; and Bahnschrift — the obvious choice for a technical numeral face — resolves to a single weight in WebView2, so weight-based escalation silently does nothing.


License

MIT — use it, change it, ship it, sell it; just keep the copyright notice.

The exe bundles a few third-party components, all under permissive terms: pywebview and pywin32-ctypes (BSD-3-Clause), and pythonnet, clr_loader, bottle, altgraph and pefile (MIT). PyInstaller itself is GPLv2-or-later, but carries an explicit exception allowing the programs it freezes to be distributed under any license, including closed-source and commercial ones — so nothing here imposes copyleft on this project or on the exe.

About

Sectioned talk timer for Windows as a single portable .exe — each section counts down and hands over automatically, then counts up in overtime.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages