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
Mid-talk — the timer running
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.
| 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 |
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) / 1000Nothing 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.
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 |
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| 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.
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.
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.

