edtr
┌──────┐ ┌──────┐ ╭──▶ nvim
│ file │ ──▶ │ edtr │ ──▶ ├──▶ code
└──────┘ └──────┘ ╰──▶ …
edtr is a fast, cross-platform CLI that takes one or more file paths and
forwards them to the right editor according to rules you write in TOML. It is
the Rust successor to hitori.vim:
same "single editor instance" idea, but editor-agnostic and with near-zero
startup cost — perfect for registering as your OS default program for text
files, or as $EDITOR.
Features
- Rule-based routing: regex patterns in TOML decide which editor opens which file. Different paths can route to different editors (VSCode for one project, nvim for another).
- Single-instance neovim via named pipes / unix sockets:
edtrconnects to a running nvim and sends:editover msgpack-RPC. Works on Windows via\\.\pipe\...— no Deno, no plugin framework, no cold start. - Sync or async per rule:
sync = truewaits for the editor to exit (perfect forgit commit),sync = falsefires and forgets (perfect for double-clicking files in the OS file explorer). - Tera templating throughout the config:
{{ file_path }},{{ env.HOME }},{% if is_windows() %}...{% endif %}, and every Tera filter. - Generic editor support: any CLI editor works (
code,vim,helix,subl,emacsclient, …) without custom code. - Fast: static Rust binary, cold start in milliseconds. On Windows this is often 10–100× faster than denops-based alternatives.
Install
Binary lives at ~/.cargo/bin/edtr. Make sure that's on your PATH.
Quick start
edtr works out of the box with a bundled default config — it routes
everything to a single shared neovim instance, except $EDITOR-callback
files (COMMIT_EDITMSG etc.) which always get a fresh sync = true instance
so git commit works.
To customize, drop a file at:
- Linux / macOS:
~/.config/edtr/edtr.toml - Windows:
%APPDATA%\edtr\edtr.toml
Minimal example:
# ~/.config/edtr/edtr.toml
[]
= "neovim"
= "nvim"
# The pipe used to reach the running nvim. is_windows()/is_linux()/is_mac()
# are edtr-provided Tera functions.
= '{% if is_windows() %}\\.\pipe\nvim-edtr-{{ group }}{% else %}/tmp/nvim-edtr-{{ group }}.sock{% endif %}'
[]
= "generic"
= "code"
= ["--reuse-window"]
= ["--new-window"]
# git commit, rebase, etc. — always a blocking fresh nvim.
[[]]
= "editor-callback"
= '(?i)/(COMMIT_EDITMSG|MERGE_MSG|git-rebase-todo)$'
= "nvim"
= "new"
= true
# Route files under ~/src/company/ to VSCode.
[[]]
= "work"
= '/src/company/'
= "code"
= "remote"
# Default: everything else goes to the shared nvim.
[[]]
= "default"
= '.*'
= "nvim"
= "default"
= "remote"
Then:
# Open any file in the right editor
# See which rule would match, without actually dispatching
# Same dispatch logic, just don't execute
As $EDITOR
The bundled default config is compatible with every $EDITOR=... caller I
know of (git, crontab, visudo, fc, mutt, …).
As OS default program (Windows)
Right-click a .txt → Open with → Choose another app → Browse → point at
edtr.exe. edtr will honor the rules and open the file in the correct
editor, spawning a new console if the target editor is a TUI.
Configuration reference
[vars]
User-defined variables available as {{ vars.NAME }} in every other
template:
[]
= "/home/me/src"
[editors.<name>]
| field | type | required | meaning |
|---|---|---|---|
kind |
"neovim" / "generic" |
yes | backend selection |
command |
string | yes | the editor binary (PATH-resolved) |
listen |
string | neovim | socket / named pipe path for RPC |
args_new |
array<string> | no | extra args when mode = "new" |
args_remote |
array<string> | no | extra args when spawning for mode = "remote" fallback |
env |
table | no | env vars passed to the spawned editor |
[[rules]]
| field | type | default | meaning |
|---|---|---|---|
name |
string | rule[N] |
human-readable label (shown in check) |
match |
regex string or [regex] |
required | path pattern(s); paths are normalized to / before matching |
exclude |
regex string or [regex] |
none | when any exclude hits, the rule is skipped even if match hits — edtr falls through to the next rule |
editor |
string | required | key from [editors.*] |
group |
string | "default" |
instance identity (one nvim per group) |
mode |
"remote" / "new" |
"remote" |
remote = reuse existing, new = always fresh |
sync |
bool | false |
true = block until editor exits |
Template context
Available in rule.group, editor.command, editor.listen, editor.args_*:
| variable | example |
|---|---|
file_path |
C:/Users/you/notes/todo.md |
file_dir |
C:/Users/you/notes |
file_name |
todo.md |
file_stem |
todo |
file_ext |
md (no leading dot) |
editor_* |
same five fields for command |
cwd |
current working directory |
group |
resolved group (phase 3 only) |
rule |
resolved rule name (phase 3) |
vars.<key> |
your [vars] entries |
env.<KEY> |
process env at edtr invocation |
And these edtr-specific Tera functions:
is_windows(),is_linux(),is_mac()— booleans for OS branching.
Plus everything Tera ships — replace, split, join, length, now(),
and all other stock Tera features.
CLI reference
edtr [FILES]... # dispatch files per rules (default action)
edtr check <FILES>... # dry-run: show matched rule per file
edtr completion <shell> # emit shell completion script
edtr --help
edtr --version
# v0.2+:
edtr list # list alive editor instances
edtr kill <group> | --all # terminate instances
edtr config path | edit | validate | show
Flags:
-c, --config <PATH>— override config path-E, --editor <NAME>— bypass rule, force editor-G, --group <NAME>— bypass rule, force group--dry-run— print the resolved plan without executing-v, --verbose—-v= info,-vv= debug,-vvv= trace
Logging is also controllable via RUST_LOG.
Roadmap
- v0.1 (this release): core dispatch, neovim + generic backends,
check,completion, default config,$EDITORcompatibility. - v0.2:
list/kill/config edit|validate|show,open/send, neovimremote + syncvianvim_buf_attach. - v0.3+:
scripteditor kind, per-file arg placement, plugin hooks.
Heritage
edtr is a Rust rewrite of hitori.vim (denops-based). The old
plugin had a slow cold start on Windows and was vim/neovim-only; edtr is
fast and editor-agnostic while preserving the core "one editor instance"
philosophy.
License
MIT — © 2026 yukimemi.