Daniel's been deep in the voice-input trenches for a year and a half, and he's hit the wall that everyone hits the moment they go beyond one app, one hotkey. He uses a USB foot pedal for dictation, and it emits F13 — smart choice, F13 is recognized everywhere and used nowhere. Some apps take it directly. A Chrome dictation extension accepts F13 as its trigger, done. But Claude Code's dictation is push-to-talk on the spacebar, and they quietly dropped the ability to reconfigure that. So in the terminal, the pedal needs to become a spacebar press. And only in the terminal — a pedal that emits spacebar globally would make ordinary typing impossible.
So what he actually needs is a keyboard remap that's conditional on which application has focus. His question isn't does some GUI remapper have a checkbox for it — it's what the nuts and bolts of building that actually are on Linux. Where in the stack does a remap like this have to live to know what window is focused? How does a tool intercepting at the evdev layer, below the display server, find out about focus at all, given that focus is a display-server concept? What changes between X11, where any client can ask what the active window is, and Wayland, where that introspection is deliberately walled off? And what does the latency and race-condition picture look like when a keypress has to be held rather than tapped?
So let's start with the uncomfortable truth about where your keyboard events actually enter the machine.
The kernel. That's the part people forget.
Right. You press a key on a USB device, the kernel's input subsystem reads it, and by the time anything above the kernel knows about it, the event has already been through several layers. The evdev layer is where the kernel exposes raw input devices as files — slash dev slash input slash event something. A tool that wants to remap keys can open that file and read keycodes before the display server ever sees them. That's where keyd lives.
And the display server is where focus lives. Two different worlds.
Two different worlds separated by design. The kernel sees keycodes. It has no concept of windows, no concept of focus, no concept of applications. The display server sits on top and knows that the terminal window in workspace two has focus, but it thinks in terms of keysyms and processed input events. So any tool that wants to make focus-conditional decisions has to bridge these two worlds, and the bridge looks completely different depending on whether you're on X11 or Wayland.
So keyd. How does it actually grab the device?
It uses an ioctl called EVIOCGRAB. This gives a process exclusive access to an input device. When keyd calls EVIOCGRAB on your foot pedal's event file, the kernel stops delivering events from that device to anyone else — including the display server. keyd reads the raw keycodes, applies its remapping rules, then creates a virtual uinput device and writes the remapped events there. The display server sees the virtual device, not the real one.
So the real pedal events never reach the display server at all.
Right. And this is where the focus problem becomes acute. keyd itself is completely focus-blind. It's reading from slash dev slash input slash event whatever — that file can't tell you which window is focused. Focus is not a kernel concept. So if you want keyd to say F13 becomes spacebar in the terminal but stays F13 in Chrome, keyd needs to know which application currently has focus, and it cannot figure that out from the evdev layer.
So how does it get that information?
There's a companion daemon called keyd-application-mapper. Its entire job is to poll the display server for the currently focused window and feed that information back to keyd. It writes the current application name to a state file, and keyd reads that file on every keypress to decide what its remapping rules should do.
So keyd checks the file on every keypress?
On every keypress. It looks up the application name from the cached state and applies whatever rules match. The critical thing is that this is a polling loop, not an event-driven system. The mapper checks the focused window every N milliseconds — the default is around a hundred — and updates the state file. Between polls, the file contains whatever was true at the last check. If focus changed three milliseconds ago, the file still says the old application.
That's the race condition right there.
That's the race condition. But let me walk through how the mapper actually gets the focus information, because the X11 path and the Wayland path are completely different beasts.
Start with X11.
X11 is generous with information. Any X client can query the window manager for the currently focused window via a property called underscore NET underscore ACTIVE underscore WINDOW, part of the EWMH spec. It's a standardized property the window manager sets on the root window, and any client can read it. The mapper sends an X11 request, gets back a window ID, then queries that window's properties for the application name.
And this is public. No permissions, no authentication.
Completely public. This is why xdotool and wmctrl work — they use the same mechanism. That's the X11 philosophy — everything is visible, everything is queryable, and security is not a priority. The mapper can ask who has focus and get an answer, synchronously, in a single round trip. The latency is negligible — it's a local socket call. The bottleneck isn't the query itself, it's the polling interval. You could set it to ten milliseconds and the focus information would be nearly real-time. The staleness is a choice, not a hard limit.
And Wayland?
Wayland is the opposite. It was designed with the explicit goal of not letting every client see everything. A client only sees events directed at its own surfaces. The same wall exists for focus introspection — you can't just ask the compositor who has focus unless the compositor chooses to tell you.
So how does the mapper work on Wayland at all?
There's a protocol extension called foreign-toplevel-list, originally from the wlroots project but adopted more broadly. It lets a client enumerate all top-level windows and track which one has focus. The compositor sends events when focus changes, so in theory this is actually better than X11 — it's event-driven rather than polling.
But there's a catch.
Several. First, foreign-toplevel-list is not universally implemented. GNOME's Mutter, KDE's KWin, and wlroots-based compositors like Sway support it. But if you're running a compositor that doesn't, the protocol simply isn't there. Second, even when implemented, the information you get may vary — some compositors give the application name, some give the window title, the format isn't perfectly standardized.
And keyd-application-mapper — does it use the event-driven part of the protocol?
This is the thing. Even though foreign-toplevel-list can push events when focus changes, keyd-application-mapper still polls. On both X11 and Wayland. It may be a simplicity thing, or the event delivery wasn't reliable across all compositors when the mapper was written. The practical result is that on both display servers, the focus information in that state file is always slightly stale.
So the fundamental architecture is the same either way. Poll, write file, keyd reads file. The difference is just which API the poll hits.
Right. And the staleness window is the polling interval. If the mapper polls every hundred milliseconds, the worst case is that focus changed one millisecond after the last poll, and keyd reads the file ninety-nine milliseconds later — so the cached value is ninety-nine milliseconds out of date.
For a tap, that's fine. The key-down and key-up happen within maybe fifty milliseconds, and the odds that a focus change happened in exactly that window are low.
For a tap, yes. You might get a spurious event once in a blue moon and not even notice.
But Daniel's use case is dictation. The pedal is held.
And this is where it gets catastrophic. When you press and hold the pedal for dictation — seconds at a time, speaking entire sentences — the critical moment is the key-down event. keyd receives the key-down, looks at the state file, and forwards it either as F13 or as spacebar. Once that key-down is forwarded, it cannot be retracted. There's no take-backs in the input event stream. If the cached focus was wrong at that moment, the entire held sequence goes to the wrong application.
So the first few words of dictation go to Chrome instead of the terminal.
Or worse. If the pedal becomes a spacebar in Chrome, you've just inserted spaces into whatever text field is focused. If it becomes F13 in the terminal, Claude Code never hears the spacebar and never starts listening. Either way, the utterance is corrupted, and you don't find out until you look at the screen and see that nothing transcribed.
So the race condition window isn't just the polling interval — it's the polling interval plus however long you hold the key. The damage scales with the hold duration.
And the worst-case scenario is the one Daniel described — you switch from Chrome to the terminal, immediately press the pedal to start dictating, and the mapper hasn't polled since the switch. The state file still says Chrome. keyd sees F13, checks the file, says ah, we're in Chrome, F13 stays F13, forwards F13 to the terminal. But the terminal wants spacebar. So F13 lands, Claude Code ignores it, and you're sitting there talking into a microphone with nothing happening.
And the polling interval determines how long you have to wait after switching windows before it's safe to press the pedal.
Yes. If the mapper polls every hundred milliseconds, you need to wait at least a hundred milliseconds after switching focus — and realistically more, because you don't know where in the polling cycle you landed. The safe thing is to wait the full interval.
A hundred milliseconds doesn't sound like much.
It's not, for a conscious pause. But it's an eternity for muscle memory. If you're in flow, switching windows and immediately starting to talk, you will hit that window. Not every time, but often enough to be maddening.
And this is the fundamental architectural problem. The kernel processes input in real-time, but focus is a display-server concept that changes asynchronously. Any bridge between them is either stale or complex.
That's the deep insight. The two systems operate on completely different timescales and in completely different semantic domains. The kernel's input subsystem is designed for microsecond-level timing and deals in physical events. The display server's focus tracking is designed for human-scale interaction and deals in abstract concepts — windows, applications, workspaces. Bridging them means imposing the display server's slower, coarser semantic layer onto the kernel's fast, fine-grained event stream. You're essentially asking a real-time system to wait for a non-real-time answer.
And the clean solution doesn't exist.
The clean solution would be for the display server itself to expose a remapping hook. If the compositor could say here is a key event, and here is the currently focused window, and you can remap based on both — that would eliminate the bridge entirely. The focus information would be authoritative and instantaneous because it comes from the same process that manages focus. But that's not on any roadmap.
Because the display server people and the input-remapping people are different communities with different priorities.
And because Wayland's whole philosophy is that the compositor is in charge and clients get what the compositor gives them. A remapping hook would mean giving a client the ability to intercept and modify input events before the compositor processes them. That's basically what EVIOCGRAB does at the kernel level, and doing it at the compositor level would require a protocol extension that's not being worked on.
So the polling bridge is what we have, and the staleness is baked in.
The staleness is baked in. And the held-key scenario is the worst case because the damage is front-loaded — the key-down event is the one that matters, and if it's wrong, everything after it is wrong. There's no recovery. You can't retract a key-down and resend it. The input event stream doesn't work that way.
Let's talk about the X11 versus Wayland divide more broadly. On X11, any client can introspect the entire window stack. It's a surveillance panopticon and it works great for power users.
It's a security nightmare. A malicious client can log every keystroke, screenshot every window, inject events into other applications. X11 has no security model to speak of — it assumes you trust every program running on your machine. That assumption was reasonable in nineteen eighty-seven when you were logged into a shared Unix server. It's not reasonable now, when you're running a web browser that executes arbitrary JavaScript from a hundred different origins.
Wayland walls it off. Each client sees only its own surfaces.
Foreign-toplevel-list is the compromise for the specific case of enumerating windows and tracking focus. It gives you what you need for a task switcher or an application-aware remapper. But it doesn't give you everything — no geometry of other windows, no contents, no event injection. The compositor decides what to expose.
The compositor might not expose anything at all.
Right. If your compositor doesn't implement foreign-toplevel-list, the mapper simply doesn't work. keyd-application-mapper will fail to initialize on Wayland. You get no focus information, and keyd can't do conditional remapping. You're back to global remaps only.
Which means Daniel's whole setup only works if his compositor supports the protocol.
Yes. Most of the major ones do now — GNOME, KDE, Sway. But if you're on a niche compositor or an older version, you're out of luck. This is the practical consequence of the X11-to-Wayland transition that doesn't get talked about enough. It's not just about security versus functionality in the abstract. It's about specific tools that power users rely on — xdotool, wmctrl, keyd-application-mapper — either not working at all or working with reduced capability.
The replacement protocols are fragmented. foreign-toplevel-list is one. There's a separate one for screenshotting. There's a separate one for global hotkeys. Each one has to be implemented by each compositor, and the implementations aren't always consistent.
The global hotkey situation is a good parallel. On X11, any client can register a global hotkey and it just works. On Wayland, there's a protocol for that now — the global-shortcuts protocol — but it's relatively new and adoption is still spotty. X11 gave you everything by default and you opted into security. Wayland gives you nothing by default and you opt into functionality, one protocol at a time.
The protocols lag behind what power users actually need.
By years, in some cases. The foreign-toplevel-list protocol was proposed in twenty-nineteen and took a couple of years to stabilize. And it's still not universal. If you're building a tool like keyd-application-mapper, you have to support both paths — X11 and Wayland — and they work completely differently, and the Wayland path might not work at all depending on the user's setup.
Let's go back to the latency picture. You said the mapper polls every hundred milliseconds by default. What's the actual latency budget for a keypress to feel instant?
The general rule of thumb in HCI research is that anything under about fifty milliseconds feels instantaneous. Between fifty and a hundred, you might notice a slight delay but it's not disruptive. Above a hundred, it starts to feel sluggish. Above two hundred, it's actively annoying.
The polling interval alone is already at the edge of what feels responsive.
That's just the polling interval. On top of that, you have the time for keyd to read the event, check the state file, apply the remapping rule, and write to the virtual device. You have the time for the display server to read from the virtual device and deliver the event to the application. You have the application's own event processing time. Total latency from pedal press to application response is probably in the fifty-to-hundred-fifty millisecond range, and the polling staleness is a significant chunk of that.
For dictation, the latency isn't the main problem — it's the misrouting. But the two are connected. If you try to reduce misrouting by polling faster, you increase CPU usage.
There's a tradeoff. You can poll every ten milliseconds and the staleness becomes negligible — worst case, nine milliseconds of wrongness. But now you're making a hundred system calls per second just to check focus. On X11, that's a hundred round trips to the X server. On Wayland, it's a hundred queries to the compositor. It's not nothing, especially on a laptop where you care about battery life.
Even with a ten-millisecond poll, you can still hit the race condition. It's just less likely.
You can always hit the race condition with a polling architecture. The only way to eliminate it entirely is to make the system event-driven — where the display server pushes focus changes to the mapper, and the mapper updates the state file immediately. And even then, there's a window between the focus change and the state file update. It's smaller — probably sub-millisecond — but it exists. The only truly race-free design is for the display server itself to resolve the remapping, because it knows the focus state authoritatively at the moment the event arrives.
What Daniel's really asking for — focus-conditional remapping that's race-free — is architecturally impossible with the current Linux stack. You can get close, but you can't get all the way there.
You can get close enough that it works reliably for taps. For held keys in dictation, you can get close enough that it works most of the time, and the failure mode is annoying but not data-destroying. You lose the first second of dictation. You release the pedal, press it again, and it works.
Unless you're switching windows rapidly, in which case you hit it over and over.
Right. And that's the workflow Daniel described — switching between a browser and a terminal, dictating in both. If you're toggling back and forth, you're going to hit the race condition regularly. The mitigation is to build a habit — switch windows, wait a beat, then press the pedal. But that's exactly the kind of conscious friction that voice input is supposed to eliminate.
There's a deeper point here about how voice input changes the input-routing problem. With a keyboard, the routing is implicit — you type, and the characters go to the focused window. You don't think about it because it's been that way for forty years. With voice, you're introducing a second input stream that has to be routed to the right place, and the routing decision has to be made before the input even starts — because the first word matters.
The routing decision is harder because voice input often spans multiple applications. Daniel's dictating in Chrome and in the terminal. He might also want to dictate in a text editor, or a chat app. Each one might need a different trigger key, or no remapping at all. The focus-conditional remap is the mechanism, but the real problem is that the system doesn't know what you intend — it only knows what's focused.
Which brings us back to F13. The reason F13 is the right choice for the hardware signal is that it's semantically empty. It doesn't mean anything until the remapper assigns it meaning. And the remapper can assign different meanings in different contexts. That's the whole architecture — a clean, uncontested signal that gets interpreted at the last possible moment based on context.
The context resolution is the hard part. F13 through F24 — they're defined in the USB HID spec, every operating system recognizes them, and exactly zero consumer keyboards ship with them. They're the pristine key namespace that nobody uses. Daniel's choice of F13 is exactly right. The problem isn't the keycode — it's the bridge between the kernel's view of the world and the display server's view of the world.
Where does this leave someone building this today? If you're on X11, the path is well-understood. keyd plus keyd-application-mapper, polling the X server for the active window, works reliably. The race condition exists but the polling interval can be tuned down. If you're on Wayland, you need to check whether your compositor supports foreign-toplevel-list, and if it does, it works — with the same polling caveat. If it doesn't, you're stuck.
If you're on Wayland with a compositor that doesn't support the protocol, your options are limited. You could write a compositor-specific plugin — GNOME Shell extensions can query focus, for instance — but that ties you to one desktop environment. You could use an accessibility bus like AT-SPI, but that's a whole different can of worms with its own latency and compatibility issues. AT-SPI events go through D-Bus, which adds serialization overhead and scheduling jitter. It's a solution for a different problem.
The polling bridge is the least bad option.
The polling bridge is the least bad option that works across display servers and compositors. It's not elegant. It's not race-free. But it's what we have, and it works well enough for most use cases. The held-key dictation scenario is the stress test that exposes its limits.
The limits are fundamental. They're not bugs — they're consequences of the architecture.
Right. The kernel and the display server are separate for good reasons. The kernel should not know about windows. The display server should not be in the kernel. The separation is correct. But it means that any tool that needs to combine kernel-level input with display-server-level context has to build a bridge, and the bridge is always going to have a gap.
This whole X11 versus Wayland thing — it reminds me of something from my radio days.
Hilbert: I spent the late nineties as a broadcast automation engineer at a small station in Hartford. We had three studios, each with a cart machine for jingles and ads, and one foot pedal that could trigger any of them. The pedal sent a MIDI note — just a note-on, note-off, dead simple. The problem was routing it to the right cart machine based on which studio was live. Sound familiar.
MIDI note instead of F13, but otherwise...
Hilbert: Identical problem. And we solved it with a rotary switch. A big four-position knob on the console — studio one, studio two, studio three, off. The pedal's MIDI signal went through a physical patch bay, and the rotary switch determined which cart machine got the note. Hardware routing. No software, no polling, no race condition.
A manual focus selector.
Hilbert: The DJ had to turn the knob when they moved between studios. If they forgot, the jingle played in the wrong room. Which happened. But it was their fault, not the equipment's. We called it operator error and moved on.
You never had to solve the software race condition because you solved it with a knob.
Hilbert: Never even occurred to me to try. This was nineteen ninety-seven. The idea of a computer knowing which studio was live and routing the pedal automatically — that was science fiction for a station with a budget of about twelve dollars. The rotary switch cost eight bucks at Radio Shack.
That's exactly what Daniel is trying to avoid. The whole point of the software approach is to make it automatic. No knob, no conscious switching, just press the pedal and the system knows where it should go.
Hilbert: Which is a harder problem. The knob never lies. The state is physical — you can look at it and see where it's pointing. The software version has to infer state from a system that wasn't designed to report it in real time.
The patch bay versus digital mixer analogy is actually perfect for the X11 and Wayland divide. The patch bay lets you see every cable, every connection, you can trace the signal path with your finger. The digital mixer hides everything behind a menu. X11 is the patch bay — any client can see the whole window stack. Wayland is the digital mixer — you only see what the compositor lets you see.
Hilbert: The guys who loved the patch bay are still furious about the digital mixer. I was one of them. You spend twenty years knowing exactly where every signal goes, and then someone puts a menu between you and the routing and says trust me. It works better, they say. Fewer cables, less noise, more flexible. And they're right. But you can't see it anymore.
The transparency is the thing. On X11, you can verify that the mapper is getting the right focus information because you can run xdotool yourself and ask the same question. On Wayland, you're trusting that the compositor's implementation of foreign-toplevel-list is correct and complete, and if something goes wrong, you can't independently verify it.
Hilbert: The rotary switch never had that problem. It was wrong sometimes, but you knew exactly why.
The fix was reaching over and turning it.
Hilbert: Eight dollars.
Where does this leave Daniel and everyone else trying to build this? The polling bridge works. It has a race condition that you can't fully eliminate, and the held-key dictation scenario is the worst case. But it works most of the time, and the failure pattern is recoverable — you release the pedal and press it again. The deeper question is whether the display server will ever expose a proper remapping hook that eliminates the bridge entirely.
I don't think it will. The incentives aren't there. The people building compositors are focused on security, performance, and the core desktop experience. Application-aware input remapping is a power-user feature, and power users are a tiny fraction of the user base. The polling bridge is probably what we're going to have for the foreseeable future.
As voice input goes mainstream — Wispr Flow and its imitators, as Daniel put it — the input-routing problem is going to get worse before it gets better. More apps will hardcode triggers like Claude Code's spacebar. More users will need focus-conditional remapping. And the architecture underneath is fundamentally the same as it was when Hilbert was routing MIDI notes with a rotary switch — two systems that don't talk to each other, bridged by whatever works.
The race condition is the real story. Not the remapping itself — that's straightforward. The gap between when focus changes and when the system knows about it is the fundamental limit of this architecture. You can shrink it but you can't close it.
The cutting-room floor detail I wanted to mention — keyd-application-mapper actually has a flag called dash t for timeout that lets you set the polling interval in milliseconds. The default is a hundred. You can set it to ten, you can set it to five. But the man page has this wonderful understated warning: setting this too low may cause high CPU usage. No mention of the race condition. No mention of the held-key problem. Just... may cause high CPU usage. As if that's the only thing you'd be worried about.
That's very Linux.
That's extremely Linux.
This has been My Weird Prompts. Thanks to Hilbert Flumingtop for producing.
We'll be back soon. If you want to yell at us about input event latency, the email is show at my weird prompts dot com.
Or leave a review wherever you get your podcasts. It helps more than you'd think.
See you tomorrow.