DEV Community

Cover image for I wrote a Mac code editor in Rust with its own Metal UI toolkit
Sujit Baruwal
Sujit Baruwal

Posted on

I wrote a Mac code editor in Rust with its own Metal UI toolkit

Every code editor I used daily was a web browser in a trench coat. VS Code,
Zed's predecessors, all of them: Chromium or a web view somewhere in the stack,
carrying the memory footprint and the uncanny almost-native feel. I wanted an
editor that feels like it belongs on a Mac — native menus, native dialogs, the
shortcuts my fingers already know — so I wrote one. In Rust. With its own GPU
UI toolkit on Metal.

It's called Orbvane. It's open source
(MIT/Apache-2.0), signed and notarized, and you can install it with:

brew install --cask sbaruwal/tap/orbvane
Enter fullscreen mode Exit fullscreen mode

This post is about the technical decisions that made it possible — and the ones
that bit me.

Orbvane in the Orbvane Night theme

Why not Electron (or Tauri, or GPUI)

The short version: control. A web view decides your memory floor, your input
latency, and how "native" your app can ever feel. I wanted keystrokes that land
in under a millisecond in a million-line file, 120Hz scrolling, and a UI that
respects macOS conventions instead of approximating them.

The longer version: I enjoy suffering, apparently. Writing your own UI toolkit
means writing your own text layout, focus management, menus, dialogs, drag and
drop — everything a platform or framework normally hands you. The bill comes
due in the least glamorous places. More on that below.

The architecture

The stack, bottom to top:

  • Windowing: winit. It handles the platform layer and, importantly, the NSTextInputClient bridging on macOS that makes input methods work.
  • Rendering: wgpu on Metal, with our own retained-mode UI toolkit on top. Retained, not immediate-mode: the toolkit keeps a scene of what's on screen and re-renders only what changed (dirty-rect tracking). Immediate mode is lovely to write, but you re-pay the layout cost every frame. Caching glyphs and layout in RAM is what makes 120Hz scroll possible — RAM usage isn't the enemy, redoing work is.
  • Text: cosmic-text for shaping (it handles ligatures, complex scripts, emoji), over our own buffer with rope-like structure for large files.
  • Languages: tree-sitter for highlighting (36 grammars), and real language servers over LSP for everything smart — diagnostics, completion, rename, go-to-definition. The editor doesn't reimplement what servers already do.
  • Debugging: DAP. lldb-dap for Rust/C/C++/Swift, debugpy for Python, Delve for Go, and our own adapter for Node.
  • Terminal: our own xterm-compatible emulator with split panes.
  • AI: the assistant talks to the CLIs you already have (claude, codex) instead of shipping another API integration.

The principle throughout: own the things that define the feel (rendering, text,
input), rent the things that are protocols (LSP, DAP, MCP-shaped tools).

Orbvane in the Orbvane Day theme

The bug that taught me the most

Shortly after launch, someone asked on X how the custom GPU toolkit handles CJK
IME input — "that's usually what breaks first in custom-drawn editors." They
were right, and it was broken: winit was emitting the IME preedit/commit
events, and nothing in the editor handled them. CJK composition simply didn't
work.

The fix shipped the same day in 0.3.2: handle the preedit events, render the
marked text with an underline at the caret, position the candidate window at
the caret, and give CJK/emoji their proper two-column width in the editor.

The lesson generalizes: in a custom-drawn editor, text input is the feature.
Not rendering, not themes — input. IME, dead keys, bidirectional text,
accessibility APIs. These are the unglamorous systems that decide whether your
editor works for everyone or just for people who type ASCII. I now treat every
one of them as load-bearing.

What's genuinely hard

Honesty section. Things that are done, things that aren't:

  • Done: fast startup, huge files, LSP/DAP/terminal/git/tests/extensions, AI assistant, IME with inline composition, no telemetry.
  • Not done: accessibility (VoiceOver) — custom GPU UI is invisible to assistive tech until you implement the macOS accessibility APIs, and that's real work touching the whole UI tree. This is next on my list after input.
  • Untested: bidirectional text. Shaping handles it; caret movement in mixed-direction text is where custom editors break, and I haven't verified ours yet.
  • By design, missing: remote dev over SSH, notebooks, running extension JavaScript. Some of these will come; some are deliberate scope cuts.

Try it, break it, tell me

Orbvane is at github.com/sbaruwal/orbvane.
Issues are open and I read all of them — the IME fix started as a reply on
social media and shipped hours later. If you type CJK daily, if you live in
lldb, if you have opinions about retained vs. immediate mode rendering: I'd
genuinely like your eyes on it.

Top comments (1)

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Việc bỏ qua Electron để tự viết Metal UI toolkit là một quyết định cực kỳ táo bạo nhưng hoàn toàn xứng đáng nếu muốn tối ưu latency. Mình từng làm việc với mấy dự án dùng framework cũ và việc xử lý rendering cho text editor thực sự là một cơn ác mộng về performance, nhất là khi phải xử lý hàng nghìn dòng code cùng lúc. Cái lỗi IME bạn nhắc đến nghe quen quá, thường thì việc đồng bộ giữa input method và custom renderer là chỗ dễ phát sinh lỗi hiển thị nhất. Cách tiếp cận bằng Rust giúp quản lý bộ nhớ an toàn hơn hẳn khi xử lý các tác vụ đồ họa nặng trên Metal.