Skip to main content
freddie

freddie

Build a bespoke control plane for your computer.

It’s your computer

Bend it to your will

Send commands to your agent

Send a screenshot or the contents of your clipboard to an agent. Or, select all, cut, send to your agent (“please fix”), and paste the result, all without ever leaving your app.

Check out a branch from GitHub

Press one key to check out the branch you’re reviewing on GitHub, open your editor, and open the changed files. Then press one key to jump back.

Jump to Google Meet

You’re scrolling, but your boss asked you a question about the sales figures. How quickly can you find Google Meet? One key foregrounds it, wherever it is. No sweat.

Rearrange windows automatically

Connect a monitor and have your application windows go back to where they belong, without any futzing on your part.

Respond to scheduled events

Nudge yourself to stop doomscrolling after 30 minutes or decrease your screen’s brightness at sundown.

Dispatch links to specific profiles

Every link on your machine can go through freddie, so work GitHub links always open in your work Chrome profile.

It’s powerful

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 application 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.

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.

It’s the elm architecture

Events in, effects out

Effects in

A freddie program responds to any arbitrary event stream. Each is incorporated into the same state, so an event handler can make complex decisions that take into account multiple sources of information.

Pure, testable core

You write individual event handlers, and freddie assembles them into a larger function: events in, descriptions of effects out. This larger function is pure and side-effect free, and thus easily tested and easy to reason about.

Effects out

You can handle these effects however you like. Resize windows, foreground apps, send keys. Because we're writing code and your handlers receive access to the state, there really is no limit to what you can accomplish.

It’s a kind of magic

Handle complexity with ease

The value of using a programming language and compiling our own program becomes apparent when we move beyond simple examples. Here, we’ll build something that is impossible (or at least, awkward) in any other framework: the ability to maximize windows, and later restore them to their previous location.

First, we add the appropriate pieces of state onto our root struct:

pub struct Mercury {
/// The focused window and where it sits.
focused: Option<(WindowId, Frame)>,
/// Where each window was before we moved it.
prior_locations: HashMap<WindowId, Frame>,

#[resolve_into]
layer: Layer,
}

A binding is a trigger and the handler it runs, written on the layer where it applies. Up maximizes the focused window, and r puts it back.

#[derive(Bind, Debug)]
#[node(parent = LayerPath)]
#[binds(MercuryStruct)]
#[bind(
Key::UpArrow.down() => maximize,
Key::KeyR.down() => restore,
)]
pub struct ResizeLayer;

Maximizing writes down where the window was:

fn maximize<'a>(
_ev: &KeyEvent,
node: Node<ResizeLayerPath<'a>, ()>,
) -> Option<MercuryEffect> {
// ResizeLayer -> Layer -> Mercury, where the frames are kept.
let root: &mut Mercury = node.parent.into_ancestor();

let (id, frame) = root.focused?;
// Only the first maximize records anything. A second one
// finds the entry already there and leaves it alone, so
// `r` still goes back to where the window started.
root.prior_locations.entry(id).or_insert(frame);

Some(MercuryEffect::Place(Placement::Maximize))
}

And restoring reads it back out:

fn restore<'a>(
_ev: &KeyEvent,
node: Node<ResizeLayerPath<'a>, ()>,
) -> Option<MercuryEffect> {
let root: &mut Mercury = node.parent.into_ancestor();

let (id, _) = root.focused?;
let frame = root.prior_locations.remove(&id)?;

Some(MercuryEffect::Place(Placement::Exactly(frame)))
}

Populating the state

So far so good — we wrote handlers that accessed and mutated the state, and emitted effects that did the right thing. But that focused field did not fill itself in. We have to hook that up ourselves, too:

pub enum MercuryEvent {
Key(KeyEvent),
Foreground(ForegroundEvent),
Timer(TimerFired),
/// Which window is focused and where it sits. New.
Window(WindowFocused),
}

Something has to make one. A source is a stream you subscribe to, turning whatever it hands you into that variant:

// In `mercury daemon`, beside the other sources.
freddie_windows::watch(move |focused| {
let _ = events.send(MercuryEvent::Window(focused));
});

And a binding at the root keeps the field current. It changes state and asks for nothing, so it returns None.

#[bind(
AnyWindowFocused => track_focus,
)]
pub struct MercuryStruct;

fn track_focus(
ev: &WindowFocused,
node: Node<MercuryPath, ()>,
) -> Option<MercuryEffect> {
node.parent.get_mut().focused = Some((ev.window, ev.frame));
None
}

Dispatch narrows an event to the kind a trigger reads before asking whether it matches, so the key bindings above never see a window event and did not have to be told this one exists.

It’s worth a look

See it running

It’s ready for you

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.

Once you want it there every time, mercury install registers it to start at login, and mercury uninstall takes that back out. The rest of the verbs drive the running one: restart replaces it after a rebuild, stop ends it through the model so a command layer hands your modifiers back, and status and logs report on it without touching it.

mercury install # start it at login
mercury restart # replace the running one
mercury logs # follow what it is doing
It’s time

Are you ready, Freddie?