# Miro: a native PDF viewer with Vim-style keybindings and a text config file

> Miro is a PDF viewer for Windows and Linux built on MuPDF and the iced toolkit, where every action is bound in a plain text configuration file with modes, and where the same actions are reachable from the mouse. The main friction is Windows, where the README says building from source often does not work.

**vincent-uden/miro** — A native pdf viewer for Windows and Linux (Wayland/X11) with configurable keybindings.

- Repository: https://github.com/vincent-uden/miro
- Website: https://crates.io/crates/miro-pdf
- Stars: 555 · Forks: 36
- Language: Rust
- License: AGPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/vincent-uden-miro

## A viewer built on MuPDF and iced

Miro is a native PDF viewer for Windows, macOS and Linux, with Wayland and X11 support and configurable keybindings. The description in the package manifest is slightly narrower, saying Windows and Linux with Wayland or X11, and the README title says all three. The topic tags are mupdf, pdf-viewer, rust and wayland.

The stack is visible in the dependency list. The rendering comes from mupdf at version 0.8, and the user interface is the iced toolkit at 0.14 with a long feature list that includes wgpu, tiny-skia, x11, wayland, tokio and thread-pool, which tells you the renderer is GPU accelerated on the platforms where that is possible. The window system bindings are arboard for the clipboard, rfd for native file dialogs, and open plus webbrowser for handing links and files to the rest of the system.

Around that are the small libraries that a real application needs. clap with derive for the command line, tokio with the full feature set for the async runtime, axum for the optional RPC server, tracing and tracing-subscriber for logs, and tracy-client for profiling. There is also a set of format handling libraries, including jsonstruct, toml, serde and serde_json, which is what a configuration file with modes and bindings needs.

The licence is AGPL-3.0, the crate is miro-pdf at version 0.12.0, and the default branch is master. The last push was on 2026-10-19, with v0.12.0 tagged on 2026-10-15 after v0.11.1 and v0.11.0 in June 2026, so the project is releasing steadily.

## Every feature is a binding, and the file format is a line of text

The feature list is long, and the interesting thing about it is that the README presents the whole application as a set of actions rather than as a set of features. Dark mode for both the interface and the PDF. Vim-like keybindings by default. A configuration file for key bindings in case you do not like Vim bindings. Mouse controls. Multiple PDFs in tabs. A CLI argument for opening PDFs from the terminal. Automatic hot-reloading of any viewed PDF, which the README calls out as especially useful when writing anything that compiles into a PDF such as LaTeX or Typst. Text copying in documents. Internal links such as a table of contents. External links, so email and websites copy on click. Bookmarks. A jumplist. An optional RPC server to control the viewer from another program. Printing via the default web browser. Opening links using keyboard controls. Fullscreen. A presentation mode that hides all UI. Recently opened files. Search through documents using plain text or regular expressions. Reading comments and showing annotations. And an overview mode showing many pages in a grid for quick navigation.

The configuration file lives at ~/.config/miro-pdf/miro.conf, or the equivalent path in the home directory of your operating system, and an example with all the default bindings is at /assets/default.conf. The README says every binding that is possible is bound at least once in the default configuration file, which is a useful invariant, because it means the example file is a complete reference rather than a starting point.

The format has three command types and comments. Key bindings are written as:

```
Bind <key_sequence> <mode> <action>
```

Mouse bindings use the same shape with a different verb:

```
MouseBind <mouse_input> <mode> <action>
```

And settings are:

```
Set <setting> <value>
```

The mouse inputs cover the basic buttons, MouseLeft, MouseRight, MouseMiddle, MouseBack and MouseForward, and they can carry modifiers like Ctrl+MouseLeft or Shift+MouseRight.

The key syntax is delegated to a crate rather than reinvented. Key sequences can be single keys such as j, k, h or l, modified keys such as Ctrl+r or Shift+Tab, or key chords with multiple keys where the quotes are required for sequences with spaces. The README points at the binding_syntax.md document in the keybinds-rs crate for the exact rules, which is the right arrangement, since the key parsing is the fiddly part and there is a spec for it.

## Modes do not inherit, and that is the sharpest edge in the config

The warning about modes is the single most important thing in the configuration documentation, and it is worth reading twice.

The mode is one of several modes available, all of them bound in the default config, and the README names some examples: Normal, Presentation and Overview. But then the warning: there is no inheritance of bindings between modes, all keys need to be bound for each mode separately. So forgetting to bind TogglePresentation inside the presentation mode will leave you unable to exit to normal mode.

That is a genuine footgun, and it is the kind of thing that happens in practice. A keybinding config where each mode is a complete separate map is simple to implement and simple to reason about, but it means every new mode has to be filled in from scratch, and one missing line locks you into that mode.

The mitigation is the default config, which binds everything in every mode, so the failure only appears if you write your own config from scratch and omit a line. If you are going to write a custom configuration, the sensible approach is to start from /assets/default.conf and modify it, rather than to create an empty file and add only the bindings you expect to need.

The error handling in the parser is designed for that workflow. The configuration parser provides detailed error messages with line numbers when parsing fails, and errors on one line do not prevent parsing of other lines, with output like:

```
Configuration parsing errors:
  Line 3: Unknown command: InvalidCommand
  Line 5: Unknown action: InvalidAction
  Line 7: Invalid port number: 'not_a_number'. Must be a valid integer
```

So a typo gives you a line number and a reason, and the rest of the file still loads. That is a much better failure mode than refusing to start, and it means you can fix one binding at a time.

## Settings split into runtime toggles and startup defaults

The settings section is where a distinction that is easy to miss gets explained.

There are runtime settings, which are:

```
Set <setting> <value>
```

with Rpc <True|False> to enable or disable the RPC server, RpcPort <number> to set the RPC server port, and TrackpadSensitivity <float> as a multiplier on panning with a trackpad.

And there are default settings, which use the same syntax:

```
Set <setting> <value>
```

but with a different lifetime. Default settings apply only at application startup, and they can later be toggled with key bindings. The available defaults are DarkModePdf <True|False> for dark mode rendering of the PDF, DarkModeUi <True|False> for dark mode for the UI, OpenSidebar <True|False> to open the sidebar, and PageBorders <True|False> for page borders.

So the same command sets both kinds of thing, and what differs is when the value is read. A default is a startup preference that a key binding can flip later, and a runtime setting is a live value. The TrackpadSensitivity multiplier is the one that is clearly a runtime setting, since it applies to the current gesture rather than to how the app starts.

The dark mode split into PDF and UI is worth noting separately, because rendering the document itself in dark mode is a separate operation from darkening the chrome around it, and the two are independent settings.

## Installing on Linux is easy, and on Windows the README tells you not to

The installation section gives three routes, and the guidance differs sharply by platform.

A pre-compiled binary is the simplest: head over to the releases and download the latest binary for your platform, then place it somewhere in your path. There is also a Nix Cachix binary cache, configured by adding the miro-pdf.cachix.org substituter and its trusted public key to your Nix settings.

The crates.io route is `cargo install miro-pdf`, and the README describes it as pretty much the same as building from source but without cloning the repo, and notes that there have been no problems compiling on Linux thus far. The source route is three commands:

```sh
git clone https://github.com/vincent-uden/miro.git
cd miro
cargo r --release
cp ./target/release/miro-pdf /usr/bin/miro-pdf
```

Linux has a dependency list beyond the obvious. Mupdf-rs requires headers for fontconfig, and the packages are libfontconfig1-dev on Ubuntu and Debian, fontconfig-devel on Fedora, and fontconfig on Arch and derivatives. Building on Linux also requires some hidden dependencies that you probably already have, which are clang and unzip.

Windows is where the README pushes back. The same rough process should work, but often does not, and the author highly recommends downloading a precompiled binary for Windows. The problem lies in compiling the crate mupdf-sys, which requires MSVC. The Visual Studio project embedded in that crate requires Visual Studio 2019, which is not available for downloading anymore, but can optionally be compiled using Visual Studio 2022, and even with that option the build might just not work sometimes due to issues with the Windows 10 and 11 SDK kits.

The workaround the author found was to get compilation working in GitHub Actions, which is what produces the release binaries that do function correctly on Windows systems.

For macOS, the author is blunt: they do not test the software for macOS since they do not own a Mac, it should work although only supporting building from source or crates.io, and no binary releases are made.

## Why the RPC server and the hot reload matter

Two features stand out from the list because they change what the viewer is for rather than adding a convenience.

The optional RPC server lets another program control the viewer. The dependency list backs this up with axum and tokio in the build, and the configuration exposes it with the Rpc setting and the RpcPort setting, and the README says the RPC server can be enabled or disabled that way. For a document viewer, a control channel is what turns it into a component: a tool that opens a PDF at a page and scrolls to a match, or a script that drives a batch of documents, is something you could not do with a plain viewer, and an axum server is a normal way to expose that.

The automatic hot-reloading of any viewed PDF is the other one. The README describes it as especially useful when writing anything that compiles into PDFs such as LaTeX or Typst, and the dependency list has async-watcher, which is a filesystem watcher. So when the file on disk changes, the viewer picks it up.

That is a small feature with a large effect on a workflow. A LaTeX or Typst workflow is otherwise a loop of compile, close the viewer, reopen, look for the page you were on. With hot reload the viewer stays on the document and updates, which is why the feature is called out separately in the list rather than buried with the tab handling.

The RPC server is a config setting rather than a compile-time feature, so it is off unless you turn it on, and the README is careful to present it as optional. That is the right default for a network listener.

## Conclusion

Adopt Miro if you want a native PDF viewer with Vim-style keys, tabs, a presentation mode, an overview grid and hot reloading of a PDF you are recompiling, and you are on Linux or you are willing to take a pre-built Windows binary. Do not build it on Windows yourself, because the README says the same process should work but often does not, and the Visual Studio project it needs is no longer downloadable. Before you configure it, read the mode inheritance warning, since there is no inheritance of bindings between modes and a forgotten binding can leave you stuck in presentation mode with no way back.

## FAQ

### How do I install Miro the PDF viewer?

Download a pre-compiled binary from the releases page and put it on your path, or run `cargo install miro-pdf`, or build from source with `git clone`, `cargo r --release` and copying the binary. On Linux you also need the fontconfig development headers, and clang and unzip.

### Can I build Miro on Windows myself?

The README says the same rough process should work but often does not, and highly recommends downloading a precompiled binary for Windows. The mupdf-sys crate needs MSVC, and the Visual Studio 2019 project it embeds is no longer downloadable, so the release binaries are produced through GitHub Actions instead.

### Where is the Miro configuration file and what can I set in it?

It is at ~/.config/miro-pdf/miro.conf, and an example with every default binding is at /assets/default.conf. The file takes Bind, MouseBind and Set commands, with runtime settings for the RPC server, its port and trackpad sensitivity, and startup defaults for dark mode in the PDF and the UI, the sidebar and page borders.

### Do Miro key bindings carry over between modes?

No. The README warns explicitly that there is no inheritance of bindings between modes, so every key has to be bound for each mode separately, and forgetting to bind TogglePresentation in presentation mode will leave you unable to exit to normal mode. The parser reports errors with line numbers and keeps parsing the rest of the file.

## Sources

- [License: AGPL-3.0](https://github.com/vincent-uden/miro/blob/master/LICENSE)
- [Project website](https://crates.io/crates/miro-pdf)
- [README](https://github.com/vincent-uden/miro/blob/master/README.md)
- [Releases](https://github.com/vincent-uden/miro/releases)
- [vincent-uden/miro on GitHub](https://github.com/vincent-uden/miro)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/vincent-uden-miro
