Counting keystrokes without recording them
Every activity tracker describes itself in more or less the same words. Activity monitoring. Productivity insights. Understand where your time goes. The phrasing is identical whether the program is counting that you pressed a key or writing down which key it was, and from the outside — from the marketing page, the screenshots, the feature list — you cannot tell the two apart.
The gap between them is not small. A tally of keypresses tells you that somebody was at the keyboard between 09:14 and 11:02. A record of keypresses is a transcript: your messages, your search terms, the password you typed into a field that had briefly lost focus. Both can be described as keyboard activity. Only one of them is a liability sitting on your disk.
So the question worth asking of any tracker isn't what it promises. It's what it writes down. Here is what Deski writes down, and why it's shaped that way.
The whole table
Deski stores keyboard activity in one table. This is its complete definition, copied out of the source:
CREATE TABLE IF NOT EXISTS key_events (
id INTEGER PRIMARY KEY,
ts INTEGER NOT NULL
);
An identifier and a timestamp. There's no column for the key, so there's nowhere to put one. The function that writes to this table takes a single argument — the moment the press happened — so the key's identity is dropped in the input handler, before anything reaches the database. It isn't stored and then withheld from the interface. It never travels far enough to be stored.
This is the part worth being precise about, because "we don't log keystrokes" is a sentence any product can write. A schema is a different kind of claim. It's a constraint you would have to visibly change in order to break, and the change would show up in a diff.
What we do keep, including the awkward part
Mouse activity is a slightly longer row: a timestamp, whether the event was a click or a movement, and — for clicks — which button. So that one isn't a pure tally, and it would be easy to leave that detail out of a post like this. Left versus right is recorded.
What isn't recorded is where. There are no coordinates in that table, and that matters far more than the button does. A stream of positions with timestamps reconstructs a surprising amount about what was on your screen and what you were doing with it — which corner you kept returning to, how you moved between windows, roughly what you clicked. A count of clicks reconstructs nothing.
It wasn't always that way. An early build did store coordinates, because they arrived in the same event and might have been useful later. Removing them meant more than dropping two columns going forward: anyone who had run that build already had those values in their file. So the upgrade removes the columns from existing databases and rebuilds the file, which zeroes the freed pages instead of leaving the old values sitting recoverably in free space.
That's the honest shape of it. Not that we never collected it — that we collected it, decided it wasn't worth what it cost, and went back for the copies already on other people's machines.
The guarantee is a test, not a promise
A schema is only a constraint while somebody is watching it. The realistic failure here isn't malice, it's some future afternoon when an extra column looks harmless and convenient. So the constraint is written down as a test that fails the build:
for banned in ["key", "keycode", "code", "value", "char"] {
assert!(!cols.contains(&banned.to_string()),
"key_events must not have a `{banned}` column");
}
There's a matching one for mouse coordinates. Neither test checks that the app behaves nicely; they check that the table cannot hold the thing we said it doesn't hold. If someone — us included, in a hurry, a year from now — adds a column for the key itself, the suite goes red and tells them why.
That idea generalises well past this app. A privacy property that lives only in a policy document degrades silently. One that lives in a test degrades loudly.
What the counts are actually for
Reasonable question: if the number is that thin, why keep it at all?
Deski uses it to tell working from not-working. A stretch of time with input in it was time you were at the machine; a long gap wasn't, and gets excluded rather than quietly billed. That's the whole job. The count decides where one time block ends and the next begins. It isn't a score, it isn't shown to anyone else, and there's no threshold you're failing to meet.
It's worth saying plainly that keystroke counts make a poor measure of work anyway. Thinking registers as idle. Reading registers as idle. A careful hour looks identical to an hour away from the desk, and any system that pays attention to that number teaches the people under it to make the number go up. We keep it because it answers "was anyone here?" and we don't ask it anything else.
Keyboard tracking is also off by default. You switch it on if you want it; for most people mouse activity alone places the boundaries well enough.
Don't take our word for it
Everything above is checkable, and that's the actual point of building it
this way. Your data is a SQLite file on your own disk — not an account, not
our server. Open it with any SQLite browser and run .schema
key_events. You'll see the two columns, or you'll see that we lied,
and you don't need our cooperation either way.
That's a lower bar than it sounds, and most trackers can't clear it at all. When the data lives on someone else's machine, the only available answer to "what did you record about me?" is a paragraph in a policy. Being auditable by an ordinary user isn't a feature we added. It's what's left over once the server is gone.
Deski is out now for Windows, with a 14-day free trial. If you'd rather have the surrounding argument first, the local-first post covers what that choice costs as well as what it buys.