00Direction · Signage

Press.
Type.
한국어.

BM Keyboard is a tiny macOS overlay that turns plain romaja keystrokes into Hangul jamo in real time. Bold, unmistakably an input app.

v0.1 · romaja → 한글
the keycap
Latency< 1msevent-tap callback
Footprinticednative Rust UI
LayoutQWERTYromaja input
TargetmacOS 12+Apple silicon · Intel
live · the overlay, paraphrased

Type romaja. Read 한글.

JAMO · synthesized
한글
ROMAJA · raw input
01What it is

A romaja keyboard
that thinks in
한글.

BM Keyboard is a small Rust app that sits quietly above your workspace. You type jang on a regular QWERTY keyboard. The overlay catches every keystroke, converts it into Korean jamo, and shows the result above whatever app you happen to be in.

No IME juggling, no system-wide input source switching. One window, one buffer, one canonical translation table. Built on iced and the macOS CGEventTap APIs — the same primitive Karabiner uses.

jangㅈㅏㅇsyllable assembly next
02How it works

One keystroke,
ten stops.

From hardware to pixels in under a millisecond. The pipeline is short on purpose — every step is a function you could read in a sitting.

step · 01

Hardware → IOKit

You press a key. IOKit lifts it into CoreGraphics as a CGEventRef carrying keycode, modifier flags, and the layout-aware unicode the OS already computed.

step · 02

CGEventTap callback

A tap registered at HID location, HeadInsert placement. We let Cmd/Ctrl/Opt shortcuts pass through untouched. Special keys (return, delete, escape) short-circuit. Everything else hands back its unicode string.

step · 03

KeyEvent → mpsc channel

The tap thread sends typed events across an iced::futures::mpsc channel — no shared mutable state. In Grab mode the original keystroke is nulled so the focused app never sees it.

step · 04

InputAgent buffers romaja

A tiny state machine on a String. Push on Char, pop on Backspace, commit on Enter, clear on Escape. The overlay window resizes itself to fit.

step · 05

SynthesizerAgent converts

Greedy left-to-right scan. At each byte, look up the longest matching romaja prefix in the token table. Push the matched jamo. Advance by the canonical length. babo → ㅂㅏㅂㅗ.

step · 06

iced renders, repeat

Both agents emit window::resize tasks; the two overlay windows snap to their new content size. CFRunLoop keeps spinning. Next keypress, repeat from step one.

keyboard
CGEventTap
KeyEvent
mpsc
InputAgent
Synth
pixels

guides/pipeline.md · the canonical trace

03Install · use

Four
commands.
Then type.

Rust toolchain required. macOS 12 or newer. The build produces a single binary; the overlay runs as a normal user process — no kernel extensions, no privileged daemons.

  1. Clone the repo

    step · 01
    git clone https://github.com/you/bm_keyboard
    cd bm_keyboard
  2. Build with cargo

    step · 02
    cargo build --release --example overlay
  3. Grant permissions

    step · 03

    System Settings → Privacy & Security → grant the launching process Accessibility and Input Monitoring. Terminal if you run from terminal, your IDE if you run from there.

  4. Run the overlay

    step · 04
    cargo run --release --example overlay
04Once it's running

The five keys to remember.

type

Type romaja anywhere — a browser, Notes, your terminal. The keystrokes flow into the overlay, not the focused app.

see

The bottom window mirrors the raw romaja buffer. The top window shows the jamo guess, updated on every keystroke.

↵ enter

Commits the current buffer (printed to stdout for now). Clears it.

⎋ esc

Clears without committing.

⌫ delete

Pops the last char.