English · Русский
A countdown timer for conference speakers: a control panel for the operator, large digits on the screen facing the stage. Windows and macOS.
Built for conferences and meetups where a talk has to fit its slot without anyone waving frantically from the wings.
At a glance
- Presets with hotkeys
1–9, edited right in the list - Send the timer to any monitor with one click or
F5 - Amber and red zones, overtime counting past zero
- A message overlaid on the timer, and screen blackout
- Keyboard control from both windows — works with a single monitor too
- The venue screen never sleeps while the timer is up
Prebuilt installers are on the Releases page.
- Windows —
Speaker Timer Setup X.Y.Z.exe, installs without admin rights - macOS, Apple Silicon (M1 and newer) —
Speaker Timer-X.Y.Z-arm64.dmg - macOS, Intel —
Speaker Timer-X.Y.Z.dmg
The builds are not code-signed, so the first launch triggers an unknown publisher warning. On macOS: right-click the app → Open.
npm install
npm startnpm start goes through scripts/electron-run.js
rather than calling electron directly. The reason: the VS Code integrated
terminal sets ELECTRON_RUN_AS_NODE=1, which makes Electron start as plain
Node — require('electron') then returns a path string instead of the module,
and the app dies on the first ipcMain call. The launcher clears the variable,
the same way on Windows and macOS.
npm run dist:mac # .dmg (arm64 + x64)
npm run dist:win # .exe (NSIS)Both targets build on macOS: since electron-builder 26, NSIS no longer needs
Wine. The GitHub Actions workflow does the same on a v* tag and attaches the
files to the release.
Presets. The list on the right is edited in place: + Add, pencil to
change, cross to remove. Presets live in config.json in the app's user data
folder and survive restarts. The first nine are bound to keys 1–9.
The time field reads bare digits as minutes: 20 → 20:00, 5 → 05:00,
90 → 1:30:00. Seconds go after a colon: 1:30 is ninety seconds,
1:30:00 is ninety minutes. The full form is filled in when you leave the
field, so typing never jumps under your fingers.
Adjust on the fly. ±10 sec, ±1 min, ±5 min change the remaining time
without interrupting the count.
Digit size. Set in settings as a percentage of the size that fits the screen: 100 % fills it edge to edge, less if you want breathing room or the venue screen is viewed at an angle. Range 40–100 %.
Colour zones. Amber and red thresholds are set in seconds. Past zero the
timer goes into overtime with a − sign and blinking — both can be turned off.
Caption and messages. The caption in the screen corner comes from the preset. A message covers the timer entirely so the speaker cannot miss it; it reaches the room only when you press Show (green), and while the text is up that button turns red — Hide. The field stays a draft: what you type is not broadcast on its own and is not wiped when you hide it.
Blackout. The button or B turns the output screen black without touching
the count.
The venue screen stays awake. While the output window is open,
powerSaveBlocker runs in prevent-display-sleep mode. Otherwise the system
puts the display to sleep mid-talk: the operator does not touch the mouse for
the whole session, and to macOS or Windows that looks like idling. The block is
released when the output window closes and when the app quits.
Settings in a separate window. Zone thresholds, overtime, blinking and what
to show on the output are hidden behind the gear in the title bar — the main
screen keeps only the timer and the presets. The dialog closes with Esc, the
cross, or a click outside. While it is open the hotkeys stay silent, so Space
does not start the timer while you are editing fields, and Tab does not
escape to the elements behind it.
Window buttons follow the platform.
On macOS these are the standard system traffic lights on the left, as in the
vast majority of Mac apps: titleBarStyle: 'hidden' with a custom title bar
while the buttons stay native. 88 px are reserved for them, then the logo.
On Windows and Linux there is no frame (frame: false) and minimise / maximise
/ close are drawn as a panel on the right, as those systems expect. The
maximise icon switches to restore based on the actual window state.
The whole title bar is a drag region, except the buttons themselves.
They work in both windows — the control panel and the output screen. In the panel they are listed in the status bar along the bottom.
| Key | Action |
|---|---|
Space |
start / pause |
R |
reset to the set time |
F5 |
send the timer to the screen / take it back |
B |
blackout the output screen |
1–9 |
load a preset |
Esc |
close the output window (there only) |
Cyrillic layout letters are accepted too (К, И).
Working with a single monitor. With no extended display, the output window
takes over the only screen and the control panel ends up behind it. That is why
the output window listens to the same keys: start, pause, reset, switch presets
and blackout without minimising anything. Esc closes it and returns you to
the panel — it doubles as the emergency exit, since the window is frameless.
Focus moves to the output window the moment it opens, so you do not have to
click it before the keys respond. A single focus() is not enough: the
fullscreen transition rebuilds the window asynchronously and can take focus
back, so the call is repeated once the transition settles.
The key set is defined once in src/shared/hotkeys.js and installed by both windows, so their behaviour cannot drift apart.
Timer state lives in the main process (src/main.js) and is
broadcast to both windows ten times a second. The source of truth is the end
timestamp (endTs), not a counter: the remainder is computed as
endTs - Date.now(), so the clock does not drift even when the system delays
the ticks.
The windows talk to the main process only through contextBridge
(src/preload.js); nodeIntegration is off and
contextIsolation is on.
Displays are enumerated with screen.getAllDisplays(). With no monitor
explicitly chosen, output goes to the first non-primary one, falling back to
the primary. Monitors plugged in or unplugged mid-session are tracked through
display-added / display-removed.
On macOS the app uses setSimpleFullScreen instead of real fullscreen: the
native one moves the window to its own Space, which makes it awkward to drive
the timer from the main monitor.
Window size. The minimum is set exactly to the content — the size at which scrollbars have not appeared yet: 820×560, the same on both platforms. Width is bounded by the column grid itself (380 + 360 plus gaps), which does not depend on the operating system. The window opens at its minimum height, so it can be grown but not shrunk.
The control panel uses a light theme in the spirit of Apple's own site: an
#f5f5f7 canvas, white borderless cards with a soft shadow, generous corner
radii, pill buttons, tight tracking and the system font.
The output screen is deliberately dark. A full-screen white sheet blinds a darkened room and the speaker in it — that is an operating requirement, not a matter of taste.
Colour carries meaning and nothing else:
| Token | Meaning |
|---|---|
--accent #0071e3 |
interaction: focus ring, selected preset |
--green #34c759 |
action: start, show, checked box |
--amber #ff9500 |
amber zone, timer paused |
--red #ff3b30 |
red zone, overtime, on-air, hide |
Three surface levels: --bg (canvas), --surface (cards), --raised (fields
and buttons on white). Headings are set in normal case.
Secondary controls are muted on purpose: the ± adjusters are text pills with
no fill, and the pencil and cross on a preset appear only on hover — otherwise
the list is visually noisy. The digits are what matters.
The source is build/icon.svg: a countdown ring around a microphone. The ring is deliberately open by a quarter — it reads as "time remaining", not just a circle. The same mark sits in the panel's title bar.
npm run iconRe-renders every PNG size (1024 down to 16) into build/. With no
rsvg/ImageMagick/Inkscape on the machine, rasterising is done by Chromium
inside Electron: one render at 2048 and downscaling from that master.
build/icon.icns for macOS is assembled from those PNGs with iconutil:
ICONSET=build/icon.iconset && mkdir -p $ICONSET
cp build/icon-16.png $ICONSET/icon_16x16.png
cp build/icon-32.png $ICONSET/icon_16x16@2x.png
cp build/icon-32.png $ICONSET/icon_32x32.png
cp build/icon-64.png $ICONSET/icon_32x32@2x.png
cp build/icon-128.png $ICONSET/icon_128x128.png
cp build/icon-256.png $ICONSET/icon_128x128@2x.png
cp build/icon-256.png $ICONSET/icon_256x256.png
cp build/icon-512.png $ICONSET/icon_256x256@2x.png
cp build/icon-512.png $ICONSET/icon_512x512.png
cp build/icon.png $ICONSET/icon_512x512@2x.png
iconutil -c icns $ICONSET -o build/icon.icns && rm -rf $ICONSETOn Windows the .ico is generated by electron-builder from build/icon.png.
src/
main.js main process: timer, displays, IPC, settings storage
preload.js bridge into the renderers
shared/format.js time parsing and formatting, colour zones
shared/hotkeys.js hotkeys shared by both windows
control/ control panel
display/ output screen
scripts/electron-run.js Electron launcher (app and icon rendering)
scripts/render-icon.js SVG → PNG at the required sizes
build/icon.svg logo source
build/icon.png .icns icons for packaging
Renderer scripts are plain scripts rather than ES modules: the pages load over
file://, where Chromium gives the document a null origin and blocks module
imports. That is why shared/format.js exposes its functions on
window.TimerFormat.
Settings moved behind the gear so the working screen keeps only the timer and the presets:
The output screen, deliberately dark:
GNU General Public License version 3 or any later version.
In short: use, study and modify it freely. If you distribute a modified version, its source must stay open under the same licence.
Copyright © 2026 Ilya Pautov
The app is built on Electron (MIT) and bundles
Chromium — their licences ship inside the packaged app as
LICENSE.electron.txt and LICENSES.chromium.html. Fonts are the system ones
and there are no third-party icons: the logo and the gear were drawn in this
repository.
Issues and suggestions are welcome in Issues.
By submitting changes you agree that they are distributed under GPL-3.0.


