Introduction to mle: Small Terminal Based Editor

· klm's blog

mle is a small terminal based editor written in C. This post is an introduction.

Original post is here: eklausmeier.goip.de

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:

 1$ ldd /bin/mle /bin/vim /bin/nvim
 2/bin/mle:
 3        linux-vdso.so.1 (0x00007fff4e28d000)
 4        libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007fb7a6246000)
 5        liblua.so.5.4 => /usr/lib/liblua.so.5.4 (0x00007fb7a61ff000)
 6        libm.so.6 => /usr/lib/libm.so.6 (0x00007fb7a6112000)
 7        libc.so.6 => /usr/lib/libc.so.6 (0x00007fb7a5f30000)
 8        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fb7a6362000)
 9/bin/vim:
10        linux-vdso.so.1 (0x00007ffefa6e9000)
11        libm.so.6 => /usr/lib/libm.so.6 (0x00007fc0cd313000)
12        libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007fc0cd8f4000)
13        libacl.so.1 => /usr/lib/libacl.so.1 (0x00007fc0cd8eb000)
14        libgpm.so.2 => /usr/lib/libgpm.so.2 (0x00007fc0cd8e3000)
15        libc.so.6 => /usr/lib/libc.so.6 (0x00007fc0cd131000)
16        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fc0cd9a1000)
17/bin/nvim:
18        linux-vdso.so.1 (0x00007ffdefdbd000)
19        libluv.so.1 => /usr/lib/libluv.so.1 (0x00007f190e1bc000)
20        libtermkey.so.1 => /usr/lib/libtermkey.so.1 (0x00007f190e1b0000)
21        libvterm.so.0 => /usr/lib/libvterm.so.0 (0x00007f190e19d000)
22        libmsgpackc.so.2 => /usr/lib/libmsgpackc.so.2 (0x00007f190e194000)
23        libtree-sitter.so.0 => /usr/lib/libtree-sitter.so.0 (0x00007f190e166000)
24        libunibilium.so.4 => /usr/lib/libunibilium.so.4 (0x00007f190e151000)
25        libluajit-5.1.so.2 => /usr/lib/libluajit-5.1.so.2 (0x00007f190e0be000)
26        libm.so.6 => /usr/lib/libm.so.6 (0x00007f190db13000)
27        libuv.so.1 => /usr/lib/libuv.so.1 (0x00007f190dadf000)
28        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f190daba000)
29        libc.so.6 => /usr/lib/libc.so.6 (0x00007f190d8d8000)
30        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f190e223000)

mle uses:

  1. uthash for hash maps and linked lists
  2. termbox2 for text-based UI
  3. PCRE2 for syntax highlighting and search
  4. Lua as a macro language

Comparing file sizes of the executables for mle, nano, neovim, and vim:

1$ ls -l /bin/mle /bin/nano /bin/vim /bin/nvim
2-rwxr-xr-x 1 root root  298752 Oct 29 22:22 /bin/mle*
3-rwxr-xr-x 1 root root  278856 Jan 18  2023 /bin/nano*
4-rwxr-xr-x 1 root root 4795872 Oct 10 13:39 /bin/nvim*
5-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:

  1. seq 999000 > x9, size is 6.6 MB, time wc x9 is 0.02s
  2. seq 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. Photo

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:

1-Kklm,,1
2-kcmd_move_beginning,C-home,
3-kcmd_move_end,C-end,
4-nklm
5-w1
6-t8
7-e1
8-a0
9     <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:

1mle -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.

  1. You define a so called kmap using command line option -K
  2. Within this kmap you specify pairs of commands and keys using option -k
  3. 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:

1mle -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.

  1. 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.
  2. 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 on xterm.
  3. When cutting text out of file1, then this text is not available, when in file2 there is also text, which has been cut.