Announcing freddie

It is with enormous pride that we announce the arrival of freddie, a set of tools for building a bespoke control plane for your computer, weighing in at fifteen crates and growing fast.
He is receiving visitors, and he is ready for you to experiment with.
Events in, effects out
A freddie program responds to whatever you can observe. A keypress, an app being foregrounded, which tab you are looking at, what devices are plugged in, or something you have not thought of yet.
Because we're integrating events from many sources, we can easily do crazy things, such as cloning the repository you're looking at without leaving GitHub.com, or muting your microphone on Google Meet (without finding the tab!)
A freddie program is centered around a single, pure function: state and event in, new state and a list of effects out. And so a freddie program remains testable and easy to reason about, even as it scales and grows more complex.
For programmers, by programmers
Other programs for remapping keys are configuration-driven, and that makes it difficult or impossible to handle unanticipated use cases.
Want to bind keys? That's fine, because these apps allow that. But, want your windows to go back where they belong the moment you connect to a monitor? You're out of luck — that's a device event, not a keybinding, and these apps don't allow you to incorporate arbitrary streams of events.
And that's a sign of a deeper problem.
Want these keybindings to do different things in different states? Well, you'd better hope that the app exposed that aspect of the state to you. Different keybindings for different active apps? That's doable, because it's anticipated and allowlisted. But, what about custom mute/unmute keybindings for when you're in an active Google Meet call? Not possible.
And, in configuration-driven frameworks, you don't write functions, so your handlers don't get access to the state at all! Want one key that maximizes a window and, pressed again, puts it back exactly where it was? Then something has to remember the window's old position, in other words, it needs to be a function that is passed state.
Now, more folks are willing to write configuration than to write and compile a Rust program. But guess what — freddie isn't for everyone. So, if you're willing to clone a repo, make some changes and run cargo build, freddie is here to give you incredible power.
What a binding looks like
A binding is a trigger and the handler it runs, written on the layer where it applies. In the nav layer, c foregrounds Chrome:
#[bind(
Key::KeyC.down() => open_chrome,
Key::KeyG.down() => open_ghostty,
)]
pub struct NavLayer {}
And the handler asks for exactly one effect:
fn open_chrome<'a>(
_ev: &KeyEvent,
_node: Node<NavLayerPath<'a>, ()>,
) -> MercuryEffect {
MercuryEffect::Foreground(App::Chrome)
}
That’s the whole thing. It reads neither the event nor the state, because it doesn’t need to: it only runs when c went down in the nav layer, and dispatch established both of those before calling it.
Note that the effect isn’t a keypress. Emitting cmd-r to refresh a page is one variant of MercuryEffect, and foregrounding an app is another, and you write as many as you need.
Handlers that do need state are handed a path to the layer they were bound on. freddie.rs builds one out in full: maximizing a window, putting it back exactly where it was, and hooking up the event source that tells the model which window is focused in the first place.
Give mercury a try
This repository ships one program built with freddie, called mercury. It is macOS-only and it requires accessibility permissions. You should not expect it to fit your use case: it is here to be read, run, studied, forked, and modified.
git clone https://github.com/freddiehg/freddie
cd freddie
cargo install --path crates/mercury
mercury
mercury boots into the typing layer, which passes all keystrokes through. Typing jk takes you to the home layer. From there, n takes you to nav, i to in-app, s to per-site, and r to resize. o displays an overlay containing the layer’s keymap.
Then open crates/mercury/src/state/ and change something. bacon restart rebuilds and replaces the running daemon, so an edited binding is live without you touching a window, and mercury logs prints one record per dispatched event carrying the event, the effects it produced, and the state it left behind.
There is also a demo of freddie!
So, are you ready, Freddie?
Getting Started with Mercury walks through the layers, and Implementing Your Own Handler writes the first binding.
