, 9 min read
Introduction to mle: Small Terminal Based Editor
1. Motivation. I am a regular user of vi/vim/neovim. But one thing, though, is a little bit annoying, when using neovim: even on a fast machine starting neovim takes quite a considerable time to start. Though, this is mostly caused by an elaborate initialization file. mle is a text editor written by Adam Saponara. Adam Saponara was mentioned multiple times in the talks of Rasmus Lerdorf, the creator of PHP. mle as of version 1.7.2 is written in C and is less than 17 kLines.
Source files | Number | LOC |
---|---|---|
*.c | 64 | 12,098 |
*.h | 5 | 4,631 |
2. Installation. The accompanying Makefile
is ready to use, i.e., no configure
is required. Just compile (=make
) and install (=make install
). On Arch Linux use AUR package mle. One particular good AUR helper is trizen.
3. Size comparison. Comparing the library dependencies for mle, vim and nvim:
$ ldd /bin/mle /bin/vim /bin/nvim
/bin/mle:
linux-vdso.so.1 (0x00007fff4e28d000)
libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007fb7a6246000)
liblua.so.5.4 => /usr/lib/liblua.so.5.4 (0x00007fb7a61ff000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fb7a6112000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fb7a5f30000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fb7a6362000)
/bin/vim:
linux-vdso.so.1 (0x00007ffefa6e9000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fc0cd313000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007fc0cd8f4000)
libacl.so.1 => /usr/lib/libacl.so.1 (0x00007fc0cd8eb000)
libgpm.so.2 => /usr/lib/libgpm.so.2 (0x00007fc0cd8e3000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fc0cd131000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fc0cd9a1000)
/bin/nvim:
linux-vdso.so.1 (0x00007ffdefdbd000)
libluv.so.1 => /usr/lib/libluv.so.1 (0x00007f190e1bc000)
libtermkey.so.1 => /usr/lib/libtermkey.so.1 (0x00007f190e1b0000)
libvterm.so.0 => /usr/lib/libvterm.so.0 (0x00007f190e19d000)
libmsgpackc.so.2 => /usr/lib/libmsgpackc.so.2 (0x00007f190e194000)
libtree-sitter.so.0 => /usr/lib/libtree-sitter.so.0 (0x00007f190e166000)
libunibilium.so.4 => /usr/lib/libunibilium.so.4 (0x00007f190e151000)
libluajit-5.1.so.2 => /usr/lib/libluajit-5.1.so.2 (0x00007f190e0be000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f190db13000)
libuv.so.1 => /usr/lib/libuv.so.1 (0x00007f190dadf000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f190daba000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f190d8d8000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f190e223000)
mle uses:
- uthash for hash maps and linked lists
- termbox2 for text-based UI
- PCRE2 for syntax highlighting and search
- Lua as a macro language
Comparing file sizes of the executables for mle, nano, neovim, and vim:
$ ls -l /bin/mle /bin/nano /bin/vim /bin/nvim
-rwxr-xr-x 1 root root 298752 Oct 29 22:22 /bin/mle*
-rwxr-xr-x 1 root root 278856 Jan 18 2023 /bin/nano*
-rwxr-xr-x 1 root root 4795872 Oct 10 13:39 /bin/nvim*
-rwxr-xr-x 1 root root 4848056 Oct 26 22:39 /bin/vim*
4. Speed comparison. Below is a comparison of the starting times for a 187 MB sized file conducted in 2016 by Adam Saponara:
Version | Command | time in s |
---|---|---|
mle 1.0 | mle -Qq bigfile | 0.531 |
vim 7.4 | vim -u NONE -c q bigfile | 1.382 |
I tried two files, all stored in /tmp
, which is in RAM on Arch Linux kernel 6.6.1:
seq 999000 > x9
, size is 6.6 MB,time wc x9
is 0.02sseq 9999000 > x9b
, size is 76 MB,time wc x9b
is 0.17s
Starting times for mle, vim, and neovim are as below:
Version | Command | real/s | Command | real/s |
---|---|---|---|---|
mle 1.7.2 | mle -N -Qq x9 | 0.04 | mle -N -Qq x9b | 0.36 |
vim 9.0.2070 | vim -u NONE -c q x9 | 0.04 | vim -u NONE -c q x9b | 0.36 |
neovim 0.9.4 | nvim -u NONE -c q x9 | 0.05 | nvim -u NONE -c q x9b | 0.33 |
Apparently, the speed advantage cannot be reproduced with these two particular files.
vim and neovim need roughly half of their time actually processing the file, as half of the time is needed for just reading the content, as can be seen by the wc
times.
More technical details on benchmarking can be found here: Full soft-wrap implementation #77.
5. Basic usage. In the following we use below abbrevations for keys. As usual, you have to press them all at once.
Key | Meaning |
---|---|
S | Shift |
M | Alt (also called Meta) |
MS | Alt-Shift |
C | Ctrl |
CS | Ctrl-Shift |
CM | Ctrl-Alt |
CMS | Ctrl-Alt-Shift |
Some basic file operations within the editor:
Task | mle | vi |
---|---|---|
Opening file | C-o | :r |
Saving file | C-s | :w |
Quit | C-x | :q |
Help text | F2 |
mle supports editing multiple files at once. Switching between buffers is by using M-1, M-2, M-3, etc.
Once you press F2 (the help key) then automatically a new buffer is opened. To switch back to your original file you would use M-1.
6. Moving the cursor around. Below commands just move the cursor around and do not change the file content in any way.
Task | mle | vi |
---|---|---|
jump over word in right direction | M-f | w |
jump over word in left direction | M-b | b |
top, bottom, or center | C-l | |
Search for string | C-f | / |
Find next | C-g | n |
Go to line | M-g | : |
Set mark a (or b , etc.) |
M-za | ma |
Go to mark a |
M-za | 'a |
Go to last mark | M-m |
7. Copying, deleting, or moving text. Below commands change the content of the file.
Task | mle | vi |
---|---|---|
Cut marked text or whole line | C-k | y |
Uncut, usually called paste | C-u | p |
Indent with one tab | M-. | >> |
Outdent one tab | M-, | << |
Delete word to the right | M-d | dw |
Delete word to the left | C-w | bdw |
Repeat last operation | F5 | . |
Inserting output from shell | M-e | r! |
When you start mle then whenever you enter a TAB, this will be changed to spaces. To change this behaviour you enter M-o a, and then enter y at the prompt. If you want to go back to the automatic tab to space conversion, then enter a number at the prompt.
8. Useful startup file. Below startup file for mle, also called rc file, named .mlerc
, is located in the user's home directory:
-Kklm,,1
-kcmd_move_beginning,C-home,
-kcmd_move_end,C-end,
-nklm
-w1
-t8
-e1
-a0
<empty line>
Ignore the first four lines for the moment. The meaning is as follows:
enable word wrap (-w
), set tabsize to 8 characters (-t
), enable mouse support (-e
), make tabs as tabs (-a
).
This is equivalent to start mle with below command line arguments:
mle -w1 -t8 -e1 -a0
If the startup file is executable then the output of the file is taken as actual rc file. So the startup can be changed conditionally.
9. Setting or redefining keys. mle allows you to set or redefine key bindings. This is a 3-step process.
- You define a so called kmap using command line option
-K
- Within this kmap you specify pairs of commands and keys using option
-k
- You instruct mle to use this new kmap with option
-n
For example the standard mle key binding for jumping to the end of the file is M-/. In Google Chrome or many editors this is C-end. Specifying this is thus:
mle -K 'klm,,1' -k 'cmd_move_end,C-end,' -n klm <file>
10. Lua macros. Below table information is extracted from uscript.lua
.
B | B-M | M-U |
---|---|---|
buffer_add_mark | bview_new | mark_find_bracket_top |
buffer_add_mark_ex | bview_open | mark_find_next_re |
buffer_add_srule | bview_pop_kmap | mark_find_next_str |
buffer_apply_styles | bview_push_kmap | mark_find_prev_re |
buffer_clear | bview_rectify_viewport | mark_find_prev_str |
buffer_delete | bview_remove_cursor | mark_get_between |
buffer_delete_w_bline | bview_remove_cursors_except | mark_get_char_after |
buffer_destroy | bview_resize | mark_get_char_before |
buffer_destroy_mark | bview_set_syntax | mark_get_nchars_between |
buffer_get | bview_set_viewport_y | mark_get_offset |
buffer_get_bline | bview_split | mark_insert_after |
buffer_get_bline_col | bview_wake_sleeping_cursors | mark_insert_before |
buffer_get_bline_w_hint | bview_zero_viewport_y | mark_is_after_col_minus_lefties |
buffer_get_lettered_mark | cursor_clone | mark_is_at_bol |
buffer_get_offset | cursor_cut_copy | mark_is_at_eol |
buffer_insert | cursor_destroy | mark_is_at_word_bound |
buffer_insert_w_bline | cursor_drop_anchor | mark_is_between |
buffer_new | cursor_get_anchor | mark_is_eq |
buffer_new_open | cursor_get_lo_hi | mark_is_gt |
buffer_open | cursor_get_mark | mark_is_gte |
buffer_redo | cursor_lift_anchor | mark_is_lt |
buffer_redo_action_group | cursor_replace | mark_is_lte |
buffer_register_append | cursor_select_between | mark_join |
buffer_register_clear | cursor_select_by | mark_move_beginning |
buffer_register_get | cursor_select_by_bracket | mark_move_bol |
buffer_register_prepend | cursor_select_by_string | mark_move_bracket_pair |
buffer_register_set | cursor_select_by_word | mark_move_bracket_pair_ex |
buffer_remove_srule | cursor_select_by_word_back | mark_move_bracket_top |
buffer_replace | cursor_select_by_word_forward | mark_move_bracket_top_ex |
buffer_replace_w_bline | cursor_toggle_anchor | mark_move_by |
buffer_save | cursor_uncut | mark_move_col |
buffer_save_as | editor_bview_edit_count | mark_move_end |
buffer_set | editor_close_bview | mark_move_eol |
buffer_set_action_group_ptr | editor_count_bviews_by_buffer | mark_move_next_re |
buffer_set_callback | editor_destroy_observer | mark_move_next_re_ex |
buffer_set_mmapped | editor_display | mark_move_next_re_nudge |
buffer_set_styles_enabled | editor_force_redraw | mark_move_next_str |
buffer_set_tab_width | editor_get_input | mark_move_next_str_ex |
buffer_substr | editor_menu | mark_move_next_str_nudge |
buffer_undo | editor_notify_observers | mark_move_offset |
buffer_undo_action_group | editor_open_bview | mark_move_prev_re |
buffer_write_to_fd | editor_prompt | mark_move_prev_re_ex |
buffer_write_to_file | editor_register_cmd | mark_move_prev_str |
bview_add_cursor | editor_register_observer | mark_move_prev_str_ex |
bview_add_cursor_asleep | editor_set_active | mark_move_to |
bview_center_viewport_y | mark_clone | mark_move_to_w_bline |
bview_destroy | mark_clone_w_letter | mark_move_vert |
bview_draw | mark_delete_after | mark_replace |
bview_draw_cursor | mark_delete_before | mark_replace_between |
bview_get_active_cursor_count | mark_delete_between | mark_swap |
bview_get_split_root | mark_destroy | util_escape_shell_arg |
bview_max_viewport_y | mark_find_bracket_pair | util_shell_exec |
11. Limitation. Unfortunately mle has some shortcomings.
- mle does not have a line-wrap functionality. So long lines do not wrap at the end of the screen. For source code files this is fine. But for Markdown files this is a severe restriction.
- This only happens on
st
:mle -Qk
does not handle Shift-home and home differently, therefore you cannot define the combination of Shift-home to mean "go to top of page". This works perfectly fine onxterm
. - When cutting text out of file1, then this text is not available, when in file2 there is also text, which has been cut.