blog · · Mohak Bajaj
Porting my Electron app to Rust: 675 MB to 91 MB
Jot is a capture app for the Mac. Select text anywhere, press Shift twice, and it becomes a Markdown file in ~/Documents/Jot. Hold fn to dictate. Up to 1.1.4, Jot was an Electron app. Jot 2.0 is the same app in Rust, on Rune, a GPU UI framework.
Both builds are universal, for Apple silicon and Intel. The Electron 1.1.4 app is 675 MB on disk. The Rust 2.0 app is 91 MB. The download went from a 302 MB dmg to a 44 MB one. 2.0 uses the same vault, settings file, Keychain items, license and bundle id, so 1.1.4 users update in place.
Why leave Electron
Size was the obvious reason. Most of the 675 MB is Chromium, for an app that shows a list of notes. The less obvious reason was the shape of the code. An Electron app has a main process, a preload script and a renderer, and every feature crosses IPC. Jot's native work already lived outside Node: a Swift helper for Apple's speech API, another for Apple Intelligence cleanup, and a separate process for local speech models. Electron was the glue, not the engine.
One process, no IPC
I did not port the Electron layout. In the Rust app, the view and the state are one struct. A click handler gets &mut Jot and changes it. There is no message to define, send, validate or version.
┌──────────────── app (Jot) ─────────────────┐
│ one process: state + view + vault │
│ handlers get &mut Jot, no IPC │
│ │
│ watcher thread ──┐ capture thread ──┐ │
│ (vault changes) │ (grab selection) │ │
│ ▼ ▼ │
│ AppHandle::update(|s, tx| ..) │
└───────┬───────────────────────┬────────────┘
│ │
┌───────▼────────┐ ┌────────▼──────────────────────────┐
│ jot-core │ │ jot-os (one API) │
│ note format │◀────│ macos/ windows/ linux.rs │
│ vault (std::fs)│ │ double_tap.rs (shared, tested) │
│ bin: jot (CLI) │ │ Link (jot:// parser) │
└────────────────┘ └───────────────────────────────────┘Work that blocks runs on its own thread: watching the vault for changes, and grabbing the selection from another app. The result comes back through AppHandle::update, so the UI thread never waits.
The note format and the vault are in jot-core. The jot CLI and the MCP server are built from the same crate, so the app, the CLI and your agents use one tested implementation. In 1.1.4 the CLI was JavaScript that shared code with the app through the bundler.
OS code in one place
Everything that calls the OS is in jot-os. It has one public API, and each OS is one module that the compiler picks:
#[cfg_attr(target_os = "macos", path = "macos/mod.rs")] #[cfg_attr(windows, path = "windows/mod.rs")] #[cfg_attr(all(unix, not(target_os = "macos")), path = "linux.rs")] mod sys;
Logic that does not need the OS is written once and tested once. Shift Shift is the best example. The OS only reports key events. A small state machine decides when two taps count:
/// A tap is a press and release shorter than this, with nothing else pressed meanwhile.
const TAP: Duration = Duration::from_millis(300);
/// The second tap must end within this of the first.
const GAP: Duration = Duration::from_millis(400);
pub enum Press {
ShiftDown,
ShiftUp,
/// Any other key or modifier. Breaks a tap in progress, so typing capitals never fires.
Other,
}Other is the important case. Any other key spoils the tap, so typing a capital letter never opens a capture. The same machine runs on macOS and Windows. On Linux, X11 gets a global chord, but Wayland gives an app no global key events, so Shift Shift is not available there.
Electron Jot only ran on the Mac. The Rust build runs on Windows and Linux too, and CI runs cargo test on all three. The Mac is still the main target.
Same vault, both apps
A note is a Markdown file with YAML front matter. The Rust app had to read every file that 1.1.4 wrote, and write files that 1.1.4 could still read, because people update at different times and some sync the folder between Macs.
One rule made this safe: unknown front-matter keys are kept. Meta::extra holds every key this build does not know. If a newer or older Jot wrote a field, a save from the Rust app keeps it. Both apps can use the same vault at the same time.
How I checked parity
Before it could ship, the port had to do everything 1.1.4 does. I audited every feature against the Electron code paths, not just the new crates, and kept the result in a parity file in the repo:
| Area | done | partly | |-------------------------------|------|--------| | Core, vault | 17 | 0 | | Settings | 7 | 0 | | CLI and MCP | 6 | 0 | | Inbox | 19 | 0 | | Capture | 5 | 1 | | Voice, note, draw | 8 | 0 | | Speech | 9 | 0 | | Settings window, onboarding | 5 | 0 | | License, updates, i18n | 7 | 0 | | Shell | 7 | 1 |
A feature counts as done only with a test, a headless snapshot or a recorded run. Many of the tests are ported from the TypeScript suite. With --snap shot.png, the app renders itself to a PNG with no window. The Electron app can do the same, so I compared both with the same demo data.
The two open items:
- Shift Shift on Wayland, which Wayland does not allow. Electron Jot had no Linux build at all.
- Developer ID signing and notarization, which happen at release time.
Better dictation along the way
While porting the speech code, I took a set of fixes from Hex, an MIT-licensed dictation app. They go beyond 1.1.4:
- The model warms with a silent run on key-down, so the first dictation after launch is faster.
- Recording continues 200 to 600 ms after you let go, so the last word is not clipped.
- Silence is judged on the loudest 30 ms window against -50 dBFS, not the clip's average, so quiet speech is no longer dropped.
- The mic opens before the front-app lookup, so the first word is not clipped either.
- Pasted transcripts are marked transient, so clipboard managers skip them. The old clipboard comes back only if nothing else changed it.
What it cost
The Rust code is not smaller. It is about 76,000 lines, against about 73,000 lines of TypeScript in 1.1.4. Rune is young, so some things Electron gives you for free, like text fields, had to be checked before I could depend on them. In return, the app is about a seventh of the size, the state has one owner, and the OS code for three platforms sits behind one API.
Jot 2.0 is at sayjot.xyz. If you are on 1.1.4, Check for Updates brings it in place.
Jot is a Mac app for capture and dictation. It's free for 3 days, then $9 once at the launch price. sayjot.xyz