Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Speaker Timer

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.

Control panel

At a glance

  • Presets with hotkeys 19, 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

Install

Prebuilt installers are on the Releases page.

  • WindowsSpeaker Timer Setup X.Y.Z.exe, installs without admin rights
  • macOS, Apple Silicon (M1 and newer) — Speaker Timer-X.Y.Z-arm64.dmg
  • macOS, IntelSpeaker 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.

Run from source

npm install
npm start

npm 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.

Build installers

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.

Features

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 19.

The time field reads bare digits as minutes: 2020:00, 505:00, 901: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.

Hotkeys

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
19 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.

How it works

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.

Design

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.

Logo

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 icon

Re-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 $ICONSET

On Windows the .ico is generated by electron-builder from build/icon.png.

Layout

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.

Screenshots

Settings moved behind the gear so the working screen keeps only the timer and the presets:

Settings

The output screen, deliberately dark:

Output screen

Licence

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.

Contributing

Issues and suggestions are welcome in Issues.

By submitting changes you agree that they are distributed under GPL-3.0.

About

Таймер обратного отсчёта для докладчиков с выводом на второй экран

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages