mle: a small terminal editor that borrows your shell tools
flexible terminal-based text editor (C)
At a glance
- What is it?
- mle is a terminal text editor in C, about twelve thousand lines of it, whose defining choice is to delegate: navigation goes to ctags, fuzzy file finding to fzf, grepping to grep, paging to less, and the file tree to tree. What is left is a small editor that can also run headless as a filter and be scripted in Lua, and the README states the trade-off in its list of aims, favouring simplicity over correctness.
- Who is it for?
- Adopt mle if you like the idea of an editor that is about twelve thousand lines of C you can read in an afternoon, and if your navigation habits are already built on ctags, fzf and grep, because the editor inherits all of it for free. It is the wrong tool if you want mouse support, since the upstream project does not provide it and points to a fork, and the wrong tool if you want a modern editor with LSP, file trees and plugins built in.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 171 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Twelve thousand lines and a stated set of priorities
mle is a small, flexible, terminal-based text editor written in C. The README calls it flexible, and the aims section is where the interesting constraints are.
Keep the codebase small and hackable. Minimize build-time and run-time dependencies. Favor simplicity over correctness. Make extensible and configurable. Use shell commands to enhance functionality, for example grep and tree.
That third aim is the one that needs a reader to sit with. A tool that prefers simplicity over correctness is making a deliberate trade, and in an editor it means that some edge cases are handled approximately rather than exactly. It is an unusual thing to state plainly, and stating it is a service to anyone evaluating the project, because it tells you that bug reports about odd behaviour in unusual conditions may not be treated as defects.
The scale is stated as well: a small codebase of around 12k lines of source. The top level listing backs that up, with buffer.c, bview.c, cmd.c, cursor.c, editor.c, main.c, mark.c, util.c for command handling, plus headers for the buffer, the editor and the key map, and a uscript.c for the scripting layer. That is a codebase a person can hold in their head, which is the point of the first aim.
The portability story is in one line: it runs on Linux, Windows through Cygwin or WSL, FreeBSD, macOS and more. The library list at the end of the README explains how little that takes, since uthash provides hash maps and linked lists, termbox2 provides the terminal user interface, and PCRE2 handles syntax highlighting and search. Three libraries, and one of them is a hash table and one is a terminal abstraction, which is a remarkably short list for an editor with syntax highlighting and regular expressions.
The editor delegates most of what you actually use
The feature list is long for a small editor, and the reason is that the list is mostly things the editor does not implement itself.
There is full UTF-8 support, syntax highlighting, stackable key maps, scriptable rc file, key macros, window splitting, regex search and replace, large file support, incremental search, linear undo and redo, multiple cursors, auto indent and headless mode.
Then there is the list of tools that are used if they exist in PATH: bash for tab completion, fzf for fuzzy file search, grep for file grep, less for less integration, perl for one-liners, readtags for ctags integration, and tree for file browsing.
That second list is the design in miniature. A fuzzy file finder is a hard thing to do well and a trivial thing to delegate to fzf, and fzf is a tool many developers already have configured to their liking. Grepping a project is grep, with the flags the user already knows. Paging through a long file is less. The jump-to-definition behaviour is ctags, which means mle inherits every language that universal-ctags supports without writing a parser for any of them.
The one that is more than delegation is the shell command feature. The cmd_shell command, bound to M-e by default, runs an arbitrary shell command, and the behaviour is the useful part: if text is selected it is sent to the standard input of the command, and the resulting standard output is inserted into the text buffer. That is a text filter built into the editor, and it is the mechanism behind using the editor as a transformation tool rather than only as a place to type.
The cost of the delegation is worth stating plainly. Every one of those features disappears when the tool is not installed, and the behaviour of the editor changes with what happens to be in your PATH. A user with fzf and one without have measurably different editors, which is a source of both delight and support questions.
Building it, vendored or against system libraries
The build is a Makefile with two modes, and the choice is between a self-contained binary and one that links against what your distribution ships.
The vendored route is the one the README leads with:
sudo apt install git build-essential # install git, make, gcc, libc-dev
git clone --recursive https://github.com/adsr/mle.git
cd mle
make mle_vendor=1The clone is recursive because the vendored dependencies arrive as submodules, and the mle_vendor variable switches the build to the copies in the vendor directory. The Makefile shows what that means: the vendored include paths for pcre2 and uthash are added, and the static libraries vendor/pcre2/libpcre2-8.a and vendor/lua/liblua5.5.a are linked instead of the shared ones.
A completely static binary is one more variable away:
make mle_vendor=1 mle_static=1which adds a -static flag. For a terminal editor, a single self-contained binary that runs on any Linux box with no shared library version to argue about is a genuinely useful artefact.
The alternative is a plain make, which links against system libraries and therefore requires uthash-dev, liblua5.4-dev and libpcre2-dev to be installed. Note that the Makefile's own flags name liblua5.5 while the README's package list names liblua5.4-dev, which is the kind of small drift a distribution packager will hit and a user will file a bug about.
Installation is make install, which puts the binary in /usr/local/bin by default, and a prefix variable changes that, for example make install prefix=/usr for /usr/bin/mle.
Packaging is wide. The README lists the install command for Ubuntu and Debian based distributions, CentOS, RHEL and Fedora, FreeBSD, Arch through the AUR, snap for all major Linux distributions, NixOS through nixpkgs, Alpine, Void, macOS through both Homebrew and MacPorts, and Cygwin through its setup executable. There is a Repology badge for the full picture, and a snapcraft.yaml in the repository root for the snap.
The rc file is a command line, which is a neat trick
The configuration mechanism is the part of mle most likely to surprise a new user, because it is not a configuration file format at all.
mle is customised via command line options, and the rc file is a file named ~/.mlerc, or /etc/mlerc for a system-wide one, whose contents are any number of command line options separated by newlines. Lines beginning with a semi-colon are comments.
So the rc file is literally a script of the same options you would type on the command line, one per line. There is no separate parser, no schema, and no second syntax to learn, which means anything you can do on the command line you can put in the file, and anything you can put in the file you can try on the command line first.
There is a second level to it. If ~/.mlerc is executable, mle executes it and interprets the resulting standard output as the same list of options. The README gives a bash example:
# Define 'test' kmap
echo '-Ktest,,1'
# M-q: replace grep with git grep if `.git` exists
if [ -d ".git" ]; then
echo '-kcmd_grep,M-q,git grep --color=never -P -i -I -n %s 2>/dev/null'
fi
# Set default kmap
echo '-n test'That is a conditional configuration, evaluated at startup, and the example overrides the built-in grep command with git grep when the current directory is a git repository. The point of the example is the pattern rather than the specific override, since a user can bind any shell command in the same way.
The options involved are worth reading for what they reveal about the key binding model. A key map is defined with -K, with a name and a mode number, so the maps are stackable, which is the feature list's stackable key maps. A binding is set with -k, naming the command and the key sequence, here cmd_grep and M-q, so the command is what the key is bound to and the command itself is redefined separately. And -n selects the default key map, so a user-defined map can become the default rather than an extra layer.
Headless mode turns the editor into a filter
The headless mode is the feature that makes mle more than an editor, and it is described as support for non-interactive editing, useful for using the editor as a regular command line tool.
In headless mode, mle reads standard input into a buffer, applies a startup macro if one is specified, and then writes the buffer contents to standard output. The example makes the point in one line:
$ echo -n hello | mle -M 'test C-e space w o r l d enter' -p test
hello worldThe -M option names a startup macro, expressed as a sequence of key presses, and the -p option names the key map to interpret it in, here the test map defined in the rc example earlier. So the macro replays the editing session that would have been typed, and the result is the transformed text. The example is the classic one of turning a stream of text into something else, and the same machinery does structured transformations, template filling and quick refactors on a file.
The trigger is convenient. If standard input is a pipe, mle goes into headless mode automatically, which means that piping into mle does the sensible thing without any flag at all. The -H option can enable or disable the mode explicitly, and -H0 is the interesting case: with headless mode disabled while standard input is a pipe, mle reads standard input into a new buffer and then runs as normal in interactive mode. That is how you open a pipe's contents in the editor.
The key macros feature and the headless mode are the same machinery, which is a good sign of a small codebase that composes its own parts. A macro recorded or written once can be replayed in an editor session or in a pipeline, and the mapping between the two is only the difference between a terminal and a pipe.
Lua scripting, a paused branch, and the forks that add what is missing
mle is extensible via the Lua programming language, and the mechanism is the -x option, which loads scripts. Commands registered by a script can be mapped to keys as normal via -k, and the README points at uscript.lua in the repository as a simple example. The scripting layer in the source is uscript.c, with uscript.inc.c and uscript.inc.php beside it, which is a good hint about how the bindings are generated.
There is also a wren branch with Wren scripting support, and the README says that work is on pause. That is a useful detail for anyone evaluating the project, because it shows the extension story is not frozen, and a branch that is paused is a smaller commitment than one that is abandoned.
Two forks are listed, and they fill the two gaps a user is most likely to hit. The eon fork adds mouse support and Notepad-like selections. The turbo-mle fork is a Turbo Vision port. Both are named with links and one line of description each, which is the lightest possible endorsement.
Mouse support is the significant gap, because a terminal editor without it is a different tool from one with it, and the upstream project has chosen not to implement it rather than to implement it badly.
The last part of the README is the acknowledgments, and it names the three libraries the project depends on: uthash for hash maps and linked lists, termbox2 for the terminal user interface, and PCRE2 for syntax highlighting and search. That list, together with the Lua dependency in the Makefile, is the complete dependency picture, and it is the clearest argument for the aims of minimising build-time and run-time dependencies.
The test structure is visible in the tree as well, with a tests directory that the Makefile distinguishes into unit tests written in C, compiled into a single test binary, and functional tests written as shell scripts, and the CI workflow badge points at a build that runs them.
Editorial conclusion
Adopt mle if you like the idea of an editor that is about twelve thousand lines of C you can read in an afternoon, and if your navigation habits are already built on ctags, fzf and grep, because the editor inherits all of it for free. It is the wrong tool if you want mouse support, since the upstream project does not provide it and points to a fork, and the wrong tool if you want a modern editor with LSP, file trees and plugins built in. Before installing, read the aims list, because favouring simplicity over correctness is a real design position, and check that the shell tools you rely on are in your PATH, since several features simply disappear when the tool is missing.
Frequently asked questions
What is mle and how big is it?
It is a small, flexible, terminal-based text editor written in C, with a codebase of about 12k lines. It runs on Linux, Windows through Cygwin or WSL, FreeBSD, macOS and more, and depends on uthash, termbox2 and PCRE2.
How do I build and install mle?
Install git and build-essential, clone the repository with --recursive because the vendored dependencies are submodules, then run make mle_vendor=1. Add mle_static=1 for a completely static binary, or run a plain make to link against system libraries, which needs uthash-dev, liblua5.4-dev and libpcre2-dev. make install places the binary, and a prefix variable changes the destination.
How is the mle rc file configured?
The file ~/.mlerc, or /etc/mlerc, contains command line options separated by newlines, with lines starting with a semi-colon as comments. If the file is executable, mle runs it and interprets its standard output the same way, which is how the README shows a conditional configuration that swaps grep for git grep inside a git repository.
What does headless mode do in mle?
It makes mle a command line filter. It reads standard input into a buffer, applies a startup macro if one is given, and writes the result to standard output. If standard input is a pipe, headless mode is entered automatically, and the -H option enables or disables it explicitly.
Which external tools does mle use?
If they are in PATH, bash for tab completion, fzf for fuzzy file search, grep for file grep, less for paging, perl for one-liners, readtags for ctags navigation and tree for file browsing. Arbitrary commands can also be run through cmd_shell, which sends any selected text to the command's standard input and inserts the output back into the buffer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/adsr-mle)Was this useful?