mdview and the missing middle between less and Electron

I wrote mdview because I wanted a fast Markdown viewer for the Wayland desktop. Just a quick way to open a README or a design note without breaking flow.

Developers spend a lot of time around Markdown files: READMEs, runbooks, changelogs, design notes, random NOTES.md files in project directories. Most of the time I am already in a terminal when I want to read one.

The usual options all work, but none of them feel quite right.

less and vim are fast, but they show raw text. That works, but I usually need longer to understand what I am looking at. Markdown is often written to be read as structured text. Headings, tables, links, anchors, images, and highlighted code blocks make it easier to work with.

Converting through pandoc or opening a browser works, but it is too much ceremony for a glance.

Electron-based viewers are comfortable, but they are too heavy for taking a quick look, especially when you do not have one open already. I do not want to launch a whole application stack just to read a README next to my terminal.

So mdview tries to sit in the missing middle: fast enough to feel like a small tool, but capable enough to actually read Markdown.

The project is available on Codeberg: codeberg.org/beleon/mdview.

bat README.md on the left, mdview rendering the same README on the right

Why I tried native Wayland

For this project, I wanted to see how far I could get without a general GUI toolkit or browser engine.

GTK would have been a perfectly reasonable choice. A toolkit would have solved many details I had to handle myself: input, clipboard, window decorations, scaling, cursors, gestures, and a lot of small desktop integration work.

I did not avoid GTK because I think it is bad. I avoided it because this project was small enough that the direct approach still felt manageable.

That is the important distinction for me. A larger application should probably make different choices. A small Markdown viewer with a narrow scope can sometimes afford to be more direct.

mdview talks to Wayland directly. It uses xdg-shell, shared-memory buffers, input handling, clipboard support, cursor shape, pointer gestures, window decorations, and toplevel icons.

mdview also does not include an XWayland fallback. It targets the environment I use it in directly, and adding another compatibility path would have made the project larger than I wanted it to be.

That is the trade-off: more code in some places, less machinery overall.

For this scope, that felt right.

The rendering pipeline

The architecture is simple enough to explain in one diagram:

.md file
   ↓
md4c
Markdown → HTML
   ↓
litehtml
HTML/CSS → layout
   ↓
Cairo + Pango
drawing + text shaping
   ↓
wl_shm buffer
   ↓
Wayland window

Markdown parsing is handled by md4c. The HTML/CSS layout step is handled by litehtml. I do not need a browser engine, JavaScript, dynamic DOM behavior, or a network stack. I need enough layout to render Markdown as structured text.

Cairo and Pango handle drawing and text. The final result is drawn into a shared-memory buffer and presented through Wayland.

I used C++17 mostly because it fit this stack. md4c is C, litehtml is C++, and Cairo, Pango, and the Wayland protocol bindings are C. Rust would have been nicer for ownership and lifetime management, but for this set of libraries C++ was the path of least resistance.

That is the whole shape of the application. Markdown in, pixels out.

It is not the most flexible architecture. It is not trying to be. But it is understandable, and that matters.

What the measurements showed

I wanted mdview to feel like something you can use while working in the terminal.

That does not mean it has to be tiny in the cat or less sense. It is doing real Markdown rendering, layout, text shaping, image loading, syntax highlighting, text selection, and UI interaction. But it should still feel like a quick viewer, not like launching a whole application.

The stripped binary is 2.6 MB. That does not make the whole runtime tiny, because the rendering stack and desktop libraries still matter, but it does say something about the shape of the tool itself.

These numbers are not meant as a universal benchmark. They are just the measurements that helped me understand where mdview spends its time and memory on my machine.

test.md (792 B, 125 spans)              267 ms      RSS 34 MB
Nextcloud README (6.1 KB, 697 spans)    344 ms      RSS 47 MB
mdview README (8.7 KB, 1420 spans)      426 ms      RSS 67 MB

As a reference point, Typora from the .deb package takes about 2.5 seconds to open on the same machine. Typora is a full Markdown editor, not a quick-look viewer, so this is not a competition. It is just a useful number for the kind of startup delay I wanted to avoid when I only want to read a file.

The rough shape was consistent across those files:

file read           :   <2 ms
state + config load :   <2 ms
md4c + heading IDs  :   <3 ms
litehtml doc build  :  120–300 ms
window create       :   17–30 ms
wait for first paint:  120–170 ms

File I/O and Markdown parsing are not the interesting costs. They are basically below the noise floor here.

The two real costs are layout and first paint. litehtml document construction and layout dominate the part that scales with the document. The compositor first-paint wait is a mostly fixed cost on my setup.

Wayland surface creation itself is not the problem. It is around 17–30 ms in these measurements. The no-toolkit decision probably saves some startup time, but I would not overstate that. A toolkit does not become slow because creating one window is inherently expensive.

The larger trade-off is the full dependency surface, memory footprint, initialization path, and how much application machinery I want to bring into a small viewer.

That is the part that matters more to me.

I also looked at memory use, but the conclusion was simpler. Most of it comes from the rendering stack and desktop text/image infrastructure around it: fontconfig, Pango, Cairo, image loading, litehtml, and allocator overhead. There are still places to optimize, but going much lower would probably mean replacing larger parts of the rendering stack. For now, that does not feel like the right trade-off.

What stayed outside the scope

There are many things mdview does not try to do.

It is read-only on purpose. Editing, plugin systems, knowledge-base features, and browser-like behavior would all make it a different kind of application.

It also does not try to render every possible document perfectly. The goal is good everyday Markdown reading, not browser-level compatibility with every corner case.

The same applies to installation and runtime assets. The binary has the icons baked in. Installation can still write icons and a .desktop entry under ~/.local/, but running the program does not depend on finding runtime files in the right place.

None of this is universally correct. It is just the shape that made sense for this project.

Small software is not automatically good. It can become awkward, limited, or hostile to users. The goal is not to remove things for the sake of removing them. The goal is to avoid bringing along machinery that does not earn its place.

Where mdview fits

The best way to describe mdview is still: somewhere between less and Electron.

less is fast and always there, but it does not give you a rendered document.

Electron can give you a rich interface, but it is often more application than I want for this job.

mdview sits somewhere in between. It renders Markdown properly, opens quickly, detaches from the shell, supports find, live reload, dark mode, zoom, text selection, links, images, highlighted code, and an outline sidebar.

But it stays narrow.

That is the kind of software I keep coming back to: small enough to understand, capable enough to be useful.

mdview is not meant to be the Markdown application for everyone. It is just a tool for a particular moment:

I am working in a terminal.
There is a Markdown file.
I want to read it without changing context too much.

Similar Posts