← Back to Field Notes
June 06, 2026 · 6 min read

Building Qeys: A Keyboard That Watches You Type

A floating overlay that mirrors your real typing in real time, and the two things that surprised me shipping it in one session.

pythontkinterdesktopbuild-in-publicqollab

I never learned to touch type properly. I’m fast, but I look down. Every typing tutor I tried had the same flaw: they make you type into theirbox. The second I glance at my hands to find a key, the muscle-memory loop I’m trying to build snaps.

So I built the opposite. Qeys is a small Windows app that floats a semi-transparent QWERTY keyboard on top of everything else on screen. You keep working in your real apps. The overlay mirrors your real typing in real time: press a key anywhere and it lights up amber, then fades slowly so you can see the trail of what your fingers just did. Your eyes stay at screen level, where your work is, instead of dropping to the keyboard.

This is the build-in-public story of how it came together in a single session, and the two things that surprised me.

Qeys floats over any app and mirrors your typing — eyes stay at screen level

The shape of the thing

The requirements settled quickly:

  • Always on top, borderless, semi-transparent, drag to move, resize from a corner.
  • Mirror typing from any app, not just when the overlay is focused.
  • Instant highlight on press, slow fade on release.
  • A practice mode that scores you (words per minute and accuracy) against a target sentence, plus a plain “free” mode that just mirrors what you type.
  • Lives in the system tray. One-click launch.

The whole thing is one Python file. The UI is standard-library tkinter. The global key capture is the keyboard library. Tray icon is pystray, image work is Pillow. No framework, no web view, no build step during development.

Surprise one: the threading rule that makes it stable

Capturing keystrokes system-wide means a hook that fires on its own thread, completely outside the UI. The naive version of this crashes, because GUI toolkits hate being touched from a background thread.

The rule that made it solid is almost embarrassingly simple: the keyboard hook never calls tkinter. Ever. It only writes to one small dictionary of shared state (which keys are held, what has been typed, which mode is active), guarded by a lock. A single redraw loop on the main thread polls that dictionary about sixty times a second and owns all the drawing.

That one rule also gave me the fade for free. I keep a set of currently-held keys. Each frame, any held key is pinned to full brightness; any released key decays a little. There are no per-key timers, no animation scheduler, no callbacks. Instant-on and slow-fade fall out of “full intensity while held, subtract a bit each frame after release.” If you want a slower fade, you change one number.

When a background source has to feed a UI, funnel everything through one lock-guarded state object and one drawing loop. Don’t let the background thread reach into the widgets. It’s less code and it doesn’t crash.

Surprise two: the export that lied about which format it would give me

I wanted a proper hero image for the repo, so I generated one through a design tool’s API. Listing the export formats worked. Creating the design worked. Then the PNG export — and every export where I specified a width and height — came back with “not allowed to access design.” Same design, same credentials.

The thing that finally worked was a plain default-size JPG export. No dimensions, different format, straight through. The takeaway is small but real:

When one path through an integration returns a permission error, try a different format or a smaller request before you assume the whole thing is broken. The wall is often narrower than it looks.

Making it something people can actually run

Qeys ran fine from source. But “download and use my app” for a normal person cannot mean “install Python, then pip install, then run a script.” So I packaged it into a single Qeys.exe with PyInstaller. One file, around thirty megabytes, bundles Python and every dependency and the logo asset. Double-click, it lands in the tray, done.

There’s one honest catch I won’t pretend away. The exe is not code-signed, so the first time anyone runs it, Windows SmartScreen shows “Windows protected your PC.” You click “More info,” then “Run anyway,” once per machine. Removing that prompt entirely requires a paid signing certificate, which isn’t worth it for a free tool. So instead of hiding it, I documented it: the release notes and the README tell you exactly what you’ll see and why it’s safe. Setting the expectation is the honest move.

The exe lives on a GitHub release, not in the source tree, which is where binaries belong.

What shipped

  • A single-file Python overlay that mirrors global typing at sixty frames per second.
  • Two modes: scored Test practice and a plain Free mirror, switchable from a toggle in the bar.
  • Live words-per-minute and accuracy, home-row anchors on F and J, drag, resize, system tray, one-click launcher.
  • A standalone Windows executable on a public release, with the SmartScreen step documented up front.

It’s a small tool. But it’s the kind of small tool that earns its keep, and it was a clean run from a one-line itch to a public download in a session.

Qeys is built by Qollab. Code is open at github.com/Shinikara08/qeys.

Working on something like this?

Start a conversation →