Skip to content
prawesh-12Public

About

Track how fast you type, anywhere on your machine.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Promptu

Promptu

A floating widget that tracks how fast you type, anywhere on your machine.

Always on top, counts keystrokes system-wide, shows live WPM. No server, no cloud, no account. Everything stays on your computer.


Table of contents


Preview

The widget. Session average leads, live speed sits beside it, and the sparkline tracks the last thirty seconds.

The Promptu widget showing session average, live speed and a sparkline

Settings. Opacity, position lock, which stats appear, the global shortcut and start on login. Every change applies immediately.

The settings window

History. Daily averages over time, with your best day, current streak and all-time average. Shown here with sample data.

The history window showing a typing trend over twelve days


Privacy

Promptu does not know what you type. The global hook counts that a key was pressed and when. It never reads, logs, stores, buffers, or sends the character or key code. There is no network code in the app at all.

If you would rather check than take that on trust, the whole listener is src/main/keylistener.ts. It is about forty lines.


Tech stack

Layer Stack Why
App framework Electron 44 The only cross-platform way to get a global keyboard hook and a floating window from one codebase
Language TypeScript 7 Strict mode, tsc --noEmit gates every change
UI React 19 Live numbers ticking twice a second are state, and state is what React is for
Bundler Vite 8 via electron-vite Hot reload on the renderer, one config for main, preload and renderer
Styling Tailwind CSS 4 Design tokens live in @theme, so colors and radii exist as utility classes and nowhere else
Keystroke hook uiohook-napi Native global hook, N-API so the prebuilt binary matches Electron without rebuilding
Charts Chart.js The sparkline and the history trend, stripped of axes, gridlines, legend and tooltips
Fonts Inter + JetBrains Mono Bundled as woff2 in the repo, so the widget never reaches the network for type
Storage electron-store A single local JSON file for settings and daily totals
Auto-launch auto-launch Start on login, since Electron's own login item API skips Linux
Tests node:test 26 tests on the stats engine and history maths, no test framework to install
Packaging electron-builder AppImage, deb and rpm on Linux, dmg on macOS, NSIS on Windows

How it works

The main process owns everything that touches the machine. The renderer only draws what it is sent.

Typing in any app reaches the uiohook-napi global hook, which passes a timestamp and never the key to stats.ts. Stats sends a snapshot to the widget every 500ms over IPC and writes day totals to electron-store every 10 seconds, which the history window reads when opened.

Live speed is measured over a rolling four second window and drops to zero after a second and a half of silence, so the number tracks what you are doing now rather than what you did ten seconds ago. Session average divides total keystrokes by active typing time, where gaps longer than five seconds count as breaks rather than slow typing.


Getting started

pnpm install
pnpm dev

The widget appears floating above your other windows and adds a tray icon. Closing the widget hides it. Quit from the tray menu.

Ctrl+Shift+P (Cmd+Shift+P on a Mac) shows and hides it from anywhere, and you can rebind that in Settings.


Commands

Command What it does
pnpm dev Run in development, with hot reload on the renderer
pnpm build Type-check, then build main, preload and renderer
pnpm lint Type-check only
pnpm test Run the stats and history tests
pnpm build:linux AppImage, deb and rpm into release/
pnpm build:mac dmg into release/
pnpm build:win NSIS installer into release/

Building installers

Each target has to be built on its own operating system. There is no cross-compiling here: the dmg needs macOS and the NSIS installer needs Windows or wine.

The Linux build produces an AppImage, a .deb and an .rpm. The AppImage needs no install and runs on Arch-based distributions too. Building the .rpm needs rpmbuild on the machine, which Ubuntu does not ship by default:

sudo apt install rpm

macOS builds are not signed, so the first launch needs a right-click then Open to get past Gatekeeper.


Platform notes

Platform Status
Linux X11 Works with no setup
Windows Works with no setup
macOS Needs Accessibility permission, granted by hand
Linux Wayland Global capture is blocked by the display server

Wayland

Wayland blocks applications from listening to global input. That is a deliberate security boundary in the display server, not a bug in Promptu, and it affects Fedora, Omarchy, and Ubuntu 22.04 and newer, which all default to Wayland.

On a Wayland session the hook starts but may never receive an event. Promptu detects the session type at startup and says so in the widget rather than showing a WPM of zero and leaving you guessing. The global shortcut is blocked the same way.

The reliable fix is to log out and pick an X11 session at the login screen.

Reading raw device events through evdev, by adding your user to the input group, sometimes works instead. It grants that user the ability to read all keyboard input on the machine, so treat it as an experiment rather than a recommendation.

macOS

Promptu needs Accessibility permission before the hook will receive anything. Grant it by hand in System Settings, under Privacy & Security then Accessibility.

Linux development

Chromium sandboxes its renderer processes, and on Ubuntu 24.04 both routes it can take are closed by default. Unprivileged user namespaces are restricted by AppArmor, and the chrome-sandbox helper that pnpm install unpacks is not setuid root, because only root can set that bit.

pnpm dev then aborts with a message about chrome-sandbox. One command fixes it for good:

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/60-apparmor-userns.conf

Installed packages are unaffected. The .deb sets the setuid bit on chrome-sandbox in its postinstall when user namespaces are unavailable.


Project structure

promptu/
├── src/
│   ├── main/                 Electron main process, owns everything OS facing
│   │   ├── index.ts          App lifecycle, windows, tray, IPC handlers
│   │   ├── keylistener.ts    Global hook, counts events and nothing else
│   │   ├── stats.ts          Rolling live WPM and session totals
│   │   ├── history.ts        Daily aggregation, streaks, best day
│   │   ├── store.ts          electron-store wrapper
│   │   ├── autolaunch.ts     Start on login
│   │   └── *.test.mts        Tests, sat beside the code they cover
│   ├── preload/              Typed bridge, the renderer's only route to main
│   │   └── index.ts
│   ├── renderer/             React UI, one root per window
│   │   ├── widget/           The floating box
│   │   ├── settings/         The settings window
│   │   ├── history/          The history window
│   │   └── shared/           Components, hooks, design tokens, bundled fonts
│   └── shared/               Types and constants used by both sides
├── build/                    App and tray icons
├── docs/                     Screenshots, logos, and the architecture diagram
│   └── architecture.mmd      Mermaid source for the diagram above
├── electron.vite.config.ts   Build config for all three entry points
└── electron-builder.yml      Packaging targets and metadata

The main process is the single source of truth for stats. The renderer never touches the hook, storage or OS APIs, only what IPC sends it.


License

MIT

About

Track how fast you type, anywhere on your machine.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages